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 fetch weblate
|
||||||
git merge weblate/master
|
git merge weblate/master
|
||||||
|
|
||||||
|
- name: Regenerate l10n_from_string
|
||||||
|
run: |
|
||||||
|
dart run scripts/gen_l10n_from_string.dart
|
||||||
|
|
||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
uses: peter-evans/create-pull-request@v3.12.0
|
uses: peter-evans/create-pull-request@v3.12.0
|
||||||
with:
|
with:
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
- Create post community picker now has autocomplete
|
||||||
- You can now add an instance from the three dots menu on the instance page
|
- You can now add an instance from the three dots menu on the instance page
|
||||||
|
|
||||||
## v0.8.0 - 2022-01-14
|
## v0.8.0 - 2022-01-14
|
||||||
|
|
|
@ -345,5 +345,11 @@
|
||||||
"add_instance": "Add instance",
|
"add_instance": "Add instance",
|
||||||
"@add_instance": {},
|
"@add_instance": {},
|
||||||
"instance_added": "Instance successfully added",
|
"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 {
|
enum CommentSortType {
|
||||||
hot,
|
hot,
|
||||||
top,
|
top,
|
||||||
// ignore: constant_identifier_names
|
|
||||||
new_,
|
new_,
|
||||||
old,
|
old,
|
||||||
chat,
|
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_posts = 'number_of_posts';
|
||||||
static const number_of_subscribers = 'number_of_subscribers';
|
static const number_of_subscribers = 'number_of_subscribers';
|
||||||
static const number_of_users = 'number_of_users';
|
static const number_of_users = 'number_of_users';
|
||||||
|
static const number_of_communities = 'number_of_communities';
|
||||||
static const unsubscribe = 'unsubscribe';
|
static const unsubscribe = 'unsubscribe';
|
||||||
static const subscribe = 'subscribe';
|
static const subscribe = 'subscribe';
|
||||||
static const messages = 'messages';
|
static const messages = 'messages';
|
||||||
|
@ -145,6 +146,24 @@ abstract class L10nStrings {
|
||||||
static const bot_account = 'bot_account';
|
static const bot_account = 'bot_account';
|
||||||
static const show_bot_accounts = 'show_bot_accounts';
|
static const show_bot_accounts = 'show_bot_accounts';
|
||||||
static const show_read_posts = 'show_read_posts';
|
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 {
|
extension L10nFromString on String {
|
||||||
|
@ -406,6 +425,40 @@ extension L10nFromString on String {
|
||||||
return L10n.of(context).show_bot_accounts;
|
return L10n.of(context).show_bot_accounts;
|
||||||
case L10nStrings.show_read_posts:
|
case L10nStrings.show_read_posts:
|
||||||
return L10n.of(context).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:
|
default:
|
||||||
return this;
|
return this;
|
||||||
|
|
|
@ -16,7 +16,7 @@ import '../../util/share.dart';
|
||||||
import '../../widgets/failed_to_load.dart';
|
import '../../widgets/failed_to_load.dart';
|
||||||
import '../../widgets/reveal_after_scroll.dart';
|
import '../../widgets/reveal_after_scroll.dart';
|
||||||
import '../../widgets/sortable_infinite_list.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_about_tab.dart';
|
||||||
import 'community_more_menu.dart';
|
import 'community_more_menu.dart';
|
||||||
import 'community_overview.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 '../util/extensions/brightness.dart';
|
||||||
import 'communities_tab.dart';
|
import 'communities_tab.dart';
|
||||||
import 'create_post.dart';
|
import 'create_post/create_post_fab.dart';
|
||||||
import 'home_tab.dart';
|
import 'home_tab.dart';
|
||||||
import 'profile_tab.dart';
|
import 'profile_tab.dart';
|
||||||
import 'search_tab.dart';
|
import 'search_tab.dart';
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.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/pictrs.dart';
|
||||||
import 'package:lemmy_api_client/v3.dart';
|
import 'package:lemmy_api_client/v3.dart';
|
||||||
|
|
||||||
import '../hooks/delayed_loading.dart';
|
import '../hooks/delayed_loading.dart';
|
||||||
import '../hooks/image_picker.dart';
|
|
||||||
import '../hooks/stores.dart';
|
import '../hooks/stores.dart';
|
||||||
import '../l10n/l10n.dart';
|
import '../l10n/l10n.dart';
|
||||||
import '../url_launcher.dart';
|
import '../url_launcher.dart';
|
||||||
|
import '../util/files.dart';
|
||||||
import '../util/icons.dart';
|
import '../util/icons.dart';
|
||||||
import '../util/pictrs.dart';
|
import '../util/pictrs.dart';
|
||||||
import '../widgets/bottom_modal.dart';
|
import '../widgets/bottom_modal.dart';
|
||||||
|
@ -414,13 +413,12 @@ class _ImagePicker extends HookWidget {
|
||||||
final url = useState(initialUrl.value);
|
final url = useState(initialUrl.value);
|
||||||
final pictrsDeleteToken = useState<PictrsUploadFile?>(null);
|
final pictrsDeleteToken = useState<PictrsUploadFile?>(null);
|
||||||
|
|
||||||
final imagePicker = useImagePicker();
|
|
||||||
final accountsStore = useAccountsStore();
|
final accountsStore = useAccountsStore();
|
||||||
final delayedLoading = useDelayedLoading();
|
final delayedLoading = useDelayedLoading();
|
||||||
|
|
||||||
uploadImage() async {
|
uploadImage() async {
|
||||||
try {
|
try {
|
||||||
final pic = await imagePicker.pickImage(source: ImageSource.gallery);
|
final pic = await pickImage();
|
||||||
// pic is null when the picker was cancelled
|
// pic is null when the picker was cancelled
|
||||||
if (pic != null) {
|
if (pic != null) {
|
||||||
delayedLoading.start();
|
delayedLoading.start();
|
||||||
|
|
|
@ -5,6 +5,8 @@ import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
import 'package:lemmy_api_client/v3.dart';
|
import 'package:lemmy_api_client/v3.dart';
|
||||||
import 'package:mobx/mobx.dart';
|
import 'package:mobx/mobx.dart';
|
||||||
|
|
||||||
|
import '../l10n/l10n_from_string.dart';
|
||||||
|
|
||||||
part 'async_store.freezed.dart';
|
part 'async_store.freezed.dart';
|
||||||
part 'async_store.g.dart';
|
part 'async_store.g.dart';
|
||||||
|
|
||||||
|
@ -28,6 +30,10 @@ abstract class _AsyncStore<T> with Store {
|
||||||
@action
|
@action
|
||||||
void setData(T data) => asyncState = AsyncState.data(data);
|
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].
|
/// runs some async action and reflects the progress in [asyncState].
|
||||||
/// If successful, the result is returned, otherwise null is returned.
|
/// 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
|
/// 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;
|
return result;
|
||||||
} on SocketException {
|
} on SocketException {
|
||||||
// TODO: use an existing l10n key
|
|
||||||
if (data != null) {
|
if (data != null) {
|
||||||
asyncState = data.copyWith(errorTerm: 'network_error');
|
asyncState = data.copyWith(errorTerm: L10nStrings.network_error);
|
||||||
} else {
|
} else {
|
||||||
asyncState = const AsyncState.error('network_error');
|
asyncState = const AsyncState.error(L10nStrings.network_error);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (data != null) {
|
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
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return '''
|
return '''
|
||||||
|
|
|
@ -12,10 +12,16 @@ class AsyncStoreListener<T> extends SingleChildStatelessWidget {
|
||||||
T data,
|
T data,
|
||||||
)? successMessageBuilder;
|
)? successMessageBuilder;
|
||||||
|
|
||||||
|
final void Function(
|
||||||
|
BuildContext context,
|
||||||
|
T data,
|
||||||
|
)? onSuccess;
|
||||||
|
|
||||||
const AsyncStoreListener({
|
const AsyncStoreListener({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.asyncStore,
|
required this.asyncStore,
|
||||||
this.successMessageBuilder,
|
this.successMessageBuilder,
|
||||||
|
this.onSuccess,
|
||||||
Widget? child,
|
Widget? child,
|
||||||
}) : super(key: key, child: child);
|
}) : super(key: key, child: child);
|
||||||
|
|
||||||
|
@ -24,20 +30,27 @@ class AsyncStoreListener<T> extends SingleChildStatelessWidget {
|
||||||
return ObserverListener<AsyncStore<T>>(
|
return ObserverListener<AsyncStore<T>>(
|
||||||
store: asyncStore,
|
store: asyncStore,
|
||||||
listener: (context, store) {
|
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) {
|
if (successMessageBuilder != null) {
|
||||||
ScaffoldMessenger.of(context)
|
ScaffoldMessenger.of(context)
|
||||||
..hideCurrentSnackBar()
|
..hideCurrentSnackBar()
|
||||||
..showSnackBar(SnackBar(content: Text(errorTerm.tr(context))));
|
..showSnackBar(
|
||||||
} else if (store.asyncState is AsyncStateData &&
|
SnackBar(
|
||||||
(successMessageBuilder != null)) {
|
content: Text(successMessageBuilder!(context, data)),
|
||||||
ScaffoldMessenger.of(context)
|
),
|
||||||
..hideCurrentSnackBar()
|
);
|
||||||
..showSnackBar(SnackBar(
|
}
|
||||||
content: Text(successMessageBuilder!(
|
},
|
||||||
context, (store.asyncState as AsyncStateData).data))));
|
);
|
||||||
}
|
|
||||||
},
|
},
|
||||||
child: child ?? const SizedBox(),
|
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 TextEditingController? controller;
|
||||||
final FocusNode? focusNode;
|
final FocusNode? focusNode;
|
||||||
final ValueChanged<String>? onSubmitted;
|
final ValueChanged<String>? onSubmitted;
|
||||||
|
final ValueChanged<String>? onChanged;
|
||||||
final int? minLines;
|
final int? minLines;
|
||||||
final int? maxLines;
|
final int? maxLines;
|
||||||
final String? labelText;
|
final String? labelText;
|
||||||
|
final String? initialValue;
|
||||||
final bool autofocus;
|
final bool autofocus;
|
||||||
|
|
||||||
/// Whether the editor should be preview the contents
|
/// Whether the editor should be preview the contents
|
||||||
|
@ -22,9 +24,11 @@ class Editor extends HookWidget {
|
||||||
this.controller,
|
this.controller,
|
||||||
this.focusNode,
|
this.focusNode,
|
||||||
this.onSubmitted,
|
this.onSubmitted,
|
||||||
|
this.onChanged,
|
||||||
this.minLines = 5,
|
this.minLines = 5,
|
||||||
this.maxLines,
|
this.maxLines,
|
||||||
this.labelText,
|
this.labelText,
|
||||||
|
this.initialValue,
|
||||||
this.fancy = false,
|
this.fancy = false,
|
||||||
required this.instanceHost,
|
required this.instanceHost,
|
||||||
this.autofocus = false,
|
this.autofocus = false,
|
||||||
|
@ -32,7 +36,7 @@ class Editor extends HookWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final defaultController = useTextEditingController();
|
final defaultController = useTextEditingController(text: initialValue);
|
||||||
final actualController = controller ?? defaultController;
|
final actualController = controller ?? defaultController;
|
||||||
|
|
||||||
if (fancy) {
|
if (fancy) {
|
||||||
|
@ -51,6 +55,7 @@ class Editor extends HookWidget {
|
||||||
autofocus: autofocus,
|
autofocus: autofocus,
|
||||||
keyboardType: TextInputType.multiline,
|
keyboardType: TextInputType.multiline,
|
||||||
textCapitalization: TextCapitalization.sentences,
|
textCapitalization: TextCapitalization.sentences,
|
||||||
|
onChanged: onChanged,
|
||||||
onSubmitted: onSubmitted,
|
onSubmitted: onSubmitted,
|
||||||
maxLines: maxLines,
|
maxLines: maxLines,
|
||||||
minLines: minLines,
|
minLines: minLines,
|
||||||
|
|
|
@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
|
||||||
import '../../hooks/logged_in_action.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 '../../pages/full_post/full_post_store.dart';
|
||||||
import '../../stores/accounts_store.dart';
|
import '../../stores/accounts_store.dart';
|
||||||
import '../../url_launcher.dart';
|
import '../../url_launcher.dart';
|
||||||
|
|
14
pubspec.lock
14
pubspec.lock
|
@ -211,6 +211,13 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.1.2"
|
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:
|
fixnum:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -992,6 +999,13 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.1"
|
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:
|
web_socket_channel:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
@ -60,6 +60,8 @@ dependencies:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
flutter_localizations:
|
flutter_localizations:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
wc_form_validators: ^1.0.0
|
||||||
|
file_picker: ^4.3.1
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|
Loading…
Reference in New Issue