mirror of
				https://gitlab.com/famedly/fluffychat.git
				synced 2025-11-03 22:07:23 +01:00 
			
		
		
		
	Merge branch 'keyboard-shortcuts' into 'main'
feat: implement keyboard shortcuts See merge request famedly/fluffychat!762
This commit is contained in:
		
						commit
						0899986042
					
				@ -2750,5 +2750,14 @@
 | 
			
		||||
  "experimentalVideoCalls": "Experimental video calls",
 | 
			
		||||
  "@experimentalVideoCalls": {},
 | 
			
		||||
  "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/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:fluffychat/config/app_config.dart';
 | 
			
		||||
@ -72,126 +74,143 @@ class ChatInputRow extends StatelessWidget {
 | 
			
		||||
                  : Container(),
 | 
			
		||||
            ]
 | 
			
		||||
          : <Widget>[
 | 
			
		||||
              AnimatedContainer(
 | 
			
		||||
                duration: const Duration(milliseconds: 200),
 | 
			
		||||
                height: 56,
 | 
			
		||||
                width: controller.inputText.isEmpty ? 56 : 0,
 | 
			
		||||
                alignment: Alignment.center,
 | 
			
		||||
                clipBehavior: Clip.hardEdge,
 | 
			
		||||
                decoration: const BoxDecoration(),
 | 
			
		||||
                child: PopupMenuButton<String>(
 | 
			
		||||
                  icon: const Icon(Icons.add_outlined),
 | 
			
		||||
                  onSelected: controller.onAddPopupMenuButtonSelected,
 | 
			
		||||
                  itemBuilder: (BuildContext context) =>
 | 
			
		||||
                      <PopupMenuEntry<String>>[
 | 
			
		||||
                    PopupMenuItem<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)
 | 
			
		||||
              KeyBoardShortcuts(
 | 
			
		||||
                child: AnimatedContainer(
 | 
			
		||||
                  duration: const Duration(milliseconds: 200),
 | 
			
		||||
                  height: 56,
 | 
			
		||||
                  width: controller.inputText.isEmpty ? 56 : 0,
 | 
			
		||||
                  alignment: Alignment.center,
 | 
			
		||||
                  clipBehavior: Clip.hardEdge,
 | 
			
		||||
                  decoration: const BoxDecoration(),
 | 
			
		||||
                  child: PopupMenuButton<String>(
 | 
			
		||||
                    icon: const Icon(Icons.add_outlined),
 | 
			
		||||
                    onSelected: controller.onAddPopupMenuButtonSelected,
 | 
			
		||||
                    itemBuilder: (BuildContext context) =>
 | 
			
		||||
                        <PopupMenuEntry<String>>[
 | 
			
		||||
                      PopupMenuItem<String>(
 | 
			
		||||
                        value: 'camera',
 | 
			
		||||
                        value: 'file',
 | 
			
		||||
                        child: ListTile(
 | 
			
		||||
                          leading: const CircleAvatar(
 | 
			
		||||
                            backgroundColor: Colors.purple,
 | 
			
		||||
                            backgroundColor: Colors.green,
 | 
			
		||||
                            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),
 | 
			
		||||
                        ),
 | 
			
		||||
                      ),
 | 
			
		||||
                    if (PlatformInfos.isMobile)
 | 
			
		||||
                      PopupMenuItem<String>(
 | 
			
		||||
                        value: 'camera-video',
 | 
			
		||||
                        value: 'image',
 | 
			
		||||
                        child: ListTile(
 | 
			
		||||
                          leading: const CircleAvatar(
 | 
			
		||||
                            backgroundColor: Colors.red,
 | 
			
		||||
                            backgroundColor: Colors.blue,
 | 
			
		||||
                            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),
 | 
			
		||||
                        ),
 | 
			
		||||
                      ),
 | 
			
		||||
                    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),
 | 
			
		||||
                      if (PlatformInfos.isMobile)
 | 
			
		||||
                        PopupMenuItem<String>(
 | 
			
		||||
                          value: 'camera',
 | 
			
		||||
                          child: ListTile(
 | 
			
		||||
                            leading: const CircleAvatar(
 | 
			
		||||
                              backgroundColor: Colors.purple,
 | 
			
		||||
                              foregroundColor: Colors.white,
 | 
			
		||||
                              child: Icon(Icons.camera_alt_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)
 | 
			
		||||
                      PopupMenuItem<String>(
 | 
			
		||||
                        value: 'location',
 | 
			
		||||
                        child: ListTile(
 | 
			
		||||
                          leading: const CircleAvatar(
 | 
			
		||||
                            backgroundColor: Colors.brown,
 | 
			
		||||
                            foregroundColor: Colors.white,
 | 
			
		||||
                            child: Icon(Icons.gps_fixed_outlined),
 | 
			
		||||
                      if (PlatformInfos.isMobile)
 | 
			
		||||
                        PopupMenuItem<String>(
 | 
			
		||||
                          value: 'camera-video',
 | 
			
		||||
                          child: ListTile(
 | 
			
		||||
                            leading: const CircleAvatar(
 | 
			
		||||
                              backgroundColor: Colors.red,
 | 
			
		||||
                              foregroundColor: Colors.white,
 | 
			
		||||
                              child: Icon(Icons.videocam_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(
 | 
			
		||||
                height: 56,
 | 
			
		||||
                alignment: Alignment.center,
 | 
			
		||||
                child: IconButton(
 | 
			
		||||
                  tooltip: L10n.of(context)!.emojis,
 | 
			
		||||
                  icon: PageTransitionSwitcher(
 | 
			
		||||
                    transitionBuilder: (
 | 
			
		||||
                      Widget child,
 | 
			
		||||
                      Animation<double> primaryAnimation,
 | 
			
		||||
                      Animation<double> secondaryAnimation,
 | 
			
		||||
                    ) {
 | 
			
		||||
                      return SharedAxisTransition(
 | 
			
		||||
                        animation: primaryAnimation,
 | 
			
		||||
                        secondaryAnimation: secondaryAnimation,
 | 
			
		||||
                        transitionType: SharedAxisTransitionType.scaled,
 | 
			
		||||
                        child: child,
 | 
			
		||||
                        fillColor: Colors.transparent,
 | 
			
		||||
                      );
 | 
			
		||||
                    },
 | 
			
		||||
                    child: Icon(
 | 
			
		||||
                      controller.showEmojiPicker
 | 
			
		||||
                          ? Icons.keyboard
 | 
			
		||||
                          : Icons.emoji_emotions_outlined,
 | 
			
		||||
                      key: ValueKey(controller.showEmojiPicker),
 | 
			
		||||
                child: KeyBoardShortcuts(
 | 
			
		||||
                  child: IconButton(
 | 
			
		||||
                    tooltip: L10n.of(context)!.emojis,
 | 
			
		||||
                    icon: PageTransitionSwitcher(
 | 
			
		||||
                      transitionBuilder: (
 | 
			
		||||
                        Widget child,
 | 
			
		||||
                        Animation<double> primaryAnimation,
 | 
			
		||||
                        Animation<double> secondaryAnimation,
 | 
			
		||||
                      ) {
 | 
			
		||||
                        return SharedAxisTransition(
 | 
			
		||||
                          animation: primaryAnimation,
 | 
			
		||||
                          secondaryAnimation: secondaryAnimation,
 | 
			
		||||
                          transitionType: SharedAxisTransitionType.scaled,
 | 
			
		||||
                          child: child,
 | 
			
		||||
                          fillColor: Colors.transparent,
 | 
			
		||||
                        );
 | 
			
		||||
                      },
 | 
			
		||||
                      child: Icon(
 | 
			
		||||
                        controller.showEmojiPicker
 | 
			
		||||
                            ? Icons.keyboard
 | 
			
		||||
                            : 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 &&
 | 
			
		||||
 | 
			
		||||
@ -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);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -2,10 +2,12 @@ import 'dart:async';
 | 
			
		||||
 | 
			
		||||
import 'package:flutter/cupertino.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:flutter/services.dart';
 | 
			
		||||
 | 
			
		||||
import 'package:adaptive_dialog/adaptive_dialog.dart';
 | 
			
		||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
 | 
			
		||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
 | 
			
		||||
import 'package:keyboard_shortcuts/keyboard_shortcuts.dart';
 | 
			
		||||
import 'package:matrix/matrix.dart';
 | 
			
		||||
import 'package:vrouter/vrouter.dart';
 | 
			
		||||
 | 
			
		||||
@ -16,6 +18,7 @@ import 'matrix.dart';
 | 
			
		||||
class ChatSettingsPopupMenu extends StatefulWidget {
 | 
			
		||||
  final Room room;
 | 
			
		||||
  final bool displayChatDetails;
 | 
			
		||||
 | 
			
		||||
  const ChatSettingsPopupMenu(this.room, this.displayChatDetails, {Key? key})
 | 
			
		||||
      : super(key: key);
 | 
			
		||||
 | 
			
		||||
@ -101,57 +104,88 @@ class _ChatSettingsPopupMenuState extends State<ChatSettingsPopupMenu> {
 | 
			
		||||
        ),
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
    return PopupMenuButton(
 | 
			
		||||
      onSelected: (String choice) async {
 | 
			
		||||
        switch (choice) {
 | 
			
		||||
          case 'widgets':
 | 
			
		||||
            [TargetPlatform.iOS, TargetPlatform.macOS]
 | 
			
		||||
                    .contains(Theme.of(context).platform)
 | 
			
		||||
                ? showCupertinoModalPopup(
 | 
			
		||||
    return Stack(
 | 
			
		||||
      alignment: Alignment.center,
 | 
			
		||||
      children: [
 | 
			
		||||
        KeyBoardShortcuts(
 | 
			
		||||
          child: Container(),
 | 
			
		||||
          keysToPress: {
 | 
			
		||||
            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,
 | 
			
		||||
                    builder: (context) =>
 | 
			
		||||
                        CupertinoWidgetsBottomSheet(room: widget.room),
 | 
			
		||||
                  )
 | 
			
		||||
                : showModalBottomSheet(
 | 
			
		||||
                    future: () => widget.room
 | 
			
		||||
                        .setPushRuleState(PushRuleState.mentionsOnly));
 | 
			
		||||
                break;
 | 
			
		||||
              case 'unmute':
 | 
			
		||||
                await showFutureLoadingDialog(
 | 
			
		||||
                    context: context,
 | 
			
		||||
                    builder: (context) => WidgetsBottomSheet(room: widget.room),
 | 
			
		||||
                  );
 | 
			
		||||
            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');
 | 
			
		||||
              }
 | 
			
		||||
                    future: () =>
 | 
			
		||||
                        widget.room.setPushRuleState(PushRuleState.notify));
 | 
			
		||||
                break;
 | 
			
		||||
              case 'details':
 | 
			
		||||
                _showChatDetails();
 | 
			
		||||
                break;
 | 
			
		||||
            }
 | 
			
		||||
            break;
 | 
			
		||||
          case 'mute':
 | 
			
		||||
            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,
 | 
			
		||||
          },
 | 
			
		||||
          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
 | 
			
		||||
      url: "https://pub.dartlang.org"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "4.4.0"
 | 
			
		||||
    version: "4.5.0"
 | 
			
		||||
  file_picker_cross:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
@ -715,7 +715,7 @@ packages:
 | 
			
		||||
      name: flutter_webrtc
 | 
			
		||||
      url: "https://pub.dartlang.org"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "0.8.2"
 | 
			
		||||
    version: "0.8.3"
 | 
			
		||||
  frontend_server_client:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@ -891,6 +891,15 @@ packages:
 | 
			
		||||
      url: "https://pub.dartlang.org"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    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:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@ -960,7 +969,7 @@ packages:
 | 
			
		||||
      name: matrix
 | 
			
		||||
      url: "https://pub.dartlang.org"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "0.8.11"
 | 
			
		||||
    version: "0.8.12"
 | 
			
		||||
  matrix_api_lite:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@ -1198,7 +1207,7 @@ packages:
 | 
			
		||||
      name: permission_handler_apple
 | 
			
		||||
      url: "https://pub.dartlang.org"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "9.0.2"
 | 
			
		||||
    version: "9.0.3"
 | 
			
		||||
  permission_handler_platform_interface:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@ -1813,6 +1822,13 @@ packages:
 | 
			
		||||
      url: "https://pub.dartlang.org"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    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:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
 | 
			
		||||
@ -55,6 +55,7 @@ dependencies:
 | 
			
		||||
  image: ^3.1.1
 | 
			
		||||
  image_picker: ^0.8.4+8
 | 
			
		||||
  intl: any
 | 
			
		||||
  keyboard_shortcuts: ^0.1.4
 | 
			
		||||
  localstorage: ^4.0.0+1
 | 
			
		||||
  lottie: ^1.2.2
 | 
			
		||||
  matrix: ^0.8.11
 | 
			
		||||
@ -135,4 +136,10 @@ dependency_overrides:
 | 
			
		||||
    hosted:
 | 
			
		||||
      name: geolocator_android
 | 
			
		||||
      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
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user