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
7ce9d6384d
commit
824fcfc27c
7 changed files with 400 additions and 180 deletions
|
|
@ -2,9 +2,11 @@ import 'dart:async';
|
|||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:animations/animations.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:keyboard_shortcuts/keyboard_shortcuts.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:vrouter/vrouter.dart';
|
||||
|
||||
|
|
@ -43,16 +45,16 @@ class ChatListView extends StatelessWidget {
|
|||
? null
|
||||
: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
leading: selectMode == SelectMode.normal
|
||||
? Matrix.of(context).isMultiAccount
|
||||
? ClientChooserButton(controller)
|
||||
: null
|
||||
: IconButton(
|
||||
tooltip: L10n.of(context)!.cancel,
|
||||
icon: const Icon(Icons.close_outlined),
|
||||
onPressed: controller.cancelAction,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
leading: Matrix.of(context).isMultiAccount
|
||||
? ClientChooserButton(controller)
|
||||
: selectMode == SelectMode.normal
|
||||
? null
|
||||
: IconButton(
|
||||
tooltip: L10n.of(context)!.cancel,
|
||||
icon: const Icon(Icons.close_outlined),
|
||||
onPressed: controller.cancelAction,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
centerTitle: false,
|
||||
actions: selectMode == SelectMode.share
|
||||
? null
|
||||
|
|
@ -93,11 +95,20 @@ class ChatListView extends StatelessWidget {
|
|||
),
|
||||
]
|
||||
: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.search_outlined),
|
||||
tooltip: L10n.of(context)!.search,
|
||||
onPressed: () =>
|
||||
KeyBoardShortcuts(
|
||||
keysToPress: {
|
||||
LogicalKeyboardKey.controlLeft,
|
||||
LogicalKeyboardKey.keyF
|
||||
},
|
||||
onKeysPressed: () =>
|
||||
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)
|
||||
IconButton(
|
||||
|
|
@ -213,12 +224,21 @@ class ChatListView extends StatelessWidget {
|
|||
Expanded(child: _ChatListViewBody(controller)),
|
||||
]),
|
||||
floatingActionButton: selectMode == SelectMode.normal
|
||||
? FloatingActionButton.extended(
|
||||
isExtended: controller.scrolledToTop,
|
||||
onPressed: () =>
|
||||
? KeyBoardShortcuts(
|
||||
child: FloatingActionButton.extended(
|
||||
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'),
|
||||
icon: const Icon(CupertinoIcons.chat_bubble),
|
||||
label: Text(L10n.of(context)!.newChat),
|
||||
helpLabel: L10n.of(context)!.newChat,
|
||||
)
|
||||
: null,
|
||||
bottomNavigationBar: Column(
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
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:fluffychat/widgets/avatar.dart';
|
||||
|
|
@ -8,6 +11,7 @@ import 'chat_list.dart';
|
|||
|
||||
class ClientChooserButton extends StatelessWidget {
|
||||
final ChatListController controller;
|
||||
|
||||
const ClientChooserButton(this.controller, {Key? key}) : super(key: key);
|
||||
|
||||
List<PopupMenuEntry<Object>> _bundleMenuItems(BuildContext context) {
|
||||
|
|
@ -81,26 +85,137 @@ class ClientChooserButton extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final matrix = Matrix.of(context);
|
||||
|
||||
int clientCount = 0;
|
||||
matrix.accountBundles.forEach((key, value) => clientCount += value.length);
|
||||
return Center(
|
||||
child: FutureBuilder<Profile>(
|
||||
future: matrix.client.ownProfile,
|
||||
builder: (context, snapshot) => PopupMenuButton<Object>(
|
||||
child: Avatar(
|
||||
mxContent: snapshot.data?.avatarUrl,
|
||||
name: snapshot.data?.displayName ?? matrix.client.userID!.localpart,
|
||||
size: 28,
|
||||
fontSize: 12,
|
||||
),
|
||||
onSelected: (Object object) {
|
||||
if (object is Client) {
|
||||
controller.setActiveClient(object);
|
||||
} else if (object is String) {
|
||||
controller.setActiveBundle(object);
|
||||
}
|
||||
},
|
||||
itemBuilder: _bundleMenuItems,
|
||||
builder: (context, snapshot) => Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
...List.generate(
|
||||
clientCount,
|
||||
(index) => KeyBoardShortcuts(
|
||||
child: Container(),
|
||||
keysToPress: _buildKeyboardShortcut(index + 1),
|
||||
helpLabel: L10n.of(context)!.switchToAccount(index + 1),
|
||||
onKeysPressed: () => _handleKeyboardShortcut(matrix, index),
|
||||
),
|
||||
),
|
||||
KeyBoardShortcuts(
|
||||
child: Container(),
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue