add store with purpose of uploading images

This commit is contained in:
Filip Krawczyk 2022-08-21 16:00:54 +02:00
parent 663b45bc21
commit 116b0d7961
4 changed files with 281 additions and 83 deletions

View File

@ -145,7 +145,10 @@ class CreatePostPage extends HookWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
const Spacer(), const Spacer(),
Toolbar(bodyController), Toolbar(
controller: bodyController,
instanceHost: context.read<CreatePostStore>().instanceHost,
),
], ],
), ),
), ),

View File

@ -2,8 +2,14 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import '../../formatter.dart'; import '../../formatter.dart';
import '../../hooks/logged_in_action.dart';
import '../../util/async_store_listener.dart';
import '../../util/extensions/spaced.dart'; import '../../util/extensions/spaced.dart';
import '../../util/files.dart';
import '../../util/mobx_provider.dart';
import '../../util/observer_consumers.dart';
import '../../util/text_lines_iterator.dart'; import '../../util/text_lines_iterator.dart';
import 'editor_toolbar_store.dart';
class Reformat { class Reformat {
final String text; final String text;
@ -111,20 +117,55 @@ extension on TextEditingController {
class Toolbar extends HookWidget { class Toolbar extends HookWidget {
final TextEditingController controller; final TextEditingController controller;
final String instanceHost;
final EditorToolbarStore store;
static const _height = 50.0; static const _height = 50.0;
const Toolbar(this.controller);
Toolbar({
required this.controller,
required this.instanceHost,
}) : store = EditorToolbarStore(instanceHost);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return MobxProvider.value(
value: store,
child: AsyncStoreListener(
asyncStore: store.imageUploadState,
child: Container(
height: _height, height: _height,
width: double.infinity, width: double.infinity,
color: Theme.of(context).cardColor, color: Theme.of(context).cardColor,
child: Material( child: Material(
child: SingleChildScrollView( child: SingleChildScrollView(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
child: Row( child: _ToolbarBody(
controller: controller,
instanceHost: instanceHost,
),
),
),
),
),
);
}
static Widget safeArea = const SizedBox(height: _height);
}
class _ToolbarBody extends HookWidget {
const _ToolbarBody({
required this.controller,
required this.instanceHost,
});
final TextEditingController controller;
final String instanceHost;
@override
Widget build(BuildContext context) {
final loggedInAction = useLoggedInAction(instanceHost);
return Row(
children: [ children: [
IconButton( IconButton(
onPressed: () => controller.surround('**'), onPressed: () => controller.surround('**'),
@ -136,15 +177,45 @@ class Toolbar extends HookWidget {
), ),
IconButton( IconButton(
onPressed: () async { onPressed: () async {
final r = await AddLinkDialog.show( final r =
context, controller.selectionText); await AddLinkDialog.show(context, controller.selectionText);
if (r != null) controller.reformat((_) => r); if (r != null) controller.reformat((_) => r);
}, },
icon: const Icon(Icons.link), icon: const Icon(Icons.link),
), ),
IconButton( // Insert image
onPressed: () {}, ObserverBuilder<EditorToolbarStore>(
icon: const Icon(Icons.image), builder: (context, store) {
return IconButton(
onPressed: loggedInAction((token) async {
if (store.imageUploadState.isLoading) {
return;
}
try {
// FIXME: for some reason it doesn't go past this line on iOS. idk why
final pic = await pickImage();
// pic is null when the picker was cancelled
if (pic != null) {
final picUrl = await context
.read<EditorToolbarStore>()
.uploadImage(pic.path, token);
if (picUrl != null) {
controller.reformat(
(selection) => Reformat(text: '![]($picUrl)'));
}
}
} on Exception catch (_) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Failed to upload image')));
}
}),
icon: store.imageUploadState.isLoading
? const CircularProgressIndicator.adaptive()
: const Icon(Icons.image),
);
},
), ),
IconButton( IconButton(
onPressed: () {}, onPressed: () {},
@ -198,13 +269,8 @@ class Toolbar extends HookWidget {
icon: const Icon(Icons.question_mark), icon: const Icon(Icons.question_mark),
), ),
], ],
),
),
),
); );
} }
static Widget safeArea = const SizedBox(height: _height);
} }
class AddLinkDialog extends HookWidget { class AddLinkDialog extends HookWidget {

View File

@ -0,0 +1,63 @@
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 'editor_toolbar_store.g.dart';
class EditorToolbarStore = _EditorToolbarStore with _$EditorToolbarStore;
abstract class _EditorToolbarStore with Store {
final String instanceHost;
_EditorToolbarStore(this.instanceHost);
@observable
String? url;
final imageUploadState = AsyncStore<PictrsUploadFile>();
@computed
bool get hasUploadedImage => imageUploadState.map(
loading: () => false,
error: (_) => false,
data: (_) => true,
);
@action
Future<String?> 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) {
final url = pathToPictrs(instanceHost, upload.file);
return url;
}
return null;
}
@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 = '';
}
}

View File

@ -0,0 +1,66 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'editor_toolbar_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, no_leading_underscores_for_local_identifiers
mixin _$EditorToolbarStore on _EditorToolbarStore, Store {
Computed<bool>? _$hasUploadedImageComputed;
@override
bool get hasUploadedImage => (_$hasUploadedImageComputed ??= Computed<bool>(
() => super.hasUploadedImage,
name: '_EditorToolbarStore.hasUploadedImage'))
.value;
late final _$urlAtom =
Atom(name: '_EditorToolbarStore.url', context: context);
@override
String? get url {
_$urlAtom.reportRead();
return super.url;
}
@override
set url(String? value) {
_$urlAtom.reportWrite(value, super.url, () {
super.url = value;
});
}
late final _$uploadImageAsyncAction =
AsyncAction('_EditorToolbarStore.uploadImage', context: context);
@override
Future<String?> uploadImage(String filePath, Jwt token) {
return _$uploadImageAsyncAction
.run(() => super.uploadImage(filePath, token));
}
late final _$_EditorToolbarStoreActionController =
ActionController(name: '_EditorToolbarStore', context: context);
@override
void removeImage() {
final _$actionInfo = _$_EditorToolbarStoreActionController.startAction(
name: '_EditorToolbarStore.removeImage');
try {
return super.removeImage();
} finally {
_$_EditorToolbarStoreActionController.endAction(_$actionInfo);
}
}
@override
String toString() {
return '''
url: ${url},
hasUploadedImage: ${hasUploadedImage}
''';
}
}