relatica/lib/screens/editor.dart

285 wiersze
8.9 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_widget_from_html_core/flutter_widget_from_html_core.dart';
import 'package:go_router/go_router.dart';
import 'package:logging/logging.dart';
import 'package:multi_trigger_autocomplete/multi_trigger_autocomplete.dart';
import 'package:provider/provider.dart';
import 'package:uuid/uuid.dart';
import '../controls/autocomplete/hashtag_autocomplete_options.dart';
import '../controls/autocomplete/mention_autocomplete_options.dart';
import '../controls/entry_media_attachments/gallery_selector_control.dart';
import '../controls/entry_media_attachments/media_uploads_control.dart';
import '../controls/padding.dart';
import '../controls/timeline/status_header_control.dart';
import '../models/image_entry.dart';
import '../models/media_attachment_uploads/new_entry_media_items.dart';
import '../models/timeline_entry.dart';
import '../services/timeline_manager.dart';
import '../utils/snackbar_builder.dart';
class EditorScreen extends StatefulWidget {
final String id;
final String parentId;
const EditorScreen({super.key, this.id = '', this.parentId = ''});
@override
State<EditorScreen> createState() => _EditorScreenState();
}
class _EditorScreenState extends State<EditorScreen> {
static final _logger = Logger('$EditorScreen');
final contentController = TextEditingController();
final spoilerController = TextEditingController();
final localEntryTemporaryId = const Uuid().v4();
TimelineEntry? parentEntry;
final newMediaItems = NewEntryMediaItems();
final existingMediaItems = <ImageEntry>[];
final focusNode = FocusNode();
var isSubmitting = false;
bool get isComment => widget.parentId.isNotEmpty;
String get statusType => widget.parentId.isEmpty ? 'Post' : 'Comment';
String get localEntryId =>
widget.id.isNotEmpty ? widget.id : localEntryTemporaryId;
@override
void initState() {
if (!isComment) {
return;
}
final manager = context.read<TimelineManager>();
manager.getEntryById(widget.parentId).match(onSuccess: (entry) {
spoilerController.text = entry.spoilerText;
parentEntry = entry;
}, onError: (error) {
_logger.finest('Error trying to get parent entry: $error');
});
}
Future<void> createStatus(
BuildContext context, TimelineManager manager) async {
if (contentController.text.isEmpty) {
buildSnackbar(context, "Can't submit an empty post/comment");
return;
}
setState(() {
isSubmitting = true;
});
final result = await manager.createNewStatus(
contentController.text,
spoilerText: spoilerController.text,
inReplyToId: widget.parentId,
newMediaItems: newMediaItems,
existingMediaItems: existingMediaItems,
);
setState(() {
isSubmitting = false;
});
if (result.isFailure) {
buildSnackbar(context, 'Error posting: ${result.error}');
return;
}
if (mounted && context.canPop()) {
context.pop();
}
}
@override
Widget build(BuildContext context) {
_logger.finest('Build editor $isComment $parentEntry');
final manager = context.read<TimelineManager>();
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,
enabled: !isSubmitting,
controller: spoilerController,
decoration: InputDecoration(
labelText: '$statusType Spoiler Text (optional)',
border: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).backgroundColor,
),
borderRadius: BorderRadius.circular(5.0),
),
),
),
const VerticalPadding(),
buildContentField(context),
const VerticalPadding(),
GallerySelectorControl(entries: existingMediaItems),
const VerticalPadding(),
MediaUploadsControl(
entryMediaItems: newMediaItems,
),
buildButtonBar(context, manager),
],
),
),
),
);
final submittingBody = 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('Submitting New $statusType'),
],
),
),
),
),
],
);
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
title: Text(widget.id.isEmpty ? 'New $statusType' : 'Edit $statusType'),
),
body: isSubmitting ? submittingBody : mainBody,
);
}
Widget buildContentField(BuildContext context) {
return MultiTriggerAutocomplete(
textEditingController: contentController,
focusNode: focusNode,
optionsAlignment: OptionsAlignment.bottomEnd,
autocompleteTriggers: [
AutocompleteTrigger(
trigger: '@',
optionsViewBuilder: (context, autocompleteQuery, controller) {
return MentionAutocompleteOptions(
query: autocompleteQuery.query,
onMentionUserTap: (user) {
final autocomplete = MultiTriggerAutocomplete.of(context);
return autocomplete.acceptAutocompleteOption(user.handle);
},
);
},
),
AutocompleteTrigger(
trigger: '#',
optionsViewBuilder: (context, autocompleteQuery, controller) {
return HashtagAutocompleteOptions(
query: autocompleteQuery.query,
onHashtagTap: (hashtag) {
final autocomplete = MultiTriggerAutocomplete.of(context);
return autocomplete.acceptAutocompleteOption(hashtag);
},
);
},
),
],
fieldViewBuilder: (context, controller, focusNode) => TextFormField(
focusNode: focusNode,
readOnly: isSubmitting,
enabled: !isSubmitting,
maxLines: 10,
controller: controller,
decoration: InputDecoration(
labelText: '$statusType Content',
alignLabelWithHint: true,
border: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).backgroundColor,
),
borderRadius: BorderRadius.circular(5.0),
),
),
),
);
}
Widget buildCommentPreview(BuildContext context, TimelineEntry entry) {
_logger.finest('Build preview');
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: [
StatusHeaderControl(
entry: entry,
openRemote: false,
showOpenControl: false,
),
const VerticalPadding(height: 3),
if (entry.spoilerText.isNotEmpty) ...[
Text(
'Content Summary: ${entry.spoilerText}',
style: Theme.of(context).textTheme.bodyLarge,
),
const VerticalPadding(height: 3)
],
HtmlWidget(entry.body),
],
),
),
),
const VerticalPadding(),
],
);
}
Widget buildButtonBar(BuildContext context, TimelineManager manager) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: isSubmitting ? null : () => createStatus(context, manager),
child: const Text('Submit'),
),
const HorizontalPadding(),
ElevatedButton(
onPressed: isSubmitting
? null
: () {
context.pop();
},
child: const Text('Cancel'),
),
],
);
}
}