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