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: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>> {
|
||||||
|
|
|
@ -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}
|
||||||
''';
|
''';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
@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
|
||||||
|
|
|
@ -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 '''
|
||||||
|
|
Loading…
Reference in New Issue