feat: Enhance emote experience
This commit is contained in:
parent
090795fa77
commit
cafd639c24
|
@ -1,4 +1,9 @@
|
|||
# Version 0.19.0 - 2020-??-??
|
||||
# Version 0.20.0 - 2020-??-??
|
||||
### Features
|
||||
- Add ability to enable / disable emotes globally
|
||||
- Add ability to manage emote packs with different state keys
|
||||
|
||||
# Version 0.19.0 - 2020-09-21
|
||||
### Features
|
||||
- Implemented ignore list
|
||||
- Jump to events in timeline: When tapping on a reply and when tapping a matrix.to link
|
||||
|
|
|
@ -237,22 +237,10 @@ class InputBar extends StatelessWidget {
|
|||
}
|
||||
if (insertText.isNotEmpty && startText.isNotEmpty) {
|
||||
controller.text = startText + afterText;
|
||||
if (startText == insertText) {
|
||||
// stupid fix for now
|
||||
FocusScope.of(context).requestFocus(FocusNode());
|
||||
Future.delayed(Duration(milliseconds: 1)).then((res) {
|
||||
focusNode.requestFocus();
|
||||
controller.selection = TextSelection(
|
||||
baseOffset: startText.length,
|
||||
extentOffset: startText.length,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
controller.selection = TextSelection(
|
||||
baseOffset: startText.length,
|
||||
extentOffset: startText.length,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -529,11 +529,21 @@
|
|||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"emotePacks": "Emote packs for room",
|
||||
"@emotePacks": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"emptyChat": "Empty chat",
|
||||
"@emptyChat": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"enableEmotesGlobally": "Enable emote pack globally",
|
||||
"@enableEmotesGlobally": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"enableEncryptionWarning": "You won't be able to disable the encryption anymore. Are you sure?",
|
||||
"@enableEncryptionWarning": {
|
||||
"type": "text",
|
||||
|
|
|
@ -19,6 +19,7 @@ import 'package:matrix_link_text/link_text.dart';
|
|||
import 'package:memoryfilepicker/memoryfilepicker.dart';
|
||||
|
||||
import './settings_emotes.dart';
|
||||
import './settings_multiple_emotes.dart';
|
||||
import '../utils/url_launcher.dart';
|
||||
|
||||
class ChatDetails extends StatefulWidget {
|
||||
|
@ -285,13 +286,31 @@ class _ChatDetailsState extends State<ChatDetails> {
|
|||
child: Icon(Icons.insert_emoticon),
|
||||
),
|
||||
title: Text(L10n.of(context).emoteSettings),
|
||||
onTap: () async =>
|
||||
onTap: () async {
|
||||
// okay, we need to test if there are any emote state events other than the default one
|
||||
// if so, we need to be directed to a selection screen for which pack we want to look at
|
||||
// otherwise, we just open the normal one.
|
||||
if ((widget.room.states
|
||||
.states['im.ponies.room_emotes'] ??
|
||||
<String, Event>{})
|
||||
.keys
|
||||
.any((String s) => s.isNotEmpty)) {
|
||||
await Navigator.of(context).push(
|
||||
AppRoute.defaultRoute(
|
||||
context,
|
||||
MultipleEmotesSettingsView(
|
||||
room: widget.room),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
await Navigator.of(context).push(
|
||||
AppRoute.defaultRoute(
|
||||
context,
|
||||
EmotesSettingsView(room: widget.room),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
PopupMenuButton(
|
||||
child: ListTile(
|
||||
|
|
|
@ -14,23 +14,25 @@ import 'chat_list.dart';
|
|||
|
||||
class EmotesSettingsView extends StatelessWidget {
|
||||
final Room room;
|
||||
final String stateKey;
|
||||
|
||||
EmotesSettingsView({this.room});
|
||||
EmotesSettingsView({this.room, this.stateKey});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AdaptivePageLayout(
|
||||
primaryPage: FocusPage.SECOND,
|
||||
firstScaffold: ChatList(),
|
||||
secondScaffold: EmotesSettings(room: room),
|
||||
secondScaffold: EmotesSettings(room: room, stateKey: stateKey),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class EmotesSettings extends StatefulWidget {
|
||||
final Room room;
|
||||
final String stateKey;
|
||||
|
||||
EmotesSettings({this.room});
|
||||
EmotesSettings({this.room, this.stateKey});
|
||||
|
||||
@override
|
||||
_EmotesSettingsState createState() => _EmotesSettingsState();
|
||||
|
@ -59,21 +61,42 @@ class _EmotesSettingsState extends State<EmotesSettings> {
|
|||
// be sure to preserve any data not in "short"
|
||||
Map<String, dynamic> content;
|
||||
if (widget.room != null) {
|
||||
content = widget.room.getState('im.ponies.room_emotes')?.content ??
|
||||
content = widget.room
|
||||
.getState('im.ponies.room_emotes', widget.stateKey ?? '')
|
||||
?.content ??
|
||||
<String, dynamic>{};
|
||||
} else {
|
||||
content = client.accountData['im.ponies.user_emotes']?.content ??
|
||||
<String, dynamic>{};
|
||||
}
|
||||
debugPrint(content.toString());
|
||||
content['short'] = <String, String>{};
|
||||
for (final emote in emotes) {
|
||||
content['short'][emote.emote] = emote.mxc;
|
||||
if (!(content['emoticons'] is Map)) {
|
||||
content['emoticons'] = <String, dynamic>{};
|
||||
}
|
||||
// add / update changed emotes
|
||||
final allowedShortcodes = <String>{};
|
||||
for (final emote in emotes) {
|
||||
allowedShortcodes.add(emote.emote);
|
||||
if (!(content['emoticons'][emote.emote] is Map)) {
|
||||
content['emoticons'][emote.emote] = <String, dynamic>{};
|
||||
}
|
||||
content['emoticons'][emote.emote]['url'] = emote.mxc;
|
||||
}
|
||||
// remove emotes no more needed
|
||||
// we make the iterator .toList() here so that we don't get into trouble modifying the very
|
||||
// thing we are iterating over
|
||||
for (final shortcode in content['emoticons'].keys.toList()) {
|
||||
if (!allowedShortcodes.contains(shortcode)) {
|
||||
content['emoticons'].remove(shortcode);
|
||||
}
|
||||
}
|
||||
// remove the old "short" key
|
||||
content.remove('short');
|
||||
debugPrint(content.toString());
|
||||
if (widget.room != null) {
|
||||
await SimpleDialogs(context).tryRequestWithLoadingDialog(
|
||||
client.sendState(widget.room.id, 'im.ponies.room_emotes', content),
|
||||
client.sendState(widget.room.id, 'im.ponies.room_emotes', content,
|
||||
widget.stateKey ?? ''),
|
||||
);
|
||||
} else {
|
||||
await SimpleDialogs(context).tryRequestWithLoadingDialog(
|
||||
|
@ -82,6 +105,43 @@ class _EmotesSettingsState extends State<EmotesSettings> {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> _setIsGloballyActive(BuildContext context, bool active) async {
|
||||
if (widget.room == null) {
|
||||
return;
|
||||
}
|
||||
final client = Matrix.of(context).client;
|
||||
final content = client.accountData['im.ponies.emote_rooms']?.content ??
|
||||
<String, dynamic>{};
|
||||
if (active) {
|
||||
if (!(content['rooms'] is Map)) {
|
||||
content['rooms'] = <String, dynamic>{};
|
||||
}
|
||||
if (!(content['rooms'][widget.room.id] is Map)) {
|
||||
content['rooms'][widget.room.id] = <String, dynamic>{};
|
||||
}
|
||||
if (!(content['rooms'][widget.room.id][widget.stateKey ?? ''] is Map)) {
|
||||
content['rooms'][widget.room.id]
|
||||
[widget.stateKey ?? ''] = <String, dynamic>{};
|
||||
}
|
||||
} else if (content['rooms'] is Map &&
|
||||
content['rooms'][widget.room.id] is Map) {
|
||||
content['rooms'][widget.room.id].remove(widget.stateKey ?? '');
|
||||
}
|
||||
// and save
|
||||
await SimpleDialogs(context).tryRequestWithLoadingDialog(
|
||||
client.setAccountData(client.userID, 'im.ponies.emote_rooms', content),
|
||||
);
|
||||
}
|
||||
|
||||
bool isGloballyActive(Client client) =>
|
||||
widget.room != null &&
|
||||
client.accountData['im.ponies.emote_rooms']?.content is Map &&
|
||||
client.accountData['im.ponies.emote_rooms'].content['rooms'] is Map &&
|
||||
client.accountData['im.ponies.emote_rooms'].content['rooms']
|
||||
[widget.room.id] is Map &&
|
||||
client.accountData['im.ponies.emote_rooms'].content['rooms']
|
||||
[widget.room.id][widget.stateKey ?? ''] is Map;
|
||||
|
||||
bool get readonly => widget.room == null
|
||||
? false
|
||||
: !(widget.room.canSendEvent('im.ponies.room_emotes'));
|
||||
|
@ -93,18 +153,33 @@ class _EmotesSettingsState extends State<EmotesSettings> {
|
|||
emotes = <_EmoteEntry>[];
|
||||
Map<String, dynamic> emoteSource;
|
||||
if (widget.room != null) {
|
||||
emoteSource = widget.room.getState('im.ponies.room_emotes')?.content;
|
||||
emoteSource = widget.room
|
||||
.getState('im.ponies.room_emotes', widget.stateKey ?? '')
|
||||
?.content;
|
||||
} else {
|
||||
emoteSource = client.accountData['im.ponies.user_emotes']?.content;
|
||||
}
|
||||
if (emoteSource != null && emoteSource['short'] is Map) {
|
||||
if (emoteSource != null) {
|
||||
if (emoteSource['emoticons'] is Map) {
|
||||
emoteSource['emoticons'].forEach((key, value) {
|
||||
if (key is String &&
|
||||
value is Map &&
|
||||
value['url'] is String &&
|
||||
value['url'].startsWith('mxc://')) {
|
||||
emotes.add(_EmoteEntry(emote: key, mxc: value['url']));
|
||||
}
|
||||
});
|
||||
} else if (emoteSource['short'] is Map) {
|
||||
emoteSource['short'].forEach((key, value) {
|
||||
if (key is String && value is String && value.startsWith('mxc://')) {
|
||||
if (key is String &&
|
||||
value is String &&
|
||||
value.startsWith('mxc://')) {
|
||||
emotes.add(_EmoteEntry(emote: key, mxc: value));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(L10n.of(context).emoteSettings),
|
||||
|
@ -166,7 +241,6 @@ class _EmotesSettingsState extends State<EmotesSettings> {
|
|||
size: 32.0,
|
||||
),
|
||||
onTap: () async {
|
||||
debugPrint('blah');
|
||||
if (newEmoteController.text == null ||
|
||||
newEmoteController.text.isEmpty ||
|
||||
newMxcController.text == null ||
|
||||
|
@ -204,7 +278,19 @@ class _EmotesSettingsState extends State<EmotesSettings> {
|
|||
vertical: 8.0,
|
||||
),
|
||||
),
|
||||
if (!readonly)
|
||||
if (widget.room != null)
|
||||
ListTile(
|
||||
title: Text(L10n.of(context).enableEmotesGlobally),
|
||||
trailing: Switch(
|
||||
value: isGloballyActive(client),
|
||||
activeColor: Theme.of(context).primaryColor,
|
||||
onChanged: (bool newValue) async {
|
||||
await _setIsGloballyActive(context, newValue);
|
||||
setState(() => null);
|
||||
},
|
||||
),
|
||||
),
|
||||
if (!readonly || widget.room != null)
|
||||
Divider(
|
||||
height: 2,
|
||||
thickness: 2,
|
||||
|
|
74
lib/views/settings_multiple_emotes.dart
Normal file
74
lib/views/settings_multiple_emotes.dart
Normal file
|
@ -0,0 +1,74 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import '../components/adaptive_page_layout.dart';
|
||||
import '../utils/app_route.dart';
|
||||
import 'chat_list.dart';
|
||||
import 'settings_emotes.dart';
|
||||
|
||||
class MultipleEmotesSettingsView extends StatelessWidget {
|
||||
final Room room;
|
||||
|
||||
MultipleEmotesSettingsView({this.room});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AdaptivePageLayout(
|
||||
primaryPage: FocusPage.SECOND,
|
||||
firstScaffold: ChatList(),
|
||||
secondScaffold: MultipleEmotesSettings(room: room),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MultipleEmotesSettings extends StatelessWidget {
|
||||
final Room room;
|
||||
|
||||
MultipleEmotesSettings({this.room});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(L10n.of(context).emotePacks),
|
||||
),
|
||||
body: StreamBuilder(
|
||||
stream: room.onUpdate.stream,
|
||||
builder: (context, snapshot) {
|
||||
final packs =
|
||||
room.states.states['im.ponies.room_emotes'] ?? <String, Event>{};
|
||||
if (!packs.containsKey('')) {
|
||||
packs[''] = null;
|
||||
}
|
||||
final keys = packs.keys.toList();
|
||||
keys.sort();
|
||||
return ListView.separated(
|
||||
separatorBuilder: (BuildContext context, int i) => Container(),
|
||||
itemCount: keys.length,
|
||||
itemBuilder: (BuildContext context, int i) {
|
||||
final event = packs[keys[i]];
|
||||
var packName = keys[i].isNotEmpty ? keys[i] : 'Default Pack';
|
||||
if (event != null && event.content['pack'] is Map) {
|
||||
if (event.content['pack']['displayname'] is String) {
|
||||
packName = event.content['pack']['displayname'];
|
||||
} else if (event.content['pack']['name'] is String) {
|
||||
packName = event.content['pack']['name'];
|
||||
}
|
||||
}
|
||||
return ListTile(
|
||||
title: Text(packName),
|
||||
onTap: () async {
|
||||
await Navigator.of(context).push(
|
||||
AppRoute.defaultRoute(
|
||||
context,
|
||||
EmotesSettingsView(room: room, stateKey: keys[i]),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
56
pubspec.lock
56
pubspec.lock
|
@ -49,7 +49,7 @@ packages:
|
|||
name: async
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.5.0-nullsafety.1"
|
||||
version: "2.4.2"
|
||||
base58check:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -63,7 +63,7 @@ packages:
|
|||
name: boolean_selector
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.0-nullsafety.1"
|
||||
version: "2.0.0"
|
||||
bot_toast:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -91,14 +91,14 @@ packages:
|
|||
name: characters
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.0-nullsafety.3"
|
||||
version: "1.0.0"
|
||||
charcode:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: charcode
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.2.0-nullsafety.1"
|
||||
version: "1.1.3"
|
||||
cli_util:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -112,14 +112,14 @@ packages:
|
|||
name: clock
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.0-nullsafety.1"
|
||||
version: "1.0.1"
|
||||
collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: collection
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.15.0-nullsafety.3"
|
||||
version: "1.14.13"
|
||||
convert:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -175,13 +175,13 @@ packages:
|
|||
name: fake_async
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.2.0-nullsafety.1"
|
||||
version: "1.1.0"
|
||||
famedlysdk:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: "5019ebfeb56f0789ab4cc8d27ccda663156b5d68"
|
||||
resolved-ref: "5019ebfeb56f0789ab4cc8d27ccda663156b5d68"
|
||||
ref: "84cc925b08e97098d00c54fff9c1244f91055de3"
|
||||
resolved-ref: "84cc925b08e97098d00c54fff9c1244f91055de3"
|
||||
url: "https://gitlab.com/famedly/famedlysdk.git"
|
||||
source: git
|
||||
version: "0.0.1"
|
||||
|
@ -456,7 +456,7 @@ packages:
|
|||
name: js
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.6.3-nullsafety.1"
|
||||
version: "0.6.2"
|
||||
localstorage:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -484,7 +484,7 @@ packages:
|
|||
name: matcher
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.12.10-nullsafety.1"
|
||||
version: "0.12.8"
|
||||
matrix_file_e2ee:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -512,7 +512,7 @@ packages:
|
|||
name: meta
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.3.0-nullsafety.3"
|
||||
version: "1.1.8"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -605,7 +605,7 @@ packages:
|
|||
name: path
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.8.0-nullsafety.1"
|
||||
version: "1.7.0"
|
||||
path_provider:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -647,7 +647,7 @@ packages:
|
|||
name: pedantic
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.10.0-nullsafety.1"
|
||||
version: "1.9.0"
|
||||
petitparser:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -689,7 +689,7 @@ packages:
|
|||
name: pool
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.5.0-nullsafety.1"
|
||||
version: "1.4.0"
|
||||
process:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -792,21 +792,21 @@ packages:
|
|||
name: source_map_stack_trace
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.0-nullsafety.2"
|
||||
version: "2.0.0"
|
||||
source_maps:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_maps
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.10.10-nullsafety.1"
|
||||
version: "0.10.9"
|
||||
source_span:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_span
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.8.0-nullsafety.2"
|
||||
version: "1.7.0"
|
||||
sqflite:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -841,21 +841,21 @@ packages:
|
|||
name: stack_trace
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.10.0-nullsafety.1"
|
||||
version: "1.9.5"
|
||||
stream_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stream_channel
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.0-nullsafety.1"
|
||||
version: "2.0.0"
|
||||
string_scanner:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: string_scanner
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.0-nullsafety.1"
|
||||
version: "1.0.5"
|
||||
synchronized:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -869,35 +869,35 @@ packages:
|
|||
name: term_glyph
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.2.0-nullsafety.1"
|
||||
version: "1.1.0"
|
||||
test:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.16.0-nullsafety.5"
|
||||
version: "1.15.2"
|
||||
test_api:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.2.19-nullsafety.2"
|
||||
version: "0.2.17"
|
||||
test_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_core
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.3.12-nullsafety.5"
|
||||
version: "0.3.10"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: typed_data
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.3.0-nullsafety.3"
|
||||
version: "1.2.0"
|
||||
universal_html:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -981,7 +981,7 @@ packages:
|
|||
name: vector_math
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.0-nullsafety.3"
|
||||
version: "2.0.8"
|
||||
vm_service:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1053,5 +1053,5 @@ packages:
|
|||
source: hosted
|
||||
version: "0.1.2"
|
||||
sdks:
|
||||
dart: ">=2.10.0-110 <2.11.0"
|
||||
dart: ">=2.9.0 <3.0.0"
|
||||
flutter: ">=1.20.0 <2.0.0"
|
||||
|
|
|
@ -27,7 +27,7 @@ dependencies:
|
|||
famedlysdk:
|
||||
git:
|
||||
url: https://gitlab.com/famedly/famedlysdk.git
|
||||
ref: 5019ebfeb56f0789ab4cc8d27ccda663156b5d68
|
||||
ref: 84cc925b08e97098d00c54fff9c1244f91055de3
|
||||
|
||||
localstorage: ^3.0.1+4
|
||||
memoryfilepicker: ^0.1.3
|
||||
|
|
Loading…
Reference in a new issue