mirror of
https://gitlab.com/famedly/fluffychat.git
synced 2024-11-06 20:19:30 +01:00
Merge branch 'soru/image-pack-creator' into 'main'
chore: cleanup image pack editor to attach metadata and prepare to make it more general See merge request famedly/fluffychat!466
This commit is contained in:
commit
a5fc4521cb
@ -2,16 +2,15 @@ import 'package:adaptive_dialog/adaptive_dialog.dart';
|
|||||||
|
|
||||||
import 'package:matrix/matrix.dart';
|
import 'package:matrix/matrix.dart';
|
||||||
import 'package:file_picker_cross/file_picker_cross.dart';
|
import 'package:file_picker_cross/file_picker_cross.dart';
|
||||||
import 'package:fluffychat/utils/platform_infos.dart';
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||||
import 'package:vrouter/vrouter.dart';
|
import 'package:vrouter/vrouter.dart';
|
||||||
|
|
||||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
|
||||||
import 'views/settings_emotes_view.dart';
|
import 'views/settings_emotes_view.dart';
|
||||||
import '../widgets/matrix.dart';
|
import '../widgets/matrix.dart';
|
||||||
|
import '../utils/resize_image.dart';
|
||||||
|
|
||||||
class EmotesSettings extends StatefulWidget {
|
class EmotesSettings extends StatefulWidget {
|
||||||
EmotesSettings({Key key}) : super(key: key);
|
EmotesSettings({Key key}) : super(key: key);
|
||||||
@ -20,24 +19,18 @@ class EmotesSettings extends StatefulWidget {
|
|||||||
EmotesSettingsController createState() => EmotesSettingsController();
|
EmotesSettingsController createState() => EmotesSettingsController();
|
||||||
}
|
}
|
||||||
|
|
||||||
class EmoteEntry {
|
|
||||||
String emote;
|
|
||||||
String mxc;
|
|
||||||
EmoteEntry({this.emote, this.mxc});
|
|
||||||
}
|
|
||||||
|
|
||||||
class EmotesSettingsController extends State<EmotesSettings> {
|
class EmotesSettingsController extends State<EmotesSettings> {
|
||||||
String get roomId => VRouter.of(context).pathParameters['roomid'];
|
String get roomId => VRouter.of(context).pathParameters['roomid'];
|
||||||
Room get room =>
|
Room get room =>
|
||||||
roomId != null ? Matrix.of(context).client.getRoomById(roomId) : null;
|
roomId != null ? Matrix.of(context).client.getRoomById(roomId) : null;
|
||||||
String get stateKey => VRouter.of(context).pathParameters['state_key'];
|
String get stateKey => VRouter.of(context).pathParameters['state_key'];
|
||||||
|
|
||||||
List<EmoteEntry> emotes;
|
|
||||||
bool showSave = false;
|
bool showSave = false;
|
||||||
TextEditingController newEmoteController = TextEditingController();
|
TextEditingController newImageCodeController = TextEditingController();
|
||||||
TextEditingController newMxcController = TextEditingController();
|
ValueNotifier<ImagePackImageContent> newImageController =
|
||||||
|
ValueNotifier<ImagePackImageContent>(null);
|
||||||
|
|
||||||
ImagePackContent _getPack(BuildContext context) {
|
ImagePackContent _getPack() {
|
||||||
final client = Matrix.of(context).client;
|
final client = Matrix.of(context).client;
|
||||||
final event = (room != null
|
final event = (room != null
|
||||||
? room.getState('im.ponies.room_emotes', stateKey ?? '')
|
? room.getState('im.ponies.room_emotes', stateKey ?? '')
|
||||||
@ -50,33 +43,20 @@ class EmotesSettingsController extends State<EmotesSettings> {
|
|||||||
return BasicEvent.fromJson(event.toJson()).parsedImagePackContent;
|
return BasicEvent.fromJson(event.toJson()).parsedImagePackContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ImagePackContent _pack;
|
||||||
|
ImagePackContent get pack {
|
||||||
|
if (_pack != null) {
|
||||||
|
return _pack;
|
||||||
|
}
|
||||||
|
_pack = _getPack();
|
||||||
|
return _pack;
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _save(BuildContext context) async {
|
Future<void> _save(BuildContext context) async {
|
||||||
if (readonly) {
|
if (readonly) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final client = Matrix.of(context).client;
|
final client = Matrix.of(context).client;
|
||||||
final pack = _getPack(context);
|
|
||||||
// add / update changed emotes
|
|
||||||
final allowedShortcodes = <String>{};
|
|
||||||
for (final emote in emotes) {
|
|
||||||
allowedShortcodes.add(emote.emote);
|
|
||||||
if (pack.images.containsKey(emote.emote)) {
|
|
||||||
pack.images[emote.emote].url = Uri.parse(emote.mxc);
|
|
||||||
} else {
|
|
||||||
pack.images[emote.emote] =
|
|
||||||
ImagePackImageContent.fromJson(<String, dynamic>{
|
|
||||||
'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 pack.images.keys.toList()) {
|
|
||||||
if (!allowedShortcodes.contains(shortcode)) {
|
|
||||||
pack.images.remove(shortcode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (room != null) {
|
if (room != null) {
|
||||||
await showFutureLoadingDialog(
|
await showFutureLoadingDialog(
|
||||||
context: context,
|
context: context,
|
||||||
@ -121,19 +101,19 @@ class EmotesSettingsController extends State<EmotesSettings> {
|
|||||||
setState(() => null);
|
setState(() => null);
|
||||||
}
|
}
|
||||||
|
|
||||||
void removeEmoteAction(EmoteEntry emote) => setState(() {
|
void removeImageAction(String oldImageCode) => setState(() {
|
||||||
emotes.removeWhere((e) => e.emote == emote.emote);
|
pack.images.remove(oldImageCode);
|
||||||
showSave = true;
|
showSave = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
void submitEmoteAction(
|
void submitImageAction(
|
||||||
String emoteCode,
|
String oldImageCode,
|
||||||
EmoteEntry emote,
|
String imageCode,
|
||||||
|
ImagePackImageContent image,
|
||||||
TextEditingController controller,
|
TextEditingController controller,
|
||||||
) {
|
) {
|
||||||
if (emotes.indexWhere((e) => e.emote == emoteCode && e.mxc != emote.mxc) !=
|
if (pack.images.keys.any((k) => k == imageCode && k != oldImageCode)) {
|
||||||
-1) {
|
controller.text = oldImageCode;
|
||||||
controller.text = emote.emote;
|
|
||||||
showOkAlertDialog(
|
showOkAlertDialog(
|
||||||
useRootNavigator: false,
|
useRootNavigator: false,
|
||||||
context: context,
|
context: context,
|
||||||
@ -142,8 +122,8 @@ class EmotesSettingsController extends State<EmotesSettings> {
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!RegExp(r'^[-\w]+$').hasMatch(emoteCode)) {
|
if (!RegExp(r'^[-\w]+$').hasMatch(imageCode)) {
|
||||||
controller.text = emote.emote;
|
controller.text = oldImageCode;
|
||||||
showOkAlertDialog(
|
showOkAlertDialog(
|
||||||
useRootNavigator: false,
|
useRootNavigator: false,
|
||||||
context: context,
|
context: context,
|
||||||
@ -153,7 +133,8 @@ class EmotesSettingsController extends State<EmotesSettings> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setState(() {
|
setState(() {
|
||||||
emote.emote = emoteCode;
|
pack.images[imageCode] = image;
|
||||||
|
pack.images.remove(oldImageCode);
|
||||||
showSave = true;
|
showSave = true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -177,11 +158,10 @@ class EmotesSettingsController extends State<EmotesSettings> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void addEmoteAction() async {
|
void addImageAction() async {
|
||||||
if (newEmoteController.text == null ||
|
if (newImageCodeController.text == null ||
|
||||||
newEmoteController.text.isEmpty ||
|
newImageCodeController.text.isEmpty ||
|
||||||
newMxcController.text == null ||
|
newImageController.value == null) {
|
||||||
newMxcController.text.isEmpty) {
|
|
||||||
await showOkAlertDialog(
|
await showOkAlertDialog(
|
||||||
useRootNavigator: false,
|
useRootNavigator: false,
|
||||||
context: context,
|
context: context,
|
||||||
@ -190,9 +170,8 @@ class EmotesSettingsController extends State<EmotesSettings> {
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final emoteCode = '${newEmoteController.text}';
|
final imageCode = newImageCodeController.text;
|
||||||
final mxc = newMxcController.text;
|
if (pack.images.containsKey(imageCode)) {
|
||||||
if (emotes.indexWhere((e) => e.emote == emoteCode && e.mxc != mxc) != -1) {
|
|
||||||
await showOkAlertDialog(
|
await showOkAlertDialog(
|
||||||
useRootNavigator: false,
|
useRootNavigator: false,
|
||||||
context: context,
|
context: context,
|
||||||
@ -201,7 +180,7 @@ class EmotesSettingsController extends State<EmotesSettings> {
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!RegExp(r'^[-\w]+$').hasMatch(emoteCode)) {
|
if (!RegExp(r'^[-\w]+$').hasMatch(imageCode)) {
|
||||||
await showOkAlertDialog(
|
await showOkAlertDialog(
|
||||||
useRootNavigator: false,
|
useRootNavigator: false,
|
||||||
context: context,
|
context: context,
|
||||||
@ -210,41 +189,28 @@ class EmotesSettingsController extends State<EmotesSettings> {
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
emotes.add(EmoteEntry(emote: emoteCode, mxc: mxc));
|
pack.images[imageCode] = newImageController.value;
|
||||||
await _save(context);
|
await _save(context);
|
||||||
setState(() {
|
setState(() {
|
||||||
newEmoteController.text = '';
|
newImageCodeController.text = '';
|
||||||
newMxcController.text = '';
|
newImageController.value = null;
|
||||||
showSave = false;
|
showSave = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void emoteImagePickerAction(TextEditingController controller) async {
|
void imagePickerAction(
|
||||||
if (kIsWeb) {
|
ValueNotifier<ImagePackImageContent> controller) async {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
final result =
|
||||||
SnackBar(content: Text(L10n.of(context).notSupportedInWeb)));
|
await FilePickerCross.importFromStorage(type: FileTypeCross.image);
|
||||||
return;
|
if (result == null) return;
|
||||||
}
|
var file = MatrixImageFile(
|
||||||
MatrixFile file;
|
bytes: result.toUint8List(),
|
||||||
if (PlatformInfos.isMobile) {
|
name: result.fileName,
|
||||||
final result = await ImagePicker().getImage(
|
);
|
||||||
source: ImageSource.gallery,
|
try {
|
||||||
imageQuality: 50,
|
file = await resizeImage(file, max: 1600);
|
||||||
maxWidth: 1600,
|
} catch (_) {
|
||||||
maxHeight: 1600);
|
// do nothing
|
||||||
if (result == null) return;
|
|
||||||
file = MatrixFile(
|
|
||||||
bytes: await result.readAsBytes(),
|
|
||||||
name: result.path,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
final result =
|
|
||||||
await FilePickerCross.importFromStorage(type: FileTypeCross.image);
|
|
||||||
if (result == null) return;
|
|
||||||
file = MatrixFile(
|
|
||||||
bytes: result.toUint8List(),
|
|
||||||
name: result.fileName,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
final uploadResp = await showFutureLoadingDialog(
|
final uploadResp = await showFutureLoadingDialog(
|
||||||
context: context,
|
context: context,
|
||||||
@ -253,21 +219,30 @@ class EmotesSettingsController extends State<EmotesSettings> {
|
|||||||
);
|
);
|
||||||
if (uploadResp.error == null) {
|
if (uploadResp.error == null) {
|
||||||
setState(() {
|
setState(() {
|
||||||
controller.text = uploadResp.result;
|
final info = <String, dynamic>{
|
||||||
|
...file.info,
|
||||||
|
};
|
||||||
|
// normalize width / height to 256, required for stickers
|
||||||
|
if (info['w'] is int && info['h'] is int) {
|
||||||
|
final ratio = info['w'] / info['h'];
|
||||||
|
if (info['w'] > info['h']) {
|
||||||
|
info['w'] = 256;
|
||||||
|
info['h'] = (256.0 / ratio).round();
|
||||||
|
} else {
|
||||||
|
info['h'] = 256;
|
||||||
|
info['w'] = (ratio * 256.0).round();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
controller.value = ImagePackImageContent.fromJson(<String, dynamic>{
|
||||||
|
'url': uploadResp.result,
|
||||||
|
'info': info,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (emotes == null) {
|
|
||||||
emotes = <EmoteEntry>[];
|
|
||||||
final pack = _getPack(context);
|
|
||||||
for (final entry in pack.images.entries) {
|
|
||||||
emotes
|
|
||||||
.add(EmoteEntry(emote: entry.key, mxc: entry.value.url.toString()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return EmotesSettingsView(this);
|
return EmotesSettingsView(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,7 @@ class EmotesSettingsView extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final client = Matrix.of(context).client;
|
final client = Matrix.of(context).client;
|
||||||
|
final imageKeys = controller.pack.images.keys.toList();
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: BackButton(),
|
leading: BackButton(),
|
||||||
@ -29,200 +30,195 @@ class EmotesSettingsView extends StatelessWidget {
|
|||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
body: MaxWidthBody(
|
body: MaxWidthBody(
|
||||||
child: StreamBuilder(
|
child: Column(
|
||||||
stream: controller.room?.onUpdate?.stream,
|
children: <Widget>[
|
||||||
builder: (context, snapshot) {
|
if (!controller.readonly)
|
||||||
return Column(
|
Container(
|
||||||
children: <Widget>[
|
padding: EdgeInsets.symmetric(
|
||||||
if (!controller.readonly)
|
vertical: 8.0,
|
||||||
Container(
|
),
|
||||||
padding: EdgeInsets.symmetric(
|
child: ListTile(
|
||||||
vertical: 8.0,
|
leading: Container(
|
||||||
|
width: 180.0,
|
||||||
|
height: 38,
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(10)),
|
||||||
|
color: Theme.of(context).secondaryHeaderColor,
|
||||||
|
),
|
||||||
|
child: TextField(
|
||||||
|
controller: controller.newImageCodeController,
|
||||||
|
autocorrect: false,
|
||||||
|
minLines: 1,
|
||||||
|
maxLines: 1,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: L10n.of(context).emoteShortcode,
|
||||||
|
prefixText: ': ',
|
||||||
|
suffixText: ':',
|
||||||
|
prefixStyle: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.secondary,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
suffixStyle: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.secondary,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
border: InputBorder.none,
|
||||||
),
|
),
|
||||||
child: ListTile(
|
),
|
||||||
leading: Container(
|
),
|
||||||
width: 180.0,
|
title: _ImagePicker(
|
||||||
height: 38,
|
controller: controller.newImageController,
|
||||||
padding: EdgeInsets.symmetric(horizontal: 8),
|
onPressed: controller.imagePickerAction,
|
||||||
decoration: BoxDecoration(
|
),
|
||||||
borderRadius: BorderRadius.all(Radius.circular(10)),
|
trailing: InkWell(
|
||||||
color: Theme.of(context).secondaryHeaderColor,
|
onTap: controller.addImageAction,
|
||||||
),
|
child: Icon(
|
||||||
child: TextField(
|
Icons.add_outlined,
|
||||||
controller: controller.newEmoteController,
|
color: Colors.green,
|
||||||
autocorrect: false,
|
size: 32.0,
|
||||||
minLines: 1,
|
),
|
||||||
maxLines: 1,
|
),
|
||||||
decoration: InputDecoration(
|
),
|
||||||
hintText: L10n.of(context).emoteShortcode,
|
),
|
||||||
prefixText: ': ',
|
if (controller.room != null)
|
||||||
suffixText: ':',
|
ListTile(
|
||||||
prefixStyle: TextStyle(
|
title: Text(L10n.of(context).enableEmotesGlobally),
|
||||||
color: Theme.of(context).colorScheme.secondary,
|
trailing: Switch(
|
||||||
fontWeight: FontWeight.bold,
|
value: controller.isGloballyActive(client),
|
||||||
),
|
onChanged: controller.setIsGloballyActive,
|
||||||
suffixStyle: TextStyle(
|
),
|
||||||
color: Theme.of(context).colorScheme.secondary,
|
),
|
||||||
fontWeight: FontWeight.bold,
|
if (!controller.readonly || controller.room != null)
|
||||||
),
|
Divider(
|
||||||
border: InputBorder.none,
|
height: 2,
|
||||||
|
thickness: 2,
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: imageKeys.isEmpty
|
||||||
|
? Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(16),
|
||||||
|
child: Text(
|
||||||
|
L10n.of(context).noEmotesFound,
|
||||||
|
style: TextStyle(fontSize: 20),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: ListView.separated(
|
||||||
|
separatorBuilder: (BuildContext context, int i) =>
|
||||||
|
Container(),
|
||||||
|
itemCount: imageKeys.length + 1,
|
||||||
|
itemBuilder: (BuildContext context, int i) {
|
||||||
|
if (i >= imageKeys.length) {
|
||||||
|
return Container(height: 70);
|
||||||
|
}
|
||||||
|
final imageCode = imageKeys[i];
|
||||||
|
final image = controller.pack.images[imageCode];
|
||||||
|
final textEditingController = TextEditingController();
|
||||||
|
textEditingController.text = imageCode;
|
||||||
|
final useShortCuts =
|
||||||
|
(PlatformInfos.isWeb || PlatformInfos.isDesktop);
|
||||||
|
return ListTile(
|
||||||
|
leading: Container(
|
||||||
|
width: 180.0,
|
||||||
|
height: 38,
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius:
|
||||||
|
BorderRadius.all(Radius.circular(10)),
|
||||||
|
color: Theme.of(context).secondaryHeaderColor,
|
||||||
),
|
),
|
||||||
),
|
child: Shortcuts(
|
||||||
),
|
shortcuts: !useShortCuts
|
||||||
title: _EmoteImagePicker(
|
? {}
|
||||||
controller: controller.newMxcController,
|
: {
|
||||||
onPressed: controller.emoteImagePickerAction,
|
LogicalKeySet(LogicalKeyboardKey.enter):
|
||||||
),
|
SubmitLineIntent(),
|
||||||
trailing: InkWell(
|
},
|
||||||
onTap: controller.addEmoteAction,
|
child: Actions(
|
||||||
child: Icon(
|
actions: !useShortCuts
|
||||||
Icons.add_outlined,
|
? {}
|
||||||
color: Colors.green,
|
: {
|
||||||
size: 32.0,
|
SubmitLineIntent:
|
||||||
),
|
CallbackAction(onInvoke: (i) {
|
||||||
),
|
controller.submitImageAction(
|
||||||
),
|
imageCode,
|
||||||
),
|
textEditingController.text,
|
||||||
if (controller.room != null)
|
image,
|
||||||
ListTile(
|
textEditingController,
|
||||||
title: Text(L10n.of(context).enableEmotesGlobally),
|
);
|
||||||
trailing: Switch(
|
return null;
|
||||||
value: controller.isGloballyActive(client),
|
}),
|
||||||
onChanged: controller.setIsGloballyActive,
|
},
|
||||||
),
|
child: TextField(
|
||||||
),
|
readOnly: controller.readonly,
|
||||||
if (!controller.readonly || controller.room != null)
|
controller: textEditingController,
|
||||||
Divider(
|
autocorrect: false,
|
||||||
height: 2,
|
minLines: 1,
|
||||||
thickness: 2,
|
maxLines: 1,
|
||||||
color: Theme.of(context).primaryColor,
|
decoration: InputDecoration(
|
||||||
),
|
hintText: L10n.of(context).emoteShortcode,
|
||||||
Expanded(
|
prefixText: ': ',
|
||||||
child: controller.emotes.isEmpty
|
suffixText: ':',
|
||||||
? Center(
|
prefixStyle: TextStyle(
|
||||||
child: Padding(
|
color: Theme.of(context)
|
||||||
padding: EdgeInsets.all(16),
|
.colorScheme
|
||||||
child: Text(
|
.secondary,
|
||||||
L10n.of(context).noEmotesFound,
|
fontWeight: FontWeight.bold,
|
||||||
style: TextStyle(fontSize: 20),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: ListView.separated(
|
|
||||||
separatorBuilder: (BuildContext context, int i) =>
|
|
||||||
Container(),
|
|
||||||
itemCount: controller.emotes.length + 1,
|
|
||||||
itemBuilder: (BuildContext context, int i) {
|
|
||||||
if (i >= controller.emotes.length) {
|
|
||||||
return Container(height: 70);
|
|
||||||
}
|
|
||||||
final emote = controller.emotes[i];
|
|
||||||
final textEditingController =
|
|
||||||
TextEditingController();
|
|
||||||
textEditingController.text = emote.emote;
|
|
||||||
final useShortCuts = (PlatformInfos.isWeb ||
|
|
||||||
PlatformInfos.isDesktop);
|
|
||||||
return ListTile(
|
|
||||||
leading: Container(
|
|
||||||
width: 180.0,
|
|
||||||
height: 38,
|
|
||||||
padding: EdgeInsets.symmetric(horizontal: 8),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius:
|
|
||||||
BorderRadius.all(Radius.circular(10)),
|
|
||||||
color:
|
|
||||||
Theme.of(context).secondaryHeaderColor,
|
|
||||||
),
|
|
||||||
child: Shortcuts(
|
|
||||||
shortcuts: !useShortCuts
|
|
||||||
? {}
|
|
||||||
: {
|
|
||||||
LogicalKeySet(
|
|
||||||
LogicalKeyboardKey.enter):
|
|
||||||
SubmitLineIntent(),
|
|
||||||
},
|
|
||||||
child: Actions(
|
|
||||||
actions: !useShortCuts
|
|
||||||
? {}
|
|
||||||
: {
|
|
||||||
SubmitLineIntent:
|
|
||||||
CallbackAction(onInvoke: (i) {
|
|
||||||
controller.submitEmoteAction(
|
|
||||||
textEditingController.text,
|
|
||||||
emote,
|
|
||||||
textEditingController,
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
child: TextField(
|
|
||||||
readOnly: controller.readonly,
|
|
||||||
controller: textEditingController,
|
|
||||||
autocorrect: false,
|
|
||||||
minLines: 1,
|
|
||||||
maxLines: 1,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
hintText:
|
|
||||||
L10n.of(context).emoteShortcode,
|
|
||||||
prefixText: ': ',
|
|
||||||
suffixText: ':',
|
|
||||||
prefixStyle: TextStyle(
|
|
||||||
color: Theme.of(context)
|
|
||||||
.colorScheme
|
|
||||||
.secondary,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
suffixStyle: TextStyle(
|
|
||||||
color: Theme.of(context)
|
|
||||||
.colorScheme
|
|
||||||
.secondary,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
border: InputBorder.none,
|
|
||||||
),
|
|
||||||
onSubmitted: (s) =>
|
|
||||||
controller.submitEmoteAction(
|
|
||||||
s,
|
|
||||||
emote,
|
|
||||||
textEditingController,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
suffixStyle: TextStyle(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.secondary,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
border: InputBorder.none,
|
||||||
|
),
|
||||||
|
onSubmitted: (s) =>
|
||||||
|
controller.submitImageAction(
|
||||||
|
imageCode,
|
||||||
|
s,
|
||||||
|
image,
|
||||||
|
textEditingController,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
title: _EmoteImage(emote.mxc),
|
),
|
||||||
trailing: controller.readonly
|
),
|
||||||
? null
|
|
||||||
: InkWell(
|
|
||||||
onTap: () =>
|
|
||||||
controller.removeEmoteAction(emote),
|
|
||||||
child: Icon(
|
|
||||||
Icons.delete_forever_outlined,
|
|
||||||
color: Colors.red,
|
|
||||||
size: 32.0,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
title: _EmoteImage(image.url),
|
||||||
],
|
trailing: controller.readonly
|
||||||
);
|
? null
|
||||||
}),
|
: InkWell(
|
||||||
|
onTap: () =>
|
||||||
|
controller.removeImageAction(imageCode),
|
||||||
|
child: Icon(
|
||||||
|
Icons.delete_forever_outlined,
|
||||||
|
color: Colors.red,
|
||||||
|
size: 32.0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _EmoteImage extends StatelessWidget {
|
class _EmoteImage extends StatelessWidget {
|
||||||
final String mxc;
|
final Uri mxc;
|
||||||
_EmoteImage(this.mxc);
|
_EmoteImage(this.mxc);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final size = 38.0;
|
final size = 38.0;
|
||||||
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
||||||
final url = Uri.parse(mxc)?.getThumbnail(
|
final url = mxc?.getThumbnail(
|
||||||
Matrix.of(context).client,
|
Matrix.of(context).client,
|
||||||
width: size * devicePixelRatio,
|
width: size * devicePixelRatio,
|
||||||
height: size * devicePixelRatio,
|
height: size * devicePixelRatio,
|
||||||
@ -237,27 +233,27 @@ class _EmoteImage extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _EmoteImagePicker extends StatefulWidget {
|
class _ImagePicker extends StatefulWidget {
|
||||||
final TextEditingController controller;
|
final ValueNotifier<ImagePackImageContent> controller;
|
||||||
|
|
||||||
final void Function(TextEditingController) onPressed;
|
final void Function(ValueNotifier<ImagePackImageContent>) onPressed;
|
||||||
|
|
||||||
_EmoteImagePicker({@required this.controller, @required this.onPressed});
|
_ImagePicker({@required this.controller, @required this.onPressed});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_EmoteImagePickerState createState() => _EmoteImagePickerState();
|
_ImagePickerState createState() => _ImagePickerState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _EmoteImagePickerState extends State<_EmoteImagePicker> {
|
class _ImagePickerState extends State<_ImagePicker> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (widget.controller.text == null || widget.controller.text.isEmpty) {
|
if (widget.controller.value == null) {
|
||||||
return ElevatedButton(
|
return ElevatedButton(
|
||||||
onPressed: () => widget.onPressed(widget.controller),
|
onPressed: () => widget.onPressed(widget.controller),
|
||||||
child: Text(L10n.of(context).pickImage),
|
child: Text(L10n.of(context).pickImage),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return _EmoteImage(widget.controller.text);
|
return _EmoteImage(widget.controller.value.url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,16 @@ Future<MatrixImageFile> resizeImage(MatrixImageFile file,
|
|||||||
// freeze up the UI a bit
|
// freeze up the UI a bit
|
||||||
|
|
||||||
// we can't do width / height fetching in a separate isolate, as that may use the UI stuff
|
// we can't do width / height fetching in a separate isolate, as that may use the UI stuff
|
||||||
await native.init();
|
|
||||||
|
// somehow doing native.init twice fixes it for linux desktop?
|
||||||
|
// TODO: once native imaging is on sound null safety the errors are consistent and
|
||||||
|
// then we can properly handle this instead
|
||||||
|
// https://gitlab.com/famedly/company/frontend/libraries/native_imaging/-/issues/5
|
||||||
|
try {
|
||||||
|
await native.init();
|
||||||
|
} catch (_) {
|
||||||
|
await native.init();
|
||||||
|
}
|
||||||
|
|
||||||
_IsolateArgs args;
|
_IsolateArgs args;
|
||||||
try {
|
try {
|
||||||
@ -47,9 +56,14 @@ Future<MatrixImageFile> resizeImage(MatrixImageFile file,
|
|||||||
mimeType: 'image/jpeg',
|
mimeType: 'image/jpeg',
|
||||||
width: res.width,
|
width: res.width,
|
||||||
height: res.height,
|
height: res.height,
|
||||||
|
blurhash: res.blurhash,
|
||||||
);
|
);
|
||||||
// only return the thumbnail if the size actually decreased
|
// only return the thumbnail if the size actually decreased
|
||||||
return thumbnail.size >= file.size ? file : thumbnail;
|
return thumbnail.size >= file.size ||
|
||||||
|
thumbnail.width >= file.width ||
|
||||||
|
thumbnail.height >= file.height
|
||||||
|
? file
|
||||||
|
: thumbnail;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _IsolateArgs {
|
class _IsolateArgs {
|
||||||
@ -70,7 +84,12 @@ class _IsolateResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<_IsolateResponse> _isolateFunction(_IsolateArgs args) async {
|
Future<_IsolateResponse> _isolateFunction(_IsolateArgs args) async {
|
||||||
await native.init();
|
// Hack for desktop, see above why
|
||||||
|
try {
|
||||||
|
await native.init();
|
||||||
|
} catch (_) {
|
||||||
|
await native.init();
|
||||||
|
}
|
||||||
var nativeImg = native.Image();
|
var nativeImg = native.Image();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
Loading…
Reference in New Issue
Block a user