Merge pull request #320 from LemmurOrg/feature/create-post-redesign
This commit is contained in:
commit
28be50a89e
|
@ -21,6 +21,10 @@ jobs:
|
|||
git fetch weblate
|
||||
git merge weblate/master
|
||||
|
||||
- name: Regenerate l10n_from_string
|
||||
run: |
|
||||
dart run scripts/gen_l10n_from_string.dart
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v3.12.0
|
||||
with:
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
### Added
|
||||
|
||||
- Create post community picker now has autocomplete
|
||||
- You can now add an instance from the three dots menu on the instance page
|
||||
|
||||
## v0.8.0 - 2022-01-14
|
||||
|
|
|
@ -345,5 +345,11 @@
|
|||
"add_instance": "Add instance",
|
||||
"@add_instance": {},
|
||||
"instance_added": "Instance successfully added",
|
||||
"@instance_added": {}
|
||||
"@instance_added": {},
|
||||
"required_field": "required field",
|
||||
"@required_field": {},
|
||||
"no_communities_found": "No communities found",
|
||||
"@no_communities_found": {},
|
||||
"network_error": "Network error",
|
||||
"@network_error": {}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,6 @@ import 'util/hot_rank.dart';
|
|||
enum CommentSortType {
|
||||
hot,
|
||||
top,
|
||||
// ignore: constant_identifier_names
|
||||
new_,
|
||||
old,
|
||||
chat,
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
|
||||
ImagePicker useImagePicker() => useMemoized(ImagePicker.new);
|
|
@ -123,6 +123,7 @@ abstract class L10nStrings {
|
|||
static const number_of_posts = 'number_of_posts';
|
||||
static const number_of_subscribers = 'number_of_subscribers';
|
||||
static const number_of_users = 'number_of_users';
|
||||
static const number_of_communities = 'number_of_communities';
|
||||
static const unsubscribe = 'unsubscribe';
|
||||
static const subscribe = 'subscribe';
|
||||
static const messages = 'messages';
|
||||
|
@ -145,6 +146,24 @@ abstract class L10nStrings {
|
|||
static const bot_account = 'bot_account';
|
||||
static const show_bot_accounts = 'show_bot_accounts';
|
||||
static const show_read_posts = 'show_read_posts';
|
||||
static const site_not_set_up = 'site_not_set_up';
|
||||
static const nerd_stuff = 'nerd_stuff';
|
||||
static const open_in_browser = 'open_in_browser';
|
||||
static const cannot_open_in_browser = 'cannot_open_in_browser';
|
||||
static const about = 'about';
|
||||
static const see_all = 'see_all';
|
||||
static const admins = 'admins';
|
||||
static const trending_communities = 'trending_communities';
|
||||
static const communities_of_instance = 'communities_of_instance';
|
||||
static const day = 'day';
|
||||
static const week = 'week';
|
||||
static const month = 'month';
|
||||
static const six_months = 'six_months';
|
||||
static const add_instance = 'add_instance';
|
||||
static const instance_added = 'instance_added';
|
||||
static const required_field = 'required_field';
|
||||
static const no_communities_found = 'no_communities_found';
|
||||
static const network_error = 'network_error';
|
||||
}
|
||||
|
||||
extension L10nFromString on String {
|
||||
|
@ -406,6 +425,40 @@ extension L10nFromString on String {
|
|||
return L10n.of(context).show_bot_accounts;
|
||||
case L10nStrings.show_read_posts:
|
||||
return L10n.of(context).show_read_posts;
|
||||
case L10nStrings.site_not_set_up:
|
||||
return L10n.of(context).site_not_set_up;
|
||||
case L10nStrings.nerd_stuff:
|
||||
return L10n.of(context).nerd_stuff;
|
||||
case L10nStrings.open_in_browser:
|
||||
return L10n.of(context).open_in_browser;
|
||||
case L10nStrings.cannot_open_in_browser:
|
||||
return L10n.of(context).cannot_open_in_browser;
|
||||
case L10nStrings.about:
|
||||
return L10n.of(context).about;
|
||||
case L10nStrings.see_all:
|
||||
return L10n.of(context).see_all;
|
||||
case L10nStrings.admins:
|
||||
return L10n.of(context).admins;
|
||||
case L10nStrings.trending_communities:
|
||||
return L10n.of(context).trending_communities;
|
||||
case L10nStrings.day:
|
||||
return L10n.of(context).day;
|
||||
case L10nStrings.week:
|
||||
return L10n.of(context).week;
|
||||
case L10nStrings.month:
|
||||
return L10n.of(context).month;
|
||||
case L10nStrings.six_months:
|
||||
return L10n.of(context).six_months;
|
||||
case L10nStrings.add_instance:
|
||||
return L10n.of(context).add_instance;
|
||||
case L10nStrings.instance_added:
|
||||
return L10n.of(context).instance_added;
|
||||
case L10nStrings.required_field:
|
||||
return L10n.of(context).required_field;
|
||||
case L10nStrings.no_communities_found:
|
||||
return L10n.of(context).no_communities_found;
|
||||
case L10nStrings.network_error:
|
||||
return L10n.of(context).network_error;
|
||||
|
||||
default:
|
||||
return this;
|
||||
|
|
|
@ -16,7 +16,7 @@ import '../../util/share.dart';
|
|||
import '../../widgets/failed_to_load.dart';
|
||||
import '../../widgets/reveal_after_scroll.dart';
|
||||
import '../../widgets/sortable_infinite_list.dart';
|
||||
import '../create_post.dart';
|
||||
import '../create_post/create_post_fab.dart';
|
||||
import 'community_about_tab.dart';
|
||||
import 'community_more_menu.dart';
|
||||
import 'community_overview.dart';
|
||||
|
|
|
@ -1,366 +0,0 @@
|
|||
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,
|
||||
);
|
||||
}
|
|
@ -0,0 +1,175 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:lemmy_api_client/v3.dart';
|
||||
import 'package:wc_form_validators/wc_form_validators.dart';
|
||||
|
||||
import '../../hooks/logged_in_action.dart';
|
||||
import '../../hooks/stores.dart';
|
||||
import '../../l10n/l10n.dart';
|
||||
import '../../stores/accounts_store.dart';
|
||||
import '../../util/async_store_listener.dart';
|
||||
import '../../util/extensions/spaced.dart';
|
||||
import '../../util/mobx_provider.dart';
|
||||
import '../../util/observer_consumers.dart';
|
||||
import '../../widgets/editor.dart';
|
||||
import '../../widgets/markdown_mode_icon.dart';
|
||||
import 'create_post_community_picker.dart';
|
||||
import 'create_post_instance_picker.dart';
|
||||
import 'create_post_store.dart';
|
||||
import 'create_post_url_field.dart';
|
||||
|
||||
/// Modal for creating a post to some community in some instance
|
||||
/// Pops the navigator stack with a [PostView]
|
||||
class CreatePostPage extends HookWidget {
|
||||
const CreatePostPage();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final formKey = useMemoized(GlobalKey<FormState>.new);
|
||||
final loggedInAction = useLoggedInAction(
|
||||
useStore((CreatePostStore store) => store.instanceHost),
|
||||
);
|
||||
|
||||
final titleFocusNode = useFocusNode();
|
||||
final bodyFocusNode = useFocusNode();
|
||||
|
||||
handleSubmit(Jwt token) async {
|
||||
if (formKey.currentState!.validate()) {
|
||||
await context.read<CreatePostStore>().submit(token);
|
||||
}
|
||||
}
|
||||
|
||||
final title = ObserverBuilder<CreatePostStore>(
|
||||
builder: (context, store) => TextFormField(
|
||||
initialValue: store.title,
|
||||
focusNode: titleFocusNode,
|
||||
keyboardType: TextInputType.text,
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
textInputAction: TextInputAction.next,
|
||||
validator: Validators.required(L10n.of(context).required_field),
|
||||
onFieldSubmitted: (_) => bodyFocusNode.requestFocus(),
|
||||
onChanged: (title) => store.title = title,
|
||||
minLines: 1,
|
||||
maxLines: 2,
|
||||
decoration: InputDecoration(labelText: L10n.of(context).title),
|
||||
),
|
||||
);
|
||||
|
||||
final body = ObserverBuilder<CreatePostStore>(
|
||||
builder: (context, store) => Editor(
|
||||
initialValue: store.body,
|
||||
focusNode: bodyFocusNode,
|
||||
onChanged: (body) => store.body = body,
|
||||
labelText: L10n.of(context).body,
|
||||
instanceHost: store.instanceHost,
|
||||
fancy: store.showFancy,
|
||||
),
|
||||
);
|
||||
|
||||
return AsyncStoreListener<PostView>(
|
||||
asyncStore: context.read<CreatePostStore>().submitState,
|
||||
onSuccess: (context, data) {
|
||||
Navigator.of(context).pop(data);
|
||||
},
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
actions: [
|
||||
ObserverBuilder<CreatePostStore>(
|
||||
builder: (context, store) => IconButton(
|
||||
icon: markdownModeIcon(fancy: store.showFancy),
|
||||
onPressed: () => store.showFancy = !store.showFancy,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(5),
|
||||
child: Form(
|
||||
key: formKey,
|
||||
child: Column(
|
||||
children: [
|
||||
if (!context.read<CreatePostStore>().isEdit) ...const [
|
||||
CreatePostInstancePicker(),
|
||||
CreatePostCommunityPicker(),
|
||||
],
|
||||
CreatePostUrlField(titleFocusNode),
|
||||
title,
|
||||
body,
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
ObserverBuilder<CreatePostStore>(
|
||||
builder: (context, store) => GestureDetector(
|
||||
onTap: () => store.nsfw = !store.nsfw,
|
||||
child: Row(
|
||||
children: [
|
||||
Checkbox(
|
||||
value: store.nsfw,
|
||||
onChanged: (val) {
|
||||
if (val != null) store.nsfw = val;
|
||||
},
|
||||
),
|
||||
Text(L10n.of(context).nsfw)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
ObserverBuilder<CreatePostStore>(
|
||||
builder: (context, store) => TextButton(
|
||||
onPressed: store.submitState.isLoading
|
||||
? () {}
|
||||
: loggedInAction(handleSubmit),
|
||||
child: store.submitState.isLoading
|
||||
? const CircularProgressIndicator.adaptive()
|
||||
: Text(
|
||||
store.isEdit
|
||||
? L10n.of(context).edit
|
||||
: L10n.of(context).post,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
].spaced(6),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static Route<PostView> route() => MaterialPageRoute(
|
||||
builder: (context) => MobxProvider(
|
||||
create: (context) => CreatePostStore(
|
||||
instanceHost: context.read<AccountsStore>().loggedInInstances.first,
|
||||
),
|
||||
child: const CreatePostPage(),
|
||||
),
|
||||
fullscreenDialog: true,
|
||||
);
|
||||
|
||||
static Route<PostView> toCommunityRoute(CommunityView community) =>
|
||||
MaterialPageRoute(
|
||||
builder: (context) => MobxProvider(
|
||||
create: (context) => CreatePostStore(
|
||||
instanceHost: community.instanceHost,
|
||||
selectedCommunity: community,
|
||||
),
|
||||
child: const CreatePostPage(),
|
||||
),
|
||||
fullscreenDialog: true,
|
||||
);
|
||||
|
||||
static Route<PostView> editRoute(Post post) => MaterialPageRoute(
|
||||
builder: (context) => MobxProvider(
|
||||
create: (context) => CreatePostStore(
|
||||
instanceHost: post.instanceHost,
|
||||
postToEdit: post,
|
||||
),
|
||||
child: const CreatePostPage(),
|
||||
),
|
||||
fullscreenDialog: true,
|
||||
);
|
||||
}
|
|
@ -0,0 +1,114 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:flutter_typeahead/flutter_typeahead.dart';
|
||||
import 'package:lemmy_api_client/v3.dart';
|
||||
import 'package:wc_form_validators/wc_form_validators.dart';
|
||||
|
||||
import '../../l10n/l10n.dart';
|
||||
import '../../util/async_store_listener.dart';
|
||||
import '../../util/extensions/api.dart';
|
||||
import '../../util/extensions/context.dart';
|
||||
import '../../util/observer_consumers.dart';
|
||||
import '../../widgets/avatar.dart';
|
||||
import 'create_post_store.dart';
|
||||
|
||||
class CreatePostCommunityPicker extends HookWidget {
|
||||
const CreatePostCommunityPicker({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final store = context.read<CreatePostStore>();
|
||||
final controller = useTextEditingController(
|
||||
text: store.selectedCommunity != null
|
||||
? _communityString(store.selectedCommunity!)
|
||||
: '',
|
||||
);
|
||||
|
||||
return AsyncStoreListener(
|
||||
asyncStore: context.read<CreatePostStore>().searchCommunitiesState,
|
||||
child: Focus(
|
||||
onFocusChange: (hasFocus) {
|
||||
if (!hasFocus && store.selectedCommunity == null) {
|
||||
controller.text = '';
|
||||
}
|
||||
},
|
||||
child: ObserverBuilder<CreatePostStore>(builder: (context, store) {
|
||||
return TypeAheadFormField<CommunityView>(
|
||||
textFieldConfiguration: TextFieldConfiguration(
|
||||
controller: controller,
|
||||
enabled: !store.isEdit,
|
||||
decoration: InputDecoration(
|
||||
hintText: L10n.of(context).community,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
vertical: 16,
|
||||
horizontal: 20,
|
||||
),
|
||||
suffixIcon:
|
||||
store.selectedCommunity == null && controller.text.isEmpty
|
||||
? const Icon(Icons.arrow_drop_down)
|
||||
: IconButton(
|
||||
onPressed: () {
|
||||
store.selectedCommunity = null;
|
||||
controller.clear();
|
||||
},
|
||||
icon: const Icon(Icons.close),
|
||||
),
|
||||
),
|
||||
onChanged: (_) => store.selectedCommunity = null,
|
||||
),
|
||||
validator: Validators.required(L10n.of(context).required_field),
|
||||
suggestionsCallback: (pattern) async {
|
||||
final communities = await store.searchCommunities(
|
||||
pattern,
|
||||
context.defaultJwt(store.instanceHost),
|
||||
);
|
||||
|
||||
return communities ?? [];
|
||||
},
|
||||
itemBuilder: (context, community) {
|
||||
return ListTile(
|
||||
leading: Avatar(
|
||||
url: community.community.icon,
|
||||
radius: 20,
|
||||
),
|
||||
title: Text(_communityString(community)),
|
||||
);
|
||||
},
|
||||
onSuggestionSelected: (community) {
|
||||
store.selectedCommunity = community;
|
||||
controller.text = _communityString(community);
|
||||
},
|
||||
noItemsFoundBuilder: (context) => SizedBox(
|
||||
width: double.infinity,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Text(
|
||||
L10n.of(context).no_communities_found,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
loadingBuilder: (context) => Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: const [
|
||||
Padding(
|
||||
padding: EdgeInsets.all(8),
|
||||
child: CircularProgressIndicator.adaptive(),
|
||||
),
|
||||
],
|
||||
),
|
||||
debounceDuration: const Duration(milliseconds: 400),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _communityString(CommunityView communityView) {
|
||||
if (communityView.community.local) {
|
||||
return communityView.community.title;
|
||||
} else {
|
||||
return '${communityView.community.originInstanceHost}/${communityView.community.title}';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:lemmy_api_client/v3.dart';
|
||||
|
||||
import '../../hooks/logged_in_action.dart';
|
||||
import '../full_post/full_post.dart';
|
||||
import 'create_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),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../stores/accounts_store.dart';
|
||||
import '../../util/observer_consumers.dart';
|
||||
import '../../widgets/radio_picker.dart';
|
||||
import 'create_post_store.dart';
|
||||
|
||||
class CreatePostInstancePicker extends StatelessWidget {
|
||||
const CreatePostInstancePicker({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final loggedInInstances =
|
||||
context.watch<AccountsStore>().loggedInInstances.toList();
|
||||
|
||||
return ObserverBuilder<CreatePostStore>(
|
||||
builder: (context, store) => RadioPicker<String>(
|
||||
values: loggedInInstances,
|
||||
groupValue: store.instanceHost,
|
||||
onChanged: store.isEdit ? null : (value) => store.instanceHost = value,
|
||||
buttonBuilder: (context, displayValue, onPressed) => TextButton(
|
||||
onPressed: onPressed,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(displayValue),
|
||||
const Icon(Icons.arrow_drop_down),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,170 @@
|
|||
import 'package:lemmy_api_client/pictrs.dart';
|
||||
import 'package:lemmy_api_client/v3.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
|
||||
import '../../util/async_store.dart';
|
||||
import '../../util/pictrs.dart';
|
||||
|
||||
part 'create_post_store.g.dart';
|
||||
|
||||
class CreatePostStore = _CreatePostStore with _$CreatePostStore;
|
||||
|
||||
abstract class _CreatePostStore with Store {
|
||||
final Post? postToEdit;
|
||||
bool get isEdit => postToEdit != null;
|
||||
|
||||
_CreatePostStore({
|
||||
required this.instanceHost,
|
||||
this.postToEdit,
|
||||
this.selectedCommunity,
|
||||
}) : title = postToEdit?.name ?? '',
|
||||
nsfw = postToEdit?.nsfw ?? false,
|
||||
body = postToEdit?.body ?? '',
|
||||
url = postToEdit?.url ?? '';
|
||||
|
||||
@observable
|
||||
bool showFancy = false;
|
||||
@observable
|
||||
String instanceHost;
|
||||
@observable
|
||||
CommunityView? selectedCommunity;
|
||||
@observable
|
||||
String url;
|
||||
@observable
|
||||
String title;
|
||||
@observable
|
||||
String body;
|
||||
@observable
|
||||
bool nsfw;
|
||||
|
||||
final submitState = AsyncStore<PostView>();
|
||||
final searchCommunitiesState = AsyncStore<List<CommunityView>>();
|
||||
final imageUploadState = AsyncStore<PictrsUploadFile>();
|
||||
|
||||
@computed
|
||||
bool get hasUploadedImage => imageUploadState.map(
|
||||
loading: () => false,
|
||||
error: (_) => false,
|
||||
data: (_) => true,
|
||||
);
|
||||
|
||||
@action
|
||||
Future<List<CommunityView>?> searchCommunities(
|
||||
String searchTerm,
|
||||
Jwt? token,
|
||||
) {
|
||||
if (searchTerm.isEmpty) {
|
||||
return searchCommunitiesState.runLemmy(
|
||||
instanceHost,
|
||||
ListCommunities(
|
||||
type: PostListingType.all,
|
||||
sort: SortType.topAll,
|
||||
limit: 10,
|
||||
auth: token?.raw,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return searchCommunitiesState.runLemmy(
|
||||
instanceHost,
|
||||
SearchCommunities(
|
||||
q: searchTerm,
|
||||
sort: SortType.topAll,
|
||||
listingType: PostListingType.all,
|
||||
limit: 10,
|
||||
auth: token?.raw,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
Future<void> submit(Jwt token) async {
|
||||
await submitState.runLemmy(
|
||||
instanceHost,
|
||||
isEdit
|
||||
? EditPost(
|
||||
url: url.isEmpty ? null : url,
|
||||
body: body.isEmpty ? null : body,
|
||||
nsfw: nsfw,
|
||||
name: title,
|
||||
postId: postToEdit!.id,
|
||||
auth: token.raw,
|
||||
)
|
||||
: CreatePost(
|
||||
url: url.isEmpty ? null : url,
|
||||
body: body.isEmpty ? null : body,
|
||||
nsfw: nsfw,
|
||||
name: title,
|
||||
communityId: selectedCommunity!.community.id,
|
||||
auth: token.raw,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@action
|
||||
Future<void> uploadImage(String filePath, Jwt token) async {
|
||||
final instanceHost = this.instanceHost;
|
||||
|
||||
final upload = await imageUploadState.run(
|
||||
() => PictrsApi(instanceHost)
|
||||
.upload(
|
||||
filePath: filePath,
|
||||
auth: token.raw,
|
||||
)
|
||||
.then((value) => value.files.single),
|
||||
);
|
||||
|
||||
if (upload != null) {
|
||||
url = pathToPictrs(instanceHost, upload.file);
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
void removeImage() {
|
||||
final pictrsFile = imageUploadState.map<PictrsUploadFile?>(
|
||||
data: (data) => data,
|
||||
loading: () => null,
|
||||
error: (_) => null,
|
||||
);
|
||||
if (pictrsFile == null) return;
|
||||
|
||||
PictrsApi(instanceHost).delete(pictrsFile).catchError((_) {});
|
||||
|
||||
imageUploadState.reset();
|
||||
url = '';
|
||||
}
|
||||
}
|
||||
|
||||
class SearchCommunities implements LemmyApiQuery<List<CommunityView>> {
|
||||
final Search base;
|
||||
|
||||
SearchCommunities({
|
||||
required String q,
|
||||
PostListingType? listingType,
|
||||
SortType? sort,
|
||||
int? page,
|
||||
int? limit,
|
||||
String? auth,
|
||||
}) : base = Search(
|
||||
q: q,
|
||||
type: SearchType.communities,
|
||||
listingType: listingType,
|
||||
sort: sort,
|
||||
page: page,
|
||||
limit: limit,
|
||||
auth: auth,
|
||||
);
|
||||
|
||||
@override
|
||||
String get path => base.path;
|
||||
|
||||
@override
|
||||
HttpMethod get httpMethod => base.httpMethod;
|
||||
|
||||
@override
|
||||
List<CommunityView> responseFactory(Map<String, dynamic> json) =>
|
||||
base.responseFactory(json).communities;
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() => base.toJson();
|
||||
}
|
|
@ -0,0 +1,180 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'create_post_store.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// StoreGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic
|
||||
|
||||
mixin _$CreatePostStore on _CreatePostStore, Store {
|
||||
Computed<bool>? _$hasUploadedImageComputed;
|
||||
|
||||
@override
|
||||
bool get hasUploadedImage => (_$hasUploadedImageComputed ??= Computed<bool>(
|
||||
() => super.hasUploadedImage,
|
||||
name: '_CreatePostStore.hasUploadedImage'))
|
||||
.value;
|
||||
|
||||
final _$showFancyAtom = Atom(name: '_CreatePostStore.showFancy');
|
||||
|
||||
@override
|
||||
bool get showFancy {
|
||||
_$showFancyAtom.reportRead();
|
||||
return super.showFancy;
|
||||
}
|
||||
|
||||
@override
|
||||
set showFancy(bool value) {
|
||||
_$showFancyAtom.reportWrite(value, super.showFancy, () {
|
||||
super.showFancy = value;
|
||||
});
|
||||
}
|
||||
|
||||
final _$instanceHostAtom = Atom(name: '_CreatePostStore.instanceHost');
|
||||
|
||||
@override
|
||||
String get instanceHost {
|
||||
_$instanceHostAtom.reportRead();
|
||||
return super.instanceHost;
|
||||
}
|
||||
|
||||
@override
|
||||
set instanceHost(String value) {
|
||||
_$instanceHostAtom.reportWrite(value, super.instanceHost, () {
|
||||
super.instanceHost = value;
|
||||
});
|
||||
}
|
||||
|
||||
final _$selectedCommunityAtom =
|
||||
Atom(name: '_CreatePostStore.selectedCommunity');
|
||||
|
||||
@override
|
||||
CommunityView? get selectedCommunity {
|
||||
_$selectedCommunityAtom.reportRead();
|
||||
return super.selectedCommunity;
|
||||
}
|
||||
|
||||
@override
|
||||
set selectedCommunity(CommunityView? value) {
|
||||
_$selectedCommunityAtom.reportWrite(value, super.selectedCommunity, () {
|
||||
super.selectedCommunity = value;
|
||||
});
|
||||
}
|
||||
|
||||
final _$urlAtom = Atom(name: '_CreatePostStore.url');
|
||||
|
||||
@override
|
||||
String get url {
|
||||
_$urlAtom.reportRead();
|
||||
return super.url;
|
||||
}
|
||||
|
||||
@override
|
||||
set url(String value) {
|
||||
_$urlAtom.reportWrite(value, super.url, () {
|
||||
super.url = value;
|
||||
});
|
||||
}
|
||||
|
||||
final _$titleAtom = Atom(name: '_CreatePostStore.title');
|
||||
|
||||
@override
|
||||
String get title {
|
||||
_$titleAtom.reportRead();
|
||||
return super.title;
|
||||
}
|
||||
|
||||
@override
|
||||
set title(String value) {
|
||||
_$titleAtom.reportWrite(value, super.title, () {
|
||||
super.title = value;
|
||||
});
|
||||
}
|
||||
|
||||
final _$bodyAtom = Atom(name: '_CreatePostStore.body');
|
||||
|
||||
@override
|
||||
String get body {
|
||||
_$bodyAtom.reportRead();
|
||||
return super.body;
|
||||
}
|
||||
|
||||
@override
|
||||
set body(String value) {
|
||||
_$bodyAtom.reportWrite(value, super.body, () {
|
||||
super.body = value;
|
||||
});
|
||||
}
|
||||
|
||||
final _$nsfwAtom = Atom(name: '_CreatePostStore.nsfw');
|
||||
|
||||
@override
|
||||
bool get nsfw {
|
||||
_$nsfwAtom.reportRead();
|
||||
return super.nsfw;
|
||||
}
|
||||
|
||||
@override
|
||||
set nsfw(bool value) {
|
||||
_$nsfwAtom.reportWrite(value, super.nsfw, () {
|
||||
super.nsfw = value;
|
||||
});
|
||||
}
|
||||
|
||||
final _$submitAsyncAction = AsyncAction('_CreatePostStore.submit');
|
||||
|
||||
@override
|
||||
Future<void> submit(Jwt token) {
|
||||
return _$submitAsyncAction.run(() => super.submit(token));
|
||||
}
|
||||
|
||||
final _$uploadImageAsyncAction = AsyncAction('_CreatePostStore.uploadImage');
|
||||
|
||||
@override
|
||||
Future<void> uploadImage(String filePath, Jwt token) {
|
||||
return _$uploadImageAsyncAction
|
||||
.run(() => super.uploadImage(filePath, token));
|
||||
}
|
||||
|
||||
final _$_CreatePostStoreActionController =
|
||||
ActionController(name: '_CreatePostStore');
|
||||
|
||||
@override
|
||||
Future<List<CommunityView>?> searchCommunities(
|
||||
String searchTerm, Jwt? token) {
|
||||
final _$actionInfo = _$_CreatePostStoreActionController.startAction(
|
||||
name: '_CreatePostStore.searchCommunities');
|
||||
try {
|
||||
return super.searchCommunities(searchTerm, token);
|
||||
} finally {
|
||||
_$_CreatePostStoreActionController.endAction(_$actionInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void removeImage() {
|
||||
final _$actionInfo = _$_CreatePostStoreActionController.startAction(
|
||||
name: '_CreatePostStore.removeImage');
|
||||
try {
|
||||
return super.removeImage();
|
||||
} finally {
|
||||
_$_CreatePostStoreActionController.endAction(_$actionInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '''
|
||||
showFancy: ${showFancy},
|
||||
instanceHost: ${instanceHost},
|
||||
selectedCommunity: ${selectedCommunity},
|
||||
url: ${url},
|
||||
title: ${title},
|
||||
body: ${body},
|
||||
nsfw: ${nsfw},
|
||||
hasUploadedImage: ${hasUploadedImage}
|
||||
''';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:lemmy_api_client/v3.dart';
|
||||
|
||||
import '../../hooks/logged_in_action.dart';
|
||||
import '../../hooks/stores.dart';
|
||||
import '../../l10n/l10n.dart';
|
||||
import '../../util/files.dart';
|
||||
import '../../util/observer_consumers.dart';
|
||||
import 'create_post_store.dart';
|
||||
|
||||
class CreatePostUrlField extends HookWidget {
|
||||
final FocusNode titleFocusNode;
|
||||
|
||||
const CreatePostUrlField(this.titleFocusNode);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final controller = useTextEditingController(
|
||||
text: context.read<CreatePostStore>().url,
|
||||
);
|
||||
final loggedInAction = useLoggedInAction(
|
||||
useStore((CreatePostStore store) => store.instanceHost),
|
||||
);
|
||||
|
||||
uploadImage(Jwt token) async {
|
||||
final pic = await pickImage();
|
||||
|
||||
// pic is null when the picker was cancelled
|
||||
if (pic != null) {
|
||||
await context.read<CreatePostStore>().uploadImage(pic.path, token);
|
||||
}
|
||||
}
|
||||
|
||||
return ObserverConsumer<CreatePostStore>(
|
||||
listener: (context, store) {
|
||||
// needed to keep the controller and store data in sync
|
||||
if (controller.text != store.url) {
|
||||
controller.text = store.url;
|
||||
}
|
||||
},
|
||||
builder: (context, store) => Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: controller,
|
||||
enabled: !store.hasUploadedImage,
|
||||
autofillHints:
|
||||
!store.hasUploadedImage ? const [AutofillHints.url] : null,
|
||||
keyboardType: TextInputType.url,
|
||||
textInputAction: TextInputAction.next,
|
||||
onFieldSubmitted: (_) => titleFocusNode.requestFocus(),
|
||||
onChanged: (url) => store.url = url,
|
||||
decoration: InputDecoration(
|
||||
labelText: L10n.of(context).url,
|
||||
suffixIcon: const Icon(Icons.link),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 5),
|
||||
IconButton(
|
||||
icon: store.imageUploadState.isLoading
|
||||
? const CircularProgressIndicator.adaptive()
|
||||
: Icon(
|
||||
store.hasUploadedImage
|
||||
? Icons.close
|
||||
: Icons.add_photo_alternate,
|
||||
),
|
||||
onPressed: store.hasUploadedImage
|
||||
? () => store.removeImage()
|
||||
: loggedInAction(uploadImage),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -4,7 +4,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
|||
|
||||
import '../util/extensions/brightness.dart';
|
||||
import 'communities_tab.dart';
|
||||
import 'create_post.dart';
|
||||
import 'create_post/create_post_fab.dart';
|
||||
import 'home_tab.dart';
|
||||
import 'profile_tab.dart';
|
||||
import 'search_tab.dart';
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
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/stores.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import '../url_launcher.dart';
|
||||
import '../util/files.dart';
|
||||
import '../util/icons.dart';
|
||||
import '../util/pictrs.dart';
|
||||
import '../widgets/bottom_modal.dart';
|
||||
|
@ -414,13 +413,12 @@ class _ImagePicker extends HookWidget {
|
|||
final url = useState(initialUrl.value);
|
||||
final pictrsDeleteToken = useState<PictrsUploadFile?>(null);
|
||||
|
||||
final imagePicker = useImagePicker();
|
||||
final accountsStore = useAccountsStore();
|
||||
final delayedLoading = useDelayedLoading();
|
||||
|
||||
uploadImage() async {
|
||||
try {
|
||||
final pic = await imagePicker.pickImage(source: ImageSource.gallery);
|
||||
final pic = await pickImage();
|
||||
// pic is null when the picker was cancelled
|
||||
if (pic != null) {
|
||||
delayedLoading.start();
|
||||
|
|
|
@ -5,6 +5,8 @@ import 'package:freezed_annotation/freezed_annotation.dart';
|
|||
import 'package:lemmy_api_client/v3.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
|
||||
import '../l10n/l10n_from_string.dart';
|
||||
|
||||
part 'async_store.freezed.dart';
|
||||
part 'async_store.g.dart';
|
||||
|
||||
|
@ -28,6 +30,10 @@ abstract class _AsyncStore<T> with Store {
|
|||
@action
|
||||
void setData(T data) => asyncState = AsyncState.data(data);
|
||||
|
||||
/// reset an asyncState to its initial one
|
||||
@action
|
||||
void reset() => asyncState = AsyncState<T>.initial();
|
||||
|
||||
/// runs some async action and reflects the progress in [asyncState].
|
||||
/// If successful, the result is returned, otherwise null is returned.
|
||||
/// If this [AsyncStore] is already running some action, it will exit immediately and do nothing
|
||||
|
@ -51,11 +57,10 @@ abstract class _AsyncStore<T> with Store {
|
|||
|
||||
return result;
|
||||
} on SocketException {
|
||||
// TODO: use an existing l10n key
|
||||
if (data != null) {
|
||||
asyncState = data.copyWith(errorTerm: 'network_error');
|
||||
asyncState = data.copyWith(errorTerm: L10nStrings.network_error);
|
||||
} else {
|
||||
asyncState = const AsyncState.error('network_error');
|
||||
asyncState = const AsyncState.error(L10nStrings.network_error);
|
||||
}
|
||||
} catch (err) {
|
||||
if (data != null) {
|
||||
|
|
|
@ -71,6 +71,17 @@ mixin _$AsyncStore<T> on _AsyncStore<T>, Store {
|
|||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void reset() {
|
||||
final _$actionInfo =
|
||||
_$_AsyncStoreActionController.startAction(name: '_AsyncStore.reset');
|
||||
try {
|
||||
return super.reset();
|
||||
} finally {
|
||||
_$_AsyncStoreActionController.endAction(_$actionInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '''
|
||||
|
|
|
@ -12,10 +12,16 @@ class AsyncStoreListener<T> extends SingleChildStatelessWidget {
|
|||
T data,
|
||||
)? successMessageBuilder;
|
||||
|
||||
final void Function(
|
||||
BuildContext context,
|
||||
T data,
|
||||
)? onSuccess;
|
||||
|
||||
const AsyncStoreListener({
|
||||
Key? key,
|
||||
required this.asyncStore,
|
||||
this.successMessageBuilder,
|
||||
this.onSuccess,
|
||||
Widget? child,
|
||||
}) : super(key: key, child: child);
|
||||
|
||||
|
@ -24,20 +30,27 @@ class AsyncStoreListener<T> extends SingleChildStatelessWidget {
|
|||
return ObserverListener<AsyncStore<T>>(
|
||||
store: asyncStore,
|
||||
listener: (context, store) {
|
||||
final errorTerm = store.errorTerm;
|
||||
store.map(
|
||||
loading: () {},
|
||||
error: (errorTerm) {
|
||||
ScaffoldMessenger.of(context)
|
||||
..hideCurrentSnackBar()
|
||||
..showSnackBar(SnackBar(content: Text(errorTerm.tr(context))));
|
||||
},
|
||||
data: (data) {
|
||||
onSuccess?.call(context, data);
|
||||
|
||||
if (errorTerm != null) {
|
||||
ScaffoldMessenger.of(context)
|
||||
..hideCurrentSnackBar()
|
||||
..showSnackBar(SnackBar(content: Text(errorTerm.tr(context))));
|
||||
} else if (store.asyncState is AsyncStateData &&
|
||||
(successMessageBuilder != null)) {
|
||||
ScaffoldMessenger.of(context)
|
||||
..hideCurrentSnackBar()
|
||||
..showSnackBar(SnackBar(
|
||||
content: Text(successMessageBuilder!(
|
||||
context, (store.asyncState as AsyncStateData).data))));
|
||||
}
|
||||
if (successMessageBuilder != null) {
|
||||
ScaffoldMessenger.of(context)
|
||||
..hideCurrentSnackBar()
|
||||
..showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(successMessageBuilder!(context, data)),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
child: child ?? const SizedBox(),
|
||||
);
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
|
||||
/// Picks a single image from the system
|
||||
Future<XFile?> pickImage() async {
|
||||
if (kIsWeb || Platform.isIOS || Platform.isAndroid) {
|
||||
return ImagePicker().pickImage(source: ImageSource.gallery);
|
||||
} else {
|
||||
final result = await FilePicker.platform.pickFiles(type: FileType.image);
|
||||
|
||||
if (result == null) return null;
|
||||
|
||||
return XFile(result.files.single.path!);
|
||||
}
|
||||
}
|
|
@ -8,9 +8,11 @@ class Editor extends HookWidget {
|
|||
final TextEditingController? controller;
|
||||
final FocusNode? focusNode;
|
||||
final ValueChanged<String>? onSubmitted;
|
||||
final ValueChanged<String>? onChanged;
|
||||
final int? minLines;
|
||||
final int? maxLines;
|
||||
final String? labelText;
|
||||
final String? initialValue;
|
||||
final bool autofocus;
|
||||
|
||||
/// Whether the editor should be preview the contents
|
||||
|
@ -22,9 +24,11 @@ class Editor extends HookWidget {
|
|||
this.controller,
|
||||
this.focusNode,
|
||||
this.onSubmitted,
|
||||
this.onChanged,
|
||||
this.minLines = 5,
|
||||
this.maxLines,
|
||||
this.labelText,
|
||||
this.initialValue,
|
||||
this.fancy = false,
|
||||
required this.instanceHost,
|
||||
this.autofocus = false,
|
||||
|
@ -32,7 +36,7 @@ class Editor extends HookWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final defaultController = useTextEditingController();
|
||||
final defaultController = useTextEditingController(text: initialValue);
|
||||
final actualController = controller ?? defaultController;
|
||||
|
||||
if (fancy) {
|
||||
|
@ -51,6 +55,7 @@ class Editor extends HookWidget {
|
|||
autofocus: autofocus,
|
||||
keyboardType: TextInputType.multiline,
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
onChanged: onChanged,
|
||||
onSubmitted: onSubmitted,
|
||||
maxLines: maxLines,
|
||||
minLines: minLines,
|
||||
|
|
|
@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
|
||||
import '../../hooks/logged_in_action.dart';
|
||||
import '../../pages/create_post.dart';
|
||||
import '../../pages/create_post/create_post.dart';
|
||||
import '../../pages/full_post/full_post_store.dart';
|
||||
import '../../stores/accounts_store.dart';
|
||||
import '../../url_launcher.dart';
|
||||
|
|
14
pubspec.lock
14
pubspec.lock
|
@ -211,6 +211,13 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "6.1.2"
|
||||
file_picker:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: file_picker
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "4.3.1"
|
||||
fixnum:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -992,6 +999,13 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
wc_form_validators:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: wc_form_validators
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
web_socket_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
@ -60,6 +60,8 @@ dependencies:
|
|||
sdk: flutter
|
||||
flutter_localizations:
|
||||
sdk: flutter
|
||||
wc_form_validators: ^1.0.0
|
||||
file_picker: ^4.3.1
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
|
Loading…
Reference in New Issue