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:
TheOneWithTheBraid 2022-03-01 20:14:49 +01:00
parent 01f20ead3a
commit d6ddd7bbff
7 changed files with 400 additions and 180 deletions

View File

@ -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"
} }

View File

@ -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 &&

View File

@ -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(

View File

@ -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);
}
} }

View File

@ -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']);
}
}
} }

View File

@ -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:

View File

@ -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