mirror of
https://gitlab.com/famedly/fluffychat.git
synced 2024-12-24 14:32:37 +01:00
feat: implement keyboard shortcuts
Added shortcuts for the following actions: - search chats - start chat - chat details - show widgets - cycle accounts - switch to account $i - toggle emoji picker - send file Related: #849 Signed-off-by: TheOneWithTheBraid <the-one@with-the-braid.cf>
This commit is contained in:
parent
01f20ead3a
commit
d6ddd7bbff
@ -2750,5 +2750,14 @@
|
|||||||
"experimentalVideoCalls": "Experimental video calls",
|
"experimentalVideoCalls": "Experimental video calls",
|
||||||
"@experimentalVideoCalls": {},
|
"@experimentalVideoCalls": {},
|
||||||
"emailOrUsername": "Email or username",
|
"emailOrUsername": "Email or username",
|
||||||
"@emailOrUsername": {}
|
"@emailOrUsername": {},
|
||||||
|
"switchToAccount": "Switch to account {number}",
|
||||||
|
"@switchToAccount": {
|
||||||
|
"type": "number",
|
||||||
|
"placeholders": {
|
||||||
|
"number": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nextAccount": "Next account",
|
||||||
|
"previousAccount": "Previous account"
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
import 'package:animations/animations.dart';
|
import 'package:animations/animations.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||||
|
import 'package:keyboard_shortcuts/keyboard_shortcuts.dart';
|
||||||
import 'package:matrix/matrix.dart';
|
import 'package:matrix/matrix.dart';
|
||||||
|
|
||||||
import 'package:fluffychat/config/app_config.dart';
|
import 'package:fluffychat/config/app_config.dart';
|
||||||
@ -72,126 +74,143 @@ class ChatInputRow extends StatelessWidget {
|
|||||||
: Container(),
|
: Container(),
|
||||||
]
|
]
|
||||||
: <Widget>[
|
: <Widget>[
|
||||||
AnimatedContainer(
|
KeyBoardShortcuts(
|
||||||
duration: const Duration(milliseconds: 200),
|
child: AnimatedContainer(
|
||||||
height: 56,
|
duration: const Duration(milliseconds: 200),
|
||||||
width: controller.inputText.isEmpty ? 56 : 0,
|
height: 56,
|
||||||
alignment: Alignment.center,
|
width: controller.inputText.isEmpty ? 56 : 0,
|
||||||
clipBehavior: Clip.hardEdge,
|
alignment: Alignment.center,
|
||||||
decoration: const BoxDecoration(),
|
clipBehavior: Clip.hardEdge,
|
||||||
child: PopupMenuButton<String>(
|
decoration: const BoxDecoration(),
|
||||||
icon: const Icon(Icons.add_outlined),
|
child: PopupMenuButton<String>(
|
||||||
onSelected: controller.onAddPopupMenuButtonSelected,
|
icon: const Icon(Icons.add_outlined),
|
||||||
itemBuilder: (BuildContext context) =>
|
onSelected: controller.onAddPopupMenuButtonSelected,
|
||||||
<PopupMenuEntry<String>>[
|
itemBuilder: (BuildContext context) =>
|
||||||
PopupMenuItem<String>(
|
<PopupMenuEntry<String>>[
|
||||||
value: 'file',
|
|
||||||
child: ListTile(
|
|
||||||
leading: const CircleAvatar(
|
|
||||||
backgroundColor: Colors.green,
|
|
||||||
foregroundColor: Colors.white,
|
|
||||||
child: Icon(Icons.attachment_outlined),
|
|
||||||
),
|
|
||||||
title: Text(L10n.of(context)!.sendFile),
|
|
||||||
contentPadding: const EdgeInsets.all(0),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
PopupMenuItem<String>(
|
|
||||||
value: 'image',
|
|
||||||
child: ListTile(
|
|
||||||
leading: const CircleAvatar(
|
|
||||||
backgroundColor: Colors.blue,
|
|
||||||
foregroundColor: Colors.white,
|
|
||||||
child: Icon(Icons.image_outlined),
|
|
||||||
),
|
|
||||||
title: Text(L10n.of(context)!.sendImage),
|
|
||||||
contentPadding: const EdgeInsets.all(0),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (PlatformInfos.isMobile)
|
|
||||||
PopupMenuItem<String>(
|
PopupMenuItem<String>(
|
||||||
value: 'camera',
|
value: 'file',
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
leading: const CircleAvatar(
|
leading: const CircleAvatar(
|
||||||
backgroundColor: Colors.purple,
|
backgroundColor: Colors.green,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
child: Icon(Icons.camera_alt_outlined),
|
child: Icon(Icons.attachment_outlined),
|
||||||
),
|
),
|
||||||
title: Text(L10n.of(context)!.openCamera),
|
title: Text(L10n.of(context)!.sendFile),
|
||||||
contentPadding: const EdgeInsets.all(0),
|
contentPadding: const EdgeInsets.all(0),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (PlatformInfos.isMobile)
|
|
||||||
PopupMenuItem<String>(
|
PopupMenuItem<String>(
|
||||||
value: 'camera-video',
|
value: 'image',
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
leading: const CircleAvatar(
|
leading: const CircleAvatar(
|
||||||
backgroundColor: Colors.red,
|
backgroundColor: Colors.blue,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
child: Icon(Icons.videocam_outlined),
|
child: Icon(Icons.image_outlined),
|
||||||
),
|
),
|
||||||
title: Text(L10n.of(context)!.openVideoCamera),
|
title: Text(L10n.of(context)!.sendImage),
|
||||||
contentPadding: const EdgeInsets.all(0),
|
contentPadding: const EdgeInsets.all(0),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (controller.room!
|
if (PlatformInfos.isMobile)
|
||||||
.getImagePacks(ImagePackUsage.sticker)
|
PopupMenuItem<String>(
|
||||||
.isNotEmpty)
|
value: 'camera',
|
||||||
PopupMenuItem<String>(
|
child: ListTile(
|
||||||
value: 'sticker',
|
leading: const CircleAvatar(
|
||||||
child: ListTile(
|
backgroundColor: Colors.purple,
|
||||||
leading: const CircleAvatar(
|
foregroundColor: Colors.white,
|
||||||
backgroundColor: Colors.orange,
|
child: Icon(Icons.camera_alt_outlined),
|
||||||
foregroundColor: Colors.white,
|
),
|
||||||
child: Icon(Icons.emoji_emotions_outlined),
|
title: Text(L10n.of(context)!.openCamera),
|
||||||
|
contentPadding: const EdgeInsets.all(0),
|
||||||
),
|
),
|
||||||
title: Text(L10n.of(context)!.sendSticker),
|
|
||||||
contentPadding: const EdgeInsets.all(0),
|
|
||||||
),
|
),
|
||||||
),
|
if (PlatformInfos.isMobile)
|
||||||
if (PlatformInfos.isMobile)
|
PopupMenuItem<String>(
|
||||||
PopupMenuItem<String>(
|
value: 'camera-video',
|
||||||
value: 'location',
|
child: ListTile(
|
||||||
child: ListTile(
|
leading: const CircleAvatar(
|
||||||
leading: const CircleAvatar(
|
backgroundColor: Colors.red,
|
||||||
backgroundColor: Colors.brown,
|
foregroundColor: Colors.white,
|
||||||
foregroundColor: Colors.white,
|
child: Icon(Icons.videocam_outlined),
|
||||||
child: Icon(Icons.gps_fixed_outlined),
|
),
|
||||||
|
title: Text(L10n.of(context)!.openVideoCamera),
|
||||||
|
contentPadding: const EdgeInsets.all(0),
|
||||||
),
|
),
|
||||||
title: Text(L10n.of(context)!.shareLocation),
|
|
||||||
contentPadding: const EdgeInsets.all(0),
|
|
||||||
),
|
),
|
||||||
),
|
if (controller.room!
|
||||||
],
|
.getImagePacks(ImagePackUsage.sticker)
|
||||||
|
.isNotEmpty)
|
||||||
|
PopupMenuItem<String>(
|
||||||
|
value: 'sticker',
|
||||||
|
child: ListTile(
|
||||||
|
leading: const CircleAvatar(
|
||||||
|
backgroundColor: Colors.orange,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
child: Icon(Icons.emoji_emotions_outlined),
|
||||||
|
),
|
||||||
|
title: Text(L10n.of(context)!.sendSticker),
|
||||||
|
contentPadding: const EdgeInsets.all(0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (PlatformInfos.isMobile)
|
||||||
|
PopupMenuItem<String>(
|
||||||
|
value: 'location',
|
||||||
|
child: ListTile(
|
||||||
|
leading: const CircleAvatar(
|
||||||
|
backgroundColor: Colors.brown,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
child: Icon(Icons.gps_fixed_outlined),
|
||||||
|
),
|
||||||
|
title: Text(L10n.of(context)!.shareLocation),
|
||||||
|
contentPadding: const EdgeInsets.all(0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
|
keysToPress: {
|
||||||
|
LogicalKeyboardKey.altLeft,
|
||||||
|
LogicalKeyboardKey.keyA
|
||||||
|
},
|
||||||
|
onKeysPressed: () =>
|
||||||
|
controller.onAddPopupMenuButtonSelected('file'),
|
||||||
|
helpLabel: L10n.of(context)!.sendFile,
|
||||||
),
|
),
|
||||||
Container(
|
Container(
|
||||||
height: 56,
|
height: 56,
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
child: IconButton(
|
child: KeyBoardShortcuts(
|
||||||
tooltip: L10n.of(context)!.emojis,
|
child: IconButton(
|
||||||
icon: PageTransitionSwitcher(
|
tooltip: L10n.of(context)!.emojis,
|
||||||
transitionBuilder: (
|
icon: PageTransitionSwitcher(
|
||||||
Widget child,
|
transitionBuilder: (
|
||||||
Animation<double> primaryAnimation,
|
Widget child,
|
||||||
Animation<double> secondaryAnimation,
|
Animation<double> primaryAnimation,
|
||||||
) {
|
Animation<double> secondaryAnimation,
|
||||||
return SharedAxisTransition(
|
) {
|
||||||
animation: primaryAnimation,
|
return SharedAxisTransition(
|
||||||
secondaryAnimation: secondaryAnimation,
|
animation: primaryAnimation,
|
||||||
transitionType: SharedAxisTransitionType.scaled,
|
secondaryAnimation: secondaryAnimation,
|
||||||
child: child,
|
transitionType: SharedAxisTransitionType.scaled,
|
||||||
fillColor: Colors.transparent,
|
child: child,
|
||||||
);
|
fillColor: Colors.transparent,
|
||||||
},
|
);
|
||||||
child: Icon(
|
},
|
||||||
controller.showEmojiPicker
|
child: Icon(
|
||||||
? Icons.keyboard
|
controller.showEmojiPicker
|
||||||
: Icons.emoji_emotions_outlined,
|
? Icons.keyboard
|
||||||
key: ValueKey(controller.showEmojiPicker),
|
: Icons.emoji_emotions_outlined,
|
||||||
|
key: ValueKey(controller.showEmojiPicker),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
|
onPressed: controller.emojiPickerAction,
|
||||||
),
|
),
|
||||||
onPressed: controller.emojiPickerAction,
|
keysToPress: {
|
||||||
|
LogicalKeyboardKey.altLeft,
|
||||||
|
LogicalKeyboardKey.keyE
|
||||||
|
},
|
||||||
|
onKeysPressed: controller.emojiPickerAction,
|
||||||
|
helpLabel: L10n.of(context)!.emojis,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (controller.matrix!.isMultiAccount &&
|
if (controller.matrix!.isMultiAccount &&
|
||||||
|
@ -2,9 +2,11 @@ import 'dart:async';
|
|||||||
|
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
import 'package:animations/animations.dart';
|
import 'package:animations/animations.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||||
|
import 'package:keyboard_shortcuts/keyboard_shortcuts.dart';
|
||||||
import 'package:matrix/matrix.dart';
|
import 'package:matrix/matrix.dart';
|
||||||
import 'package:vrouter/vrouter.dart';
|
import 'package:vrouter/vrouter.dart';
|
||||||
|
|
||||||
@ -43,16 +45,16 @@ class ChatListView extends StatelessWidget {
|
|||||||
? null
|
? null
|
||||||
: Theme.of(context).colorScheme.primary,
|
: Theme.of(context).colorScheme.primary,
|
||||||
),
|
),
|
||||||
leading: selectMode == SelectMode.normal
|
leading: Matrix.of(context).isMultiAccount
|
||||||
? Matrix.of(context).isMultiAccount
|
? ClientChooserButton(controller)
|
||||||
? ClientChooserButton(controller)
|
: selectMode == SelectMode.normal
|
||||||
: null
|
? null
|
||||||
: IconButton(
|
: IconButton(
|
||||||
tooltip: L10n.of(context)!.cancel,
|
tooltip: L10n.of(context)!.cancel,
|
||||||
icon: const Icon(Icons.close_outlined),
|
icon: const Icon(Icons.close_outlined),
|
||||||
onPressed: controller.cancelAction,
|
onPressed: controller.cancelAction,
|
||||||
color: Theme.of(context).colorScheme.primary,
|
color: Theme.of(context).colorScheme.primary,
|
||||||
),
|
),
|
||||||
centerTitle: false,
|
centerTitle: false,
|
||||||
actions: selectMode == SelectMode.share
|
actions: selectMode == SelectMode.share
|
||||||
? null
|
? null
|
||||||
@ -93,11 +95,20 @@ class ChatListView extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
IconButton(
|
KeyBoardShortcuts(
|
||||||
icon: const Icon(Icons.search_outlined),
|
keysToPress: {
|
||||||
tooltip: L10n.of(context)!.search,
|
LogicalKeyboardKey.controlLeft,
|
||||||
onPressed: () =>
|
LogicalKeyboardKey.keyF
|
||||||
|
},
|
||||||
|
onKeysPressed: () =>
|
||||||
VRouter.of(context).to('/search'),
|
VRouter.of(context).to('/search'),
|
||||||
|
helpLabel: L10n.of(context)!.search,
|
||||||
|
child: IconButton(
|
||||||
|
icon: const Icon(Icons.search_outlined),
|
||||||
|
tooltip: L10n.of(context)!.search,
|
||||||
|
onPressed: () =>
|
||||||
|
VRouter.of(context).to('/search'),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
if (selectMode == SelectMode.normal)
|
if (selectMode == SelectMode.normal)
|
||||||
IconButton(
|
IconButton(
|
||||||
@ -213,12 +224,21 @@ class ChatListView extends StatelessWidget {
|
|||||||
Expanded(child: _ChatListViewBody(controller)),
|
Expanded(child: _ChatListViewBody(controller)),
|
||||||
]),
|
]),
|
||||||
floatingActionButton: selectMode == SelectMode.normal
|
floatingActionButton: selectMode == SelectMode.normal
|
||||||
? FloatingActionButton.extended(
|
? KeyBoardShortcuts(
|
||||||
isExtended: controller.scrolledToTop,
|
child: FloatingActionButton.extended(
|
||||||
onPressed: () =>
|
isExtended: controller.scrolledToTop,
|
||||||
|
onPressed: () =>
|
||||||
|
VRouter.of(context).to('/newprivatechat'),
|
||||||
|
icon: const Icon(CupertinoIcons.chat_bubble),
|
||||||
|
label: Text(L10n.of(context)!.newChat),
|
||||||
|
),
|
||||||
|
keysToPress: {
|
||||||
|
LogicalKeyboardKey.controlLeft,
|
||||||
|
LogicalKeyboardKey.keyN
|
||||||
|
},
|
||||||
|
onKeysPressed: () =>
|
||||||
VRouter.of(context).to('/newprivatechat'),
|
VRouter.of(context).to('/newprivatechat'),
|
||||||
icon: const Icon(CupertinoIcons.chat_bubble),
|
helpLabel: L10n.of(context)!.newChat,
|
||||||
label: Text(L10n.of(context)!.newChat),
|
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
bottomNavigationBar: Column(
|
bottomNavigationBar: Column(
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||||
|
import 'package:keyboard_shortcuts/keyboard_shortcuts.dart';
|
||||||
import 'package:matrix/matrix.dart';
|
import 'package:matrix/matrix.dart';
|
||||||
|
|
||||||
import 'package:fluffychat/widgets/avatar.dart';
|
import 'package:fluffychat/widgets/avatar.dart';
|
||||||
@ -8,6 +11,7 @@ import 'chat_list.dart';
|
|||||||
|
|
||||||
class ClientChooserButton extends StatelessWidget {
|
class ClientChooserButton extends StatelessWidget {
|
||||||
final ChatListController controller;
|
final ChatListController controller;
|
||||||
|
|
||||||
const ClientChooserButton(this.controller, {Key? key}) : super(key: key);
|
const ClientChooserButton(this.controller, {Key? key}) : super(key: key);
|
||||||
|
|
||||||
List<PopupMenuEntry<Object>> _bundleMenuItems(BuildContext context) {
|
List<PopupMenuEntry<Object>> _bundleMenuItems(BuildContext context) {
|
||||||
@ -81,26 +85,137 @@ class ClientChooserButton extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final matrix = Matrix.of(context);
|
final matrix = Matrix.of(context);
|
||||||
|
|
||||||
|
int clientCount = 0;
|
||||||
|
matrix.accountBundles.forEach((key, value) => clientCount += value.length);
|
||||||
return Center(
|
return Center(
|
||||||
child: FutureBuilder<Profile>(
|
child: FutureBuilder<Profile>(
|
||||||
future: matrix.client.ownProfile,
|
future: matrix.client.ownProfile,
|
||||||
builder: (context, snapshot) => PopupMenuButton<Object>(
|
builder: (context, snapshot) => Stack(
|
||||||
child: Avatar(
|
alignment: Alignment.center,
|
||||||
mxContent: snapshot.data?.avatarUrl,
|
children: [
|
||||||
name: snapshot.data?.displayName ?? matrix.client.userID!.localpart,
|
...List.generate(
|
||||||
size: 28,
|
clientCount,
|
||||||
fontSize: 12,
|
(index) => KeyBoardShortcuts(
|
||||||
),
|
child: Container(),
|
||||||
onSelected: (Object object) {
|
keysToPress: _buildKeyboardShortcut(index + 1),
|
||||||
if (object is Client) {
|
helpLabel: L10n.of(context)!.switchToAccount(index + 1),
|
||||||
controller.setActiveClient(object);
|
onKeysPressed: () => _handleKeyboardShortcut(matrix, index),
|
||||||
} else if (object is String) {
|
),
|
||||||
controller.setActiveBundle(object);
|
),
|
||||||
}
|
KeyBoardShortcuts(
|
||||||
},
|
child: Container(),
|
||||||
itemBuilder: _bundleMenuItems,
|
keysToPress: {
|
||||||
|
LogicalKeyboardKey.controlLeft,
|
||||||
|
LogicalKeyboardKey.tab
|
||||||
|
},
|
||||||
|
helpLabel: L10n.of(context)!.nextAccount,
|
||||||
|
onKeysPressed: () => _nextAccount(matrix),
|
||||||
|
),
|
||||||
|
KeyBoardShortcuts(
|
||||||
|
child: Container(),
|
||||||
|
keysToPress: {
|
||||||
|
LogicalKeyboardKey.controlLeft,
|
||||||
|
LogicalKeyboardKey.shiftLeft,
|
||||||
|
LogicalKeyboardKey.tab
|
||||||
|
},
|
||||||
|
helpLabel: L10n.of(context)!.previousAccount,
|
||||||
|
onKeysPressed: () => _previousAccount(matrix),
|
||||||
|
),
|
||||||
|
PopupMenuButton<Object>(
|
||||||
|
child: Avatar(
|
||||||
|
mxContent: snapshot.data?.avatarUrl,
|
||||||
|
name: snapshot.data?.displayName ??
|
||||||
|
matrix.client.userID!.localpart,
|
||||||
|
size: 28,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
onSelected: _clientSelected,
|
||||||
|
itemBuilder: _bundleMenuItems,
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Set<LogicalKeyboardKey>? _buildKeyboardShortcut(int index) {
|
||||||
|
if (index > 0 && index < 10) {
|
||||||
|
return {
|
||||||
|
LogicalKeyboardKey.altLeft,
|
||||||
|
LogicalKeyboardKey(0x00000000030 + index)
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _clientSelected(Object object) {
|
||||||
|
if (object is Client) {
|
||||||
|
controller.setActiveClient(object);
|
||||||
|
} else if (object is String) {
|
||||||
|
controller.setActiveBundle(object);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleKeyboardShortcut(MatrixState matrix, int index) {
|
||||||
|
final bundles = matrix.accountBundles.keys.toList()
|
||||||
|
..sort((a, b) => a!.isValidMatrixId == b!.isValidMatrixId
|
||||||
|
? 0
|
||||||
|
: a.isValidMatrixId && !b.isValidMatrixId
|
||||||
|
? -1
|
||||||
|
: 1);
|
||||||
|
// beginning from end if negative
|
||||||
|
if (index < 0) {
|
||||||
|
int clientCount = 0;
|
||||||
|
matrix.accountBundles
|
||||||
|
.forEach((key, value) => clientCount += value.length);
|
||||||
|
_handleKeyboardShortcut(matrix, clientCount);
|
||||||
|
}
|
||||||
|
for (final bundleName in bundles) {
|
||||||
|
final bundle = matrix.accountBundles[bundleName];
|
||||||
|
if (bundle != null) {
|
||||||
|
if (index < bundle.length) {
|
||||||
|
return _clientSelected(bundle[index]!);
|
||||||
|
} else {
|
||||||
|
index -= bundle.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// if index too high, restarting from 0
|
||||||
|
_handleKeyboardShortcut(matrix, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
int? _shortcutIndexOfClient(MatrixState matrix, Client client) {
|
||||||
|
int index = 0;
|
||||||
|
|
||||||
|
final bundles = matrix.accountBundles.keys.toList()
|
||||||
|
..sort((a, b) => a!.isValidMatrixId == b!.isValidMatrixId
|
||||||
|
? 0
|
||||||
|
: a.isValidMatrixId && !b.isValidMatrixId
|
||||||
|
? -1
|
||||||
|
: 1);
|
||||||
|
for (final bundleName in bundles) {
|
||||||
|
final bundle = matrix.accountBundles[bundleName];
|
||||||
|
if (bundle == null) return null;
|
||||||
|
if (bundle.contains(client)) {
|
||||||
|
return index + bundle.indexOf(client);
|
||||||
|
} else {
|
||||||
|
index += bundle.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _nextAccount(MatrixState matrix) {
|
||||||
|
final client = matrix.client;
|
||||||
|
final lastIndex = _shortcutIndexOfClient(matrix, client);
|
||||||
|
_handleKeyboardShortcut(matrix, lastIndex! + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _previousAccount(MatrixState matrix) {
|
||||||
|
final client = matrix.client;
|
||||||
|
final lastIndex = _shortcutIndexOfClient(matrix, client);
|
||||||
|
_handleKeyboardShortcut(matrix, lastIndex! - 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,10 +2,12 @@ import 'dart:async';
|
|||||||
|
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
import 'package:adaptive_dialog/adaptive_dialog.dart';
|
import 'package:adaptive_dialog/adaptive_dialog.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||||
|
import 'package:keyboard_shortcuts/keyboard_shortcuts.dart';
|
||||||
import 'package:matrix/matrix.dart';
|
import 'package:matrix/matrix.dart';
|
||||||
import 'package:vrouter/vrouter.dart';
|
import 'package:vrouter/vrouter.dart';
|
||||||
|
|
||||||
@ -16,6 +18,7 @@ import 'matrix.dart';
|
|||||||
class ChatSettingsPopupMenu extends StatefulWidget {
|
class ChatSettingsPopupMenu extends StatefulWidget {
|
||||||
final Room room;
|
final Room room;
|
||||||
final bool displayChatDetails;
|
final bool displayChatDetails;
|
||||||
|
|
||||||
const ChatSettingsPopupMenu(this.room, this.displayChatDetails, {Key? key})
|
const ChatSettingsPopupMenu(this.room, this.displayChatDetails, {Key? key})
|
||||||
: super(key: key);
|
: super(key: key);
|
||||||
|
|
||||||
@ -101,57 +104,88 @@ class _ChatSettingsPopupMenuState extends State<ChatSettingsPopupMenu> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return PopupMenuButton(
|
return Stack(
|
||||||
onSelected: (String choice) async {
|
alignment: Alignment.center,
|
||||||
switch (choice) {
|
children: [
|
||||||
case 'widgets':
|
KeyBoardShortcuts(
|
||||||
[TargetPlatform.iOS, TargetPlatform.macOS]
|
child: Container(),
|
||||||
.contains(Theme.of(context).platform)
|
keysToPress: {
|
||||||
? showCupertinoModalPopup(
|
LogicalKeyboardKey.controlLeft,
|
||||||
|
LogicalKeyboardKey.keyI
|
||||||
|
},
|
||||||
|
helpLabel: L10n.of(context)!.chatDetails,
|
||||||
|
onKeysPressed: _showChatDetails,
|
||||||
|
),
|
||||||
|
KeyBoardShortcuts(
|
||||||
|
child: Container(),
|
||||||
|
keysToPress: {
|
||||||
|
LogicalKeyboardKey.controlLeft,
|
||||||
|
LogicalKeyboardKey.keyW
|
||||||
|
},
|
||||||
|
helpLabel: L10n.of(context)!.matrixWidgets,
|
||||||
|
onKeysPressed: _showWidgets,
|
||||||
|
),
|
||||||
|
PopupMenuButton(
|
||||||
|
onSelected: (String choice) async {
|
||||||
|
switch (choice) {
|
||||||
|
case 'widgets':
|
||||||
|
_showWidgets();
|
||||||
|
break;
|
||||||
|
case 'leave':
|
||||||
|
final confirmed = await showOkCancelAlertDialog(
|
||||||
|
useRootNavigator: false,
|
||||||
|
context: context,
|
||||||
|
title: L10n.of(context)!.areYouSure,
|
||||||
|
okLabel: L10n.of(context)!.ok,
|
||||||
|
cancelLabel: L10n.of(context)!.cancel,
|
||||||
|
);
|
||||||
|
if (confirmed == OkCancelResult.ok) {
|
||||||
|
final success = await showFutureLoadingDialog(
|
||||||
|
context: context, future: () => widget.room.leave());
|
||||||
|
if (success.error == null) {
|
||||||
|
VRouter.of(context).to('/rooms');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'mute':
|
||||||
|
await showFutureLoadingDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) =>
|
future: () => widget.room
|
||||||
CupertinoWidgetsBottomSheet(room: widget.room),
|
.setPushRuleState(PushRuleState.mentionsOnly));
|
||||||
)
|
break;
|
||||||
: showModalBottomSheet(
|
case 'unmute':
|
||||||
|
await showFutureLoadingDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => WidgetsBottomSheet(room: widget.room),
|
future: () =>
|
||||||
);
|
widget.room.setPushRuleState(PushRuleState.notify));
|
||||||
break;
|
break;
|
||||||
case 'leave':
|
case 'details':
|
||||||
final confirmed = await showOkCancelAlertDialog(
|
_showChatDetails();
|
||||||
useRootNavigator: false,
|
break;
|
||||||
context: context,
|
|
||||||
title: L10n.of(context)!.areYouSure,
|
|
||||||
okLabel: L10n.of(context)!.ok,
|
|
||||||
cancelLabel: L10n.of(context)!.cancel,
|
|
||||||
);
|
|
||||||
if (confirmed == OkCancelResult.ok) {
|
|
||||||
final success = await showFutureLoadingDialog(
|
|
||||||
context: context, future: () => widget.room.leave());
|
|
||||||
if (success.error == null) {
|
|
||||||
VRouter.of(context).to('/rooms');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
break;
|
},
|
||||||
case 'mute':
|
itemBuilder: (BuildContext context) => items,
|
||||||
await showFutureLoadingDialog(
|
),
|
||||||
context: context,
|
],
|
||||||
future: () =>
|
|
||||||
widget.room.setPushRuleState(PushRuleState.mentionsOnly));
|
|
||||||
break;
|
|
||||||
case 'unmute':
|
|
||||||
await showFutureLoadingDialog(
|
|
||||||
context: context,
|
|
||||||
future: () =>
|
|
||||||
widget.room.setPushRuleState(PushRuleState.notify));
|
|
||||||
break;
|
|
||||||
case 'details':
|
|
||||||
VRouter.of(context)
|
|
||||||
.toSegments(['rooms', widget.room.id, 'details']);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
itemBuilder: (BuildContext context) => items,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _showWidgets() => [TargetPlatform.iOS, TargetPlatform.macOS]
|
||||||
|
.contains(Theme.of(context).platform)
|
||||||
|
? showCupertinoModalPopup(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => CupertinoWidgetsBottomSheet(room: widget.room),
|
||||||
|
)
|
||||||
|
: showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => WidgetsBottomSheet(room: widget.room),
|
||||||
|
);
|
||||||
|
|
||||||
|
void _showChatDetails() {
|
||||||
|
if (VRouter.of(context).path.endsWith('/details')) {
|
||||||
|
VRouter.of(context).toSegments(['rooms', widget.room.id]);
|
||||||
|
} else {
|
||||||
|
VRouter.of(context).toSegments(['rooms', widget.room.id, 'details']);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
24
pubspec.lock
24
pubspec.lock
@ -429,7 +429,7 @@ packages:
|
|||||||
name: file_picker
|
name: file_picker
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.4.0"
|
version: "4.5.0"
|
||||||
file_picker_cross:
|
file_picker_cross:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -715,7 +715,7 @@ packages:
|
|||||||
name: flutter_webrtc
|
name: flutter_webrtc
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.8.2"
|
version: "0.8.3"
|
||||||
frontend_server_client:
|
frontend_server_client:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -891,6 +891,15 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.3"
|
version: "0.6.3"
|
||||||
|
keyboard_shortcuts:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
path: "."
|
||||||
|
ref: null-safety
|
||||||
|
resolved-ref: "5aa8786475bca1b90ff35409eff3e0f5a4768601"
|
||||||
|
url: "https://github.com/TheOneWithTheBraid/keyboard_shortcuts.git"
|
||||||
|
source: git
|
||||||
|
version: "0.1.4"
|
||||||
latlong2:
|
latlong2:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -960,7 +969,7 @@ packages:
|
|||||||
name: matrix
|
name: matrix
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.8.11"
|
version: "0.8.12"
|
||||||
matrix_api_lite:
|
matrix_api_lite:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1198,7 +1207,7 @@ packages:
|
|||||||
name: permission_handler_apple
|
name: permission_handler_apple
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "9.0.2"
|
version: "9.0.3"
|
||||||
permission_handler_platform_interface:
|
permission_handler_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1813,6 +1822,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.7"
|
version: "2.0.7"
|
||||||
|
visibility_detector:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: visibility_detector
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.2"
|
||||||
vm_service:
|
vm_service:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -55,6 +55,7 @@ dependencies:
|
|||||||
image: ^3.1.1
|
image: ^3.1.1
|
||||||
image_picker: ^0.8.4+8
|
image_picker: ^0.8.4+8
|
||||||
intl: any
|
intl: any
|
||||||
|
keyboard_shortcuts: ^0.1.4
|
||||||
localstorage: ^4.0.0+1
|
localstorage: ^4.0.0+1
|
||||||
lottie: ^1.2.2
|
lottie: ^1.2.2
|
||||||
matrix: ^0.8.11
|
matrix: ^0.8.11
|
||||||
@ -135,4 +136,10 @@ dependency_overrides:
|
|||||||
hosted:
|
hosted:
|
||||||
name: geolocator_android
|
name: geolocator_android
|
||||||
url: https://hanntech-gmbh.gitlab.io/free2pass/flutter-geolocator-floss
|
url: https://hanntech-gmbh.gitlab.io/free2pass/flutter-geolocator-floss
|
||||||
|
# waiting for null safety
|
||||||
|
# Upstream pull request: https://github.com/AntoineMarcel/keyboard_shortcuts/pull/13
|
||||||
|
keyboard_shortcuts:
|
||||||
|
git:
|
||||||
|
url: https://github.com/TheOneWithTheBraid/keyboard_shortcuts.git
|
||||||
|
ref: null-safety
|
||||||
provider: 5.0.0
|
provider: 5.0.0
|
||||||
|
Loading…
Reference in New Issue
Block a user