add store with purpose of uploading images
This commit is contained in:
parent
663b45bc21
commit
116b0d7961
|
@ -145,7 +145,10 @@ class CreatePostPage extends HookWidget {
|
|||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Spacer(),
|
||||
Toolbar(bodyController),
|
||||
Toolbar(
|
||||
controller: bodyController,
|
||||
instanceHost: context.read<CreatePostStore>().instanceHost,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 = '';
|
||||
}
|
||||
}
|
|
@ -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}
|
||||
''';
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue