Add image upload

This commit is contained in:
shilangyu 2022-01-16 16:37:25 +01:00
parent 85f9d3fd0e
commit d64fe48328
5 changed files with 179 additions and 1 deletions

View File

@ -1,7 +1,9 @@
import 'package:lemmy_api_client/pictrs.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 '../../util/async_store.dart'; import '../../util/async_store.dart';
import '../../util/pictrs.dart';
part 'create_post_store.g.dart'; part 'create_post_store.g.dart';
@ -37,6 +39,14 @@ abstract class _CreatePostStore with Store {
final submitState = AsyncStore<PostView>(); final submitState = AsyncStore<PostView>();
final searchCommunitiesState = AsyncStore<List<CommunityView>>(); final searchCommunitiesState = AsyncStore<List<CommunityView>>();
final imageUploadState = AsyncStore<PictrsUploadFile>();
@computed
bool get hasUploadedImage => imageUploadState.map(
loading: () => false,
error: (_) => false,
data: (_) => true,
);
@action @action
Future<List<CommunityView>?> searchCommunities( 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>> { class SearchCommunities implements LemmyApiQuery<List<CommunityView>> {

View File

@ -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 // 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 { 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'); final _$showFancyAtom = Atom(name: '_CreatePostStore.showFancy');
@override @override
@ -122,6 +130,40 @@ mixin _$CreatePostStore on _CreatePostStore, Store {
return _$submitAsyncAction.run(() => super.submit(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 @override
String toString() { String toString() {
return ''' return '''
@ -131,7 +173,8 @@ selectedCommunity: ${selectedCommunity},
url: ${url}, url: ${url},
title: ${title}, title: ${title},
body: ${body}, body: ${body},
nsfw: ${nsfw} nsfw: ${nsfw},
hasUploadedImage: ${hasUploadedImage}
'''; ''';
} }
} }

View File

@ -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),
),
],
),
);
}
}

View File

@ -30,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

View File

@ -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 @override
String toString() { String toString() {
return ''' return '''