lemmur-app-android/lib/pages/create_post.dart

367 lines
12 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:image_picker/image_picker.dart';
import 'package:lemmy_api_client/pictrs.dart';
import 'package:lemmy_api_client/v3.dart';
import '../hooks/delayed_loading.dart';
import '../hooks/image_picker.dart';
import '../hooks/logged_in_action.dart';
import '../hooks/memo_future.dart';
import '../hooks/stores.dart';
import '../l10n/l10n.dart';
import '../util/extensions/api.dart';
import '../util/extensions/spaced.dart';
import '../util/pictrs.dart';
import '../widgets/editor.dart';
import '../widgets/markdown_mode_icon.dart';
import '../widgets/radio_picker.dart';
import 'full_post/full_post.dart';
/// Fab that triggers the [CreatePost] modal
/// After creation it will navigate to the newly created post
class CreatePostFab extends HookWidget {
final CommunityView? community;
const CreatePostFab({this.community});
@override
Widget build(BuildContext context) {
final loggedInAction = useAnyLoggedInAction();
return FloatingActionButton(
onPressed: loggedInAction((_) async {
final postView = await Navigator.of(context).push(
community == null
? CreatePostPage.route()
: CreatePostPage.toCommunityRoute(community!),
);
if (postView != null) {
await Navigator.of(context)
.push(FullPostPage.fromPostViewRoute(postView));
}
}),
child: const Icon(Icons.add),
);
}
}
/// Modal for creating a post to some community in some instance
/// Pops the navigator stack with a [PostView]
class CreatePostPage extends HookWidget {
final CommunityView? community;
final bool _isEdit;
final Post? post;
const CreatePostPage()
: community = null,
_isEdit = false,
post = null;
const CreatePostPage.toCommunity(CommunityView this.community)
: _isEdit = false,
post = null;
const CreatePostPage.edit(Post this.post)
: _isEdit = true,
community = null;
@override
Widget build(BuildContext context) {
final urlController =
useTextEditingController(text: _isEdit ? post?.url : null);
final titleController =
useTextEditingController(text: _isEdit ? post?.name : null);
final bodyController =
useTextEditingController(text: _isEdit ? post?.body : null);
final accStore = useAccountsStore();
final selectedInstance = useState(_isEdit
? post!.instanceHost
: community?.instanceHost ?? accStore.loggedInInstances.first);
final selectedCommunity = useState(community);
final showFancy = useState(false);
final nsfw = useState(_isEdit && post!.nsfw);
final delayed = useDelayedLoading();
final imagePicker = useImagePicker();
final imageUploadLoading = useState(false);
final pictrsDeleteToken = useState<PictrsUploadFile?>(null);
final loggedInAction = useLoggedInAction(selectedInstance.value);
final titleFocusNode = useFocusNode();
final bodyFocusNode = useFocusNode();
final allCommunitiesSnap = useMemoFuture(
() => LemmyApiV3(selectedInstance.value)
.run(ListCommunities(
type: PostListingType.all,
sort: SortType.hot,
limit: 9999,
auth: accStore.defaultUserDataFor(selectedInstance.value)?.jwt.raw,
))
.then(
(value) {
value.sort((a, b) => a.community.name.compareTo(b.community.name));
return value;
},
),
[selectedInstance.value],
);
uploadPicture(Jwt token) async {
try {
final pic = await imagePicker.pickImage(source: ImageSource.gallery);
// pic is null when the picker was cancelled
if (pic != null) {
imageUploadLoading.value = true;
final pictrs = PictrsApi(selectedInstance.value);
final upload =
await pictrs.upload(filePath: pic.path, auth: token.raw);
pictrsDeleteToken.value = upload.files[0];
urlController.text =
pathToPictrs(selectedInstance.value, upload.files[0].file);
}
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Failed to upload image')));
} finally {
imageUploadLoading.value = false;
}
}
removePicture(PictrsUploadFile deleteToken) {
PictrsApi(selectedInstance.value).delete(deleteToken).catchError((_) {});
pictrsDeleteToken.value = null;
urlController.text = '';
}
final instanceDropdown = RadioPicker<String>(
values: accStore.loggedInInstances.toList(),
groupValue: selectedInstance.value,
onChanged: _isEdit ? null : (value) => selectedInstance.value = value,
buttonBuilder: (context, displayValue, onPressed) => TextButton(
onPressed: onPressed,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(displayValue),
const Icon(Icons.arrow_drop_down),
],
),
),
);
DropdownMenuItem<int> communityDropDownItem(CommunityView e) =>
DropdownMenuItem(
value: e.community.id,
child: Text(e.community.local
? e.community.name
: '${e.community.originInstanceHost}/${e.community.name}'),
);
List<DropdownMenuItem<int>> communitiesList() {
if (allCommunitiesSnap.hasData) {
return allCommunitiesSnap.data!.map(communityDropDownItem).toList();
} else {
if (selectedCommunity.value != null) {
return [communityDropDownItem(selectedCommunity.value!)];
} else {
return const [
DropdownMenuItem(
value: -1,
child: CircularProgressIndicator.adaptive(),
)
];
}
}
}
handleSubmit(Jwt token) async {
if ((!_isEdit && selectedCommunity.value == null) ||
titleController.text.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
content: Text('Choosing a community and a title is required'),
));
return;
}
final api = LemmyApiV3(selectedInstance.value);
delayed.start();
try {
final res = await () {
if (_isEdit) {
return api.run(EditPost(
url: urlController.text.isEmpty ? null : urlController.text,
body: bodyController.text.isEmpty ? null : bodyController.text,
nsfw: nsfw.value,
name: titleController.text,
postId: post!.id,
auth: token.raw,
));
} else {
return api.run(CreatePost(
url: urlController.text.isEmpty ? null : urlController.text,
body: bodyController.text.isEmpty ? null : bodyController.text,
nsfw: nsfw.value,
name: titleController.text,
communityId: selectedCommunity.value!.community.id,
auth: token.raw,
));
}
}();
Navigator.of(context).pop(res);
return;
} catch (e) {
ScaffoldMessenger.of(context)
.showSnackBar(const SnackBar(content: Text('Failed to post')));
}
delayed.cancel();
}
// TODO: use lazy autocomplete
final communitiesDropdown = InputDecorator(
decoration: const InputDecoration(
contentPadding: EdgeInsets.symmetric(vertical: 1, horizontal: 20),
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(10)),
),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<int>(
value: selectedCommunity.value?.community.id,
hint: Text(L10n.of(context).community),
onChanged: _isEdit
? null
: (communityId) {
selectedCommunity.value = allCommunitiesSnap.data
?.firstWhere((e) => e.community.id == communityId);
},
items: communitiesList(),
),
),
);
final enabledUrlField = pictrsDeleteToken.value == null;
final url = Row(children: [
Expanded(
child: TextField(
enabled: enabledUrlField,
controller: urlController,
autofillHints: enabledUrlField ? const [AutofillHints.url] : null,
keyboardType: TextInputType.url,
onSubmitted: (_) => titleFocusNode.requestFocus(),
decoration: InputDecoration(
labelText: L10n.of(context).url,
suffixIcon: const Icon(Icons.link),
),
),
),
const SizedBox(width: 5),
IconButton(
icon: imageUploadLoading.value
? const CircularProgressIndicator.adaptive()
: Icon(pictrsDeleteToken.value == null
? Icons.add_photo_alternate
: Icons.close),
onPressed: pictrsDeleteToken.value == null
? loggedInAction(uploadPicture)
: () => removePicture(pictrsDeleteToken.value!),
tooltip:
pictrsDeleteToken.value == null ? 'Add picture' : 'Delete picture',
)
]);
final title = TextField(
controller: titleController,
focusNode: titleFocusNode,
keyboardType: TextInputType.text,
textCapitalization: TextCapitalization.sentences,
onSubmitted: (_) => bodyFocusNode.requestFocus(),
minLines: 1,
maxLines: 2,
decoration: InputDecoration(labelText: L10n.of(context).title),
);
final body = Editor(
controller: bodyController,
focusNode: bodyFocusNode,
onSubmitted: (_) =>
delayed.pending ? () {} : loggedInAction(handleSubmit),
labelText: L10n.of(context).body,
instanceHost: selectedInstance.value,
fancy: showFancy.value,
);
return Scaffold(
appBar: AppBar(
leading: const CloseButton(),
actions: [
IconButton(
icon: markdownModeIcon(fancy: showFancy.value),
onPressed: () => showFancy.value = !showFancy.value,
),
],
),
body: SafeArea(
child: ListView(
padding: const EdgeInsets.all(5),
children: [
instanceDropdown,
if (!_isEdit) communitiesDropdown,
url,
title,
body,
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
GestureDetector(
onTap: () => nsfw.value = !nsfw.value,
child: Row(
children: [
Checkbox(
value: nsfw.value,
onChanged: (val) {
if (val != null) nsfw.value = val;
},
),
Text(L10n.of(context).nsfw)
],
),
),
TextButton(
onPressed:
delayed.pending ? () {} : loggedInAction(handleSubmit),
child: delayed.loading
? const CircularProgressIndicator.adaptive()
: Text(_isEdit
? L10n.of(context).edit
: L10n.of(context).post),
)
],
),
].spaced(6),
),
),
);
}
static Route<PostView> route() => MaterialPageRoute(
builder: (context) => const CreatePostPage(),
fullscreenDialog: true,
);
static Route<PostView> toCommunityRoute(CommunityView community) =>
MaterialPageRoute(
builder: (context) => CreatePostPage.toCommunity(community),
fullscreenDialog: true,
);
static Route<PostView> editRoute(Post post) => MaterialPageRoute(
builder: (context) => CreatePostPage.edit(post),
fullscreenDialog: true,
);
}