♻️ Better image loading animation and more commonly used blurhash
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -74,14 +74,10 @@ class UpdateProfileScreen extends HookConsumerWidget {
|
||||
|
||||
submitting.value = true;
|
||||
try {
|
||||
final cloudFile =
|
||||
await FileUploader.createCloudFile(
|
||||
ref: ref,
|
||||
fileData: UniversalFile(
|
||||
data: result,
|
||||
type: UniversalFileType.image,
|
||||
),
|
||||
).future;
|
||||
final cloudFile = await FileUploader.createCloudFile(
|
||||
ref: ref,
|
||||
fileData: UniversalFile(data: result, type: UniversalFileType.image),
|
||||
).future;
|
||||
if (cloudFile == null) {
|
||||
throw ArgumentError('Failed to upload the file...');
|
||||
}
|
||||
@@ -188,8 +184,9 @@ class UpdateProfileScreen extends HookConsumerWidget {
|
||||
if (usernameColorType.value == 'gradient') ...{
|
||||
if (usernameColorDirection.text.isNotEmpty)
|
||||
'direction': usernameColorDirection.text,
|
||||
'colors':
|
||||
usernameColorColors.value.where((c) => c.isNotEmpty).toList(),
|
||||
'colors': usernameColorColors.value
|
||||
.where((c) => c.isNotEmpty)
|
||||
.toList(),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -206,18 +203,16 @@ class UpdateProfileScreen extends HookConsumerWidget {
|
||||
'time_zone': timeZoneController.text,
|
||||
'birthday': birthday.value?.toUtc().toIso8601String(),
|
||||
'username_color': usernameColorData,
|
||||
'links':
|
||||
links.value
|
||||
.where((e) => e.name.isNotEmpty && e.url.isNotEmpty)
|
||||
.toList(),
|
||||
'links': links.value
|
||||
.where((e) => e.name.isNotEmpty && e.url.isNotEmpty)
|
||||
.toList(),
|
||||
},
|
||||
);
|
||||
final userNotifier = ref.read(userInfoProvider.notifier);
|
||||
userNotifier.fetchUser();
|
||||
links.value =
|
||||
links.value
|
||||
.where((e) => e.name.isNotEmpty && e.url.isNotEmpty)
|
||||
.toList();
|
||||
links.value = links.value
|
||||
.where((e) => e.name.isNotEmpty && e.url.isNotEmpty)
|
||||
.toList();
|
||||
} catch (err) {
|
||||
showErrorAlert(err);
|
||||
} finally {
|
||||
@@ -244,13 +239,12 @@ class UpdateProfileScreen extends HookConsumerWidget {
|
||||
GestureDetector(
|
||||
child: Container(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
child:
|
||||
user.value!.profile.background?.id != null
|
||||
? CloudImageWidget(
|
||||
fileId: user.value!.profile.background!.id,
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
child: user.value!.profile.background != null
|
||||
? CloudImageWidget(
|
||||
file: user.value!.profile.background,
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
onTap: () {
|
||||
updateProfilePicture('background');
|
||||
@@ -261,7 +255,7 @@ class UpdateProfileScreen extends HookConsumerWidget {
|
||||
bottom: -32,
|
||||
child: GestureDetector(
|
||||
child: ProfilePictureWidget(
|
||||
fileId: user.value!.profile.picture?.id,
|
||||
file: user.value!.profile.picture,
|
||||
radius: 40,
|
||||
),
|
||||
onTap: () {
|
||||
@@ -291,14 +285,14 @@ class UpdateProfileScreen extends HookConsumerWidget {
|
||||
),
|
||||
controller: usernameController,
|
||||
readOnly: true,
|
||||
onTapOutside:
|
||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
TextFormField(
|
||||
decoration: InputDecoration(labelText: 'nickname'.tr()),
|
||||
controller: nicknameController,
|
||||
onTapOutside:
|
||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
DropdownButtonFormField2<String>(
|
||||
decoration: InputDecoration(
|
||||
@@ -385,9 +379,8 @@ class UpdateProfileScreen extends HookConsumerWidget {
|
||||
labelText: 'firstName'.tr(),
|
||||
),
|
||||
controller: firstNameController,
|
||||
onTapOutside:
|
||||
(_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
@@ -396,9 +389,8 @@ class UpdateProfileScreen extends HookConsumerWidget {
|
||||
labelText: 'middleName'.tr(),
|
||||
),
|
||||
controller: middleNameController,
|
||||
onTapOutside:
|
||||
(_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
@@ -407,9 +399,8 @@ class UpdateProfileScreen extends HookConsumerWidget {
|
||||
labelText: 'lastName'.tr(),
|
||||
),
|
||||
controller: lastNameController,
|
||||
onTapOutside:
|
||||
(_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -423,8 +414,8 @@ class UpdateProfileScreen extends HookConsumerWidget {
|
||||
maxLines: null,
|
||||
minLines: 3,
|
||||
controller: bioController,
|
||||
onTapOutside:
|
||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
Row(
|
||||
spacing: 16,
|
||||
@@ -445,33 +436,34 @@ class UpdateProfileScreen extends HookConsumerWidget {
|
||||
onSelected: (String selection) {
|
||||
genderController.text = selection;
|
||||
},
|
||||
fieldViewBuilder: (
|
||||
context,
|
||||
controller,
|
||||
focusNode,
|
||||
onFieldSubmitted,
|
||||
) {
|
||||
// Initialize the controller with the current value
|
||||
if (controller.text.isEmpty &&
|
||||
genderController.text.isNotEmpty) {
|
||||
controller.text = genderController.text;
|
||||
}
|
||||
fieldViewBuilder:
|
||||
(
|
||||
context,
|
||||
controller,
|
||||
focusNode,
|
||||
onFieldSubmitted,
|
||||
) {
|
||||
// Initialize the controller with the current value
|
||||
if (controller.text.isEmpty &&
|
||||
genderController.text.isNotEmpty) {
|
||||
controller.text = genderController.text;
|
||||
}
|
||||
|
||||
return TextFormField(
|
||||
controller: controller,
|
||||
focusNode: focusNode,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'gender'.tr(),
|
||||
),
|
||||
onChanged: (value) {
|
||||
genderController.text = value;
|
||||
return TextFormField(
|
||||
controller: controller,
|
||||
focusNode: focusNode,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'gender'.tr(),
|
||||
),
|
||||
onChanged: (value) {
|
||||
genderController.text = value;
|
||||
},
|
||||
onTapOutside: (_) => FocusManager
|
||||
.instance
|
||||
.primaryFocus
|
||||
?.unfocus(),
|
||||
);
|
||||
},
|
||||
onTapOutside:
|
||||
(_) =>
|
||||
FocusManager.instance.primaryFocus
|
||||
?.unfocus(),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
@@ -480,9 +472,8 @@ class UpdateProfileScreen extends HookConsumerWidget {
|
||||
labelText: 'pronouns'.tr(),
|
||||
),
|
||||
controller: pronounsController,
|
||||
onTapOutside:
|
||||
(_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -496,9 +487,8 @@ class UpdateProfileScreen extends HookConsumerWidget {
|
||||
labelText: 'location'.tr(),
|
||||
),
|
||||
controller: locationController,
|
||||
onTapOutside:
|
||||
(_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
@@ -507,8 +497,8 @@ class UpdateProfileScreen extends HookConsumerWidget {
|
||||
if (textEditingValue.text.isEmpty) {
|
||||
return const Iterable<String>.empty();
|
||||
}
|
||||
final lowercaseQuery =
|
||||
textEditingValue.text.toLowerCase();
|
||||
final lowercaseQuery = textEditingValue.text
|
||||
.toLowerCase();
|
||||
return getAvailableTz().where((tz) {
|
||||
return tz.toLowerCase().contains(lowercaseQuery);
|
||||
});
|
||||
@@ -516,46 +506,49 @@ class UpdateProfileScreen extends HookConsumerWidget {
|
||||
onSelected: (String selection) {
|
||||
timeZoneController.text = selection;
|
||||
},
|
||||
fieldViewBuilder: (
|
||||
context,
|
||||
controller,
|
||||
focusNode,
|
||||
onFieldSubmitted,
|
||||
) {
|
||||
// Sync the controller with timeZoneController when the widget is built
|
||||
if (controller.text != timeZoneController.text) {
|
||||
controller.text = timeZoneController.text;
|
||||
}
|
||||
fieldViewBuilder:
|
||||
(
|
||||
context,
|
||||
controller,
|
||||
focusNode,
|
||||
onFieldSubmitted,
|
||||
) {
|
||||
// Sync the controller with timeZoneController when the widget is built
|
||||
if (controller.text !=
|
||||
timeZoneController.text) {
|
||||
controller.text = timeZoneController.text;
|
||||
}
|
||||
|
||||
return TextFormField(
|
||||
controller: controller,
|
||||
focusNode: focusNode,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'timeZone'.tr(),
|
||||
suffix: InkWell(
|
||||
child: const Icon(
|
||||
Symbols.my_location,
|
||||
size: 18,
|
||||
return TextFormField(
|
||||
controller: controller,
|
||||
focusNode: focusNode,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'timeZone'.tr(),
|
||||
suffix: InkWell(
|
||||
child: const Icon(
|
||||
Symbols.my_location,
|
||||
size: 18,
|
||||
),
|
||||
onTap: () async {
|
||||
try {
|
||||
showLoadingModal(context);
|
||||
final machineTz =
|
||||
await getMachineTz();
|
||||
controller.text = machineTz;
|
||||
timeZoneController.text = machineTz;
|
||||
} finally {
|
||||
if (context.mounted) {
|
||||
hideLoadingModal(context);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
onTap: () async {
|
||||
try {
|
||||
showLoadingModal(context);
|
||||
final machineTz = await getMachineTz();
|
||||
controller.text = machineTz;
|
||||
timeZoneController.text = machineTz;
|
||||
} finally {
|
||||
if (context.mounted) {
|
||||
hideLoadingModal(context);
|
||||
}
|
||||
}
|
||||
onChanged: (value) {
|
||||
timeZoneController.text = value;
|
||||
},
|
||||
),
|
||||
),
|
||||
onChanged: (value) {
|
||||
timeZoneController.text = value;
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
optionsViewBuilder: (context, onSelected, options) {
|
||||
return Align(
|
||||
alignment: Alignment.topLeft,
|
||||
@@ -569,21 +562,21 @@ class UpdateProfileScreen extends HookConsumerWidget {
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
itemCount: options.length,
|
||||
itemBuilder: (
|
||||
BuildContext context,
|
||||
int index,
|
||||
) {
|
||||
final option = options.elementAt(index);
|
||||
return ListTile(
|
||||
title: Text(
|
||||
option,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
onTap: () {
|
||||
onSelected(option);
|
||||
itemBuilder:
|
||||
(BuildContext context, int index) {
|
||||
final option = options.elementAt(
|
||||
index,
|
||||
);
|
||||
return ListTile(
|
||||
title: Text(
|
||||
option,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
onTap: () {
|
||||
onSelected(option);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -644,10 +637,9 @@ class UpdateProfileScreen extends HookConsumerWidget {
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainerHighest,
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
@@ -664,25 +656,23 @@ class UpdateProfileScreen extends HookConsumerWidget {
|
||||
type: usernameColorType.value,
|
||||
value:
|
||||
usernameColorType.value == 'plain' &&
|
||||
usernameColorValue
|
||||
.text
|
||||
.isNotEmpty
|
||||
? usernameColorValue.text
|
||||
: null,
|
||||
usernameColorValue.text.isNotEmpty
|
||||
? usernameColorValue.text
|
||||
: null,
|
||||
direction:
|
||||
usernameColorType.value ==
|
||||
'gradient' &&
|
||||
usernameColorDirection
|
||||
.text
|
||||
.isNotEmpty
|
||||
? usernameColorDirection.text
|
||||
: null,
|
||||
'gradient' &&
|
||||
usernameColorDirection
|
||||
.text
|
||||
.isNotEmpty
|
||||
? usernameColorDirection.text
|
||||
: null,
|
||||
colors:
|
||||
usernameColorType.value == 'gradient'
|
||||
? usernameColorColors.value
|
||||
.where((c) => c.isNotEmpty)
|
||||
.toList()
|
||||
: null,
|
||||
? usernameColorColors.value
|
||||
.where((c) => c.isNotEmpty)
|
||||
.toList()
|
||||
: null,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -724,10 +714,9 @@ class UpdateProfileScreen extends HookConsumerWidget {
|
||||
? Symbols.check_circle
|
||||
: Symbols.error,
|
||||
size: 16,
|
||||
color:
|
||||
canUseColor
|
||||
? Colors.green
|
||||
: Colors.red,
|
||||
color: canUseColor
|
||||
? Colors.green
|
||||
: Colors.red,
|
||||
),
|
||||
const Gap(4),
|
||||
Text(
|
||||
@@ -736,10 +725,9 @@ class UpdateProfileScreen extends HookConsumerWidget {
|
||||
: 'upgradeRequired'.tr(),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color:
|
||||
canUseColor
|
||||
? Colors.green
|
||||
: Colors.red,
|
||||
color: canUseColor
|
||||
? Colors.green
|
||||
: Colors.red,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -792,34 +780,35 @@ class UpdateProfileScreen extends HookConsumerWidget {
|
||||
onSelected: (String selection) {
|
||||
usernameColorValue.text = selection;
|
||||
},
|
||||
fieldViewBuilder: (
|
||||
context,
|
||||
controller,
|
||||
focusNode,
|
||||
onFieldSubmitted,
|
||||
) {
|
||||
// Initialize the controller with the current value
|
||||
if (controller.text.isEmpty &&
|
||||
usernameColorValue.text.isNotEmpty) {
|
||||
controller.text = usernameColorValue.text;
|
||||
}
|
||||
fieldViewBuilder:
|
||||
(
|
||||
context,
|
||||
controller,
|
||||
focusNode,
|
||||
onFieldSubmitted,
|
||||
) {
|
||||
// Initialize the controller with the current value
|
||||
if (controller.text.isEmpty &&
|
||||
usernameColorValue.text.isNotEmpty) {
|
||||
controller.text = usernameColorValue.text;
|
||||
}
|
||||
|
||||
return TextFormField(
|
||||
controller: controller,
|
||||
focusNode: focusNode,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'colorValue'.tr(),
|
||||
hintText: 'e.g. red or #ff6600',
|
||||
),
|
||||
onChanged: (value) {
|
||||
usernameColorValue.text = value;
|
||||
return TextFormField(
|
||||
controller: controller,
|
||||
focusNode: focusNode,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'colorValue'.tr(),
|
||||
hintText: 'e.g. red or #ff6600',
|
||||
),
|
||||
onChanged: (value) {
|
||||
usernameColorValue.text = value;
|
||||
},
|
||||
onTapOutside: (_) => FocusManager
|
||||
.instance
|
||||
.primaryFocus
|
||||
?.unfocus(),
|
||||
);
|
||||
},
|
||||
onTapOutside:
|
||||
(_) =>
|
||||
FocusManager.instance.primaryFocus
|
||||
?.unfocus(),
|
||||
);
|
||||
},
|
||||
),
|
||||
if (usernameColorType.value == 'gradient') ...[
|
||||
DropdownButtonFormField2<String>(
|
||||
@@ -862,10 +851,9 @@ class UpdateProfileScreen extends HookConsumerWidget {
|
||||
child: Text('gradientDirectionToTopLeft'.tr()),
|
||||
),
|
||||
],
|
||||
value:
|
||||
usernameColorDirection.text.isNotEmpty
|
||||
? usernameColorDirection.text
|
||||
: 'to right',
|
||||
value: usernameColorDirection.text.isNotEmpty
|
||||
? usernameColorDirection.text
|
||||
: 'to right',
|
||||
onChanged: (value) {
|
||||
usernameColorDirection.text = value ?? 'to right';
|
||||
},
|
||||
@@ -911,21 +899,19 @@ class UpdateProfileScreen extends HookConsumerWidget {
|
||||
onChanged: (value) {
|
||||
usernameColorColors.value[i] = value;
|
||||
},
|
||||
onTapOutside:
|
||||
(_) =>
|
||||
FocusManager.instance.primaryFocus
|
||||
?.unfocus(),
|
||||
onTapOutside: (_) => FocusManager
|
||||
.instance
|
||||
.primaryFocus
|
||||
?.unfocus(),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.delete),
|
||||
onPressed: () {
|
||||
usernameColorColors.value =
|
||||
usernameColorColors.value
|
||||
.whereIndexed(
|
||||
(idx, _) => idx != i,
|
||||
)
|
||||
.toList();
|
||||
usernameColorColors
|
||||
.value = usernameColorColors.value
|
||||
.whereIndexed((idx, _) => idx != i)
|
||||
.toList();
|
||||
},
|
||||
),
|
||||
],
|
||||
@@ -968,10 +954,10 @@ class UpdateProfileScreen extends HookConsumerWidget {
|
||||
name: value,
|
||||
);
|
||||
},
|
||||
onTapOutside:
|
||||
(_) =>
|
||||
FocusManager.instance.primaryFocus
|
||||
?.unfocus(),
|
||||
onTapOutside: (_) => FocusManager
|
||||
.instance
|
||||
.primaryFocus
|
||||
?.unfocus(),
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
@@ -987,19 +973,18 @@ class UpdateProfileScreen extends HookConsumerWidget {
|
||||
url: value,
|
||||
);
|
||||
},
|
||||
onTapOutside:
|
||||
(_) =>
|
||||
FocusManager.instance.primaryFocus
|
||||
?.unfocus(),
|
||||
onTapOutside: (_) => FocusManager
|
||||
.instance
|
||||
.primaryFocus
|
||||
?.unfocus(),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.delete),
|
||||
onPressed: () {
|
||||
links.value =
|
||||
links.value
|
||||
.whereIndexed((idx, _) => idx != i)
|
||||
.toList();
|
||||
links.value = links.value
|
||||
.whereIndexed((idx, _) => idx != i)
|
||||
.toList();
|
||||
},
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -51,10 +51,9 @@ class EditAppScreen extends HookConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final isNew = id == null;
|
||||
final app =
|
||||
isNew
|
||||
? null
|
||||
: ref.watch(customAppProvider(publisherName, projectId, id!));
|
||||
final app = isNew
|
||||
? null
|
||||
: ref.watch(customAppProvider(publisherName, projectId, id!));
|
||||
|
||||
final formKey = useMemoized(() => GlobalKey<FormState>());
|
||||
|
||||
@@ -139,14 +138,10 @@ class EditAppScreen extends HookConsumerWidget {
|
||||
|
||||
submitting.value = true;
|
||||
try {
|
||||
final cloudFile =
|
||||
await FileUploader.createCloudFile(
|
||||
ref: ref,
|
||||
fileData: UniversalFile(
|
||||
data: result,
|
||||
type: UniversalFileType.image,
|
||||
),
|
||||
).future;
|
||||
final cloudFile = await FileUploader.createCloudFile(
|
||||
ref: ref,
|
||||
fileData: UniversalFile(data: result, type: UniversalFileType.image),
|
||||
).future;
|
||||
if (cloudFile == null) {
|
||||
throw ArgumentError('Failed to upload the file...');
|
||||
}
|
||||
@@ -169,41 +164,40 @@ class EditAppScreen extends HookConsumerWidget {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder:
|
||||
(context) => SheetScaffold(
|
||||
titleText: 'addScope'.tr(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: scopeController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'scopeName'.tr(),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
),
|
||||
builder: (context) => SheetScaffold(
|
||||
titleText: 'addScope'.tr(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: scopeController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'scopeName'.tr(),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
FilledButton.tonalIcon(
|
||||
onPressed: () {
|
||||
if (scopeController.text.isNotEmpty) {
|
||||
allowedScopes.value = [
|
||||
...allowedScopes.value,
|
||||
scopeController.text,
|
||||
];
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
icon: const Icon(Symbols.add),
|
||||
label: Text('add').tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
FilledButton.tonalIcon(
|
||||
onPressed: () {
|
||||
if (scopeController.text.isNotEmpty) {
|
||||
allowedScopes.value = [
|
||||
...allowedScopes.value,
|
||||
scopeController.text,
|
||||
];
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
icon: const Icon(Symbols.add),
|
||||
label: Text('add').tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -212,57 +206,56 @@ class EditAppScreen extends HookConsumerWidget {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder:
|
||||
(context) => SheetScaffold(
|
||||
titleText: 'addRedirectUri'.tr(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: uriController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'redirectUri'.tr(),
|
||||
hintText: 'https://example.com/auth/callback',
|
||||
helperText: 'redirectUriHint'.tr(),
|
||||
helperMaxLines: 3,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
),
|
||||
keyboardType: TextInputType.url,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'uriRequired'.tr();
|
||||
}
|
||||
final uri = Uri.tryParse(value);
|
||||
if (uri == null || !uri.hasAbsolutePath) {
|
||||
return 'invalidUri'.tr();
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onTapOutside:
|
||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
builder: (context) => SheetScaffold(
|
||||
titleText: 'addRedirectUri'.tr(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: uriController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'redirectUri'.tr(),
|
||||
hintText: 'https://example.com/auth/callback',
|
||||
helperText: 'redirectUriHint'.tr(),
|
||||
helperMaxLines: 3,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
FilledButton.tonalIcon(
|
||||
onPressed: () {
|
||||
if (uriController.text.isNotEmpty) {
|
||||
redirectUris.value = [
|
||||
...redirectUris.value,
|
||||
uriController.text,
|
||||
];
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
icon: const Icon(Symbols.add),
|
||||
label: Text('add').tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
keyboardType: TextInputType.url,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'uriRequired'.tr();
|
||||
}
|
||||
final uri = Uri.tryParse(value);
|
||||
if (uri == null || !uri.hasAbsolutePath) {
|
||||
return 'invalidUri'.tr();
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
FilledButton.tonalIcon(
|
||||
onPressed: () {
|
||||
if (uriController.text.isNotEmpty) {
|
||||
redirectUris.value = [
|
||||
...redirectUris.value,
|
||||
uriController.text,
|
||||
];
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
icon: const Icon(Symbols.add),
|
||||
label: Text('add').tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -275,31 +268,28 @@ class EditAppScreen extends HookConsumerWidget {
|
||||
'picture_id': picture.value?.id,
|
||||
'background_id': background.value?.id,
|
||||
'links': {
|
||||
'home_page':
|
||||
homePageController.text.isNotEmpty
|
||||
? homePageController.text
|
||||
: null,
|
||||
'privacy_policy':
|
||||
privacyPolicyController.text.isNotEmpty
|
||||
? privacyPolicyController.text
|
||||
: null,
|
||||
'terms_of_service':
|
||||
termsController.text.isNotEmpty ? termsController.text : null,
|
||||
'home_page': homePageController.text.isNotEmpty
|
||||
? homePageController.text
|
||||
: null,
|
||||
'privacy_policy': privacyPolicyController.text.isNotEmpty
|
||||
? privacyPolicyController.text
|
||||
: null,
|
||||
'terms_of_service': termsController.text.isNotEmpty
|
||||
? termsController.text
|
||||
: null,
|
||||
},
|
||||
'oauth_config':
|
||||
oauthEnabled.value
|
||||
? {
|
||||
'redirect_uris': redirectUris.value,
|
||||
'post_logout_redirect_uris':
|
||||
postLogoutUris.value.isNotEmpty
|
||||
? postLogoutUris.value
|
||||
: null,
|
||||
'allowed_scopes': allowedScopes.value,
|
||||
'allowed_grant_types': allowedGrantTypes.value,
|
||||
'require_pkce': requirePkce.value,
|
||||
'allow_offline_access': allowOfflineAccess.value,
|
||||
}
|
||||
: null,
|
||||
'oauth_config': oauthEnabled.value
|
||||
? {
|
||||
'redirect_uris': redirectUris.value,
|
||||
'post_logout_redirect_uris': postLogoutUris.value.isNotEmpty
|
||||
? postLogoutUris.value
|
||||
: null,
|
||||
'allowed_scopes': allowedScopes.value,
|
||||
'allowed_grant_types': allowedGrantTypes.value,
|
||||
'require_pkce': requirePkce.value,
|
||||
'allow_offline_access': allowOfflineAccess.value,
|
||||
}
|
||||
: null,
|
||||
};
|
||||
try {
|
||||
showLoadingModal(context);
|
||||
@@ -326,287 +316,269 @@ class EditAppScreen extends HookConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
final bodyContent =
|
||||
app == null && !isNew
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: app?.hasError == true && !isNew
|
||||
? ResponseErrorWidget(
|
||||
error: app!.error,
|
||||
onRetry:
|
||||
() => ref.invalidate(
|
||||
customAppProvider(publisherName, projectId, id!),
|
||||
),
|
||||
)
|
||||
: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: 16 / 7,
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
GestureDetector(
|
||||
child: Container(
|
||||
color:
|
||||
Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainerHigh,
|
||||
child:
|
||||
background.value != null
|
||||
? CloudFileWidget(
|
||||
item: background.value!,
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
final bodyContent = app == null && !isNew
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: app?.hasError == true && !isNew
|
||||
? ResponseErrorWidget(
|
||||
error: app!.error,
|
||||
onRetry: () => ref.invalidate(
|
||||
customAppProvider(publisherName, projectId, id!),
|
||||
),
|
||||
)
|
||||
: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: 16 / 7,
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
GestureDetector(
|
||||
child: Container(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainerHigh,
|
||||
child: background.value != null
|
||||
? CloudFileWidget(
|
||||
item: background.value!,
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
onTap: () {
|
||||
setPicture('background');
|
||||
},
|
||||
),
|
||||
Positioned(
|
||||
left: 20,
|
||||
bottom: -32,
|
||||
child: GestureDetector(
|
||||
child: ProfilePictureWidget(
|
||||
file: picture.value,
|
||||
radius: 40,
|
||||
fallbackIcon: Symbols.apps,
|
||||
),
|
||||
onTap: () {
|
||||
setPicture('background');
|
||||
setPicture('picture');
|
||||
},
|
||||
),
|
||||
Positioned(
|
||||
left: 20,
|
||||
bottom: -32,
|
||||
child: GestureDetector(
|
||||
child: ProfilePictureWidget(
|
||||
fileId: picture.value?.id,
|
||||
radius: 40,
|
||||
fallbackIcon: Symbols.apps,
|
||||
),
|
||||
onTap: () {
|
||||
setPicture('picture');
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
).padding(bottom: 32),
|
||||
Form(
|
||||
key: formKey,
|
||||
child: Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: nameController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'name'.tr(),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
onTapOutside:
|
||||
(_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: slugController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'slug'.tr(),
|
||||
helperText: 'slugHint'.tr(),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
onTapOutside:
|
||||
(_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: descriptionController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'description'.tr(),
|
||||
alignLabelWithHint: true,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
maxLines: 3,
|
||||
onTapOutside:
|
||||
(_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ExpansionPanelList(
|
||||
expansionCallback: (index, isExpanded) {
|
||||
switch (index) {
|
||||
case 0:
|
||||
enableLinks.value = isExpanded;
|
||||
break;
|
||||
case 1:
|
||||
oauthEnabled.value = isExpanded;
|
||||
break;
|
||||
}
|
||||
},
|
||||
children: [
|
||||
ExpansionPanel(
|
||||
headerBuilder:
|
||||
(context, isExpanded) =>
|
||||
ListTile(title: Text('appLinks').tr()),
|
||||
body: Column(
|
||||
spacing: 16,
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: homePageController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'homePageUrl'.tr(),
|
||||
hintText: 'https://example.com',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
keyboardType: TextInputType.url,
|
||||
),
|
||||
TextFormField(
|
||||
controller: privacyPolicyController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'privacyPolicyUrl'.tr(),
|
||||
hintText: 'https://example.com/privacy',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
keyboardType: TextInputType.url,
|
||||
),
|
||||
TextFormField(
|
||||
controller: termsController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'termsOfServiceUrl'.tr(),
|
||||
hintText: 'https://example.com/terms',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
keyboardType: TextInputType.url,
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 16, bottom: 24),
|
||||
isExpanded: enableLinks.value,
|
||||
),
|
||||
ExpansionPanel(
|
||||
headerBuilder:
|
||||
(context, isExpanded) =>
|
||||
ListTile(title: Text('oauthConfig').tr()),
|
||||
body: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('redirectUris'.tr()),
|
||||
Card(
|
||||
margin: const EdgeInsets.symmetric(
|
||||
vertical: 8,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
...redirectUris.value.map(
|
||||
(uri) => ListTile(
|
||||
title: Text(uri),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Symbols.delete),
|
||||
onPressed: () {
|
||||
redirectUris.value =
|
||||
redirectUris.value
|
||||
.where((u) => u != uri)
|
||||
.toList();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
if (redirectUris.value.isNotEmpty)
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
leading: const Icon(Symbols.add),
|
||||
title: Text('addRedirectUri'.tr()),
|
||||
onTap: showAddRedirectUriDialog,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(
|
||||
8,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text('allowedScopes'.tr()),
|
||||
Card(
|
||||
margin: const EdgeInsets.symmetric(
|
||||
vertical: 8,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
...allowedScopes.value.map(
|
||||
(scope) => ListTile(
|
||||
title: Text(scope),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Symbols.delete),
|
||||
onPressed: () {
|
||||
allowedScopes.value =
|
||||
allowedScopes.value
|
||||
.where(
|
||||
(s) => s != scope,
|
||||
)
|
||||
.toList();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
if (allowedScopes.value.isNotEmpty)
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
leading: const Icon(Symbols.add),
|
||||
title: Text('add').tr(),
|
||||
onTap: showAddScopeDialog,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SwitchListTile(
|
||||
title: Text('requirePkce'.tr()),
|
||||
value: requirePkce.value,
|
||||
onChanged:
|
||||
(value) => requirePkce.value = value,
|
||||
),
|
||||
SwitchListTile(
|
||||
title: Text('allowOfflineAccess'.tr()),
|
||||
value: allowOfflineAccess.value,
|
||||
onChanged:
|
||||
(value) =>
|
||||
allowOfflineAccess.value = value,
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 16, bottom: 24),
|
||||
isExpanded: oauthEnabled.value,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: TextButton.icon(
|
||||
onPressed: submitting.value ? null : performAction,
|
||||
label: Text('saveChanges'.tr()),
|
||||
icon: const Icon(Symbols.save),
|
||||
),
|
||||
),
|
||||
],
|
||||
).padding(all: 24),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
).padding(bottom: 32),
|
||||
Form(
|
||||
key: formKey,
|
||||
child: Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: nameController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'name'.tr(),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: slugController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'slug'.tr(),
|
||||
helperText: 'slugHint'.tr(),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: descriptionController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'description'.tr(),
|
||||
alignLabelWithHint: true,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
),
|
||||
maxLines: 3,
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ExpansionPanelList(
|
||||
expansionCallback: (index, isExpanded) {
|
||||
switch (index) {
|
||||
case 0:
|
||||
enableLinks.value = isExpanded;
|
||||
break;
|
||||
case 1:
|
||||
oauthEnabled.value = isExpanded;
|
||||
break;
|
||||
}
|
||||
},
|
||||
children: [
|
||||
ExpansionPanel(
|
||||
headerBuilder: (context, isExpanded) =>
|
||||
ListTile(title: Text('appLinks').tr()),
|
||||
body: Column(
|
||||
spacing: 16,
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: homePageController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'homePageUrl'.tr(),
|
||||
hintText: 'https://example.com',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
keyboardType: TextInputType.url,
|
||||
),
|
||||
TextFormField(
|
||||
controller: privacyPolicyController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'privacyPolicyUrl'.tr(),
|
||||
hintText: 'https://example.com/privacy',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
keyboardType: TextInputType.url,
|
||||
),
|
||||
TextFormField(
|
||||
controller: termsController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'termsOfServiceUrl'.tr(),
|
||||
hintText: 'https://example.com/terms',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
keyboardType: TextInputType.url,
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 16, bottom: 24),
|
||||
isExpanded: enableLinks.value,
|
||||
),
|
||||
ExpansionPanel(
|
||||
headerBuilder: (context, isExpanded) =>
|
||||
ListTile(title: Text('oauthConfig').tr()),
|
||||
body: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('redirectUris'.tr()),
|
||||
Card(
|
||||
margin: const EdgeInsets.symmetric(
|
||||
vertical: 8,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
...redirectUris.value.map(
|
||||
(uri) => ListTile(
|
||||
title: Text(uri),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Symbols.delete),
|
||||
onPressed: () {
|
||||
redirectUris.value = redirectUris
|
||||
.value
|
||||
.where((u) => u != uri)
|
||||
.toList();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
if (redirectUris.value.isNotEmpty)
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
leading: const Icon(Symbols.add),
|
||||
title: Text('addRedirectUri'.tr()),
|
||||
onTap: showAddRedirectUriDialog,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(
|
||||
8,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text('allowedScopes'.tr()),
|
||||
Card(
|
||||
margin: const EdgeInsets.symmetric(
|
||||
vertical: 8,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
...allowedScopes.value.map(
|
||||
(scope) => ListTile(
|
||||
title: Text(scope),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Symbols.delete),
|
||||
onPressed: () {
|
||||
allowedScopes.value =
|
||||
allowedScopes.value
|
||||
.where((s) => s != scope)
|
||||
.toList();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
if (allowedScopes.value.isNotEmpty)
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
leading: const Icon(Symbols.add),
|
||||
title: Text('add').tr(),
|
||||
onTap: showAddScopeDialog,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SwitchListTile(
|
||||
title: Text('requirePkce'.tr()),
|
||||
value: requirePkce.value,
|
||||
onChanged: (value) =>
|
||||
requirePkce.value = value,
|
||||
),
|
||||
SwitchListTile(
|
||||
title: Text('allowOfflineAccess'.tr()),
|
||||
value: allowOfflineAccess.value,
|
||||
onChanged: (value) =>
|
||||
allowOfflineAccess.value = value,
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 16, bottom: 24),
|
||||
isExpanded: oauthEnabled.value,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: TextButton.icon(
|
||||
onPressed: submitting.value ? null : performAction,
|
||||
label: Text('saveChanges'.tr()),
|
||||
icon: const Icon(Symbols.save),
|
||||
),
|
||||
),
|
||||
],
|
||||
).padding(all: 24),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (isModal) {
|
||||
return bodyContent;
|
||||
|
||||
@@ -50,8 +50,9 @@ class EditBotScreen extends HookConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final isNew = id == null;
|
||||
final botData =
|
||||
isNew ? null : ref.watch(botProvider(publisherName, projectId, id!));
|
||||
final botData = isNew
|
||||
? null
|
||||
: ref.watch(botProvider(publisherName, projectId, id!));
|
||||
|
||||
final formKey = useMemoized(() => GlobalKey<FormState>());
|
||||
final submitting = useState(false);
|
||||
@@ -125,14 +126,10 @@ class EditBotScreen extends HookConsumerWidget {
|
||||
|
||||
submitting.value = true;
|
||||
try {
|
||||
final cloudFile =
|
||||
await FileUploader.createCloudFile(
|
||||
ref: ref,
|
||||
fileData: UniversalFile(
|
||||
data: result,
|
||||
type: UniversalFileType.image,
|
||||
),
|
||||
).future;
|
||||
final cloudFile = await FileUploader.createCloudFile(
|
||||
ref: ref,
|
||||
fileData: UniversalFile(data: result, type: UniversalFileType.image),
|
||||
).future;
|
||||
if (cloudFile == null) {
|
||||
throw ArgumentError('Failed to upload the file...');
|
||||
}
|
||||
@@ -193,284 +190,267 @@ class EditBotScreen extends HookConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
final bodyContent =
|
||||
botData == null && !isNew
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: botData?.hasError == true && !isNew
|
||||
? ResponseErrorWidget(
|
||||
error: botData!.error,
|
||||
onRetry:
|
||||
() => ref.invalidate(
|
||||
botProvider(publisherName, projectId, id!),
|
||||
),
|
||||
)
|
||||
: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: 16 / 7,
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
GestureDetector(
|
||||
child: Container(
|
||||
color:
|
||||
Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainerHigh,
|
||||
child:
|
||||
background.value != null
|
||||
? CloudFileWidget(
|
||||
item: background.value!,
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
final bodyContent = botData == null && !isNew
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: botData?.hasError == true && !isNew
|
||||
? ResponseErrorWidget(
|
||||
error: botData!.error,
|
||||
onRetry: () =>
|
||||
ref.invalidate(botProvider(publisherName, projectId, id!)),
|
||||
)
|
||||
: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: 16 / 7,
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
GestureDetector(
|
||||
child: Container(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainerHigh,
|
||||
child: background.value != null
|
||||
? CloudFileWidget(
|
||||
item: background.value!,
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
onTap: () {
|
||||
setPicture('background');
|
||||
},
|
||||
),
|
||||
Positioned(
|
||||
left: 20,
|
||||
bottom: -32,
|
||||
child: GestureDetector(
|
||||
child: ProfilePictureWidget(
|
||||
file: picture.value,
|
||||
radius: 40,
|
||||
fallbackIcon: Symbols.smart_toy,
|
||||
),
|
||||
onTap: () {
|
||||
setPicture('background');
|
||||
setPicture('picture');
|
||||
},
|
||||
),
|
||||
Positioned(
|
||||
left: 20,
|
||||
bottom: -32,
|
||||
child: GestureDetector(
|
||||
child: ProfilePictureWidget(
|
||||
fileId: picture.value?.id,
|
||||
radius: 40,
|
||||
fallbackIcon: Symbols.smart_toy,
|
||||
),
|
||||
onTap: () {
|
||||
setPicture('picture');
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
).padding(bottom: 32),
|
||||
Form(
|
||||
key: formKey,
|
||||
child: Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: nameController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'name'.tr(),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: nickController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'nickname'.tr(),
|
||||
alignLabelWithHint: true,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: slugController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'slug'.tr(),
|
||||
helperText: 'slugHint'.tr(),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: bioController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'bio'.tr(),
|
||||
alignLabelWithHint: true,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
maxLines: 3,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
spacing: 16,
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: firstNameController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'firstName'.tr(),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: middleNameController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'middleName'.tr(),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: lastNameController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'lastName'.tr(),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
spacing: 16,
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: genderController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'gender'.tr(),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: pronounsController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'pronouns'.tr(),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
spacing: 16,
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: locationController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'location'.tr(),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: timeZoneController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'timeZone'.tr(),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
final date = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: birthday.value ?? DateTime.now(),
|
||||
firstDate: DateTime(1900),
|
||||
lastDate: DateTime.now(),
|
||||
);
|
||||
if (date != null) {
|
||||
birthday.value = date;
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1,
|
||||
),
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(12),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
'birthday'.tr(),
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
birthday.value != null
|
||||
? DateFormat.yMMMd().format(
|
||||
birthday.value!,
|
||||
)
|
||||
: 'Select a date'.tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: TextButton.icon(
|
||||
onPressed: submitting.value ? null : performAction,
|
||||
label: Text('saveChanges').tr(),
|
||||
icon: const Icon(Symbols.save),
|
||||
),
|
||||
),
|
||||
],
|
||||
).padding(all: 24),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
).padding(bottom: 32),
|
||||
Form(
|
||||
key: formKey,
|
||||
child: Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: nameController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'name'.tr(),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: nickController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'nickname'.tr(),
|
||||
alignLabelWithHint: true,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: slugController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'slug'.tr(),
|
||||
helperText: 'slugHint'.tr(),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: bioController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'bio'.tr(),
|
||||
alignLabelWithHint: true,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
),
|
||||
maxLines: 3,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
spacing: 16,
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: firstNameController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'firstName'.tr(),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: middleNameController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'middleName'.tr(),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: lastNameController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'lastName'.tr(),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
spacing: 16,
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: genderController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'gender'.tr(),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: pronounsController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'pronouns'.tr(),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
spacing: 16,
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: locationController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'location'.tr(),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: timeZoneController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'timeZone'.tr(),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
final date = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: birthday.value ?? DateTime.now(),
|
||||
firstDate: DateTime(1900),
|
||||
lastDate: DateTime.now(),
|
||||
);
|
||||
if (date != null) {
|
||||
birthday.value = date;
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1,
|
||||
),
|
||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
'birthday'.tr(),
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
birthday.value != null
|
||||
? DateFormat.yMMMd().format(birthday.value!)
|
||||
: 'Select a date'.tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: TextButton.icon(
|
||||
onPressed: submitting.value ? null : performAction,
|
||||
label: Text('saveChanges').tr(),
|
||||
icon: const Icon(Symbols.save),
|
||||
),
|
||||
),
|
||||
],
|
||||
).padding(all: 24),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (isModal) {
|
||||
return bodyContent;
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -34,16 +34,14 @@ class ArticleEditScreen extends HookConsumerWidget {
|
||||
final post = ref.watch(postProvider(id));
|
||||
return post.when(
|
||||
data: (post) => ArticleComposeScreen(originalPost: post),
|
||||
loading:
|
||||
() => AppScaffold(
|
||||
appBar: AppBar(leading: const PageBackButton()),
|
||||
body: const Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
error:
|
||||
(e, _) => AppScaffold(
|
||||
appBar: AppBar(leading: const PageBackButton()),
|
||||
body: Text('Error: $e', textAlign: TextAlign.center),
|
||||
),
|
||||
loading: () => AppScaffold(
|
||||
appBar: AppBar(leading: const PageBackButton()),
|
||||
body: const Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
error: (e, _) => AppScaffold(
|
||||
appBar: AppBar(leading: const PageBackButton()),
|
||||
body: Text('Error: $e', textAlign: TextAlign.center),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -127,8 +125,8 @@ class ArticleComposeScreen extends HookConsumerWidget {
|
||||
final mostRecentDraft = drafts.values.reduce(
|
||||
(a, b) =>
|
||||
(a.updatedAt ?? DateTime(0)).isAfter(b.updatedAt ?? DateTime(0))
|
||||
? a
|
||||
: b,
|
||||
? a
|
||||
: b,
|
||||
);
|
||||
|
||||
// Only load if the draft has meaningful content
|
||||
@@ -191,12 +189,11 @@ class ArticleComposeScreen extends HookConsumerWidget {
|
||||
MarkdownTextContent(
|
||||
content: contentValue.text,
|
||||
textStyle: theme.textTheme.bodyMedium,
|
||||
attachments:
|
||||
state.attachments.value
|
||||
.where((e) => e.isOnCloud)
|
||||
.map((e) => e.data)
|
||||
.cast<SnCloudFile>()
|
||||
.toList(),
|
||||
attachments: state.attachments.value
|
||||
.where((e) => e.isOnCloud)
|
||||
.map((e) => e.data)
|
||||
.cast<SnCloudFile>()
|
||||
.toList(),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -290,22 +287,21 @@ class ArticleComposeScreen extends HookConsumerWidget {
|
||||
onExpansionChanged: (expanded) {
|
||||
isAttachmentsExpanded.value = expanded;
|
||||
},
|
||||
collapsedBackgroundColor:
|
||||
Theme.of(context).colorScheme.surfaceContainer,
|
||||
collapsedBackgroundColor: Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainer,
|
||||
title: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('attachments').tr(),
|
||||
Text(
|
||||
'articleAttachmentHint'.tr(),
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodySmall?.copyWith(
|
||||
color:
|
||||
Theme.of(
|
||||
style: Theme.of(context).textTheme.bodySmall
|
||||
?.copyWith(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -336,13 +332,12 @@ class ArticleComposeScreen extends HookConsumerWidget {
|
||||
>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder:
|
||||
(context) =>
|
||||
AttachmentUploaderSheet(
|
||||
ref: ref,
|
||||
state: state,
|
||||
index: idx,
|
||||
),
|
||||
builder: (context) =>
|
||||
AttachmentUploaderSheet(
|
||||
ref: ref,
|
||||
state: state,
|
||||
index: idx,
|
||||
),
|
||||
);
|
||||
if (config != null) {
|
||||
await ComposeLogic.uploadAttachment(
|
||||
@@ -353,21 +348,20 @@ class ArticleComposeScreen extends HookConsumerWidget {
|
||||
);
|
||||
}
|
||||
},
|
||||
onUpdate:
|
||||
(value) =>
|
||||
ComposeLogic.updateAttachment(
|
||||
state,
|
||||
value,
|
||||
idx,
|
||||
),
|
||||
onDelete:
|
||||
() => ComposeLogic.deleteAttachment(
|
||||
onUpdate: (value) =>
|
||||
ComposeLogic.updateAttachment(
|
||||
state,
|
||||
value,
|
||||
idx,
|
||||
),
|
||||
onDelete: () =>
|
||||
ComposeLogic.deleteAttachment(
|
||||
ref,
|
||||
state,
|
||||
idx,
|
||||
),
|
||||
onInsert:
|
||||
() => ComposeLogic.insertAttachment(
|
||||
onInsert: () =>
|
||||
ComposeLogic.insertAttachment(
|
||||
ref,
|
||||
state,
|
||||
idx,
|
||||
@@ -413,12 +407,11 @@ class ArticleComposeScreen extends HookConsumerWidget {
|
||||
const SizedBox.shrink(),
|
||||
IconButton(
|
||||
icon: ProfilePictureWidget(
|
||||
fileId: state.currentPublisher.value?.picture?.id,
|
||||
file: state.currentPublisher.value?.picture,
|
||||
radius: 12,
|
||||
fallbackIcon:
|
||||
state.currentPublisher.value == null
|
||||
? Symbols.question_mark
|
||||
: null,
|
||||
fallbackIcon: state.currentPublisher.value == null
|
||||
? Symbols.question_mark
|
||||
: null,
|
||||
),
|
||||
onPressed: () {
|
||||
showModalBottomSheet(
|
||||
@@ -448,30 +441,26 @@ class ArticleComposeScreen extends HookConsumerWidget {
|
||||
valueListenable: state.submitting,
|
||||
builder: (context, submitting, _) {
|
||||
return IconButton(
|
||||
onPressed:
|
||||
submitting
|
||||
? null
|
||||
: () => ComposeLogic.performAction(
|
||||
ref,
|
||||
state,
|
||||
context,
|
||||
originalPost: originalPost,
|
||||
),
|
||||
icon:
|
||||
submitting
|
||||
? SizedBox(
|
||||
width: 28,
|
||||
height: 28,
|
||||
child: const CircularProgressIndicator(
|
||||
color: Colors.white,
|
||||
strokeWidth: 2.5,
|
||||
),
|
||||
).center()
|
||||
: Icon(
|
||||
originalPost != null
|
||||
? Symbols.edit
|
||||
: Symbols.upload,
|
||||
onPressed: submitting
|
||||
? null
|
||||
: () => ComposeLogic.performAction(
|
||||
ref,
|
||||
state,
|
||||
context,
|
||||
originalPost: originalPost,
|
||||
),
|
||||
icon: submitting
|
||||
? SizedBox(
|
||||
width: 28,
|
||||
height: 28,
|
||||
child: const CircularProgressIndicator(
|
||||
color: Colors.white,
|
||||
strokeWidth: 2.5,
|
||||
),
|
||||
).center()
|
||||
: Icon(
|
||||
originalPost != null ? Symbols.edit : Symbols.upload,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -483,23 +472,22 @@ class ArticleComposeScreen extends HookConsumerWidget {
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16),
|
||||
child:
|
||||
isWideScreen(context)
|
||||
? Row(
|
||||
spacing: 16,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: showPreview.value ? 1 : 2,
|
||||
child: buildEditorPane(),
|
||||
),
|
||||
if (showPreview.value) const VerticalDivider(),
|
||||
if (showPreview.value)
|
||||
Expanded(child: buildPreviewPane()),
|
||||
],
|
||||
)
|
||||
: showPreview.value
|
||||
? buildPreviewPane()
|
||||
: buildEditorPane(),
|
||||
child: isWideScreen(context)
|
||||
? Row(
|
||||
spacing: 16,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: showPreview.value ? 1 : 2,
|
||||
child: buildEditorPane(),
|
||||
),
|
||||
if (showPreview.value) const VerticalDivider(),
|
||||
if (showPreview.value)
|
||||
Expanded(child: buildPreviewPane()),
|
||||
],
|
||||
)
|
||||
: showPreview.value
|
||||
? buildPreviewPane()
|
||||
: buildEditorPane(),
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -163,7 +163,7 @@ class MarketplaceStickerPackDetailScreen extends HookConsumerWidget {
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: CloudImageWidget(
|
||||
fileId: sticker.image.id,
|
||||
file: sticker.image,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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()),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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')),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -133,10 +133,9 @@ class _ExpandedSection extends StatelessWidget {
|
||||
},
|
||||
child: Card(
|
||||
margin: EdgeInsets.zero,
|
||||
color:
|
||||
Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainer,
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainer,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
@@ -144,8 +143,9 @@ class _ExpandedSection extends StatelessWidget {
|
||||
const Gap(4),
|
||||
Text(
|
||||
'Poll',
|
||||
style:
|
||||
Theme.of(context).textTheme.bodySmall,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -160,8 +160,8 @@ class _ExpandedSection extends StatelessWidget {
|
||||
await showModalBottomSheet<SnWalletFund>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder:
|
||||
(context) => const ComposeFundSheet(),
|
||||
builder: (context) =>
|
||||
const ComposeFundSheet(),
|
||||
);
|
||||
if (fund != null) {
|
||||
onFundSelected(fund);
|
||||
@@ -169,10 +169,9 @@ class _ExpandedSection extends StatelessWidget {
|
||||
},
|
||||
child: Card(
|
||||
margin: EdgeInsets.zero,
|
||||
color:
|
||||
Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainer,
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainer,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
@@ -180,8 +179,9 @@ class _ExpandedSection extends StatelessWidget {
|
||||
const Gap(4),
|
||||
Text(
|
||||
'fund'.tr(),
|
||||
style:
|
||||
Theme.of(context).textTheme.bodySmall,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -192,11 +192,8 @@ class _ExpandedSection extends StatelessWidget {
|
||||
),
|
||||
StickerPickerEmbedded(
|
||||
height: kInputDrawerExpandedHeight,
|
||||
onPick:
|
||||
(placeholder) => _insertPlaceholder(
|
||||
messageController,
|
||||
placeholder,
|
||||
),
|
||||
onPick: (placeholder) =>
|
||||
_insertPlaceholder(messageController, placeholder),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -373,15 +370,16 @@ class ChatInput extends HookConsumerWidget {
|
||||
switchOutCurve: Curves.fastEaseInToSlowEaseOut,
|
||||
transitionBuilder: (Widget child, Animation<double> animation) {
|
||||
return SlideTransition(
|
||||
position: Tween<Offset>(
|
||||
begin: const Offset(0, -0.3),
|
||||
end: Offset.zero,
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: animation,
|
||||
curve: Curves.easeOutCubic,
|
||||
),
|
||||
),
|
||||
position:
|
||||
Tween<Offset>(
|
||||
begin: const Offset(0, -0.3),
|
||||
end: Offset.zero,
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: animation,
|
||||
curve: Curves.easeOutCubic,
|
||||
),
|
||||
),
|
||||
child: SizeTransition(
|
||||
sizeFactor: animation,
|
||||
axisAlignment: -1.0,
|
||||
@@ -389,41 +387,40 @@ class ChatInput extends HookConsumerWidget {
|
||||
),
|
||||
);
|
||||
},
|
||||
child:
|
||||
chatSubscribe.isNotEmpty
|
||||
? Container(
|
||||
key: const ValueKey('typing-indicator'),
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 4,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Symbols.more_horiz,
|
||||
size: 16,
|
||||
).padding(horizontal: 8),
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'typingHint'.plural(
|
||||
chatSubscribe.length,
|
||||
args: [
|
||||
chatSubscribe
|
||||
.map((x) => x.nick ?? x.account.nick)
|
||||
.join(', '),
|
||||
],
|
||||
),
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(
|
||||
key: ValueKey('typing-indicator-none'),
|
||||
child: chatSubscribe.isNotEmpty
|
||||
? Container(
|
||||
key: const ValueKey('typing-indicator'),
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 4,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Symbols.more_horiz,
|
||||
size: 16,
|
||||
).padding(horizontal: 8),
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'typingHint'.plural(
|
||||
chatSubscribe.length,
|
||||
args: [
|
||||
chatSubscribe
|
||||
.map((x) => x.nick ?? x.account.nick)
|
||||
.join(', '),
|
||||
],
|
||||
),
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(
|
||||
key: ValueKey('typing-indicator-none'),
|
||||
),
|
||||
),
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
@@ -445,41 +442,36 @@ class ChatInput extends HookConsumerWidget {
|
||||
),
|
||||
);
|
||||
},
|
||||
child:
|
||||
attachments.isNotEmpty
|
||||
? SizedBox(
|
||||
key: ValueKey('attachments-${attachments.length}'),
|
||||
height: 180,
|
||||
child: ListView.separated(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12),
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: attachments.length,
|
||||
itemBuilder: (context, idx) {
|
||||
return SizedBox(
|
||||
width: 180,
|
||||
child: AttachmentPreview(
|
||||
isCompact: true,
|
||||
item: attachments[idx],
|
||||
progress:
|
||||
attachmentProgress['chat-upload']?[idx],
|
||||
onRequestUpload:
|
||||
() => onUploadAttachment(idx),
|
||||
onDelete: () => onDeleteAttachment(idx),
|
||||
onUpdate: (value) {
|
||||
attachments[idx] = value;
|
||||
onAttachmentsChanged(attachments);
|
||||
},
|
||||
onMove:
|
||||
(delta) => onMoveAttachment(idx, delta),
|
||||
),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (_, _) => const Gap(8),
|
||||
),
|
||||
).padding(vertical: 12)
|
||||
: const SizedBox.shrink(
|
||||
key: ValueKey('no-attachments'),
|
||||
child: attachments.isNotEmpty
|
||||
? SizedBox(
|
||||
key: ValueKey('attachments-${attachments.length}'),
|
||||
height: 180,
|
||||
child: ListView.separated(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12),
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: attachments.length,
|
||||
itemBuilder: (context, idx) {
|
||||
return SizedBox(
|
||||
width: 180,
|
||||
child: AttachmentPreview(
|
||||
isCompact: true,
|
||||
item: attachments[idx],
|
||||
progress:
|
||||
attachmentProgress['chat-upload']?[idx],
|
||||
onRequestUpload: () => onUploadAttachment(idx),
|
||||
onDelete: () => onDeleteAttachment(idx),
|
||||
onUpdate: (value) {
|
||||
attachments[idx] = value;
|
||||
onAttachmentsChanged(attachments);
|
||||
},
|
||||
onMove: (delta) => onMoveAttachment(idx, delta),
|
||||
),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (_, _) => const Gap(8),
|
||||
),
|
||||
).padding(vertical: 12)
|
||||
: const SizedBox.shrink(key: ValueKey('no-attachments')),
|
||||
),
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
@@ -501,66 +493,62 @@ class ChatInput extends HookConsumerWidget {
|
||||
),
|
||||
);
|
||||
},
|
||||
child:
|
||||
selectedPoll != null
|
||||
? Container(
|
||||
key: const ValueKey('selected-poll'),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
border: Border.all(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.outline.withOpacity(0.2),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
margin: const EdgeInsets.only(
|
||||
left: 8,
|
||||
right: 8,
|
||||
top: 8,
|
||||
bottom: 8,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Symbols.how_to_vote,
|
||||
size: 18,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
selectedPoll!.title ?? 'Poll',
|
||||
style: Theme.of(context).textTheme.bodySmall!
|
||||
.copyWith(fontWeight: FontWeight.w500),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
icon: const Icon(Icons.close, size: 18),
|
||||
onPressed: () => onPollSelected(null),
|
||||
tooltip: 'clear'.tr(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(
|
||||
key: ValueKey('no-selected-poll'),
|
||||
child: selectedPoll != null
|
||||
? Container(
|
||||
key: const ValueKey('selected-poll'),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
border: Border.all(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.outline.withOpacity(0.2),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
margin: const EdgeInsets.only(
|
||||
left: 8,
|
||||
right: 8,
|
||||
top: 8,
|
||||
bottom: 8,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Symbols.how_to_vote,
|
||||
size: 18,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
selectedPoll!.title ?? 'Poll',
|
||||
style: Theme.of(context).textTheme.bodySmall!
|
||||
.copyWith(fontWeight: FontWeight.w500),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
icon: const Icon(Icons.close, size: 18),
|
||||
onPressed: () => onPollSelected(null),
|
||||
tooltip: 'clear'.tr(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(key: ValueKey('no-selected-poll')),
|
||||
),
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
@@ -582,93 +570,88 @@ class ChatInput extends HookConsumerWidget {
|
||||
),
|
||||
);
|
||||
},
|
||||
child:
|
||||
selectedFund != null
|
||||
? Container(
|
||||
key: const ValueKey('selected-fund'),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
child: selectedFund != null
|
||||
? Container(
|
||||
key: const ValueKey('selected-fund'),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
border: Border.all(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.outline.withOpacity(0.2),
|
||||
width: 1,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
border: Border.all(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.outline.withOpacity(0.2),
|
||||
width: 1,
|
||||
),
|
||||
margin: const EdgeInsets.only(
|
||||
left: 8,
|
||||
right: 8,
|
||||
top: 8,
|
||||
bottom: 8,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Symbols.currency_exchange,
|
||||
size: 18,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
margin: const EdgeInsets.only(
|
||||
left: 8,
|
||||
right: 8,
|
||||
top: 8,
|
||||
bottom: 8,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Symbols.currency_exchange,
|
||||
size: 18,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'${selectedFund!.totalAmount.toStringAsFixed(2)} ${selectedFund!.currency}',
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodySmall!.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'${selectedFund!.totalAmount.toStringAsFixed(2)} ${selectedFund!.currency}',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall!
|
||||
.copyWith(fontWeight: FontWeight.w500),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
if (selectedFund!.message != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 2),
|
||||
child: Text(
|
||||
selectedFund!.message!,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall!
|
||||
.copyWith(
|
||||
fontSize: 10,
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
if (selectedFund!.message != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 2),
|
||||
child: Text(
|
||||
selectedFund!.message!,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodySmall!.copyWith(
|
||||
fontSize: 10,
|
||||
color:
|
||||
Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
icon: const Icon(Icons.close, size: 18),
|
||||
onPressed: () => onFundSelected(null),
|
||||
tooltip: 'clear'.tr(),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
icon: const Icon(Icons.close, size: 18),
|
||||
onPressed: () => onFundSelected(null),
|
||||
tooltip: 'clear'.tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(
|
||||
key: ValueKey('no-selected-fund'),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(key: ValueKey('no-selected-fund')),
|
||||
),
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
@@ -692,59 +675,57 @@ class ChatInput extends HookConsumerWidget {
|
||||
},
|
||||
child:
|
||||
(messageReplyingTo != null ||
|
||||
messageForwardingTo != null ||
|
||||
messageEditingTo != null)
|
||||
? Container(
|
||||
key: ValueKey(
|
||||
messageReplyingTo?.id ??
|
||||
messageForwardingTo?.id ??
|
||||
messageEditingTo?.id ??
|
||||
'action',
|
||||
messageForwardingTo != null ||
|
||||
messageEditingTo != null)
|
||||
? Container(
|
||||
key: ValueKey(
|
||||
messageReplyingTo?.id ??
|
||||
messageForwardingTo?.id ??
|
||||
messageEditingTo?.id ??
|
||||
'action',
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
border: Border.all(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.outline.withOpacity(0.2),
|
||||
width: 1,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
border: Border.all(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.outline.withOpacity(0.2),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
margin: const EdgeInsets.only(
|
||||
left: 8,
|
||||
right: 8,
|
||||
top: 8,
|
||||
bottom: 8,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
),
|
||||
margin: const EdgeInsets.only(
|
||||
left: 8,
|
||||
right: 8,
|
||||
top: 8,
|
||||
bottom: 8,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
messageReplyingTo != null
|
||||
? Symbols.reply
|
||||
: messageForwardingTo != null
|
||||
? Symbols.forward
|
||||
: Symbols.edit,
|
||||
size: 18,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
messageReplyingTo != null
|
||||
? Symbols.reply
|
||||
: messageForwardingTo != null
|
||||
? Symbols.forward
|
||||
: Symbols.edit,
|
||||
size: 18,
|
||||
color:
|
||||
Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
messageReplyingTo != null
|
||||
? 'chatReplyingTo'.tr(
|
||||
? 'chatReplyingTo'.tr(
|
||||
args: [
|
||||
messageReplyingTo
|
||||
?.sender
|
||||
@@ -753,60 +734,57 @@ class ChatInput extends HookConsumerWidget {
|
||||
'unknown'.tr(),
|
||||
],
|
||||
)
|
||||
: messageForwardingTo != null
|
||||
? 'chatForwarding'.tr()
|
||||
: 'chatEditing'.tr(),
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodySmall!.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
icon: const Icon(Icons.close, size: 18),
|
||||
onPressed: onClear,
|
||||
tooltip: 'clear'.tr(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (messageReplyingTo != null ||
|
||||
messageForwardingTo != null ||
|
||||
messageEditingTo != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 6,
|
||||
left: 26,
|
||||
),
|
||||
child: Text(
|
||||
(messageReplyingTo ??
|
||||
messageForwardingTo ??
|
||||
messageEditingTo)
|
||||
?.content ??
|
||||
'chatNoContent'.tr(),
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodySmall!.copyWith(
|
||||
color:
|
||||
Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
maxLines: 2,
|
||||
: messageForwardingTo != null
|
||||
? 'chatForwarding'.tr()
|
||||
: 'chatEditing'.tr(),
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall!
|
||||
.copyWith(fontWeight: FontWeight.w500),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(key: ValueKey('no-action')),
|
||||
SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
icon: const Icon(Icons.close, size: 18),
|
||||
onPressed: onClear,
|
||||
tooltip: 'clear'.tr(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (messageReplyingTo != null ||
|
||||
messageForwardingTo != null ||
|
||||
messageEditingTo != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 6,
|
||||
left: 26,
|
||||
),
|
||||
child: Text(
|
||||
(messageReplyingTo ??
|
||||
messageForwardingTo ??
|
||||
messageEditingTo)
|
||||
?.content ??
|
||||
'chatNoContent'.tr(),
|
||||
style: Theme.of(context).textTheme.bodySmall!
|
||||
.copyWith(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(key: ValueKey('no-action')),
|
||||
),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -815,25 +793,19 @@ class ChatInput extends HookConsumerWidget {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
tooltip:
|
||||
isExpanded.value ? 'collapse'.tr() : 'more'.tr(),
|
||||
tooltip: isExpanded.value
|
||||
? 'collapse'.tr()
|
||||
: 'more'.tr(),
|
||||
icon: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
transitionBuilder:
|
||||
(child, animation) => FadeTransition(
|
||||
opacity: animation,
|
||||
child: child,
|
||||
),
|
||||
child:
|
||||
isExpanded.value
|
||||
? const Icon(
|
||||
Symbols.close,
|
||||
key: ValueKey('close'),
|
||||
)
|
||||
: const Icon(
|
||||
Symbols.add,
|
||||
key: ValueKey('add'),
|
||||
),
|
||||
transitionBuilder: (child, animation) =>
|
||||
FadeTransition(opacity: animation, child: child),
|
||||
child: isExpanded.value
|
||||
? const Icon(
|
||||
Symbols.close,
|
||||
key: ValueKey('close'),
|
||||
)
|
||||
: const Icon(Symbols.add, key: ValueKey('add')),
|
||||
),
|
||||
onPressed: () {
|
||||
isExpanded.value = !isExpanded.value;
|
||||
@@ -885,46 +857,43 @@ class ChatInput extends HookConsumerWidget {
|
||||
hintMaxLines: 1,
|
||||
hintText:
|
||||
(chatRoom.type == 1 && chatRoom.name == null)
|
||||
? 'chatDirectMessageHint'.tr(
|
||||
args: [
|
||||
getValidMembers(
|
||||
chatRoom.members!,
|
||||
).map((e) => e.account.nick).join(', '),
|
||||
],
|
||||
)
|
||||
: 'chatMessageHint'.tr(
|
||||
args: [chatRoom.name!],
|
||||
),
|
||||
? 'chatDirectMessageHint'.tr(
|
||||
args: [
|
||||
getValidMembers(
|
||||
chatRoom.members!,
|
||||
).map((e) => e.account.nick).join(', '),
|
||||
],
|
||||
)
|
||||
: 'chatMessageHint'.tr(args: [chatRoom.name!]),
|
||||
border: InputBorder.none,
|
||||
isDense: true,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 12,
|
||||
),
|
||||
counterText:
|
||||
messageController.text.length > 1024
|
||||
? '${messageController.text.length}/4096'
|
||||
: null,
|
||||
counterText: messageController.text.length > 1024
|
||||
? '${messageController.text.length}/4096'
|
||||
: null,
|
||||
),
|
||||
maxLines: 5,
|
||||
minLines: 1,
|
||||
onTapOutside:
|
||||
(_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
textInputAction:
|
||||
settings.enterToSend
|
||||
? TextInputAction.send
|
||||
: null,
|
||||
onSubmitted:
|
||||
settings.enterToSend ? (_) => send() : null,
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
textInputAction: settings.enterToSend
|
||||
? TextInputAction.send
|
||||
: null,
|
||||
onSubmitted: settings.enterToSend
|
||||
? (_) => send()
|
||||
: null,
|
||||
);
|
||||
},
|
||||
suggestionsCallback: (pattern) async {
|
||||
// Only trigger on @ or :
|
||||
final atIndex = pattern.lastIndexOf('@');
|
||||
final colonIndex = pattern.lastIndexOf(':');
|
||||
final triggerIndex =
|
||||
atIndex > colonIndex ? atIndex : colonIndex;
|
||||
final triggerIndex = atIndex > colonIndex
|
||||
? atIndex
|
||||
: colonIndex;
|
||||
if (triggerIndex == -1) return [];
|
||||
final chopped = pattern.substring(triggerIndex);
|
||||
if (chopped.contains(' ')) return [];
|
||||
@@ -986,9 +955,7 @@ class ChatInput extends HookConsumerWidget {
|
||||
child: SizedBox(
|
||||
width: 28,
|
||||
height: 28,
|
||||
child: CloudImageWidget(
|
||||
fileId: sticker.image.id,
|
||||
),
|
||||
child: CloudImageWidget(file: sticker.image),
|
||||
),
|
||||
);
|
||||
break;
|
||||
@@ -1005,8 +972,9 @@ class ChatInput extends HookConsumerWidget {
|
||||
final text = messageController.text;
|
||||
final atIndex = text.lastIndexOf('@');
|
||||
final colonIndex = text.lastIndexOf(':');
|
||||
final triggerIndex =
|
||||
atIndex > colonIndex ? atIndex : colonIndex;
|
||||
final triggerIndex = atIndex > colonIndex
|
||||
? atIndex
|
||||
: colonIndex;
|
||||
if (triggerIndex == -1) return;
|
||||
final newText = text.replaceRange(
|
||||
triggerIndex,
|
||||
@@ -1053,16 +1021,15 @@ class ChatInput extends HookConsumerWidget {
|
||||
),
|
||||
);
|
||||
},
|
||||
child:
|
||||
isExpanded.value
|
||||
? _ExpandedSection(
|
||||
messageController: messageController,
|
||||
selectedPoll: selectedPoll,
|
||||
onPollSelected: onPollSelected,
|
||||
selectedFund: selectedFund,
|
||||
onFundSelected: onFundSelected,
|
||||
)
|
||||
: const SizedBox.shrink(key: ValueKey('collapsed')),
|
||||
child: isExpanded.value
|
||||
? _ExpandedSection(
|
||||
messageController: messageController,
|
||||
selectedPoll: selectedPoll,
|
||||
onPollSelected: onPollSelected,
|
||||
selectedFund: selectedFund,
|
||||
onFundSelected: onFundSelected,
|
||||
)
|
||||
: const SizedBox.shrink(key: ValueKey('collapsed')),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -577,6 +577,9 @@ class ProfilePictureWidget extends ConsumerWidget {
|
||||
final serverUrl = ref.watch(serverUrlProvider);
|
||||
final String? id = file?.id ?? fileId;
|
||||
|
||||
final meta = file?.fileMeta is Map ? (file!.fileMeta as Map) : const {};
|
||||
final blurHash = meta['blur'] as String?;
|
||||
|
||||
final fallback = Icon(
|
||||
fallbackIcon ?? Symbols.account_circle,
|
||||
size: radius,
|
||||
@@ -590,6 +593,7 @@ class ProfilePictureWidget extends ConsumerWidget {
|
||||
placeholder: fallback,
|
||||
content: () => UniversalImage(
|
||||
uri: '$serverUrl/drive/files/$id',
|
||||
blurHash: blurHash,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
);
|
||||
@@ -625,14 +629,14 @@ class ProfilePictureWidget extends ConsumerWidget {
|
||||
}
|
||||
|
||||
class SplitAvatarWidget extends ConsumerWidget {
|
||||
final List<String?> filesId;
|
||||
final List<SnCloudFile?> files;
|
||||
final double radius;
|
||||
final IconData fallbackIcon;
|
||||
final Color? fallbackColor;
|
||||
|
||||
const SplitAvatarWidget({
|
||||
super.key,
|
||||
required this.filesId,
|
||||
required this.files,
|
||||
this.radius = 20,
|
||||
this.fallbackIcon = Symbols.account_circle,
|
||||
this.fallbackColor,
|
||||
@@ -640,17 +644,17 @@ class SplitAvatarWidget extends ConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
if (filesId.isEmpty) {
|
||||
if (files.isEmpty) {
|
||||
return ProfilePictureWidget(
|
||||
fileId: null,
|
||||
file: null,
|
||||
radius: radius,
|
||||
fallbackIcon: fallbackIcon,
|
||||
fallbackColor: fallbackColor,
|
||||
);
|
||||
}
|
||||
if (filesId.length == 1) {
|
||||
if (files.length == 1) {
|
||||
return ProfilePictureWidget(
|
||||
fileId: filesId[0],
|
||||
file: files[0],
|
||||
radius: radius,
|
||||
fallbackIcon: fallbackIcon,
|
||||
fallbackColor: fallbackColor,
|
||||
@@ -665,32 +669,32 @@ class SplitAvatarWidget extends ConsumerWidget {
|
||||
color: Theme.of(context).colorScheme.primaryContainer,
|
||||
child: Stack(
|
||||
children: [
|
||||
if (filesId.length == 2)
|
||||
if (files.length == 2)
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildQuadrant(context, filesId[0], ref, radius),
|
||||
child: _buildQuadrant(context, files[0], ref, radius),
|
||||
),
|
||||
Expanded(
|
||||
child: _buildQuadrant(context, filesId[1], ref, radius),
|
||||
child: _buildQuadrant(context, files[1], ref, radius),
|
||||
),
|
||||
],
|
||||
)
|
||||
else if (filesId.length == 3)
|
||||
else if (files.length == 3)
|
||||
Row(
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildQuadrant(context, filesId[0], ref, radius),
|
||||
child: _buildQuadrant(context, files[0], ref, radius),
|
||||
),
|
||||
Expanded(
|
||||
child: _buildQuadrant(context, filesId[1], ref, radius),
|
||||
child: _buildQuadrant(context, files[1], ref, radius),
|
||||
),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
child: _buildQuadrant(context, filesId[2], ref, radius),
|
||||
child: _buildQuadrant(context, files[2], ref, radius),
|
||||
),
|
||||
],
|
||||
)
|
||||
@@ -701,20 +705,10 @@ class SplitAvatarWidget extends ConsumerWidget {
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildQuadrant(
|
||||
context,
|
||||
filesId[0],
|
||||
ref,
|
||||
radius,
|
||||
),
|
||||
child: _buildQuadrant(context, files[0], ref, radius),
|
||||
),
|
||||
Expanded(
|
||||
child: _buildQuadrant(
|
||||
context,
|
||||
filesId[1],
|
||||
ref,
|
||||
radius,
|
||||
),
|
||||
child: _buildQuadrant(context, files[1], ref, radius),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -723,22 +717,17 @@ class SplitAvatarWidget extends ConsumerWidget {
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildQuadrant(
|
||||
context,
|
||||
filesId[2],
|
||||
ref,
|
||||
radius,
|
||||
),
|
||||
child: _buildQuadrant(context, files[2], ref, radius),
|
||||
),
|
||||
Expanded(
|
||||
child: filesId.length > 4
|
||||
child: files.length > 4
|
||||
? Container(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.primaryContainer,
|
||||
child: Center(
|
||||
child: Text(
|
||||
'+${filesId.length - 3}',
|
||||
'+${files.length - 3}',
|
||||
style: TextStyle(
|
||||
fontSize: radius * 0.4,
|
||||
color: Theme.of(
|
||||
@@ -748,12 +737,7 @@ class SplitAvatarWidget extends ConsumerWidget {
|
||||
),
|
||||
),
|
||||
)
|
||||
: _buildQuadrant(
|
||||
context,
|
||||
filesId[3],
|
||||
ref,
|
||||
radius,
|
||||
),
|
||||
: _buildQuadrant(context, files[3], ref, radius),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -768,11 +752,11 @@ class SplitAvatarWidget extends ConsumerWidget {
|
||||
|
||||
Widget _buildQuadrant(
|
||||
BuildContext context,
|
||||
String? fileId,
|
||||
SnCloudFile? file,
|
||||
WidgetRef ref,
|
||||
double radius,
|
||||
) {
|
||||
if (fileId == null) {
|
||||
if (file == null) {
|
||||
return Container(
|
||||
width: radius,
|
||||
height: radius,
|
||||
@@ -787,7 +771,7 @@ class SplitAvatarWidget extends ConsumerWidget {
|
||||
}
|
||||
|
||||
final serverUrl = ref.watch(serverUrlProvider);
|
||||
final uri = '$serverUrl/drive/files/$fileId';
|
||||
final uri = '$serverUrl/drive/files/${file.id}';
|
||||
|
||||
return SizedBox(
|
||||
width: radius,
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_blurhash/flutter_blurhash.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
|
||||
class UniversalImage extends StatelessWidget {
|
||||
class UniversalImage extends HookWidget {
|
||||
final String uri;
|
||||
final String? blurHash;
|
||||
final BoxFit fit;
|
||||
@@ -27,6 +28,7 @@ class UniversalImage extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final loaded = useState(false);
|
||||
final isSvgImage = isSvg || uri.toLowerCase().endsWith('.svg');
|
||||
|
||||
if (isSvgImage) {
|
||||
@@ -35,9 +37,8 @@ class UniversalImage extends StatelessWidget {
|
||||
fit: fit,
|
||||
width: width,
|
||||
height: height,
|
||||
placeholderBuilder:
|
||||
(BuildContext context) =>
|
||||
Center(child: CircularProgressIndicator()),
|
||||
placeholderBuilder: (BuildContext context) =>
|
||||
Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -46,8 +47,9 @@ class UniversalImage extends StatelessWidget {
|
||||
if (width != null && height != null && !noCacheOptimization) {
|
||||
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
||||
cacheWidth = width != null ? (width! * devicePixelRatio).round() : null;
|
||||
cacheHeight =
|
||||
height != null ? (height! * devicePixelRatio).round() : null;
|
||||
cacheHeight = height != null
|
||||
? (height! * devicePixelRatio).round()
|
||||
: null;
|
||||
}
|
||||
|
||||
return SizedBox(
|
||||
@@ -66,21 +68,72 @@ class UniversalImage extends StatelessWidget {
|
||||
memCacheWidth: cacheWidth,
|
||||
progressIndicatorBuilder: (context, url, progress) {
|
||||
return Center(
|
||||
child: CircularProgressIndicator(value: progress.progress),
|
||||
child: AnimatedCircularProgressIndicator(
|
||||
value: progress.progress,
|
||||
color: Colors.white.withOpacity(0.5),
|
||||
),
|
||||
);
|
||||
},
|
||||
errorWidget:
|
||||
(context, url, error) =>
|
||||
useFallbackImage
|
||||
? Image.asset(
|
||||
'assets/images/media-offline.jpg',
|
||||
fit: BoxFit.cover,
|
||||
key: Key('image-broke-$uri'),
|
||||
)
|
||||
: SizedBox.shrink(),
|
||||
imageBuilder: (context, imageProvider) {
|
||||
Future(() => loaded.value = true);
|
||||
return AnimatedOpacity(
|
||||
opacity: loaded.value ? 1.0 : 0.0,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: Image(
|
||||
image: imageProvider,
|
||||
fit: fit,
|
||||
width: width,
|
||||
height: height,
|
||||
),
|
||||
);
|
||||
},
|
||||
errorWidget: (context, url, error) => useFallbackImage
|
||||
? Image.asset(
|
||||
'assets/images/media-offline.jpg',
|
||||
fit: BoxFit.cover,
|
||||
key: Key('image-broke-$uri'),
|
||||
)
|
||||
: SizedBox.shrink(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AnimatedCircularProgressIndicator extends HookWidget {
|
||||
final double? value;
|
||||
final Color? color;
|
||||
final double strokeWidth;
|
||||
final Duration duration;
|
||||
|
||||
const AnimatedCircularProgressIndicator({
|
||||
super.key,
|
||||
this.value,
|
||||
this.color,
|
||||
this.strokeWidth = 4.0,
|
||||
this.duration = const Duration(milliseconds: 200),
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final animationController = useAnimationController(duration: duration);
|
||||
final animation = useAnimation(
|
||||
Tween<double>(begin: 0.0, end: value ?? 0.0).animate(
|
||||
CurvedAnimation(parent: animationController, curve: Curves.linear),
|
||||
),
|
||||
);
|
||||
|
||||
useEffect(() {
|
||||
animationController.animateTo(value ?? 0.0);
|
||||
return null;
|
||||
}, [value]);
|
||||
|
||||
return CircularProgressIndicator(
|
||||
value: animation,
|
||||
color: color,
|
||||
strokeWidth: strokeWidth,
|
||||
backgroundColor: Colors.transparent,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -54,7 +54,7 @@ class PostAwardSheet extends HookConsumerWidget {
|
||||
}
|
||||
}
|
||||
// Fallback
|
||||
return ProfilePictureWidget(fileId: null, radius: radius);
|
||||
return ProfilePictureWidget(file: null, radius: radius);
|
||||
}
|
||||
|
||||
String _getPublisherName() {
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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'),
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -49,7 +49,7 @@ class RealmSelectionDropdown extends StatelessWidget {
|
||||
child: Row(
|
||||
children: [
|
||||
ProfilePictureWidget(
|
||||
fileId: realm.picture?.id,
|
||||
file: realm.picture,
|
||||
fallbackIcon: Symbols.workspaces,
|
||||
radius: 16,
|
||||
),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -75,31 +75,29 @@ class StickerPicker extends HookConsumerWidget {
|
||||
},
|
||||
);
|
||||
},
|
||||
loading:
|
||||
() => const SizedBox(
|
||||
width: 320,
|
||||
height: 320,
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
error:
|
||||
(err, _) => SizedBox(
|
||||
width: 360,
|
||||
height: 200,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Symbols.error, size: 28),
|
||||
const Gap(8),
|
||||
Text('Error: $err', textAlign: TextAlign.center),
|
||||
const Gap(12),
|
||||
FilledButton.icon(
|
||||
onPressed: () => ref.invalidate(myStickerPacksProvider),
|
||||
icon: const Icon(Symbols.refresh),
|
||||
label: Text('retry').tr(),
|
||||
),
|
||||
],
|
||||
).padding(all: 16),
|
||||
),
|
||||
loading: () => const SizedBox(
|
||||
width: 320,
|
||||
height: 320,
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
error: (err, _) => SizedBox(
|
||||
width: 360,
|
||||
height: 200,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Symbols.error, size: 28),
|
||||
const Gap(8),
|
||||
Text('Error: $err', textAlign: TextAlign.center),
|
||||
const Gap(12),
|
||||
FilledButton.icon(
|
||||
onPressed: () => ref.invalidate(myStickerPacksProvider),
|
||||
icon: const Icon(Symbols.refresh),
|
||||
label: Text('retry').tr(),
|
||||
),
|
||||
],
|
||||
).padding(all: 16),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -263,7 +261,7 @@ class _StickersGrid extends StatelessWidget {
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: CloudImageWidget(
|
||||
fileId: sticker.image.id,
|
||||
file: sticker.image,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
@@ -310,31 +308,29 @@ class StickerPickerEmbedded extends HookConsumerWidget {
|
||||
},
|
||||
);
|
||||
},
|
||||
loading:
|
||||
() => SizedBox(
|
||||
width: 320,
|
||||
height: height ?? 320,
|
||||
child: const Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
error:
|
||||
(err, _) => SizedBox(
|
||||
width: 360,
|
||||
height: height ?? 200,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Symbols.error, size: 28),
|
||||
const Gap(8),
|
||||
Text('Error: $err', textAlign: TextAlign.center),
|
||||
const Gap(12),
|
||||
FilledButton.icon(
|
||||
onPressed: () => ref.invalidate(myStickerPacksProvider),
|
||||
icon: const Icon(Symbols.refresh),
|
||||
label: Text('retry').tr(),
|
||||
),
|
||||
],
|
||||
).padding(all: 16),
|
||||
),
|
||||
loading: () => SizedBox(
|
||||
width: 320,
|
||||
height: height ?? 320,
|
||||
child: const Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
error: (err, _) => SizedBox(
|
||||
width: 360,
|
||||
height: height ?? 200,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Symbols.error, size: 28),
|
||||
const Gap(8),
|
||||
Text('Error: $err', textAlign: TextAlign.center),
|
||||
const Gap(12),
|
||||
FilledButton.icon(
|
||||
onPressed: () => ref.invalidate(myStickerPacksProvider),
|
||||
icon: const Icon(Symbols.refresh),
|
||||
label: Text('retry').tr(),
|
||||
),
|
||||
],
|
||||
).padding(all: 16),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -386,18 +382,16 @@ class _EmbeddedPackSwitcherState extends State<_EmbeddedPackSwitcher> {
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeInOut,
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
selected
|
||||
? Theme.of(context).colorScheme.primaryContainer
|
||||
: Theme.of(context).colorScheme.surfaceContainer,
|
||||
color: selected
|
||||
? Theme.of(context).colorScheme.primaryContainer
|
||||
: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
border:
|
||||
selected
|
||||
? Border.all(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
width: 4,
|
||||
)
|
||||
: null,
|
||||
border: selected
|
||||
? Border.all(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
width: 4,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
margin: const EdgeInsets.only(right: 8),
|
||||
child: InkWell(
|
||||
@@ -413,11 +407,11 @@ class _EmbeddedPackSwitcherState extends State<_EmbeddedPackSwitcher> {
|
||||
builder: (context, value, _) {
|
||||
return packs[i].icon != null
|
||||
? CloudImageWidget(
|
||||
file: packs[i].icon!,
|
||||
).clipRRect(all: value)
|
||||
file: packs[i].icon!,
|
||||
).clipRRect(all: value)
|
||||
: CloudImageWidget(
|
||||
file: packs[i].stickers.firstOrNull?.image,
|
||||
).clipRRect(all: value);
|
||||
file: packs[i].stickers.firstOrNull?.image,
|
||||
).clipRRect(all: value);
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -458,18 +452,17 @@ Future<void> showStickerPickerPopover(
|
||||
offset: offset,
|
||||
alignment: alignment ?? Alignment.topLeft,
|
||||
dimBackground: true,
|
||||
builder:
|
||||
(ctx) => SizedBox(
|
||||
width: math.min(480, MediaQuery.of(context).size.width * 0.9),
|
||||
height: 480,
|
||||
child: ProviderScope(
|
||||
child: StickerPicker(
|
||||
onPick: (ph) {
|
||||
onPick(ph);
|
||||
Navigator.of(ctx).maybePop();
|
||||
},
|
||||
),
|
||||
),
|
||||
builder: (ctx) => SizedBox(
|
||||
width: math.min(480, MediaQuery.of(context).size.width * 0.9),
|
||||
height: 480,
|
||||
child: ProviderScope(
|
||||
child: StickerPicker(
|
||||
onPick: (ph) {
|
||||
onPick(ph);
|
||||
Navigator.of(ctx).maybePop();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user