✨ Editing article thumbnail
This commit is contained in:
@@ -1592,5 +1592,7 @@
|
||||
"tasksCount": {
|
||||
"one": "{} task",
|
||||
"other": "{} tasks"
|
||||
}
|
||||
},
|
||||
"setAsThumbnail": "Set as thumbnail",
|
||||
"unsetAsThumbnail": "Unset as thumbnail"
|
||||
}
|
||||
|
||||
@@ -13,12 +13,11 @@ import 'package:island/screens/posts/post_detail.dart';
|
||||
import 'package:island/services/compose_storage_db.dart';
|
||||
import 'package:island/services/responsive.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:island/widgets/attachment_uploader.dart';
|
||||
import 'package:island/widgets/content/attachment_preview.dart';
|
||||
import 'package:island/widgets/content/cloud_files.dart';
|
||||
import 'package:island/widgets/content/markdown.dart';
|
||||
import 'package:island/widgets/post/compose_form_fields.dart';
|
||||
import 'package:island/widgets/post/compose_shared.dart';
|
||||
import 'package:island/widgets/post/compose_attachments.dart';
|
||||
import 'package:island/widgets/post/compose_settings_sheet.dart';
|
||||
import 'package:island/widgets/post/compose_toolbar.dart';
|
||||
import 'package:island/widgets/post/publishers_modal.dart';
|
||||
@@ -88,9 +87,6 @@ class ArticleComposeScreen extends HookConsumerWidget {
|
||||
}, [state]);
|
||||
|
||||
final showPreview = useState(false);
|
||||
final isAttachmentsExpanded = useState(
|
||||
true,
|
||||
); // New state for attachments section
|
||||
|
||||
// Initialize publisher once when data is available
|
||||
useEffect(() {
|
||||
@@ -274,111 +270,7 @@ class ArticleComposeScreen extends HookConsumerWidget {
|
||||
),
|
||||
|
||||
// Attachments preview
|
||||
ValueListenableBuilder<List<UniversalFile>>(
|
||||
valueListenable: state.attachments,
|
||||
builder: (context, attachments, _) {
|
||||
if (attachments.isEmpty) return const SizedBox.shrink();
|
||||
return Theme(
|
||||
data: Theme.of(
|
||||
context,
|
||||
).copyWith(dividerColor: Colors.transparent),
|
||||
child: ExpansionTile(
|
||||
initiallyExpanded: isAttachmentsExpanded.value,
|
||||
onExpansionChanged: (expanded) {
|
||||
isAttachmentsExpanded.value = expanded;
|
||||
},
|
||||
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(
|
||||
context,
|
||||
).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
children: [
|
||||
ValueListenableBuilder<Map<int, double?>>(
|
||||
valueListenable: state.attachmentProgress,
|
||||
builder: (context, progressMap, _) {
|
||||
return Wrap(
|
||||
runSpacing: 8,
|
||||
spacing: 8,
|
||||
children: [
|
||||
for (
|
||||
var idx = 0;
|
||||
idx < attachments.length;
|
||||
idx++
|
||||
)
|
||||
SizedBox(
|
||||
width: 180,
|
||||
height: 180,
|
||||
child: AttachmentPreview(
|
||||
isCompact: true,
|
||||
item: attachments[idx],
|
||||
progress: progressMap[idx],
|
||||
isUploading: progressMap.containsKey(idx),
|
||||
onRequestUpload: () async {
|
||||
final config =
|
||||
await showModalBottomSheet<
|
||||
AttachmentUploadConfig
|
||||
>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) =>
|
||||
AttachmentUploaderSheet(
|
||||
ref: ref,
|
||||
state: state,
|
||||
index: idx,
|
||||
),
|
||||
);
|
||||
if (config != null) {
|
||||
await ComposeLogic.uploadAttachment(
|
||||
ref,
|
||||
state,
|
||||
idx,
|
||||
poolId: config.poolId,
|
||||
);
|
||||
}
|
||||
},
|
||||
onUpdate: (value) =>
|
||||
ComposeLogic.updateAttachment(
|
||||
state,
|
||||
value,
|
||||
idx,
|
||||
),
|
||||
onDelete: () =>
|
||||
ComposeLogic.deleteAttachment(
|
||||
ref,
|
||||
state,
|
||||
idx,
|
||||
),
|
||||
onInsert: () =>
|
||||
ComposeLogic.insertAttachment(
|
||||
ref,
|
||||
state,
|
||||
idx,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
Gap(16),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
ArticleComposeAttachments(state: state),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -93,6 +93,8 @@ class AttachmentPreview extends HookConsumerWidget {
|
||||
final Function(UniversalFile)? onUpdate;
|
||||
final Function? onRequestUpload;
|
||||
final bool isCompact;
|
||||
final String? thumbnailId;
|
||||
final Function(String?)? onSetThumbnail;
|
||||
|
||||
const AttachmentPreview({
|
||||
super.key,
|
||||
@@ -105,6 +107,8 @@ class AttachmentPreview extends HookConsumerWidget {
|
||||
this.onUpdate,
|
||||
this.onInsert,
|
||||
this.isCompact = false,
|
||||
this.thumbnailId,
|
||||
this.onSetThumbnail,
|
||||
});
|
||||
|
||||
// GlobalKey for selector
|
||||
@@ -453,6 +457,39 @@ class AttachmentPreview extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
if (thumbnailId != null &&
|
||||
item.isOnCloud &&
|
||||
(item.data as SnCloudFile).id == thumbnailId)
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
width: 3,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (thumbnailId != null &&
|
||||
item.isOnCloud &&
|
||||
(item.data as SnCloudFile).id == thumbnailId)
|
||||
Positioned(
|
||||
top: 8,
|
||||
right: 8,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(6),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
Symbols.image,
|
||||
size: 16,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -641,6 +678,24 @@ class AttachmentPreview extends HookConsumerWidget {
|
||||
await _showSensitiveDialog(context, ref);
|
||||
},
|
||||
),
|
||||
if (item.isOnCloud &&
|
||||
item.type == UniversalFileType.image &&
|
||||
onSetThumbnail != null)
|
||||
MenuAction(
|
||||
title: thumbnailId == (item.data as SnCloudFile).id
|
||||
? 'unsetAsThumbnail'.tr()
|
||||
: 'setAsThumbnail'.tr(),
|
||||
image: MenuImage.icon(Symbols.image),
|
||||
callback: () {
|
||||
final isCurrentlyThumbnail =
|
||||
thumbnailId == (item.data as SnCloudFile).id;
|
||||
if (isCurrentlyThumbnail) {
|
||||
onSetThumbnail?.call(null);
|
||||
} else {
|
||||
onSetThumbnail?.call((item.data as SnCloudFile).id);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
child: contentWidget,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/file.dart';
|
||||
@@ -110,84 +111,101 @@ class ArticleComposeAttachments extends ConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return ValueListenableBuilder<List<UniversalFile>>(
|
||||
valueListenable: state.attachments,
|
||||
builder: (context, attachments, _) {
|
||||
if (attachments.isEmpty) return const SizedBox.shrink();
|
||||
return Theme(
|
||||
data: Theme.of(context).copyWith(dividerColor: Colors.transparent),
|
||||
child: ExpansionTile(
|
||||
initiallyExpanded: true,
|
||||
title: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('attachments'),
|
||||
Text(
|
||||
'articleAttachmentHint',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
return ValueListenableBuilder<String?>(
|
||||
valueListenable: state.thumbnailId,
|
||||
builder: (context, thumbnailId, _) {
|
||||
return ValueListenableBuilder<List<UniversalFile>>(
|
||||
valueListenable: state.attachments,
|
||||
builder: (context, attachments, _) {
|
||||
if (attachments.isEmpty) return const SizedBox.shrink();
|
||||
return Theme(
|
||||
data: Theme.of(
|
||||
context,
|
||||
).copyWith(dividerColor: Colors.transparent),
|
||||
child: ExpansionTile(
|
||||
initiallyExpanded: true,
|
||||
title: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('attachments').tr(),
|
||||
Text(
|
||||
'articleAttachmentHint'.tr(),
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
children: [
|
||||
ValueListenableBuilder<Map<int, double?>>(
|
||||
valueListenable: state.attachmentProgress,
|
||||
builder: (context, progressMap, _) {
|
||||
return Wrap(
|
||||
runSpacing: 8,
|
||||
spacing: 8,
|
||||
children: [
|
||||
for (var idx = 0; idx < attachments.length; idx++)
|
||||
SizedBox(
|
||||
width: 180,
|
||||
height: 180,
|
||||
child: AttachmentPreview(
|
||||
isCompact: true,
|
||||
item: attachments[idx],
|
||||
progress: progressMap[idx],
|
||||
isUploading: progressMap.containsKey(idx),
|
||||
onRequestUpload: () async {
|
||||
final config =
|
||||
await showModalBottomSheet<
|
||||
AttachmentUploadConfig
|
||||
>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) =>
|
||||
AttachmentUploaderSheet(
|
||||
ref: ref,
|
||||
state: state,
|
||||
index: idx,
|
||||
),
|
||||
);
|
||||
if (config != null) {
|
||||
await ComposeLogic.uploadAttachment(
|
||||
children: [
|
||||
ValueListenableBuilder<Map<int, double?>>(
|
||||
valueListenable: state.attachmentProgress,
|
||||
builder: (context, progressMap, _) {
|
||||
return Wrap(
|
||||
runSpacing: 8,
|
||||
spacing: 8,
|
||||
children: [
|
||||
for (var idx = 0; idx < attachments.length; idx++)
|
||||
SizedBox(
|
||||
width: 180,
|
||||
height: 180,
|
||||
child: AttachmentPreview(
|
||||
isCompact: true,
|
||||
item: attachments[idx],
|
||||
progress: progressMap[idx],
|
||||
isUploading: progressMap.containsKey(idx),
|
||||
thumbnailId: thumbnailId,
|
||||
onSetThumbnail: (id) =>
|
||||
ComposeLogic.setThumbnail(state, id),
|
||||
onRequestUpload: () async {
|
||||
final config =
|
||||
await showModalBottomSheet<
|
||||
AttachmentUploadConfig
|
||||
>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) =>
|
||||
AttachmentUploaderSheet(
|
||||
ref: ref,
|
||||
state: state,
|
||||
index: idx,
|
||||
),
|
||||
);
|
||||
if (config != null) {
|
||||
await ComposeLogic.uploadAttachment(
|
||||
ref,
|
||||
state,
|
||||
idx,
|
||||
poolId: config.poolId,
|
||||
);
|
||||
}
|
||||
},
|
||||
onUpdate: (value) =>
|
||||
ComposeLogic.updateAttachment(
|
||||
state,
|
||||
value,
|
||||
idx,
|
||||
),
|
||||
onDelete: () => ComposeLogic.deleteAttachment(
|
||||
ref,
|
||||
state,
|
||||
idx,
|
||||
poolId: config.poolId,
|
||||
);
|
||||
}
|
||||
},
|
||||
onUpdate: (value) => ComposeLogic.updateAttachment(
|
||||
state,
|
||||
value,
|
||||
idx,
|
||||
),
|
||||
onInsert: () => ComposeLogic.insertAttachment(
|
||||
ref,
|
||||
state,
|
||||
idx,
|
||||
),
|
||||
),
|
||||
),
|
||||
onDelete: () =>
|
||||
ComposeLogic.deleteAttachment(ref, state, idx),
|
||||
onInsert: () =>
|
||||
ComposeLogic.insertAttachment(ref, state, idx),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -49,6 +49,8 @@ class ComposeState {
|
||||
final ValueNotifier<String?> pollId;
|
||||
// Linked fund id for this compose session (nullable)
|
||||
final ValueNotifier<String?> fundId;
|
||||
// Thumbnail id for article type post (nullable)
|
||||
final ValueNotifier<String?> thumbnailId;
|
||||
Timer? _autoSaveTimer;
|
||||
|
||||
ComposeState({
|
||||
@@ -69,8 +71,10 @@ class ComposeState {
|
||||
this.postType = 0,
|
||||
String? pollId,
|
||||
String? fundId,
|
||||
String? thumbnailId,
|
||||
}) : pollId = ValueNotifier<String?>(pollId),
|
||||
fundId = ValueNotifier<String?>(fundId);
|
||||
fundId = ValueNotifier<String?>(fundId),
|
||||
thumbnailId = ValueNotifier<String?>(thumbnailId);
|
||||
|
||||
void startAutoSave(WidgetRef ref) {
|
||||
_autoSaveTimer?.cancel();
|
||||
@@ -121,6 +125,9 @@ class ComposeLogic {
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
// Extract thumbnail ID from meta
|
||||
final thumbnailId = originalPost?.meta?['thumbnail'] as String?;
|
||||
|
||||
return ComposeState(
|
||||
attachments: ValueNotifier<List<UniversalFile>>(
|
||||
originalPost?.attachments
|
||||
@@ -156,11 +163,13 @@ class ComposeLogic {
|
||||
postType: postType,
|
||||
pollId: pollId,
|
||||
fundId: fundId,
|
||||
thumbnailId: thumbnailId,
|
||||
);
|
||||
}
|
||||
|
||||
static ComposeState createStateFromDraft(SnPost draft, {int postType = 0}) {
|
||||
final tags = draft.tags.map((tag) => tag.slug).toList();
|
||||
final thumbnailId = draft.meta?['thumbnail'] as String?;
|
||||
|
||||
return ComposeState(
|
||||
attachments: ValueNotifier<List<UniversalFile>>(
|
||||
@@ -183,6 +192,7 @@ class ComposeLogic {
|
||||
pollId: null,
|
||||
// initialize without fund by default
|
||||
fundId: null,
|
||||
thumbnailId: thumbnailId,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -230,7 +240,9 @@ class ComposeLogic {
|
||||
visibility: state.visibility.value,
|
||||
content: state.contentController.text,
|
||||
type: state.postType,
|
||||
meta: null,
|
||||
meta: state.postType == 1 && state.thumbnailId.value != null
|
||||
? {'thumbnail': state.thumbnailId.value}
|
||||
: null,
|
||||
viewsUnique: 0,
|
||||
viewsTotal: 0,
|
||||
upvotes: 0,
|
||||
@@ -302,7 +314,9 @@ class ComposeLogic {
|
||||
visibility: state.visibility.value,
|
||||
content: state.contentController.text,
|
||||
type: state.postType,
|
||||
meta: null,
|
||||
meta: state.postType == 1 && state.thumbnailId.value != null
|
||||
? {'thumbnail': state.thumbnailId.value}
|
||||
: null,
|
||||
viewsUnique: 0,
|
||||
viewsTotal: 0,
|
||||
upvotes: 0,
|
||||
@@ -612,6 +626,10 @@ class ComposeLogic {
|
||||
state.embedView.value = null;
|
||||
}
|
||||
|
||||
static void setThumbnail(ComposeState state, String? thumbnailId) {
|
||||
state.thumbnailId.value = thumbnailId;
|
||||
}
|
||||
|
||||
static Future<void> pickPoll(
|
||||
WidgetRef ref,
|
||||
ComposeState state,
|
||||
@@ -720,6 +738,8 @@ class ComposeLogic {
|
||||
if (state.realm.value != null) 'realm_id': state.realm.value?.id,
|
||||
if (state.pollId.value != null) 'poll_id': state.pollId.value,
|
||||
if (state.fundId.value != null) 'fund_id': state.fundId.value,
|
||||
if (state.postType == 1 && state.thumbnailId.value != null)
|
||||
'thumbnail_id': state.thumbnailId.value,
|
||||
if (state.embedView.value != null)
|
||||
'embed_view': state.embedView.value!.toJson(),
|
||||
};
|
||||
@@ -872,5 +892,6 @@ class ComposeLogic {
|
||||
state.embedView.dispose();
|
||||
state.pollId.dispose();
|
||||
state.fundId.dispose();
|
||||
state.thumbnailId.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user