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

310 lines
9.9 KiB
Dart
Raw Normal View History

2020-09-22 23:50:18 +02:00
import 'package:flutter/cupertino.dart';
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-09-22 23:50:18 +02:00
import '../util/goto.dart';
2020-10-24 00:12:42 +02:00
import '../util/pictrs.dart';
2021-01-03 18:03:59 +01:00
import '../util/unawaited.dart';
2021-02-24 20:52:18 +01:00
import '../widgets/markdown_mode_icon.dart';
2020-09-22 23:50:18 +02:00
import '../widgets/markdown_text.dart';
2021-02-09 15:12:13 +01:00
import '../widgets/radio_picker.dart';
2020-09-22 23:50:18 +02:00
import 'full_post.dart';
2020-09-30 19:05:00 +02:00
/// Fab that triggers the [CreatePost] modal
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(
onPressed: loggedInAction((_) => showCupertinoModalPopup(
context: context,
builder: (_) => community == null
? const CreatePostPage()
: CreatePostPage.toCommunity(community!))),
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-01-24 20:01:55 +01:00
class CreatePostPage extends HookWidget {
final CommunityView? community;
2020-09-22 23:50:18 +02:00
2021-03-10 08:34:30 +01:00
const CreatePostPage() : community = null;
const CreatePostPage.toCommunity(CommunityView this.community);
2020-09-22 23:50:18 +02:00
@override
Widget build(BuildContext context) {
final urlController = useTextEditingController();
final titleController = useTextEditingController();
final bodyController = useTextEditingController();
final accStore = useAccountsStore();
2020-09-26 12:43:34 +02:00
final selectedInstance =
useState(community?.instanceHost ?? accStore.loggedInInstances.first);
final selectedCommunity = useState(community);
2020-09-22 23:50:18 +02:00
final showFancy = useState(false);
final nsfw = useState(false);
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
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,
auth: accStore.defaultTokenFor(selectedInstance.value)?.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.getImage(source: ImageSource.gallery);
// 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);
}
2020-12-04 23:26:47 +01:00
print(urlController.text);
2020-10-24 19:58:38 +02:00
// ignore: avoid_catches_without_on_clauses
} 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,
onChanged: (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),
],
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(),
)
];
}
}
}
// TODO: use lazy autocomplete
final communitiesDropdown = InputDecorator(
decoration: const InputDecoration(
contentPadding: EdgeInsets.symmetric(vertical: 1, horizontal: 20),
2021-02-09 15:12:13 +01:00
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-01-31 16:56:08 +01:00
onChanged: (communityId) => selectedCommunity.value =
allCommunitiesSnap.data
?.firstWhere((e) => e.community.id == communityId),
items: communitiesList(),
),
),
);
2020-10-24 00:12:42 +02:00
final url = Row(children: [
Expanded(
child: TextField(
2020-10-24 11:26:52 +02:00
enabled: pictrsDeleteToken.value == null,
2020-10-24 00:12:42 +02:00
controller: urlController,
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
2021-01-03 19:43:39 +01:00
? const CircularProgressIndicator()
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,
minLines: 1,
maxLines: 2,
decoration: InputDecoration(labelText: L10n.of(context)!.title),
2020-09-22 23:50:18 +02:00
);
final body = IndexedStack(
index: showFancy.value ? 1 : 0,
children: [
TextField(
controller: bodyController,
2020-10-24 22:43:29 +02:00
keyboardType: TextInputType.multiline,
2020-09-22 23:50:18 +02:00
maxLines: null,
2020-10-25 01:16:58 +02:00
minLines: 5,
decoration: InputDecoration(labelText: L10n.of(context)!.body),
2020-09-22 23:50:18 +02:00
),
Padding(
padding: const EdgeInsets.all(16),
child: MarkdownText(
bodyController.text,
instanceHost: selectedInstance.value,
2020-09-22 23:50:18 +02:00
),
2020-10-24 22:43:29 +02:00
),
2020-09-22 23:50:18 +02:00
],
);
handleSubmit(Jwt token) async {
if (selectedCommunity.value == null || titleController.text.isEmpty) {
2021-03-10 08:34:30 +01:00
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
content: Text('Choosing a community and a title is required'),
));
return;
}
2021-04-05 20:14:39 +02:00
final api = LemmyApiV3(selectedInstance.value);
2020-09-22 23:50:18 +02:00
delayed.start();
try {
2021-01-24 20:01:55 +01:00
final res = await 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,
2021-01-24 20:01:55 +01:00
auth: token.raw,
));
2021-01-03 18:03:59 +01:00
unawaited(goToReplace(context, (_) => FullPostPage.fromPostView(res)));
2020-09-22 23:50:18 +02:00
return;
// ignore: avoid_catches_without_on_clauses
} catch (e) {
2021-03-10 08:34:30 +01:00
ScaffoldMessenger.of(context)
2021-01-03 19:43:39 +01:00
.showSnackBar(const SnackBar(content: Text('Failed to post')));
2020-09-22 23:50:18 +02:00
}
delayed.cancel();
}
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,
communitiesDropdown,
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
2021-01-03 19:43:39 +01:00
? const CircularProgressIndicator()
: Text(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
),
);
}
}