mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 13:05:32 +09:00
fix(app): default voice shortcuts to global and add a toggle (#2097)
This commit is contained in:
1 parent
dbc63e9ef7
commit
04d3afaf7b
4 files changed
+155
-76
No files matched your search
@@ -381,6 +381,12 @@ class KeybindManager {
|
||||
private async checkInputMonitoringPermission(): Promise<boolean> {
|
||||
if (!isNativeMacOS()) return true;
|
||||
if (this.inputMonitoringHookStatus === 'granted') return true;
|
||||
const electronApi = getElectronAPI();
|
||||
if (await electronApi?.checkInputMonitoringAccess?.()) {
|
||||
NativePermission.setInputMonitoringStatus('granted');
|
||||
this.inputMonitoringHookStatus = 'granted';
|
||||
return true;
|
||||
}
|
||||
const result = await ensureMacPermission('input-monitoring', {behavior: 'passive'});
|
||||
switch (result) {
|
||||
case 'granted':
|
||||
|
||||
@@ -82,6 +82,7 @@ import {
|
||||
ZOOM_IN_DESCRIPTOR,
|
||||
ZOOM_OUT_DESCRIPTOR,
|
||||
} from '@app/features/input/state/input_keybind/shared';
|
||||
import AppStorage from '@app/features/platform/state/PersistentStorage';
|
||||
import {awaitHydration, makePersistent} from '@app/features/platform/utils/MobXPersistence';
|
||||
import {makeSyncedField} from '@app/features/user/state/SyncedField';
|
||||
import UserSettings from '@app/features/user/state/UserSettings';
|
||||
@@ -285,6 +286,8 @@ export interface CustomKeybindEntry {
|
||||
}
|
||||
|
||||
const STORE_VERSION = 5 as const;
|
||||
|
||||
const GLOBAL_KEYBIND_DEFAULT_MIGRATION_KEY = 'Keybind:globalDefaultMigration:v1';
|
||||
const generateId = (): string => {
|
||||
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
||||
return crypto.randomUUID();
|
||||
@@ -748,7 +751,7 @@ const getDefaultKeybinds = (
|
||||
{
|
||||
action: 'voice_toggle_mute',
|
||||
label: i18n._(TOGGLE_MUTE_DESCRIPTOR),
|
||||
combo: {key: 'm', ctrlOrMeta: true, shift: true, global: false, enabled: true},
|
||||
combo: {key: 'm', ctrlOrMeta: true, shift: true, global: true, enabled: true},
|
||||
allowGlobal: true,
|
||||
assignable: true,
|
||||
section: 'voice_and_video',
|
||||
@@ -756,7 +759,7 @@ const getDefaultKeybinds = (
|
||||
{
|
||||
action: 'voice_toggle_deafen',
|
||||
label: i18n._(VOICE_TOGGLE_DEAFEN_DESCRIPTOR),
|
||||
combo: {key: 'd', ctrlOrMeta: true, shift: true, global: false, enabled: true},
|
||||
combo: {key: 'd', ctrlOrMeta: true, shift: true, global: true, enabled: true},
|
||||
allowGlobal: true,
|
||||
assignable: true,
|
||||
section: 'voice_and_video',
|
||||
@@ -1025,6 +1028,18 @@ class Keybind {
|
||||
});
|
||||
}
|
||||
|
||||
private migrateGlobalCapableKeybindsToGlobal(): void {
|
||||
if (AppStorage.getItem(GLOBAL_KEYBIND_DEFAULT_MIGRATION_KEY) === '1') return;
|
||||
for (const entry of this.customKeybinds) {
|
||||
if (entry.action == null) continue;
|
||||
const config = this.getDefaultByAction(entry.action);
|
||||
if (config?.allowGlobal && entry.combo.global !== true) {
|
||||
entry.combo = {...entry.combo, global: true};
|
||||
}
|
||||
}
|
||||
AppStorage.setItem(GLOBAL_KEYBIND_DEFAULT_MIGRATION_KEY, '1');
|
||||
}
|
||||
|
||||
setI18n(i18n: I18n): void {
|
||||
this.i18n = i18n;
|
||||
if (this.initialized) return;
|
||||
@@ -1034,6 +1049,7 @@ class Keybind {
|
||||
if (!Array.isArray(this.customKeybinds)) {
|
||||
this.customKeybinds = [];
|
||||
}
|
||||
this.migrateGlobalCapableKeybindsToGlobal();
|
||||
this.initialized = true;
|
||||
});
|
||||
});
|
||||
@@ -1200,6 +1216,21 @@ class Keybind {
|
||||
return created;
|
||||
}
|
||||
|
||||
isActionGlobalCapable(action: KeybindCommand): boolean {
|
||||
return this.getDefaultByAction(action)?.allowGlobal === true;
|
||||
}
|
||||
|
||||
isActionGlobal(action: KeybindCommand): boolean {
|
||||
const combo = this.getPrimaryCustomKeybind(action)?.combo ?? this.getDefaultByAction(action)?.combo;
|
||||
return combo?.global === true;
|
||||
}
|
||||
|
||||
setActionGlobal(action: KeybindCommand, global: boolean): void {
|
||||
const baseCombo = this.getPrimaryCustomKeybind(action)?.combo ??
|
||||
this.getDefaultByAction(action)?.combo ?? {key: ''};
|
||||
this.setPrimaryCustomKeybindCombo(action, {...baseCombo, global});
|
||||
}
|
||||
|
||||
setTransmitMode(mode: TransmitMode): void {
|
||||
runInAction(() => {
|
||||
this.transmitMode = mode;
|
||||
|
||||
+28
-13
@@ -7,6 +7,7 @@ import Keybind, {
|
||||
type KeyCombo,
|
||||
} from '@app/features/input/state/InputKeybind';
|
||||
import {remFromPx} from '@app/features/theme/layout/RemFromPx';
|
||||
import {CheckboxItem} from '@app/features/ui/action_menu/ContextMenu';
|
||||
import {MoreOptionsVerticalIcon} from '@app/features/ui/action_menu/ContextMenuIcons';
|
||||
import {MenuGroup} from '@app/features/ui/action_menu/MenuGroup';
|
||||
import {MenuItem} from '@app/features/ui/action_menu/MenuItem';
|
||||
@@ -68,19 +69,33 @@ export const CustomKeybindRow = observer(
|
||||
};
|
||||
const openRowMenu = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
ContextMenuCommands.openFromEvent(event, ({onClose}) => (
|
||||
<MenuGroup data-flx="user.keybinds-tab.open-row-menu.menu-group">
|
||||
<MenuItem
|
||||
icon={<TrashIcon size={remFromPx(16)} data-flx="user.keybinds-tab.open-row-menu.trash-icon" />}
|
||||
danger
|
||||
onClick={() => {
|
||||
onClose();
|
||||
onRemove();
|
||||
}}
|
||||
data-flx="user.keybinds-tab.open-row-menu.menu-item.close--2"
|
||||
>
|
||||
{i18n._(DELETE_SHORTCUT_DESCRIPTOR)}
|
||||
</MenuItem>
|
||||
</MenuGroup>
|
||||
<>
|
||||
{entry.action && Keybind.isActionGlobalCapable(entry.action) ? (
|
||||
<MenuGroup data-flx="user.keybinds-tab.open-row-menu.global-menu-group">
|
||||
<CheckboxItem
|
||||
checked={entry.combo.global === true}
|
||||
onCheckedChange={(value) => Keybind.updateCustomKeybindCombo(entry.id, {...entry.combo, global: value})}
|
||||
closeOnChange
|
||||
data-flx="user.keybinds-tab.open-row-menu.checkbox-item.global"
|
||||
>
|
||||
<Trans>Global shortcut</Trans>
|
||||
</CheckboxItem>
|
||||
</MenuGroup>
|
||||
) : null}
|
||||
<MenuGroup data-flx="user.keybinds-tab.open-row-menu.menu-group">
|
||||
<MenuItem
|
||||
icon={<TrashIcon size={remFromPx(16)} data-flx="user.keybinds-tab.open-row-menu.trash-icon" />}
|
||||
danger
|
||||
onClick={() => {
|
||||
onClose();
|
||||
onRemove();
|
||||
}}
|
||||
data-flx="user.keybinds-tab.open-row-menu.menu-item.close--2"
|
||||
>
|
||||
{i18n._(DELETE_SHORTCUT_DESCRIPTOR)}
|
||||
</MenuItem>
|
||||
</MenuGroup>
|
||||
</>
|
||||
));
|
||||
};
|
||||
return (
|
||||
|
||||
+88
-61
@@ -1,6 +1,7 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import Keybind, {type KeybindCommand} from '@app/features/input/state/InputKeybind';
|
||||
import {CheckboxItem} from '@app/features/ui/action_menu/ContextMenu';
|
||||
import {
|
||||
EditSimpleIcon,
|
||||
HideIcon,
|
||||
@@ -53,6 +54,10 @@ const MORE_SHORTCUT_OPTIONS_DESCRIPTOR = msg({
|
||||
message: 'More shortcut options',
|
||||
comment: 'Short label in the keybinds tab. Keep it concise.',
|
||||
});
|
||||
const GLOBAL_SHORTCUT_DESCRIPTOR = msg({
|
||||
message: 'Global shortcut',
|
||||
comment: 'Toggle in the keybinds tab. When on, the shortcut works even when Fluxer is not focused. Keep it concise.',
|
||||
});
|
||||
export const DefaultShortcutRow: React.FC<{row: ShortcutRowModel; overriddenActions: ReadonlySet<KeybindCommand>}> = ({
|
||||
row,
|
||||
overriddenActions,
|
||||
@@ -74,97 +79,119 @@ export const DefaultShortcutRow: React.FC<{row: ShortcutRowModel; overriddenActi
|
||||
});
|
||||
const allHaveCustom = perAction.every((p) => p.customBindings.length > 0);
|
||||
const noneHaveCustom = perAction.every((p) => p.customBindings.length === 0);
|
||||
if (noneHaveCustom) {
|
||||
return (
|
||||
<MenuGroup data-flx="user.keybinds-tab.open-row-menu.menu-group--2">
|
||||
<MenuItem
|
||||
icon={<EditSimpleIcon size={16} data-flx="user.keybinds-tab.open-row-menu.edit-simple-icon--2" />}
|
||||
onClick={() => {
|
||||
onClose();
|
||||
for (const {action} of perAction) {
|
||||
Keybind.addCustomKeybindForAction(action.action);
|
||||
}
|
||||
}}
|
||||
data-flx="user.keybinds-tab.open-row-menu.menu-item.close--3"
|
||||
>
|
||||
{i18n._(SET_CUSTOM_SHORTCUT_DESCRIPTOR)}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
icon={<HideIcon size={16} data-flx="user.keybinds-tab.open-row-menu.hide-icon--2" />}
|
||||
onClick={() => {
|
||||
onClose();
|
||||
for (const {action} of perAction) {
|
||||
Keybind.addCustomKeybindForAction(action.action);
|
||||
}
|
||||
}}
|
||||
data-flx="user.keybinds-tab.open-row-menu.menu-item.close--4"
|
||||
>
|
||||
{i18n._(DISABLE_BUILT_IN_SHORTCUT_DESCRIPTOR)}
|
||||
</MenuItem>
|
||||
</MenuGroup>
|
||||
);
|
||||
}
|
||||
if (allHaveCustom && perAction.length === 1) {
|
||||
const {action, hasEnabledCustomBinding} = perAction[0];
|
||||
return (
|
||||
<MenuGroup data-flx="user.keybinds-tab.open-row-menu.menu-group--3">
|
||||
{hasEnabledCustomBinding ? (
|
||||
const globalAction =
|
||||
actions.length === 1 && Keybind.isActionGlobalCapable(actions[0].action) ? actions[0].action : null;
|
||||
const globalGroup = globalAction ? (
|
||||
<MenuGroup data-flx="user.keybinds-tab.open-row-menu.global-menu-group">
|
||||
<CheckboxItem
|
||||
checked={Keybind.isActionGlobal(globalAction)}
|
||||
onCheckedChange={(value) => Keybind.setActionGlobal(globalAction, value)}
|
||||
closeOnChange
|
||||
data-flx="user.keybinds-tab.open-row-menu.checkbox-item.global"
|
||||
>
|
||||
{i18n._(GLOBAL_SHORTCUT_DESCRIPTOR)}
|
||||
</CheckboxItem>
|
||||
</MenuGroup>
|
||||
) : null;
|
||||
const renderBody = () => {
|
||||
if (noneHaveCustom) {
|
||||
return (
|
||||
<MenuGroup data-flx="user.keybinds-tab.open-row-menu.menu-group--2">
|
||||
<MenuItem
|
||||
icon={<RetryIcon size={16} data-flx="user.keybinds-tab.open-row-menu.retry-icon" />}
|
||||
danger
|
||||
icon={<EditSimpleIcon size={16} data-flx="user.keybinds-tab.open-row-menu.edit-simple-icon--2" />}
|
||||
onClick={() => {
|
||||
onClose();
|
||||
Keybind.removeCustomKeybindsForAction(action.action);
|
||||
for (const {action} of perAction) {
|
||||
Keybind.addCustomKeybindForAction(action.action);
|
||||
}
|
||||
}}
|
||||
data-flx="user.keybinds-tab.open-row-menu.menu-item.close--5"
|
||||
data-flx="user.keybinds-tab.open-row-menu.menu-item.close--3"
|
||||
>
|
||||
{i18n._(RESET_TO_BUILT_IN_DESCRIPTOR)}
|
||||
{i18n._(SET_CUSTOM_SHORTCUT_DESCRIPTOR)}
|
||||
</MenuItem>
|
||||
) : (
|
||||
<MenuItem
|
||||
icon={<EditSimpleIcon size={16} data-flx="user.keybinds-tab.open-row-menu.edit-simple-icon--3" />}
|
||||
icon={<HideIcon size={16} data-flx="user.keybinds-tab.open-row-menu.hide-icon--2" />}
|
||||
onClick={() => {
|
||||
onClose();
|
||||
Keybind.addCustomKeybindForAction(action.action);
|
||||
for (const {action} of perAction) {
|
||||
Keybind.addCustomKeybindForAction(action.action);
|
||||
}
|
||||
}}
|
||||
data-flx="user.keybinds-tab.open-row-menu.menu-item.close--6"
|
||||
data-flx="user.keybinds-tab.open-row-menu.menu-item.close--4"
|
||||
>
|
||||
{i18n._(ADD_ANOTHER_CUSTOM_SHORTCUT_DESCRIPTOR)}
|
||||
{i18n._(DISABLE_BUILT_IN_SHORTCUT_DESCRIPTOR)}
|
||||
</MenuItem>
|
||||
)}
|
||||
</MenuGroup>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{perAction.map(({action, label, customBindings, hasEnabledCustomBinding}) => (
|
||||
<MenuGroup key={action.action} data-flx="user.keybinds-tab.open-row-menu.menu-group--4">
|
||||
{customBindings.length > 0 && hasEnabledCustomBinding ? (
|
||||
</MenuGroup>
|
||||
);
|
||||
}
|
||||
if (allHaveCustom && perAction.length === 1) {
|
||||
const {action, hasEnabledCustomBinding} = perAction[0];
|
||||
return (
|
||||
<MenuGroup data-flx="user.keybinds-tab.open-row-menu.menu-group--3">
|
||||
{hasEnabledCustomBinding ? (
|
||||
<MenuItem
|
||||
icon={<RetryIcon size={16} data-flx="user.keybinds-tab.open-row-menu.retry-icon--2" />}
|
||||
icon={<RetryIcon size={16} data-flx="user.keybinds-tab.open-row-menu.retry-icon" />}
|
||||
danger
|
||||
onClick={() => {
|
||||
onClose();
|
||||
Keybind.removeCustomKeybindsForAction(action.action);
|
||||
}}
|
||||
data-flx="user.keybinds-tab.open-row-menu.menu-item.close--7"
|
||||
data-flx="user.keybinds-tab.open-row-menu.menu-item.close--5"
|
||||
>
|
||||
{i18n._(RESET_TO_BUILT_IN_2_DESCRIPTOR, {label})}
|
||||
{i18n._(RESET_TO_BUILT_IN_DESCRIPTOR)}
|
||||
</MenuItem>
|
||||
) : (
|
||||
<MenuItem
|
||||
icon={<EditSimpleIcon size={16} data-flx="user.keybinds-tab.open-row-menu.edit-simple-icon--4" />}
|
||||
icon={<EditSimpleIcon size={16} data-flx="user.keybinds-tab.open-row-menu.edit-simple-icon--3" />}
|
||||
onClick={() => {
|
||||
onClose();
|
||||
Keybind.addCustomKeybindForAction(action.action);
|
||||
}}
|
||||
data-flx="user.keybinds-tab.open-row-menu.menu-item.close--8"
|
||||
data-flx="user.keybinds-tab.open-row-menu.menu-item.close--6"
|
||||
>
|
||||
{i18n._(SET_CUSTOM_SHORTCUT_FOR_DESCRIPTOR, {label})}
|
||||
{i18n._(ADD_ANOTHER_CUSTOM_SHORTCUT_DESCRIPTOR)}
|
||||
</MenuItem>
|
||||
)}
|
||||
</MenuGroup>
|
||||
))}
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{perAction.map(({action, label, customBindings, hasEnabledCustomBinding}) => (
|
||||
<MenuGroup key={action.action} data-flx="user.keybinds-tab.open-row-menu.menu-group--4">
|
||||
{customBindings.length > 0 && hasEnabledCustomBinding ? (
|
||||
<MenuItem
|
||||
icon={<RetryIcon size={16} data-flx="user.keybinds-tab.open-row-menu.retry-icon--2" />}
|
||||
danger
|
||||
onClick={() => {
|
||||
onClose();
|
||||
Keybind.removeCustomKeybindsForAction(action.action);
|
||||
}}
|
||||
data-flx="user.keybinds-tab.open-row-menu.menu-item.close--7"
|
||||
>
|
||||
{i18n._(RESET_TO_BUILT_IN_2_DESCRIPTOR, {label})}
|
||||
</MenuItem>
|
||||
) : (
|
||||
<MenuItem
|
||||
icon={<EditSimpleIcon size={16} data-flx="user.keybinds-tab.open-row-menu.edit-simple-icon--4" />}
|
||||
onClick={() => {
|
||||
onClose();
|
||||
Keybind.addCustomKeybindForAction(action.action);
|
||||
}}
|
||||
data-flx="user.keybinds-tab.open-row-menu.menu-item.close--8"
|
||||
>
|
||||
{i18n._(SET_CUSTOM_SHORTCUT_FOR_DESCRIPTOR, {label})}
|
||||
</MenuItem>
|
||||
)}
|
||||
</MenuGroup>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
{globalGroup}
|
||||
{renderBody()}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user