Add image upload
This commit is contained in:
parent
85f9d3fd0e
commit
d64fe48328
|
@ -1,7 +1,9 @@
|
|||
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';
|
||||
|
||||
|
@ -37,6 +39,14 @@ abstract class _CreatePostStore with Store {
|
|||
|
||||
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(
|
||||
|
@ -90,6 +100,39 @@ abstract class _CreatePostStore with Store {
|
|||
),
|
||||
);
|
||||
}
|
||||
|
||||
@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>> {
|
||||
|
|
|
@ -9,6 +9,14 @@ part of 'create_post_store.dart';
|
|||
// 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
|
||||
|
@ -122,6 +130,40 @@ mixin _$CreatePostStore on _CreatePostStore, Store {
|
|||
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 '''
|
||||
|
@ -131,7 +173,8 @@ selectedCommunity: ${selectedCommunity},
|
|||
url: ${url},
|
||||
title: ${title},
|
||||
body: ${body},
|
||||
nsfw: ${nsfw}
|
||||
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 since flutter's TextFields cannot work as dumb widgets
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -30,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
|
||||
|
|
|
@ -67,6 +67,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 '''
|
||||
|
|
Loading…
Reference in New Issue