2023-03-21 01:55:47 +00:00
|
|
|
import 'package:flutter/material.dart' hide Visibility;
|
2022-11-17 16:04:14 +00:00
|
|
|
import 'package:go_router/go_router.dart';
|
2022-11-23 02:59:08 +00:00
|
|
|
import 'package:logging/logging.dart';
|
2022-12-28 20:56:27 +00:00
|
|
|
import 'package:multi_trigger_autocomplete/multi_trigger_autocomplete.dart';
|
2022-11-17 16:04:14 +00:00
|
|
|
import 'package:provider/provider.dart';
|
2023-04-20 20:26:11 +00:00
|
|
|
import 'package:result_monad/result_monad.dart';
|
2022-12-26 20:26:30 +00:00
|
|
|
import 'package:uuid/uuid.dart';
|
2022-11-17 16:04:14 +00:00
|
|
|
|
2022-12-28 22:04:48 +00:00
|
|
|
import '../controls/autocomplete/hashtag_autocomplete_options.dart';
|
2022-12-28 20:56:27 +00:00
|
|
|
import '../controls/autocomplete/mention_autocomplete_options.dart';
|
2022-12-27 03:00:28 +00:00
|
|
|
import '../controls/entry_media_attachments/gallery_selector_control.dart';
|
2022-12-26 20:26:30 +00:00
|
|
|
import '../controls/entry_media_attachments/media_uploads_control.dart';
|
2023-04-27 19:19:51 +00:00
|
|
|
import '../controls/html_text_viewer_control.dart';
|
2023-03-20 14:06:44 +00:00
|
|
|
import '../controls/login_aware_cached_network_image.dart';
|
2022-11-22 18:55:50 +00:00
|
|
|
import '../controls/padding.dart';
|
2023-03-14 03:47:40 +00:00
|
|
|
import '../controls/standard_appbar.dart';
|
2022-11-23 02:59:08 +00:00
|
|
|
import '../controls/timeline/status_header_control.dart';
|
2023-03-16 15:37:46 +00:00
|
|
|
import '../globals.dart';
|
2023-04-20 20:26:11 +00:00
|
|
|
import '../models/exec_error.dart';
|
2023-03-21 01:55:47 +00:00
|
|
|
import '../models/group_data.dart';
|
2022-12-27 03:00:28 +00:00
|
|
|
import '../models/image_entry.dart';
|
2023-03-19 20:27:57 +00:00
|
|
|
import '../models/link_preview_data.dart';
|
2022-12-27 03:00:28 +00:00
|
|
|
import '../models/media_attachment_uploads/new_entry_media_items.dart';
|
2022-11-23 02:59:08 +00:00
|
|
|
import '../models/timeline_entry.dart';
|
2023-03-21 01:55:47 +00:00
|
|
|
import '../models/visibility.dart';
|
2023-03-19 20:27:57 +00:00
|
|
|
import '../serializers/friendica/link_preview_friendica_extensions.dart';
|
2023-03-17 20:52:49 +00:00
|
|
|
import '../services/feature_version_checker.dart';
|
2022-11-22 18:55:50 +00:00
|
|
|
import '../services/timeline_manager.dart';
|
2023-03-14 03:47:40 +00:00
|
|
|
import '../utils/active_profile_selector.dart';
|
2023-03-18 18:49:20 +00:00
|
|
|
import '../utils/html_to_edit_text_helper.dart';
|
2023-03-19 20:27:57 +00:00
|
|
|
import '../utils/opengraph_preview_grabber.dart';
|
2022-11-17 16:04:14 +00:00
|
|
|
import '../utils/snackbar_builder.dart';
|
2023-03-19 20:27:57 +00:00
|
|
|
import '../utils/string_utils.dart';
|
2022-11-17 16:04:14 +00:00
|
|
|
|
|
|
|
class EditorScreen extends StatefulWidget {
|
|
|
|
final String id;
|
2022-11-23 02:59:08 +00:00
|
|
|
final String parentId;
|
2023-03-16 15:37:46 +00:00
|
|
|
final bool forEditing;
|
2022-11-17 16:04:14 +00:00
|
|
|
|
2023-03-16 15:37:46 +00:00
|
|
|
const EditorScreen(
|
|
|
|
{super.key, this.id = '', this.parentId = '', required this.forEditing});
|
2022-11-17 16:04:14 +00:00
|
|
|
|
|
|
|
@override
|
|
|
|
State<EditorScreen> createState() => _EditorScreenState();
|
|
|
|
}
|
|
|
|
|
|
|
|
class _EditorScreenState extends State<EditorScreen> {
|
2022-11-23 02:59:08 +00:00
|
|
|
static final _logger = Logger('$EditorScreen');
|
2022-11-22 18:55:50 +00:00
|
|
|
final contentController = TextEditingController();
|
|
|
|
final spoilerController = TextEditingController();
|
2022-12-26 20:26:30 +00:00
|
|
|
final localEntryTemporaryId = const Uuid().v4();
|
2022-11-23 02:59:08 +00:00
|
|
|
TimelineEntry? parentEntry;
|
2023-03-19 20:27:57 +00:00
|
|
|
final linkPreviewController = TextEditingController();
|
|
|
|
LinkPreviewData? linkPreviewData;
|
2022-12-27 03:00:28 +00:00
|
|
|
final newMediaItems = NewEntryMediaItems();
|
|
|
|
final existingMediaItems = <ImageEntry>[];
|
2022-12-28 20:56:27 +00:00
|
|
|
final focusNode = FocusNode();
|
2023-03-21 01:55:47 +00:00
|
|
|
Visibility visibility = Visibility.public();
|
|
|
|
GroupData? currentGroup;
|
2022-11-22 18:55:50 +00:00
|
|
|
|
2022-12-17 18:34:47 +00:00
|
|
|
var isSubmitting = false;
|
|
|
|
|
2022-11-23 02:59:08 +00:00
|
|
|
bool get isComment => widget.parentId.isNotEmpty;
|
2022-11-22 18:55:50 +00:00
|
|
|
|
2022-11-23 02:59:08 +00:00
|
|
|
String get statusType => widget.parentId.isEmpty ? 'Post' : 'Comment';
|
|
|
|
|
2022-12-26 20:26:30 +00:00
|
|
|
String get localEntryId =>
|
|
|
|
widget.id.isNotEmpty ? widget.id : localEntryTemporaryId;
|
|
|
|
|
2023-03-16 15:37:46 +00:00
|
|
|
bool loaded = false;
|
|
|
|
|
2022-11-23 02:59:08 +00:00
|
|
|
@override
|
|
|
|
void initState() {
|
2023-03-16 15:37:46 +00:00
|
|
|
if (isComment) {
|
|
|
|
final manager = context
|
|
|
|
.read<ActiveProfileSelector<TimelineManager>>()
|
|
|
|
.activeEntry
|
|
|
|
.value;
|
|
|
|
manager.getEntryById(widget.parentId).match(onSuccess: (entry) {
|
|
|
|
spoilerController.text = entry.spoilerText;
|
|
|
|
parentEntry = entry;
|
2023-03-21 14:23:40 +00:00
|
|
|
visibility = entry.visibility;
|
2023-03-16 15:37:46 +00:00
|
|
|
}, onError: (error) {
|
|
|
|
_logger.finest('Error trying to get parent entry: $error');
|
|
|
|
});
|
2022-11-23 02:59:08 +00:00
|
|
|
}
|
|
|
|
|
2023-03-16 15:37:46 +00:00
|
|
|
if (widget.forEditing) {
|
|
|
|
restoreStatusData();
|
|
|
|
} else {
|
|
|
|
loaded = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void restoreStatusData() async {
|
|
|
|
_logger.fine('Attempting to load status for editing');
|
|
|
|
loaded = false;
|
|
|
|
final result = await getIt<ActiveProfileSelector<TimelineManager>>()
|
2023-03-14 03:47:40 +00:00
|
|
|
.activeEntry
|
2023-03-16 15:37:46 +00:00
|
|
|
.andThenAsync((manager) async => await manager.getEntryById(widget.id));
|
|
|
|
result.match(onSuccess: (entry) {
|
|
|
|
_logger.fine('Loading status ${widget.id} information into fields');
|
2023-03-21 18:27:38 +00:00
|
|
|
contentController.text = htmlToSimpleText(entry.body);
|
2022-11-23 02:59:08 +00:00
|
|
|
spoilerController.text = entry.spoilerText;
|
2023-03-16 15:37:46 +00:00
|
|
|
existingMediaItems
|
|
|
|
.addAll(entry.mediaAttachments.map((e) => e.toImageEntry()));
|
2023-03-19 20:27:57 +00:00
|
|
|
if (entry.linkPreviewData?.link.isNotEmpty ?? false) {
|
|
|
|
restoreLinkPreviewData(entry.linkPreviewData!);
|
|
|
|
}
|
2023-03-21 01:55:47 +00:00
|
|
|
visibility = entry.visibility;
|
2023-03-16 15:37:46 +00:00
|
|
|
setState(() {
|
|
|
|
loaded = true;
|
|
|
|
});
|
2022-11-23 02:59:08 +00:00
|
|
|
}, onError: (error) {
|
2023-04-20 20:26:11 +00:00
|
|
|
buildSnackbar(context, 'Error getting post for editing: $error');
|
|
|
|
logError(error, _logger);
|
2022-11-23 02:59:08 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2023-03-19 20:27:57 +00:00
|
|
|
void restoreLinkPreviewData(LinkPreviewData preview) {
|
|
|
|
linkPreviewController.text = preview.link;
|
|
|
|
linkPreviewData = preview;
|
|
|
|
Future.delayed(const Duration(seconds: 1), () async {
|
|
|
|
final updatedPreview = await getLinkPreview(preview.link);
|
|
|
|
if (updatedPreview.isSuccess) {
|
|
|
|
linkPreviewData = linkPreviewData?.copy(
|
|
|
|
availableImageUrls: updatedPreview.value.availableImageUrls);
|
|
|
|
setState(() {});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
String get bodyText =>
|
|
|
|
'${contentController.text} ${linkPreviewData?.toBodyAttachment() ?? ''}';
|
|
|
|
|
|
|
|
bool get isEmptyPost =>
|
|
|
|
bodyText.isEmpty &&
|
|
|
|
existingMediaItems.isEmpty &&
|
|
|
|
newMediaItems.attachments.isEmpty;
|
|
|
|
|
2022-11-23 02:59:08 +00:00
|
|
|
Future<void> createStatus(
|
|
|
|
BuildContext context, TimelineManager manager) async {
|
2023-03-19 20:27:57 +00:00
|
|
|
if (isEmptyPost) {
|
|
|
|
buildSnackbar(context, "Can't submit an empty $statusType");
|
2022-11-22 18:55:50 +00:00
|
|
|
return;
|
|
|
|
}
|
2022-12-26 22:37:46 +00:00
|
|
|
|
2022-12-17 18:34:47 +00:00
|
|
|
setState(() {
|
2022-12-26 22:37:46 +00:00
|
|
|
isSubmitting = true;
|
2022-12-17 18:34:47 +00:00
|
|
|
});
|
2022-12-26 22:37:46 +00:00
|
|
|
|
2023-04-20 20:26:11 +00:00
|
|
|
final result = await manager
|
|
|
|
.createNewStatus(
|
2023-03-19 20:27:57 +00:00
|
|
|
bodyText,
|
2022-11-22 18:55:50 +00:00
|
|
|
spoilerText: spoilerController.text,
|
2022-11-23 02:59:08 +00:00
|
|
|
inReplyToId: widget.parentId,
|
2022-12-27 03:00:28 +00:00
|
|
|
newMediaItems: newMediaItems,
|
|
|
|
existingMediaItems: existingMediaItems,
|
2023-03-21 01:55:47 +00:00
|
|
|
visibility: visibility,
|
2023-04-20 20:26:11 +00:00
|
|
|
)
|
|
|
|
.withError((error) {
|
|
|
|
buildSnackbar(context, 'Error posting: $error');
|
|
|
|
logError(error, _logger);
|
|
|
|
});
|
|
|
|
|
2022-12-17 18:34:47 +00:00
|
|
|
setState(() {
|
|
|
|
isSubmitting = false;
|
|
|
|
});
|
2022-11-22 18:55:50 +00:00
|
|
|
|
|
|
|
if (result.isFailure) {
|
|
|
|
return;
|
|
|
|
}
|
2022-12-26 22:37:46 +00:00
|
|
|
|
|
|
|
if (mounted && context.canPop()) {
|
|
|
|
context.pop();
|
|
|
|
}
|
2022-11-22 18:55:50 +00:00
|
|
|
}
|
2022-11-17 16:04:14 +00:00
|
|
|
|
2023-03-16 15:37:46 +00:00
|
|
|
Future<void> editStatus(BuildContext context, TimelineManager manager) async {
|
2023-03-19 20:27:57 +00:00
|
|
|
if (isEmptyPost) {
|
|
|
|
buildSnackbar(context, "Can't submit an empty $statusType");
|
2023-03-16 15:37:46 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
setState(() {
|
|
|
|
isSubmitting = true;
|
|
|
|
});
|
|
|
|
|
2023-04-20 20:26:11 +00:00
|
|
|
final result = await manager
|
|
|
|
.editStatus(
|
2023-03-16 15:37:46 +00:00
|
|
|
widget.id,
|
2023-03-19 20:27:57 +00:00
|
|
|
bodyText,
|
2023-03-16 15:37:46 +00:00
|
|
|
spoilerText: spoilerController.text,
|
|
|
|
inReplyToId: widget.parentId,
|
|
|
|
newMediaItems: newMediaItems,
|
|
|
|
existingMediaItems: existingMediaItems,
|
2023-03-21 01:55:47 +00:00
|
|
|
newMediaItemVisibility: visibility,
|
2023-04-20 20:26:11 +00:00
|
|
|
)
|
|
|
|
.withError((error) {
|
|
|
|
buildSnackbar(context, 'Error updating $statusType: $error');
|
|
|
|
logError(error, _logger);
|
|
|
|
});
|
|
|
|
|
2023-03-16 15:37:46 +00:00
|
|
|
setState(() {
|
|
|
|
isSubmitting = false;
|
|
|
|
});
|
|
|
|
|
|
|
|
if (result.isFailure) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (mounted && context.canPop()) {
|
|
|
|
context.pop();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-11-17 16:04:14 +00:00
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
2022-12-26 22:37:46 +00:00
|
|
|
_logger.finest('Build editor $isComment $parentEntry');
|
2023-03-14 03:47:40 +00:00
|
|
|
final manager = context
|
|
|
|
.read<ActiveProfileSelector<TimelineManager>>()
|
|
|
|
.activeEntry
|
|
|
|
.value;
|
2022-11-17 16:04:14 +00:00
|
|
|
|
2023-03-17 20:52:49 +00:00
|
|
|
final vc = getIt<FriendicaVersionChecker>();
|
|
|
|
final canEdit = vc.canUseFeature(RelaticaFeatures.statusEditing);
|
2023-03-18 19:13:42 +00:00
|
|
|
final canSpoilerText = vc.canUseFeature(RelaticaFeatures.postSpoilerText) ||
|
|
|
|
widget.parentId.isNotEmpty;
|
2023-03-17 20:52:49 +00:00
|
|
|
|
|
|
|
late final body;
|
|
|
|
|
|
|
|
if (widget.forEditing && !canEdit) {
|
|
|
|
body = Center(
|
|
|
|
child: Column(
|
|
|
|
children: [
|
|
|
|
Text(vc.versionErrorString(RelaticaFeatures.statusEditing)),
|
|
|
|
const VerticalPadding(),
|
|
|
|
ElevatedButton(
|
|
|
|
onPressed: () => context.pop(), child: const Text('Back')),
|
|
|
|
],
|
|
|
|
),
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
final mainBody = Padding(
|
|
|
|
padding: const EdgeInsets.all(8.0),
|
|
|
|
child: Container(
|
|
|
|
child: SingleChildScrollView(
|
|
|
|
child: Column(
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
mainAxisAlignment: MainAxisAlignment.start,
|
|
|
|
children: [
|
|
|
|
if (isComment && parentEntry != null)
|
|
|
|
buildCommentPreview(context, parentEntry!),
|
|
|
|
TextFormField(
|
|
|
|
readOnly: isSubmitting,
|
2023-03-18 19:13:42 +00:00
|
|
|
enabled: !isSubmitting && canSpoilerText,
|
2023-03-17 20:52:49 +00:00
|
|
|
controller: spoilerController,
|
2023-03-22 21:33:35 +00:00
|
|
|
textCapitalization: TextCapitalization.sentences,
|
2023-03-17 20:52:49 +00:00
|
|
|
decoration: InputDecoration(
|
2023-03-18 19:13:42 +00:00
|
|
|
labelText: canSpoilerText
|
|
|
|
? '$statusType Spoiler Text (optional)'
|
|
|
|
: 'Your server doesnt support $statusType Spoiler Text',
|
2023-03-17 20:52:49 +00:00
|
|
|
border: OutlineInputBorder(
|
2023-03-18 19:13:42 +00:00
|
|
|
borderSide: const BorderSide(),
|
2023-03-17 20:52:49 +00:00
|
|
|
borderRadius: BorderRadius.circular(5.0),
|
2022-11-22 18:55:50 +00:00
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
2023-03-17 20:52:49 +00:00
|
|
|
const VerticalPadding(),
|
2023-03-21 01:55:47 +00:00
|
|
|
buildVisibilitySelector(context),
|
|
|
|
const VerticalPadding(),
|
2023-03-17 20:52:49 +00:00
|
|
|
buildContentField(context),
|
|
|
|
const VerticalPadding(),
|
2023-03-19 20:27:57 +00:00
|
|
|
buildLinkWithPreview(context),
|
|
|
|
const VerticalPadding(),
|
2023-03-21 01:55:47 +00:00
|
|
|
GallerySelectorControl(
|
|
|
|
entries: existingMediaItems,
|
|
|
|
visibilityFilter: visibility,
|
|
|
|
),
|
2023-03-17 20:52:49 +00:00
|
|
|
const VerticalPadding(),
|
|
|
|
MediaUploadsControl(
|
|
|
|
entryMediaItems: newMediaItems,
|
|
|
|
),
|
|
|
|
buildButtonBar(context, manager),
|
|
|
|
],
|
|
|
|
),
|
2022-12-26 22:37:46 +00:00
|
|
|
),
|
|
|
|
),
|
2023-03-17 20:52:49 +00:00
|
|
|
);
|
2022-12-26 22:37:46 +00:00
|
|
|
|
2023-03-17 20:52:49 +00:00
|
|
|
if (widget.forEditing && !loaded) {
|
|
|
|
body = buildBusyBody(context, mainBody, 'Loading status');
|
|
|
|
} else if (isSubmitting) {
|
|
|
|
body = buildBusyBody(context, mainBody, 'Submitting $statusType');
|
|
|
|
} else {
|
|
|
|
body = mainBody;
|
|
|
|
}
|
2023-03-16 15:37:46 +00:00
|
|
|
}
|
2022-12-26 22:37:46 +00:00
|
|
|
|
|
|
|
return Scaffold(
|
2023-03-14 03:47:40 +00:00
|
|
|
appBar: StandardAppBar.build(
|
|
|
|
context, widget.id.isEmpty ? 'New $statusType' : 'Edit $statusType',
|
|
|
|
withDrawer: true),
|
2023-03-16 15:37:46 +00:00
|
|
|
body: body,
|
2022-11-17 16:04:14 +00:00
|
|
|
);
|
|
|
|
}
|
2022-11-23 02:59:08 +00:00
|
|
|
|
2022-12-28 20:56:27 +00:00
|
|
|
Widget buildContentField(BuildContext context) {
|
|
|
|
return MultiTriggerAutocomplete(
|
|
|
|
textEditingController: contentController,
|
|
|
|
focusNode: focusNode,
|
2023-04-06 19:49:25 +00:00
|
|
|
optionsAlignment: OptionsAlignment.top,
|
2022-12-28 20:56:27 +00:00
|
|
|
autocompleteTriggers: [
|
|
|
|
AutocompleteTrigger(
|
|
|
|
trigger: '@',
|
|
|
|
optionsViewBuilder: (context, autocompleteQuery, controller) {
|
|
|
|
return MentionAutocompleteOptions(
|
|
|
|
query: autocompleteQuery.query,
|
|
|
|
onMentionUserTap: (user) {
|
|
|
|
final autocomplete = MultiTriggerAutocomplete.of(context);
|
|
|
|
return autocomplete.acceptAutocompleteOption(user.handle);
|
|
|
|
},
|
|
|
|
);
|
|
|
|
},
|
|
|
|
),
|
2022-12-28 22:04:48 +00:00
|
|
|
AutocompleteTrigger(
|
|
|
|
trigger: '#',
|
|
|
|
optionsViewBuilder: (context, autocompleteQuery, controller) {
|
|
|
|
return HashtagAutocompleteOptions(
|
|
|
|
query: autocompleteQuery.query,
|
|
|
|
onHashtagTap: (hashtag) {
|
|
|
|
final autocomplete = MultiTriggerAutocomplete.of(context);
|
|
|
|
return autocomplete.acceptAutocompleteOption(hashtag);
|
|
|
|
},
|
|
|
|
);
|
|
|
|
},
|
|
|
|
),
|
2022-12-28 20:56:27 +00:00
|
|
|
],
|
|
|
|
fieldViewBuilder: (context, controller, focusNode) => TextFormField(
|
|
|
|
focusNode: focusNode,
|
|
|
|
readOnly: isSubmitting,
|
|
|
|
enabled: !isSubmitting,
|
2023-03-22 21:33:35 +00:00
|
|
|
textCapitalization: TextCapitalization.sentences,
|
2022-12-28 20:56:27 +00:00
|
|
|
maxLines: 10,
|
|
|
|
controller: controller,
|
|
|
|
decoration: InputDecoration(
|
|
|
|
labelText: '$statusType Content',
|
|
|
|
alignLabelWithHint: true,
|
|
|
|
border: OutlineInputBorder(
|
2023-03-18 19:13:42 +00:00
|
|
|
borderSide: const BorderSide(),
|
2022-12-28 20:56:27 +00:00
|
|
|
borderRadius: BorderRadius.circular(5.0),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2022-11-23 02:59:08 +00:00
|
|
|
Widget buildCommentPreview(BuildContext context, TimelineEntry entry) {
|
2022-12-26 22:37:46 +00:00
|
|
|
_logger.finest('Build preview');
|
2022-11-23 02:59:08 +00:00
|
|
|
return Column(
|
|
|
|
mainAxisAlignment: MainAxisAlignment.start,
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
children: [
|
|
|
|
Text(
|
|
|
|
'Comment for status: ',
|
|
|
|
style: Theme.of(context).textTheme.bodyLarge,
|
|
|
|
),
|
|
|
|
Card(
|
|
|
|
child: Padding(
|
|
|
|
padding: const EdgeInsets.all(8.0),
|
|
|
|
child: Column(
|
|
|
|
mainAxisAlignment: MainAxisAlignment.start,
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
children: [
|
2022-11-23 20:48:09 +00:00
|
|
|
StatusHeaderControl(
|
|
|
|
entry: entry,
|
|
|
|
),
|
2022-11-23 02:59:08 +00:00
|
|
|
const VerticalPadding(height: 3),
|
|
|
|
if (entry.spoilerText.isNotEmpty) ...[
|
|
|
|
Text(
|
|
|
|
'Content Summary: ${entry.spoilerText}',
|
|
|
|
style: Theme.of(context).textTheme.bodyLarge,
|
|
|
|
),
|
|
|
|
const VerticalPadding(height: 3)
|
|
|
|
],
|
2023-04-27 19:19:51 +00:00
|
|
|
HtmlTextViewerControl(content: entry.body),
|
2022-11-23 02:59:08 +00:00
|
|
|
],
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
const VerticalPadding(),
|
|
|
|
],
|
|
|
|
);
|
|
|
|
}
|
2022-12-26 20:26:30 +00:00
|
|
|
|
|
|
|
Widget buildButtonBar(BuildContext context, TimelineManager manager) {
|
|
|
|
return Row(
|
|
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
|
|
children: [
|
2023-03-16 15:37:46 +00:00
|
|
|
if (!widget.forEditing)
|
|
|
|
ElevatedButton(
|
|
|
|
onPressed:
|
|
|
|
isSubmitting ? null : () => createStatus(context, manager),
|
|
|
|
child: const Text('Submit'),
|
|
|
|
),
|
|
|
|
if (widget.forEditing)
|
|
|
|
ElevatedButton(
|
|
|
|
onPressed: isSubmitting ? null : () => editStatus(context, manager),
|
|
|
|
child: const Text('Edit'),
|
|
|
|
),
|
2022-12-26 20:26:30 +00:00
|
|
|
const HorizontalPadding(),
|
|
|
|
ElevatedButton(
|
|
|
|
onPressed: isSubmitting
|
|
|
|
? null
|
|
|
|
: () {
|
|
|
|
context.pop();
|
|
|
|
},
|
|
|
|
child: const Text('Cancel'),
|
|
|
|
),
|
|
|
|
],
|
|
|
|
);
|
|
|
|
}
|
2023-03-16 15:37:46 +00:00
|
|
|
|
|
|
|
Widget buildBusyBody(BuildContext context, Widget mainBody, String status) {
|
|
|
|
return Stack(
|
|
|
|
children: [
|
|
|
|
mainBody,
|
|
|
|
Card(
|
|
|
|
color: Theme.of(context).canvasColor.withOpacity(0.8),
|
|
|
|
child: SizedBox(
|
|
|
|
width: MediaQuery.of(context).size.width,
|
|
|
|
height: MediaQuery.of(context).size.height,
|
|
|
|
child: Center(
|
|
|
|
child: Column(
|
|
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
|
|
children: [
|
|
|
|
const CircularProgressIndicator(),
|
|
|
|
const VerticalPadding(),
|
|
|
|
Text(status),
|
|
|
|
],
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
],
|
|
|
|
);
|
|
|
|
}
|
2023-03-19 20:27:57 +00:00
|
|
|
|
|
|
|
Widget buildLinkWithPreview(BuildContext context) {
|
|
|
|
return Column(
|
|
|
|
children: [
|
|
|
|
Row(
|
|
|
|
children: [
|
|
|
|
Expanded(
|
|
|
|
child: TextField(
|
|
|
|
controller: linkPreviewController,
|
|
|
|
decoration: InputDecoration(
|
|
|
|
labelText: 'Link with preview (optional)',
|
|
|
|
border: OutlineInputBorder(
|
|
|
|
borderSide: const BorderSide(),
|
|
|
|
borderRadius: BorderRadius.circular(5.0),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
IconButton(
|
|
|
|
onPressed: () async {
|
|
|
|
final newPreviewResult =
|
|
|
|
await getLinkPreview(linkPreviewController.text);
|
|
|
|
newPreviewResult.match(
|
|
|
|
onSuccess: (preview) => setState(() {
|
|
|
|
linkPreviewData = preview;
|
|
|
|
}),
|
|
|
|
onError: (error) {
|
|
|
|
if (mounted) {
|
|
|
|
buildSnackbar(
|
|
|
|
context, 'Error building link preview: $error');
|
2023-04-20 20:26:11 +00:00
|
|
|
logError(error, _logger);
|
2023-03-19 20:27:57 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
},
|
|
|
|
icon: Icon(Icons.refresh),
|
|
|
|
),
|
|
|
|
],
|
|
|
|
),
|
|
|
|
const VerticalPadding(),
|
|
|
|
if (linkPreviewData != null) buildPreviewCard(linkPreviewData!),
|
|
|
|
],
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
Widget buildPreviewCard(LinkPreviewData preview) {
|
|
|
|
return Row(
|
|
|
|
children: [
|
|
|
|
buildPreviewImageSelector(preview),
|
|
|
|
Expanded(
|
|
|
|
child: ListTile(
|
|
|
|
title: Text(preview.title),
|
|
|
|
subtitle: Text(preview.description.truncate(length: 128)),
|
|
|
|
trailing: IconButton(
|
|
|
|
onPressed: () {
|
|
|
|
setState(() {
|
|
|
|
linkPreviewController.text = '';
|
|
|
|
linkPreviewData = null;
|
|
|
|
});
|
|
|
|
},
|
|
|
|
icon: Icon(Icons.delete),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
],
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
Widget buildPreviewImageSelector(LinkPreviewData preview) {
|
|
|
|
const width = 128.0;
|
|
|
|
const height = 128.0;
|
|
|
|
if (preview.selectedImageUrl.isEmpty &&
|
|
|
|
preview.availableImageUrls.isEmpty) {
|
|
|
|
return Container(
|
|
|
|
width: width,
|
|
|
|
height: height,
|
|
|
|
color: Colors.grey,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
final currentImage = Container(
|
|
|
|
width: width,
|
|
|
|
height: height,
|
2023-03-20 14:06:44 +00:00
|
|
|
child:
|
|
|
|
LoginAwareCachedNetworkImage(imageUrl: preview.selectedImageUrl));
|
2023-03-19 20:27:57 +00:00
|
|
|
|
|
|
|
if (preview.availableImageUrls.length < 2) {
|
|
|
|
return currentImage;
|
|
|
|
}
|
|
|
|
|
|
|
|
return Column(
|
|
|
|
children: [
|
|
|
|
currentImage,
|
|
|
|
// TODO Add in when Friendica no longer stomps on image previews
|
|
|
|
// Row(
|
|
|
|
// mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
|
|
// children: [
|
|
|
|
// IconButton(
|
|
|
|
// onPressed: () => updateLinkPreviewThumbnail(preview, -1),
|
|
|
|
// icon: Icon(size: iconSize, Icons.arrow_back_ios),
|
|
|
|
// ),
|
|
|
|
// IconButton(
|
|
|
|
// onPressed: () => updateLinkPreviewThumbnail(preview, 1),
|
|
|
|
// icon: Icon(size: iconSize, Icons.arrow_forward_ios),
|
|
|
|
// ),
|
|
|
|
// ],
|
|
|
|
// )
|
|
|
|
],
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-03-21 01:55:47 +00:00
|
|
|
Widget buildVisibilitySelector(BuildContext context) {
|
2023-03-21 14:23:40 +00:00
|
|
|
if (widget.forEditing || widget.parentId.isNotEmpty) {
|
|
|
|
return Row(
|
|
|
|
children: [
|
|
|
|
const Text('Visibility:'),
|
|
|
|
const HorizontalPadding(),
|
|
|
|
visibility.type == VisibilityType.public
|
|
|
|
? const Icon(Icons.public)
|
|
|
|
: const Icon(Icons.lock),
|
|
|
|
],
|
|
|
|
);
|
|
|
|
}
|
2023-03-21 01:55:47 +00:00
|
|
|
final groups = context
|
|
|
|
.watch<ActiveProfileSelector<TimelineManager>>()
|
|
|
|
.activeEntry
|
|
|
|
.andThen((tm) => tm.getGroups())
|
|
|
|
.getValueOrElse(() => []);
|
|
|
|
groups.sort((g1, g2) => g1.name.compareTo(g2.name));
|
|
|
|
|
|
|
|
final groupMenuItems = <DropdownMenuEntry<GroupData>>[];
|
|
|
|
groupMenuItems.add(DropdownMenuEntry(
|
|
|
|
value: GroupData.followersPseudoGroup,
|
|
|
|
label: GroupData.followersPseudoGroup.name));
|
|
|
|
groupMenuItems.add(DropdownMenuEntry(
|
|
|
|
value: GroupData('', ''), label: '-', enabled: false));
|
|
|
|
groupMenuItems.addAll(groups.map((g) => DropdownMenuEntry(
|
|
|
|
value: g,
|
|
|
|
label: g.name,
|
|
|
|
)));
|
|
|
|
if (!groups.contains(currentGroup)) {
|
|
|
|
currentGroup = null;
|
|
|
|
}
|
|
|
|
return Row(
|
|
|
|
children: [
|
|
|
|
const Text('Visibility:'),
|
|
|
|
const HorizontalPadding(),
|
|
|
|
DropdownMenu<VisibilityType>(
|
|
|
|
initialSelection: visibility.type,
|
|
|
|
enabled: !widget.forEditing,
|
|
|
|
onSelected: (value) {
|
|
|
|
setState(() {
|
|
|
|
if (value == VisibilityType.public) {
|
|
|
|
visibility = Visibility.public();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (value == VisibilityType.private && currentGroup == null) {
|
|
|
|
visibility = Visibility.private();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
visibility = Visibility(
|
|
|
|
type: VisibilityType.private,
|
|
|
|
allowedGroupIds: [currentGroup!.id],
|
|
|
|
);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
dropdownMenuEntries: VisibilityType.values
|
|
|
|
.map((v) => DropdownMenuEntry(
|
|
|
|
value: v,
|
|
|
|
label: v.toLabel(),
|
|
|
|
))
|
|
|
|
.toList(),
|
|
|
|
),
|
|
|
|
const HorizontalPadding(),
|
|
|
|
if (visibility.type == VisibilityType.private)
|
|
|
|
DropdownMenu<GroupData>(
|
|
|
|
enabled: !widget.forEditing,
|
|
|
|
initialSelection: currentGroup,
|
|
|
|
onSelected: (value) {
|
|
|
|
setState(() {
|
|
|
|
currentGroup = value;
|
|
|
|
visibility = Visibility(
|
|
|
|
type: VisibilityType.private,
|
|
|
|
allowedGroupIds:
|
|
|
|
currentGroup == null ? [] : [currentGroup!.id],
|
|
|
|
);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
dropdownMenuEntries: groupMenuItems,
|
|
|
|
),
|
|
|
|
],
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-03-19 20:27:57 +00:00
|
|
|
void updateLinkPreviewThumbnail(LinkPreviewData preview, int increment) {
|
|
|
|
var currentIndex =
|
|
|
|
preview.availableImageUrls.indexOf(preview.selectedImageUrl) +
|
|
|
|
increment;
|
|
|
|
if (currentIndex < 0) {
|
|
|
|
currentIndex = preview.availableImageUrls.length - 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (currentIndex > preview.availableImageUrls.length - 1) {
|
|
|
|
currentIndex = 0;
|
|
|
|
}
|
|
|
|
setState(() {
|
|
|
|
linkPreviewData = preview.copy(
|
|
|
|
selectedImageUrl: preview.availableImageUrls[currentIndex]);
|
|
|
|
});
|
|
|
|
}
|
2022-11-17 16:04:14 +00:00
|
|
|
}
|