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,
children: [
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 '../../formatter.dart';
import '../../hooks/logged_in_action.dart';
import '../../util/async_store_listener.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 'editor_toolbar_store.dart';
class Reformat {
final String text;
@ -111,93 +117,33 @@ extension on TextEditingController {
class Toolbar extends HookWidget {
final TextEditingController controller;
final String instanceHost;
final EditorToolbarStore store;
static const _height = 50.0;
const Toolbar(this.controller);
Toolbar({
required this.controller,
required this.instanceHost,
}) : store = EditorToolbarStore(instanceHost);
@override
Widget build(BuildContext context) {
return Container(
height: _height,
width: double.infinity,
color: Theme.of(context).cardColor,
child: Material(
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
IconButton(
onPressed: () => controller.surround('**'),
icon: const Icon(Icons.format_bold),
return MobxProvider.value(
value: store,
child: AsyncStoreListener(
asyncStore: store.imageUploadState,
child: Container(
height: _height,
width: double.infinity,
color: Theme.of(context).cardColor,
child: Material(
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: _ToolbarBody(
controller: controller,
instanceHost: instanceHost,
),
IconButton(
onPressed: () => controller.surround('*'),
icon: const Icon(Icons.format_italic),
),
IconButton(
onPressed: () async {
final r = await AddLinkDialog.show(
context, controller.selectionText);
if (r != null) controller.reformat((_) => r);
},
icon: const Icon(Icons.link),
),
IconButton(
onPressed: () {},
icon: const Icon(Icons.image),
),
IconButton(
onPressed: () {},
icon: const Icon(Icons.person),
),
IconButton(
onPressed: () {},
icon: const Icon(Icons.home),
),
IconButton(
onPressed: () {},
icon: const Icon(Icons.h_mobiledata),
),
IconButton(
onPressed: () => controller.surround('~~'),
icon: const Icon(Icons.format_strikethrough),
),
IconButton(
onPressed: () {},
icon: const Icon(Icons.format_quote),
),
IconButton(
onPressed: () {
final line = controller.firstSelectedLine;
if (line.startsWith(RegExp.escape('* '))) {
controller.removeAtBeginningOfEverySelectedLine('* ');
} else if (line.startsWith('- ')) {
controller.removeAtBeginningOfEverySelectedLine('- ');
} else {
controller.insertAtBeginningOfEverySelectedLine('- ');
}
},
icon: const Icon(Icons.format_list_bulleted)),
IconButton(
onPressed: () => controller.surround('`'),
icon: const Icon(Icons.code),
),
IconButton(
onPressed: () => controller.surround('~'),
icon: const Icon(Icons.subscript),
),
IconButton(
onPressed: () => controller.surround('^'),
icon: const Icon(Icons.superscript),
),
//spoiler
IconButton(onPressed: () {}, icon: const Icon(Icons.warning)),
IconButton(
onPressed: () {},
icon: const Icon(Icons.question_mark),
),
],
),
),
),
),
@ -207,6 +153,126 @@ class Toolbar extends HookWidget {
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: [
IconButton(
onPressed: () => controller.surround('**'),
icon: const Icon(Icons.format_bold),
),
IconButton(
onPressed: () => controller.surround('*'),
icon: const Icon(Icons.format_italic),
),
IconButton(
onPressed: () async {
final r =
await AddLinkDialog.show(context, controller.selectionText);
if (r != null) controller.reformat((_) => r);
},
icon: const Icon(Icons.link),
),
// Insert image
ObserverBuilder<EditorToolbarStore>(
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(
onPressed: () {},
icon: const Icon(Icons.person),
),
IconButton(
onPressed: () {},
icon: const Icon(Icons.home),
),
IconButton(
onPressed: () {},
icon: const Icon(Icons.h_mobiledata),
),
IconButton(
onPressed: () => controller.surround('~~'),
icon: const Icon(Icons.format_strikethrough),
),
IconButton(
onPressed: () {},
icon: const Icon(Icons.format_quote),
),
IconButton(
onPressed: () {
final line = controller.firstSelectedLine;
if (line.startsWith(RegExp.escape('* '))) {
controller.removeAtBeginningOfEverySelectedLine('* ');
} else if (line.startsWith('- ')) {
controller.removeAtBeginningOfEverySelectedLine('- ');
} else {
controller.insertAtBeginningOfEverySelectedLine('- ');
}
},
icon: const Icon(Icons.format_list_bulleted)),
IconButton(
onPressed: () => controller.surround('`'),
icon: const Icon(Icons.code),
),
IconButton(
onPressed: () => controller.surround('~'),
icon: const Icon(Icons.subscript),
),
IconButton(
onPressed: () => controller.surround('^'),
icon: const Icon(Icons.superscript),
),
//spoiler
IconButton(onPressed: () {}, icon: const Icon(Icons.warning)),
IconButton(
onPressed: () {},
icon: const Icon(Icons.question_mark),
),
],
);
}
}
class AddLinkDialog extends HookWidget {
final String title;
final String url;

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}
''';
}
}