1
0
mirror of https://github.com/krawieck/lemmur/ synced 2024-12-24 15:21:07 +01:00
lemmur-app-android/lib/pages/create_post.dart

367 lines
12 KiB
Dart
Raw Normal View History

2020-09-22 23:50:18 +02:00
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
2020-10-24 00:12:42 +02:00
import 'package:image_picker/image_picker.dart';
2021-01-24 20:01:55 +01:00
import 'package:lemmy_api_client/pictrs.dart';
2021-04-05 20:14:39 +02:00
import 'package:lemmy_api_client/v3.dart';
2020-09-22 23:50:18 +02:00
import '../hooks/delayed_loading.dart';
2020-10-24 00:12:42 +02:00
import '../hooks/image_picker.dart';
2020-09-22 23:50:18 +02:00
import '../hooks/logged_in_action.dart';
import '../hooks/memo_future.dart';
2020-09-22 23:50:18 +02:00
import '../hooks/stores.dart';
2021-03-01 14:21:45 +01:00
import '../l10n/l10n.dart';
2021-01-31 16:56:08 +01:00
import '../util/extensions/api.dart';
2020-10-23 00:38:25 +02:00
import '../util/extensions/spaced.dart';
2020-10-24 00:12:42 +02:00
import '../util/pictrs.dart';
2021-04-21 21:05:15 +02:00
import '../widgets/editor.dart';
2021-02-24 20:52:18 +01:00
import '../widgets/markdown_mode_icon.dart';
2021-02-09 15:12:13 +01:00
import '../widgets/radio_picker.dart';
2021-09-11 01:04:15 +02:00
import 'full_post/full_post.dart';
2020-09-22 23:50:18 +02:00
2020-09-30 19:05:00 +02:00
/// Fab that triggers the [CreatePost] modal
2021-04-21 18:59:46 +02:00
/// After creation it will navigate to the newly created post
2020-09-22 23:50:18 +02:00
class CreatePostFab extends HookWidget {
final CommunityView? community;
2021-02-19 14:03:19 +01:00
const CreatePostFab({this.community});
2021-01-03 19:43:39 +01:00
2020-09-22 23:50:18 +02:00
@override
Widget build(BuildContext context) {
final loggedInAction = useAnyLoggedInAction();
2020-09-22 23:50:18 +02:00
return FloatingActionButton(
2021-04-21 18:59:46 +02:00
onPressed: loggedInAction((_) async {
final postView = await Navigator.of(context).push(
community == null
? CreatePostPage.route()
: CreatePostPage.toCommunityRoute(community!),
2021-04-21 18:59:46 +02:00
);
if (postView != null) {
2021-10-19 19:29:26 +02:00
await Navigator.of(context)
.push(FullPostPage.fromPostViewRoute(postView));
2021-04-21 18:59:46 +02:00
}
}),
2021-01-03 19:43:39 +01:00
child: const Icon(Icons.add),
2020-09-22 23:50:18 +02:00
);
}
}
2020-09-30 19:05:00 +02:00
/// Modal for creating a post to some community in some instance
2021-04-21 18:59:46 +02:00
/// Pops the navigator stack with a [PostView]
2021-01-24 20:01:55 +01:00
class CreatePostPage extends HookWidget {
final CommunityView? community;
2020-09-22 23:50:18 +02:00
2021-04-17 23:59:33 +02:00
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)
2021-04-17 23:59:33 +02:00
: _isEdit = true,
community = null;
2020-09-22 23:50:18 +02:00
@override
Widget build(BuildContext context) {
2021-04-17 23:59:33 +02:00
final urlController =
useTextEditingController(text: _isEdit ? post?.url : null);
final titleController =
useTextEditingController(text: _isEdit ? post?.name : null);
final bodyController =
useTextEditingController(text: _isEdit ? post?.body : null);
2020-09-22 23:50:18 +02:00
final accStore = useAccountsStore();
2021-04-17 23:59:33 +02:00
final selectedInstance = useState(_isEdit
? post!.instanceHost
: community?.instanceHost ?? accStore.loggedInInstances.first);
final selectedCommunity = useState(community);
2020-09-22 23:50:18 +02:00
final showFancy = useState(false);
2021-04-17 23:59:33 +02:00
final nsfw = useState(_isEdit && post!.nsfw);
2020-09-22 23:50:18 +02:00
final delayed = useDelayedLoading();
2020-10-24 00:12:42 +02:00
final imagePicker = useImagePicker();
final imageUploadLoading = useState(false);
final pictrsDeleteToken = useState<PictrsUploadFile?>(null);
final loggedInAction = useLoggedInAction(selectedInstance.value);
2020-09-22 23:50:18 +02:00
2021-04-11 17:19:44 +02:00
final titleFocusNode = useFocusNode();
final bodyFocusNode = useFocusNode();
2020-09-29 15:00:13 +02:00
final allCommunitiesSnap = useMemoFuture(
2021-04-05 20:14:39 +02:00
() => LemmyApiV3(selectedInstance.value)
2021-01-24 20:01:55 +01:00
.run(ListCommunities(
2021-01-31 16:56:08 +01:00
type: PostListingType.all,
2021-01-24 20:01:55 +01:00
sort: SortType.hot,
limit: 9999,
2021-04-11 18:27:22 +02:00
auth: accStore.defaultUserDataFor(selectedInstance.value)?.jwt.raw,
2021-01-24 20:01:55 +01:00
))
.then(
(value) {
2021-01-24 20:01:55 +01:00
value.sort((a, b) => a.community.name.compareTo(b.community.name));
return value;
},
),
[selectedInstance.value],
);
uploadPicture(Jwt token) async {
2020-10-24 19:58:38 +02:00
try {
final pic = await imagePicker.pickImage(source: ImageSource.gallery);
2020-10-24 19:58:38 +02:00
// pic is null when the picker was cancelled
if (pic != null) {
imageUploadLoading.value = true;
2020-10-24 11:26:52 +02:00
2021-01-24 20:01:55 +01:00
final pictrs = PictrsApi(selectedInstance.value);
2020-12-04 23:26:47 +01:00
final upload =
await pictrs.upload(filePath: pic.path, auth: token.raw);
2020-10-24 19:58:38 +02:00
pictrsDeleteToken.value = upload.files[0];
urlController.text =
pathToPictrs(selectedInstance.value, upload.files[0].file);
}
} catch (e) {
2021-03-10 08:34:30 +01:00
ScaffoldMessenger.of(context).showSnackBar(
2021-01-03 19:43:39 +01:00
const SnackBar(content: Text('Failed to upload image')));
2020-10-24 19:58:38 +02:00
} finally {
2020-10-24 00:12:42 +02:00
imageUploadLoading.value = false;
}
}
removePicture(PictrsUploadFile deleteToken) {
PictrsApi(selectedInstance.value).delete(deleteToken).catchError((_) {});
2020-10-24 11:26:52 +02:00
pictrsDeleteToken.value = null;
urlController.text = '';
}
2021-02-09 15:12:13 +01:00
final instanceDropdown = RadioPicker<String>(
values: accStore.loggedInInstances.toList(),
groupValue: selectedInstance.value,
2021-04-17 23:59:33 +02:00
onChanged: _isEdit ? null : (value) => selectedInstance.value = value,
2021-02-09 15:12:13 +01:00
buttonBuilder: (context, displayValue, onPressed) => TextButton(
onPressed: onPressed,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(displayValue),
const Icon(Icons.arrow_drop_down),
],
2020-09-22 23:50:18 +02:00
),
),
);
2021-02-19 14:03:19 +01:00
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(),
)
];
}
}
}
2021-04-11 17:19:44 +02:00
handleSubmit(Jwt token) async {
2021-04-17 23:59:33 +02:00
if ((!_isEdit && selectedCommunity.value == null) ||
titleController.text.isEmpty) {
2021-04-11 17:19:44 +02:00
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 {
2021-04-17 23:59:33 +02:00
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,
));
}
}();
2021-04-21 18:59:46 +02:00
Navigator.of(context).pop(res);
2021-04-11 17:19:44 +02:00
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(
2021-04-17 23:59:33 +02:00
contentPadding: EdgeInsets.symmetric(vertical: 1, horizontal: 20),
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(10)),
),
),
child: DropdownButtonHideUnderline(
2021-01-31 16:56:08 +01:00
child: DropdownButton<int>(
value: selectedCommunity.value?.community.id,
hint: Text(L10n.of(context).community),
2021-04-17 23:59:33 +02:00
onChanged: _isEdit
? null
: (communityId) {
selectedCommunity.value = allCommunitiesSnap.data
?.firstWhere((e) => e.community.id == communityId);
},
items: communitiesList(),
),
),
);
2021-04-17 23:59:33 +02:00
final enabledUrlField = pictrsDeleteToken.value == null;
2020-10-24 00:12:42 +02:00
final url = Row(children: [
Expanded(
child: TextField(
2021-04-17 23:59:33 +02:00
enabled: enabledUrlField,
2020-10-24 00:12:42 +02:00
controller: urlController,
2021-04-17 23:59:33 +02:00
autofillHints: enabledUrlField ? const [AutofillHints.url] : null,
2021-04-11 17:19:44 +02:00
keyboardType: TextInputType.url,
onSubmitted: (_) => titleFocusNode.requestFocus(),
2021-03-01 14:21:45 +01:00
decoration: InputDecoration(
labelText: L10n.of(context).url,
2021-03-01 14:21:45 +01:00
suffixIcon: const Icon(Icons.link),
2021-02-09 15:12:13 +01:00
),
2020-10-24 00:12:42 +02:00
),
),
2021-01-03 19:43:39 +01:00
const SizedBox(width: 5),
2020-10-24 00:12:42 +02:00
IconButton(
icon: imageUploadLoading.value
? const CircularProgressIndicator.adaptive()
2020-10-24 11:26:52 +02:00
: Icon(pictrsDeleteToken.value == null
? Icons.add_photo_alternate
: Icons.close),
onPressed: pictrsDeleteToken.value == null
? loggedInAction(uploadPicture)
: () => removePicture(pictrsDeleteToken.value!),
2020-10-24 11:26:52 +02:00
tooltip:
pictrsDeleteToken.value == null ? 'Add picture' : 'Delete picture',
2020-10-24 00:12:42 +02:00
)
]);
2020-09-22 23:50:18 +02:00
final title = TextField(
controller: titleController,
2021-04-11 17:19:44 +02:00
focusNode: titleFocusNode,
keyboardType: TextInputType.text,
textCapitalization: TextCapitalization.sentences,
onSubmitted: (_) => bodyFocusNode.requestFocus(),
2020-09-22 23:50:18 +02:00
minLines: 1,
maxLines: 2,
decoration: InputDecoration(labelText: L10n.of(context).title),
2020-09-22 23:50:18 +02:00
);
2021-04-21 21:05:15 +02:00
final body = Editor(
controller: bodyController,
focusNode: bodyFocusNode,
onSubmitted: (_) =>
delayed.pending ? () {} : loggedInAction(handleSubmit),
labelText: L10n.of(context).body,
2021-04-21 21:05:15 +02:00
instanceHost: selectedInstance.value,
fancy: showFancy.value,
2020-09-22 23:50:18 +02:00
);
return Scaffold(
appBar: AppBar(
2021-02-09 15:12:13 +01:00
leading: const CloseButton(),
2020-09-22 23:50:18 +02:00
actions: [
IconButton(
2021-02-24 20:52:18 +01:00
icon: markdownModeIcon(fancy: showFancy.value),
2020-09-22 23:50:18 +02:00
onPressed: () => showFancy.value = !showFancy.value,
),
],
),
2020-09-29 15:00:50 +02:00
body: SafeArea(
2020-10-24 22:43:29 +02:00
child: ListView(
2021-01-03 19:43:39 +01:00
padding: const EdgeInsets.all(5),
2020-10-23 00:38:25 +02:00
children: [
2020-09-29 15:00:50 +02:00
instanceDropdown,
2021-04-21 21:30:21 +02:00
if (!_isEdit) communitiesDropdown,
2020-09-29 15:00:50 +02:00
url,
title,
2020-10-24 22:43:29 +02:00
body,
2020-09-29 15:00:50 +02:00
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;
},
2020-09-29 15:00:50 +02:00
),
Text(L10n.of(context).nsfw)
2020-09-29 15:00:50 +02:00
],
),
2020-09-22 23:50:18 +02:00
),
2021-02-09 15:12:13 +01:00
TextButton(
onPressed:
delayed.pending ? () {} : loggedInAction(handleSubmit),
2020-09-29 15:00:50 +02:00
child: delayed.loading
? const CircularProgressIndicator.adaptive()
2021-04-17 23:59:33 +02:00
: Text(_isEdit
? L10n.of(context).edit
: L10n.of(context).post),
2020-09-29 15:00:50 +02:00
)
],
),
2020-10-23 00:38:25 +02:00
].spaced(6),
2020-09-29 15:00:50 +02:00
),
2020-09-22 23:50:18 +02:00
),
);
}
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,
);
2020-09-22 23:50:18 +02:00
}