🎨 Optimized code of post compose
This commit is contained in:
parent
52111c4b95
commit
ab4f4faafe
@ -63,10 +63,14 @@ class PostComposeScreen extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final publishers = ref.watch(publishersManagedProvider);
|
// Extract common theme and localization to avoid repeated lookups
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final colorScheme = theme.colorScheme;
|
||||||
|
|
||||||
|
final publishers = ref.watch(publishersManagedProvider);
|
||||||
final currentPublisher = useState<SnPublisher?>(null);
|
final currentPublisher = useState<SnPublisher?>(null);
|
||||||
|
|
||||||
|
// Initialize publisher once when data is available
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
if (publishers.value?.isNotEmpty ?? false) {
|
if (publishers.value?.isNotEmpty ?? false) {
|
||||||
currentPublisher.value = publishers.value!.first;
|
currentPublisher.value = publishers.value!.first;
|
||||||
@ -74,7 +78,7 @@ class PostComposeScreen extends HookConsumerWidget {
|
|||||||
return null;
|
return null;
|
||||||
}, [publishers]);
|
}, [publishers]);
|
||||||
|
|
||||||
// Contains the XFile, ByteData, or SnCloudFile
|
// State management
|
||||||
final attachments = useState<List<UniversalFile>>(
|
final attachments = useState<List<UniversalFile>>(
|
||||||
originalPost?.attachments
|
originalPost?.attachments
|
||||||
.map(
|
.map(
|
||||||
@ -100,12 +104,11 @@ class PostComposeScreen extends HookConsumerWidget {
|
|||||||
originalPost?.content ??
|
originalPost?.content ??
|
||||||
(forwardedPost != null ? '> ${forwardedPost!.content}\n\n' : null),
|
(forwardedPost != null ? '> ${forwardedPost!.content}\n\n' : null),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Add visibility state with default value from original post or 0 (public)
|
|
||||||
final visibility = useState<int>(originalPost?.visibility ?? 0);
|
final visibility = useState<int>(originalPost?.visibility ?? 0);
|
||||||
|
|
||||||
final submitting = useState(false);
|
final submitting = useState(false);
|
||||||
|
final attachmentProgress = useState<Map<int, double>>({});
|
||||||
|
|
||||||
|
// Media handling functions
|
||||||
Future<void> pickPhotoMedia() async {
|
Future<void> pickPhotoMedia() async {
|
||||||
final result = await ref
|
final result = await ref
|
||||||
.watch(imagePickerProvider)
|
.watch(imagePickerProvider)
|
||||||
@ -130,16 +133,30 @@ class PostComposeScreen extends HookConsumerWidget {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
final attachmentProgress = useState<Map<int, double>>({});
|
// Helper method to get mimetype from file type
|
||||||
|
String getMimeTypeFromFileType(UniversalFileType type) {
|
||||||
|
return switch (type) {
|
||||||
|
UniversalFileType.image => 'image/unknown',
|
||||||
|
UniversalFileType.video => 'video/unknown',
|
||||||
|
UniversalFileType.audio => 'audio/unknown',
|
||||||
|
UniversalFileType.file => 'application/octet-stream',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attachment management functions
|
||||||
Future<void> uploadAttachment(int index) async {
|
Future<void> uploadAttachment(int index) async {
|
||||||
final attachment = attachments.value[index];
|
final attachment = attachments.value[index];
|
||||||
if (attachment is SnCloudFile) return;
|
if (attachment.isOnCloud) return;
|
||||||
|
|
||||||
final baseUrl = ref.watch(serverUrlProvider);
|
final baseUrl = ref.watch(serverUrlProvider);
|
||||||
final token = await getToken(ref.watch(tokenProvider));
|
final token = await getToken(ref.watch(tokenProvider));
|
||||||
if (token == null) throw ArgumentError('Token is null');
|
if (token == null) throw ArgumentError('Token is null');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Update progress state
|
||||||
attachmentProgress.value = {...attachmentProgress.value, index: 0};
|
attachmentProgress.value = {...attachmentProgress.value, index: 0};
|
||||||
|
|
||||||
|
// Upload file to cloud
|
||||||
final cloudFile =
|
final cloudFile =
|
||||||
await putMediaToCloud(
|
await putMediaToCloud(
|
||||||
fileData: attachment,
|
fileData: attachment,
|
||||||
@ -148,32 +165,45 @@ class PostComposeScreen extends HookConsumerWidget {
|
|||||||
filename: attachment.data.name ?? 'Post media',
|
filename: attachment.data.name ?? 'Post media',
|
||||||
mimetype:
|
mimetype:
|
||||||
attachment.data.mimeType ??
|
attachment.data.mimeType ??
|
||||||
switch (attachment.type) {
|
getMimeTypeFromFileType(attachment.type),
|
||||||
UniversalFileType.image => 'image/unknown',
|
onProgress: (progress, _) {
|
||||||
UniversalFileType.video => 'video/unknown',
|
|
||||||
UniversalFileType.audio => 'audio/unknown',
|
|
||||||
UniversalFileType.file => 'application/octet-stream',
|
|
||||||
},
|
|
||||||
onProgress: (progress, estimate) {
|
|
||||||
attachmentProgress.value = {
|
attachmentProgress.value = {
|
||||||
...attachmentProgress.value,
|
...attachmentProgress.value,
|
||||||
index: progress,
|
index: progress,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
).future;
|
).future;
|
||||||
|
|
||||||
if (cloudFile == null) {
|
if (cloudFile == null) {
|
||||||
throw ArgumentError('Failed to upload the file...');
|
throw ArgumentError('Failed to upload the file...');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update attachments list with cloud file
|
||||||
final clone = List.of(attachments.value);
|
final clone = List.of(attachments.value);
|
||||||
clone[index] = UniversalFile(data: cloudFile, type: attachment.type);
|
clone[index] = UniversalFile(data: cloudFile, type: attachment.type);
|
||||||
attachments.value = clone;
|
attachments.value = clone;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showErrorAlert(err);
|
showErrorAlert(err);
|
||||||
} finally {
|
} finally {
|
||||||
attachmentProgress.value = attachmentProgress.value..remove(index);
|
// Clean up progress state
|
||||||
|
attachmentProgress.value = {...attachmentProgress.value}..remove(index);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper method to move attachment in the list
|
||||||
|
List<UniversalFile> moveAttachment(
|
||||||
|
List<UniversalFile> attachments,
|
||||||
|
int idx,
|
||||||
|
int delta,
|
||||||
|
) {
|
||||||
|
if (idx + delta < 0 || idx + delta >= attachments.length) {
|
||||||
|
return attachments;
|
||||||
|
}
|
||||||
|
final clone = List.of(attachments);
|
||||||
|
clone.insert(idx + delta, clone.removeAt(idx));
|
||||||
|
return clone;
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> deleteAttachment(int index) async {
|
Future<void> deleteAttachment(int index) async {
|
||||||
final attachment = attachments.value[index];
|
final attachment = attachments.value[index];
|
||||||
if (attachment.isOnCloud) {
|
if (attachment.isOnCloud) {
|
||||||
@ -185,25 +215,33 @@ class PostComposeScreen extends HookConsumerWidget {
|
|||||||
attachments.value = clone;
|
attachments.value = clone;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Form submission
|
||||||
Future<void> performAction() async {
|
Future<void> performAction() async {
|
||||||
|
if (submitting.value) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
submitting.value = true;
|
submitting.value = true;
|
||||||
|
|
||||||
|
// Upload any local attachments first
|
||||||
await Future.wait(
|
await Future.wait(
|
||||||
attachments.value
|
attachments.value
|
||||||
.where((e) => e.isOnDevice)
|
.asMap()
|
||||||
.mapIndexed((idx, e) => uploadAttachment(idx)),
|
.entries
|
||||||
|
.where((entry) => entry.value.isOnDevice)
|
||||||
|
.map((entry) => uploadAttachment(entry.key)),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Prepare API request
|
||||||
final client = ref.watch(apiClientProvider);
|
final client = ref.watch(apiClientProvider);
|
||||||
await client.request(
|
final isNewPost = originalPost == null;
|
||||||
originalPost == null ? '/posts' : '/posts/${originalPost!.id}',
|
final endpoint = isNewPost ? '/posts' : '/posts/${originalPost!.id}';
|
||||||
data: {
|
|
||||||
|
// Create request payload
|
||||||
|
final payload = {
|
||||||
'title': titleController.text,
|
'title': titleController.text,
|
||||||
'description': descriptionController.text,
|
'description': descriptionController.text,
|
||||||
'content': contentController.text,
|
'content': contentController.text,
|
||||||
'visibility':
|
'visibility': visibility.value,
|
||||||
visibility.value, // Add visibility field to API request
|
|
||||||
'attachments':
|
'attachments':
|
||||||
attachments.value
|
attachments.value
|
||||||
.where((e) => e.isOnCloud)
|
.where((e) => e.isOnCloud)
|
||||||
@ -211,12 +249,18 @@ class PostComposeScreen extends HookConsumerWidget {
|
|||||||
.toList(),
|
.toList(),
|
||||||
if (repliedPost != null) 'replied_post_id': repliedPost!.id,
|
if (repliedPost != null) 'replied_post_id': repliedPost!.id,
|
||||||
if (forwardedPost != null) 'forwarded_post_id': forwardedPost!.id,
|
if (forwardedPost != null) 'forwarded_post_id': forwardedPost!.id,
|
||||||
},
|
};
|
||||||
|
|
||||||
|
// Send request
|
||||||
|
await client.request(
|
||||||
|
endpoint,
|
||||||
|
data: payload,
|
||||||
options: Options(
|
options: Options(
|
||||||
headers: {'X-Pub': currentPublisher.value?.name},
|
headers: {'X-Pub': currentPublisher.value?.name},
|
||||||
method: originalPost == null ? 'POST' : 'PATCH',
|
method: isNewPost ? 'POST' : 'PATCH',
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
context.maybePop(true);
|
context.maybePop(true);
|
||||||
}
|
}
|
||||||
@ -227,6 +271,7 @@ class PostComposeScreen extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clipboard handling
|
||||||
Future<void> handlePaste() async {
|
Future<void> handlePaste() async {
|
||||||
final clipboard = await Pasteboard.image;
|
final clipboard = await Pasteboard.image;
|
||||||
if (clipboard == null) return;
|
if (clipboard == null) return;
|
||||||
@ -245,60 +290,30 @@ class PostComposeScreen extends HookConsumerWidget {
|
|||||||
|
|
||||||
final isPaste = event.logicalKey == LogicalKeyboardKey.keyV;
|
final isPaste = event.logicalKey == LogicalKeyboardKey.keyV;
|
||||||
final isModifierPressed = event.isMetaPressed || event.isControlPressed;
|
final isModifierPressed = event.isMetaPressed || event.isControlPressed;
|
||||||
|
final isSubmit = event.logicalKey == LogicalKeyboardKey.enter;
|
||||||
|
|
||||||
if (isPaste && isModifierPressed) {
|
if (isPaste && isModifierPressed) {
|
||||||
handlePaste();
|
handlePaste();
|
||||||
|
} else if (isSubmit && isModifierPressed && !submitting.value) {
|
||||||
|
performAction();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void showVisibilityModal() {
|
// Helper method to build visibility option
|
||||||
showDialog(
|
Widget buildVisibilityOption(
|
||||||
context: context,
|
BuildContext context,
|
||||||
builder:
|
int value,
|
||||||
(context) => AlertDialog(
|
IconData icon,
|
||||||
title: Text('postVisibility'.tr()),
|
String textKey,
|
||||||
content: Column(
|
) {
|
||||||
mainAxisSize: MainAxisSize.min,
|
return ListTile(
|
||||||
children: [
|
leading: Icon(icon),
|
||||||
ListTile(
|
title: Text(textKey.tr()),
|
||||||
leading: Icon(Symbols.public),
|
|
||||||
title: Text('postVisibilityPublic'.tr()),
|
|
||||||
onTap: () {
|
onTap: () {
|
||||||
visibility.value = 0;
|
visibility.value = value;
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
},
|
},
|
||||||
selected: visibility.value == 0,
|
selected: visibility.value == value,
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
leading: Icon(Symbols.group),
|
|
||||||
title: Text('postVisibilityFriends'.tr()),
|
|
||||||
onTap: () {
|
|
||||||
visibility.value = 1;
|
|
||||||
Navigator.pop(context);
|
|
||||||
},
|
|
||||||
selected: visibility.value == 1,
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
leading: Icon(Symbols.link_off),
|
|
||||||
title: Text('postVisibilityUnlisted'.tr()),
|
|
||||||
onTap: () {
|
|
||||||
visibility.value = 2;
|
|
||||||
Navigator.pop(context);
|
|
||||||
},
|
|
||||||
selected: visibility.value == 2,
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
leading: Icon(Symbols.lock),
|
|
||||||
title: Text('postVisibilityPrivate'.tr()),
|
|
||||||
onTap: () {
|
|
||||||
visibility.value = 3;
|
|
||||||
Navigator.pop(context);
|
|
||||||
},
|
|
||||||
selected: visibility.value == 3,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -330,20 +345,48 @@ class PostComposeScreen extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return AppScaffold(
|
// Visibility handling
|
||||||
appBar: AppBar(
|
void showVisibilityModal() {
|
||||||
leading: const PageBackButton(),
|
showDialog(
|
||||||
title:
|
context: context,
|
||||||
isWideScreen(context)
|
builder:
|
||||||
? Text(originalPost != null ? 'editPost'.tr() : 'newPost'.tr())
|
(context) => AlertDialog(
|
||||||
: null,
|
title: Text('postVisibility'.tr()),
|
||||||
actions: [
|
content: Column(
|
||||||
if (isWideScreen(context))
|
mainAxisSize: MainAxisSize.min,
|
||||||
Tooltip(
|
children: [
|
||||||
message: 'keyboard_shortcuts'.tr(),
|
buildVisibilityOption(
|
||||||
child: IconButton(
|
context,
|
||||||
icon: const Icon(Symbols.keyboard),
|
0,
|
||||||
onPressed: () {
|
Symbols.public,
|
||||||
|
'postVisibilityPublic',
|
||||||
|
),
|
||||||
|
buildVisibilityOption(
|
||||||
|
context,
|
||||||
|
1,
|
||||||
|
Symbols.group,
|
||||||
|
'postVisibilityFriends',
|
||||||
|
),
|
||||||
|
buildVisibilityOption(
|
||||||
|
context,
|
||||||
|
2,
|
||||||
|
Symbols.link_off,
|
||||||
|
'postVisibilityUnlisted',
|
||||||
|
),
|
||||||
|
buildVisibilityOption(
|
||||||
|
context,
|
||||||
|
3,
|
||||||
|
Symbols.lock,
|
||||||
|
'postVisibilityPrivate',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show keyboard shortcuts dialog
|
||||||
|
void showKeyboardShortcutsDialog() {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder:
|
builder:
|
||||||
@ -367,7 +410,69 @@ class PostComposeScreen extends HookConsumerWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
|
|
||||||
|
// Helper method to build wide attachment grid
|
||||||
|
Widget buildWideAttachmentGrid(
|
||||||
|
BoxConstraints constraints,
|
||||||
|
List<UniversalFile> attachments,
|
||||||
|
Map<int, double> progress,
|
||||||
|
) {
|
||||||
|
return Wrap(
|
||||||
|
spacing: 8,
|
||||||
|
runSpacing: 8,
|
||||||
|
children: [
|
||||||
|
for (var idx = 0; idx < attachments.length; idx++)
|
||||||
|
SizedBox(
|
||||||
|
width: constraints.maxWidth / 2 - 4,
|
||||||
|
child: AttachmentPreview(
|
||||||
|
item: attachments[idx],
|
||||||
|
progress: progress[idx],
|
||||||
|
onRequestUpload: () => uploadAttachment(idx),
|
||||||
|
onDelete: () => deleteAttachment(idx),
|
||||||
|
onMove: (delta) => moveAttachment(attachments, idx, delta),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper method to build narrow attachment list
|
||||||
|
Widget buildNarrowAttachmentList(
|
||||||
|
List<UniversalFile> attachments,
|
||||||
|
Map<int, double> progress,
|
||||||
|
) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
spacing: 8,
|
||||||
|
children: [
|
||||||
|
for (var idx = 0; idx < attachments.length; idx++)
|
||||||
|
AttachmentPreview(
|
||||||
|
item: attachments[idx],
|
||||||
|
progress: progress[idx],
|
||||||
|
onRequestUpload: () => uploadAttachment(idx),
|
||||||
|
onDelete: () => deleteAttachment(idx),
|
||||||
|
onMove: (delta) => moveAttachment(attachments, idx, delta),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build UI
|
||||||
|
return AppScaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
leading: const PageBackButton(),
|
||||||
|
title:
|
||||||
|
isWideScreen(context)
|
||||||
|
? Text(originalPost != null ? 'editPost'.tr() : 'newPost'.tr())
|
||||||
|
: null,
|
||||||
|
actions: [
|
||||||
|
if (isWideScreen(context))
|
||||||
|
Tooltip(
|
||||||
|
message: 'keyboard_shortcuts'.tr(),
|
||||||
|
child: IconButton(
|
||||||
|
icon: const Icon(Symbols.keyboard),
|
||||||
|
onPressed: showKeyboardShortcutsDialog,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
@ -382,9 +487,9 @@ class PostComposeScreen extends HookConsumerWidget {
|
|||||||
strokeWidth: 2.5,
|
strokeWidth: 2.5,
|
||||||
),
|
),
|
||||||
).center()
|
).center()
|
||||||
: originalPost != null
|
: Icon(
|
||||||
? const Icon(Symbols.edit)
|
originalPost != null ? Symbols.edit : Symbols.upload,
|
||||||
: const Icon(Symbols.upload),
|
),
|
||||||
),
|
),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
],
|
],
|
||||||
@ -392,53 +497,29 @@ class PostComposeScreen extends HookConsumerWidget {
|
|||||||
body: Column(
|
body: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
// Reply/Forward info section
|
||||||
if (repliedPost != null)
|
if (repliedPost != null)
|
||||||
Container(
|
_buildInfoBanner(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
||||||
color: Theme.of(
|
|
||||||
context,
|
context,
|
||||||
).colorScheme.surfaceVariant.withOpacity(0.5),
|
Symbols.reply,
|
||||||
child: Row(
|
'reply',
|
||||||
children: [
|
repliedPost!.publisher.nick,
|
||||||
const Icon(Symbols.reply, size: 16),
|
|
||||||
const Gap(8),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
'${'reply'.tr()}: ${repliedPost!.publisher.nick}',
|
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
if (forwardedPost != null)
|
if (forwardedPost != null)
|
||||||
Container(
|
_buildInfoBanner(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
||||||
color: Theme.of(
|
|
||||||
context,
|
context,
|
||||||
).colorScheme.surfaceVariant.withOpacity(0.5),
|
Symbols.forward,
|
||||||
child: Row(
|
'forward',
|
||||||
children: [
|
forwardedPost!.publisher.nick,
|
||||||
const Icon(Symbols.forward, size: 16),
|
|
||||||
const Gap(8),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
'${'forward'.tr()}: ${forwardedPost!.publisher.nick}',
|
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
|
||||||
|
// Main content area
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Row(
|
child: Row(
|
||||||
spacing: 12,
|
spacing: 12,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
// Publisher profile picture
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
child: ProfilePictureWidget(
|
child: ProfilePictureWidget(
|
||||||
fileId: currentPublisher.value?.picture?.id,
|
fileId: currentPublisher.value?.picture?.id,
|
||||||
@ -458,28 +539,29 @@ class PostComposeScreen extends HookConsumerWidget {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
).padding(top: 16),
|
).padding(top: 16),
|
||||||
|
|
||||||
|
// Post content form
|
||||||
Expanded(
|
Expanded(
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
padding: EdgeInsets.symmetric(vertical: 16),
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
// Visibility selector
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
onPressed: () {
|
onPressed: showVisibilityModal,
|
||||||
showVisibilityModal();
|
|
||||||
},
|
|
||||||
style: OutlinedButton.styleFrom(
|
style: OutlinedButton.styleFrom(
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
),
|
),
|
||||||
side: BorderSide(
|
side: BorderSide(
|
||||||
color: Theme.of(
|
color: colorScheme.primary.withOpacity(0.5),
|
||||||
context,
|
),
|
||||||
).colorScheme.primary.withOpacity(0.5),
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
),
|
),
|
||||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
|
||||||
visualDensity: const VisualDensity(
|
visualDensity: const VisualDensity(
|
||||||
vertical: -2,
|
vertical: -2,
|
||||||
horizontal: -4,
|
horizontal: -4,
|
||||||
@ -491,16 +573,14 @@ class PostComposeScreen extends HookConsumerWidget {
|
|||||||
Icon(
|
Icon(
|
||||||
getVisibilityIcon(visibility.value),
|
getVisibilityIcon(visibility.value),
|
||||||
size: 16,
|
size: 16,
|
||||||
color:
|
color: colorScheme.primary,
|
||||||
Theme.of(context).colorScheme.primary,
|
|
||||||
),
|
),
|
||||||
const SizedBox(width: 6),
|
const SizedBox(width: 6),
|
||||||
Text(
|
Text(
|
||||||
getVisibilityText(visibility.value).tr(),
|
getVisibilityText(visibility.value).tr(),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color:
|
color: colorScheme.primary,
|
||||||
Theme.of(context).colorScheme.primary,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -508,33 +588,40 @@ class PostComposeScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
).padding(bottom: 6),
|
).padding(bottom: 6),
|
||||||
|
|
||||||
|
// Title field
|
||||||
TextField(
|
TextField(
|
||||||
controller: titleController,
|
controller: titleController,
|
||||||
decoration: InputDecoration.collapsed(
|
decoration: InputDecoration.collapsed(
|
||||||
hintText: 'postTitle'.tr(),
|
hintText: 'postTitle'.tr(),
|
||||||
),
|
),
|
||||||
style: TextStyle(fontSize: 16),
|
style: const TextStyle(fontSize: 16),
|
||||||
onTapOutside:
|
onTapOutside:
|
||||||
(_) =>
|
(_) =>
|
||||||
FocusManager.instance.primaryFocus?.unfocus(),
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
// Description field
|
||||||
TextField(
|
TextField(
|
||||||
controller: descriptionController,
|
controller: descriptionController,
|
||||||
decoration: InputDecoration.collapsed(
|
decoration: InputDecoration.collapsed(
|
||||||
hintText: 'postDescription'.tr(),
|
hintText: 'postDescription'.tr(),
|
||||||
),
|
),
|
||||||
style: TextStyle(fontSize: 16),
|
style: const TextStyle(fontSize: 16),
|
||||||
onTapOutside:
|
onTapOutside:
|
||||||
(_) =>
|
(_) =>
|
||||||
FocusManager.instance.primaryFocus?.unfocus(),
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
),
|
),
|
||||||
|
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
|
|
||||||
|
// Content field with keyboard listener
|
||||||
RawKeyboardListener(
|
RawKeyboardListener(
|
||||||
focusNode: FocusNode(),
|
focusNode: FocusNode(),
|
||||||
onKey: handleKeyPress,
|
onKey: handleKeyPress,
|
||||||
child: TextField(
|
child: TextField(
|
||||||
controller: contentController,
|
controller: contentController,
|
||||||
style: TextStyle(fontSize: 14),
|
style: const TextStyle(fontSize: 14),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
border: InputBorder.none,
|
border: InputBorder.none,
|
||||||
hintText: 'postPlaceholder'.tr(),
|
hintText: 'postPlaceholder'.tr(),
|
||||||
@ -547,80 +634,22 @@ class PostComposeScreen extends HookConsumerWidget {
|
|||||||
?.unfocus(),
|
?.unfocus(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
|
|
||||||
|
// Attachments preview
|
||||||
LayoutBuilder(
|
LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
final isWide = isWideScreen(context);
|
final isWide = isWideScreen(context);
|
||||||
return isWide
|
return isWide
|
||||||
? Wrap(
|
? buildWideAttachmentGrid(
|
||||||
spacing: 8,
|
constraints,
|
||||||
runSpacing: 8,
|
|
||||||
children: [
|
|
||||||
for (
|
|
||||||
var idx = 0;
|
|
||||||
idx < attachments.value.length;
|
|
||||||
idx++
|
|
||||||
)
|
|
||||||
SizedBox(
|
|
||||||
width: constraints.maxWidth / 2 - 4,
|
|
||||||
child: AttachmentPreview(
|
|
||||||
item: attachments.value[idx],
|
|
||||||
progress:
|
|
||||||
attachmentProgress.value[idx],
|
|
||||||
onRequestUpload:
|
|
||||||
() => uploadAttachment(idx),
|
|
||||||
onDelete: () => deleteAttachment(idx),
|
|
||||||
onMove: (delta) {
|
|
||||||
if (idx + delta < 0 ||
|
|
||||||
idx + delta >=
|
|
||||||
attachments.value.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final clone = List.of(
|
|
||||||
attachments.value,
|
attachments.value,
|
||||||
);
|
attachmentProgress.value,
|
||||||
clone.insert(
|
|
||||||
idx + delta,
|
|
||||||
clone.removeAt(idx),
|
|
||||||
);
|
|
||||||
attachments.value = clone;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
)
|
||||||
: Column(
|
: buildNarrowAttachmentList(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
spacing: 8,
|
|
||||||
children: [
|
|
||||||
for (
|
|
||||||
var idx = 0;
|
|
||||||
idx < attachments.value.length;
|
|
||||||
idx++
|
|
||||||
)
|
|
||||||
AttachmentPreview(
|
|
||||||
item: attachments.value[idx],
|
|
||||||
progress: attachmentProgress.value[idx],
|
|
||||||
onRequestUpload:
|
|
||||||
() => uploadAttachment(idx),
|
|
||||||
onDelete: () => deleteAttachment(idx),
|
|
||||||
onMove: (delta) {
|
|
||||||
if (idx + delta < 0 ||
|
|
||||||
idx + delta >=
|
|
||||||
attachments.value.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final clone = List.of(
|
|
||||||
attachments.value,
|
attachments.value,
|
||||||
);
|
attachmentProgress.value,
|
||||||
clone.insert(
|
|
||||||
idx + delta,
|
|
||||||
clone.removeAt(idx),
|
|
||||||
);
|
|
||||||
attachments.value = clone;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -631,6 +660,8 @@ class PostComposeScreen extends HookConsumerWidget {
|
|||||||
],
|
],
|
||||||
).padding(horizontal: 16),
|
).padding(horizontal: 16),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
// Bottom toolbar
|
||||||
Material(
|
Material(
|
||||||
elevation: 4,
|
elevation: 4,
|
||||||
child: Row(
|
child: Row(
|
||||||
@ -638,12 +669,12 @@ class PostComposeScreen extends HookConsumerWidget {
|
|||||||
IconButton(
|
IconButton(
|
||||||
onPressed: pickPhotoMedia,
|
onPressed: pickPhotoMedia,
|
||||||
icon: const Icon(Symbols.add_a_photo),
|
icon: const Icon(Symbols.add_a_photo),
|
||||||
color: Theme.of(context).colorScheme.primary,
|
color: colorScheme.primary,
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: pickVideoMedia,
|
onPressed: pickVideoMedia,
|
||||||
icon: const Icon(Symbols.videocam),
|
icon: const Icon(Symbols.videocam),
|
||||||
color: Theme.of(context).colorScheme.primary,
|
color: colorScheme.primary,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
).padding(
|
).padding(
|
||||||
@ -656,4 +687,31 @@ class PostComposeScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper method to build info banner for replied/forwarded posts
|
||||||
|
Widget _buildInfoBanner(
|
||||||
|
BuildContext context,
|
||||||
|
IconData icon,
|
||||||
|
String labelKey,
|
||||||
|
String publisherNick,
|
||||||
|
) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.5),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(icon, size: 16),
|
||||||
|
const Gap(8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'${'labelKey'.tr()}: $publisherNick',
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -53,7 +53,7 @@ enum PostItemType {
|
|||||||
regular,
|
regular,
|
||||||
|
|
||||||
/// Creator view with analytics and metadata
|
/// Creator view with analytics and metadata
|
||||||
creator
|
creator,
|
||||||
}
|
}
|
||||||
|
|
||||||
class SliverPostList extends HookConsumerWidget {
|
class SliverPostList extends HookConsumerWidget {
|
||||||
@ -93,10 +93,7 @@ class SliverPostList extends HookConsumerWidget {
|
|||||||
final post = data.items[index];
|
final post = data.items[index];
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [_buildPostItem(post), const Divider(height: 1)],
|
||||||
_buildPostItem(post),
|
|
||||||
const Divider(height: 1),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -115,7 +112,6 @@ class SliverPostList extends HookConsumerWidget {
|
|||||||
onUpdate: onUpdate,
|
onUpdate: onUpdate,
|
||||||
);
|
);
|
||||||
case PostItemType.regular:
|
case PostItemType.regular:
|
||||||
default:
|
|
||||||
return PostItem(item: post);
|
return PostItem(item: post);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user