Initial commit
This commit is contained in:
385
lib/pages/settings_emotes/settings_emotes_view.dart
Normal file
385
lib/pages/settings_emotes/settings_emotes_view.dart
Normal file
@@ -0,0 +1,385 @@
|
||||
// SPDX-FileCopyrightText: 2019-Present Christian Kußowski
|
||||
// SPDX-FileCopyrightText: 2019-Present Contributors to FluffyChat
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import 'package:tatchat/l10n/l10n.dart';
|
||||
import 'package:tatchat/utils/platform_infos.dart';
|
||||
import 'package:tatchat/utils/url_launcher.dart';
|
||||
import 'package:tatchat/widgets/layouts/max_width_body.dart';
|
||||
import 'package:tatchat/widgets/mxc_image.dart';
|
||||
import 'package:tatchat/widgets/mxc_image_viewer.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import '../../widgets/matrix.dart';
|
||||
import 'settings_emotes.dart';
|
||||
|
||||
enum PopupMenuEmojiActions { import, export }
|
||||
|
||||
class EmotesSettingsView extends StatelessWidget {
|
||||
final EmotesSettingsController controller;
|
||||
|
||||
const EmotesSettingsView(this.controller, {super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (controller.widget.roomId != null && controller.room == null) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text(L10n.of(context).oopsSomethingWentWrong)),
|
||||
body: Center(
|
||||
child: Text(L10n.of(context).youAreNoLongerParticipatingInThisChat),
|
||||
),
|
||||
);
|
||||
}
|
||||
final theme = Theme.of(context);
|
||||
|
||||
final client = Matrix.of(context).client;
|
||||
final imageKeys = controller.pack!.images.keys.toList();
|
||||
final packKeys = controller.packKeys;
|
||||
if (packKeys != null && packKeys.isEmpty) {
|
||||
packKeys.add('');
|
||||
}
|
||||
final attributionUrl = Uri.tryParse(
|
||||
controller.packAttributionController.text,
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: controller.showSave
|
||||
? TextButton(
|
||||
onPressed: controller.resetAction,
|
||||
child: Text(L10n.of(context).cancel),
|
||||
)
|
||||
: null,
|
||||
title: Text(L10n.of(context).customEmojisAndStickers),
|
||||
actions: [
|
||||
if (controller.showSave)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 16.0),
|
||||
child: TextButton(
|
||||
onPressed: () => controller.save(context),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: theme.colorScheme.primary,
|
||||
foregroundColor: theme.colorScheme.onPrimary,
|
||||
),
|
||||
child: Text(L10n.of(context).saveChanges),
|
||||
),
|
||||
)
|
||||
else
|
||||
PopupMenuButton<PopupMenuEmojiActions>(
|
||||
useRootNavigator: true,
|
||||
onSelected: (value) {
|
||||
switch (value) {
|
||||
case PopupMenuEmojiActions.export:
|
||||
controller.exportAsZip();
|
||||
break;
|
||||
case PopupMenuEmojiActions.import:
|
||||
controller.importEmojiZip();
|
||||
break;
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
if (!controller.readonly)
|
||||
PopupMenuItem(
|
||||
value: PopupMenuEmojiActions.import,
|
||||
child: Text(L10n.of(context).importFromZipFile),
|
||||
),
|
||||
if (imageKeys.isNotEmpty)
|
||||
PopupMenuItem(
|
||||
value: PopupMenuEmojiActions.export,
|
||||
child: Text(L10n.of(context).exportEmotePack),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
bottom: packKeys == null
|
||||
? null
|
||||
: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(48),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(4.0),
|
||||
child: SizedBox(
|
||||
height: 40,
|
||||
child: ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: packKeys.length + 1,
|
||||
itemBuilder: (context, i) {
|
||||
if (i == 0) {
|
||||
if (controller.readonly) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 4.0,
|
||||
),
|
||||
child: FilterChip(
|
||||
label: const Icon(Icons.add_outlined, size: 20),
|
||||
onSelected: controller.showSave
|
||||
? null
|
||||
: (_) => controller.createImagePack(),
|
||||
),
|
||||
);
|
||||
}
|
||||
i--;
|
||||
final key = packKeys[i];
|
||||
final event = controller.room?.getState(
|
||||
'im.ponies.room_emotes',
|
||||
packKeys[i],
|
||||
);
|
||||
|
||||
final eventPack = event?.content
|
||||
.tryGetMap<String, Object?>('pack');
|
||||
final packName =
|
||||
eventPack?.tryGet<String>('display_name') ??
|
||||
eventPack?.tryGet<String>('name') ??
|
||||
(key.isNotEmpty ? key : 'Default');
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||
child: FilterChip(
|
||||
label: Text(packName),
|
||||
selected:
|
||||
controller.stateKey == key ||
|
||||
(controller.stateKey == null && key.isEmpty),
|
||||
onSelected: controller.showSave
|
||||
? null
|
||||
: (_) => controller.setStateKey(key),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
body: MaxWidthBody(
|
||||
child: Column(
|
||||
mainAxisSize: .min,
|
||||
crossAxisAlignment: .stretch,
|
||||
children: <Widget>[
|
||||
if (controller.room != null) ...[
|
||||
const SizedBox(height: 16),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: TextField(
|
||||
maxLength: 256,
|
||||
controller: controller.packDisplayNameController,
|
||||
readOnly: controller.readonly,
|
||||
onSubmitted: (_) => controller.submitDisplaynameAction(),
|
||||
decoration: InputDecoration(
|
||||
counter: const SizedBox.shrink(),
|
||||
hintText: controller.stateKey,
|
||||
labelText: L10n.of(context).stickerPackName,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: TextField(
|
||||
maxLength: 256,
|
||||
controller: controller.packAttributionController,
|
||||
readOnly: controller.readonly,
|
||||
keyboardType: TextInputType.url,
|
||||
onSubmitted: (_) => controller.submitAttributionAction(),
|
||||
decoration: InputDecoration(
|
||||
counter: const SizedBox.shrink(),
|
||||
labelText: L10n.of(context).attribution,
|
||||
suffixIcon: attributionUrl == null
|
||||
? null
|
||||
: IconButton(
|
||||
icon: const Icon(Icons.link_outlined),
|
||||
onPressed: () => UrlLauncher(
|
||||
context,
|
||||
attributionUrl.toString(),
|
||||
).launchUrl(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
if (!controller.readonly) ...[
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: controller.createStickers,
|
||||
icon: const Icon(Icons.upload_outlined),
|
||||
label: Text(L10n.of(context).createSticker),
|
||||
),
|
||||
),
|
||||
const Divider(),
|
||||
],
|
||||
if (controller.room != null && imageKeys.isNotEmpty)
|
||||
SwitchListTile.adaptive(
|
||||
title: Text(L10n.of(context).enableEmotesGlobally),
|
||||
value: controller.isGloballyActive(client),
|
||||
onChanged: controller.setIsGloballyActive,
|
||||
),
|
||||
imageKeys.isEmpty
|
||||
? Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Text(
|
||||
L10n.of(context).noEmotesFound,
|
||||
style: const TextStyle(fontSize: 20),
|
||||
),
|
||||
),
|
||||
)
|
||||
: ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
separatorBuilder: (BuildContext context, int i) =>
|
||||
const SizedBox.shrink(),
|
||||
itemCount: imageKeys.length,
|
||||
itemBuilder: (BuildContext context, int i) {
|
||||
final imageCode = imageKeys[i];
|
||||
final image = controller.pack!.images[imageCode]!;
|
||||
// TODO: Dispose this
|
||||
final textEditingController = TextEditingController();
|
||||
textEditingController.text = imageCode;
|
||||
final useShortCuts =
|
||||
(PlatformInfos.isWeb || PlatformInfos.isDesktop);
|
||||
return ListTile(
|
||||
title: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Shortcuts(
|
||||
shortcuts: !useShortCuts
|
||||
? {}
|
||||
: {
|
||||
LogicalKeySet(LogicalKeyboardKey.enter):
|
||||
SubmitLineIntent(),
|
||||
},
|
||||
child: Actions(
|
||||
actions: !useShortCuts
|
||||
? {}
|
||||
: {
|
||||
SubmitLineIntent: CallbackAction(
|
||||
onInvoke: (i) {
|
||||
controller.submitImageAction(
|
||||
imageCode,
|
||||
image,
|
||||
textEditingController,
|
||||
);
|
||||
return null;
|
||||
},
|
||||
),
|
||||
},
|
||||
child: TextField(
|
||||
readOnly: controller.readonly,
|
||||
controller: textEditingController,
|
||||
autocorrect: false,
|
||||
minLines: 1,
|
||||
maxLines: 1,
|
||||
maxLength: 128,
|
||||
decoration: InputDecoration(
|
||||
hintText: L10n.of(context).emoteShortcode,
|
||||
prefixText: ': ',
|
||||
suffixText: ':',
|
||||
counter: const SizedBox.shrink(),
|
||||
filled: false,
|
||||
enabledBorder: const OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Colors.transparent,
|
||||
),
|
||||
),
|
||||
),
|
||||
onSubmitted: (s) =>
|
||||
controller.submitImageAction(
|
||||
imageCode,
|
||||
image,
|
||||
textEditingController,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!controller.readonly)
|
||||
PopupMenuButton<ImagePackUsage>(
|
||||
onSelected: (usage) =>
|
||||
controller.toggleUsage(imageCode, usage),
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
value: ImagePackUsage.sticker,
|
||||
child: Row(
|
||||
mainAxisSize: .min,
|
||||
children: [
|
||||
if (image.usage?.contains(
|
||||
ImagePackUsage.sticker,
|
||||
) ??
|
||||
true)
|
||||
const Icon(Icons.check_outlined),
|
||||
const SizedBox(width: 12),
|
||||
Text(L10n.of(context).useAsSticker),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: ImagePackUsage.emoticon,
|
||||
child: Row(
|
||||
mainAxisSize: .min,
|
||||
children: [
|
||||
if (image.usage?.contains(
|
||||
ImagePackUsage.emoticon,
|
||||
) ??
|
||||
true)
|
||||
const Icon(Icons.check_outlined),
|
||||
const SizedBox(width: 12),
|
||||
Text(L10n.of(context).useAsEmoji),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
icon: const Icon(Icons.edit_outlined),
|
||||
),
|
||||
],
|
||||
),
|
||||
leading: _EmoteImage(image.url),
|
||||
trailing: controller.readonly
|
||||
? null
|
||||
: IconButton(
|
||||
tooltip: L10n.of(context).delete,
|
||||
onPressed: () =>
|
||||
controller.removeImageAction(imageCode),
|
||||
icon: const Icon(Icons.delete_outlined),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _EmoteImage extends StatelessWidget {
|
||||
final Uri mxc;
|
||||
|
||||
const _EmoteImage(this.mxc);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const size = 44.0;
|
||||
final key = 'sticker_preview_$mxc';
|
||||
return InkWell(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
onTap: () =>
|
||||
showDialog(context: context, builder: (_) => MxcImageViewer(mxc)),
|
||||
child: MxcImage(
|
||||
key: ValueKey(key),
|
||||
cacheKey: key,
|
||||
uri: mxc,
|
||||
fit: BoxFit.contain,
|
||||
width: size,
|
||||
height: size,
|
||||
isThumbnail: true,
|
||||
animated: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SubmitLineIntent extends Intent {}
|
||||
Reference in New Issue
Block a user