mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 21:15: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> {
|
private async checkInputMonitoringPermission(): Promise<boolean> {
|
||||||
if (!isNativeMacOS()) return true;
|
if (!isNativeMacOS()) return true;
|
||||||
if (this.inputMonitoringHookStatus === 'granted') 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'});
|
const result = await ensureMacPermission('input-monitoring', {behavior: 'passive'});
|
||||||
switch (result) {
|
switch (result) {
|
||||||
case 'granted':
|
case 'granted':
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ import {
|
|||||||
ZOOM_IN_DESCRIPTOR,
|
ZOOM_IN_DESCRIPTOR,
|
||||||
ZOOM_OUT_DESCRIPTOR,
|
ZOOM_OUT_DESCRIPTOR,
|
||||||
} from '@app/features/input/state/input_keybind/shared';
|
} 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 {awaitHydration, makePersistent} from '@app/features/platform/utils/MobXPersistence';
|
||||||
import {makeSyncedField} from '@app/features/user/state/SyncedField';
|
import {makeSyncedField} from '@app/features/user/state/SyncedField';
|
||||||
import UserSettings from '@app/features/user/state/UserSettings';
|
import UserSettings from '@app/features/user/state/UserSettings';
|
||||||
@@ -285,6 +286,8 @@ export interface CustomKeybindEntry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const STORE_VERSION = 5 as const;
|
const STORE_VERSION = 5 as const;
|
||||||
|
|
||||||
|
const GLOBAL_KEYBIND_DEFAULT_MIGRATION_KEY = 'Keybind:globalDefaultMigration:v1';
|
||||||
const generateId = (): string => {
|
const generateId = (): string => {
|
||||||
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
||||||
return crypto.randomUUID();
|
return crypto.randomUUID();
|
||||||
@@ -748,7 +751,7 @@ const getDefaultKeybinds = (
|
|||||||
{
|
{
|
||||||
action: 'voice_toggle_mute',
|
action: 'voice_toggle_mute',
|
||||||
label: i18n._(TOGGLE_MUTE_DESCRIPTOR),
|
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,
|
allowGlobal: true,
|
||||||
assignable: true,
|
assignable: true,
|
||||||
section: 'voice_and_video',
|
section: 'voice_and_video',
|
||||||
@@ -756,7 +759,7 @@ const getDefaultKeybinds = (
|
|||||||
{
|
{
|
||||||
action: 'voice_toggle_deafen',
|
action: 'voice_toggle_deafen',
|
||||||
label: i18n._(VOICE_TOGGLE_DEAFEN_DESCRIPTOR),
|
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,
|
allowGlobal: true,
|
||||||
assignable: true,
|
assignable: true,
|
||||||
section: 'voice_and_video',
|
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 {
|
setI18n(i18n: I18n): void {
|
||||||
this.i18n = i18n;
|
this.i18n = i18n;
|
||||||
if (this.initialized) return;
|
if (this.initialized) return;
|
||||||
@@ -1034,6 +1049,7 @@ class Keybind {
|
|||||||
if (!Array.isArray(this.customKeybinds)) {
|
if (!Array.isArray(this.customKeybinds)) {
|
||||||
this.customKeybinds = [];
|
this.customKeybinds = [];
|
||||||
}
|
}
|
||||||
|
this.migrateGlobalCapableKeybindsToGlobal();
|
||||||
this.initialized = true;
|
this.initialized = true;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1200,6 +1216,21 @@ class Keybind {
|
|||||||
return created;
|
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 {
|
setTransmitMode(mode: TransmitMode): void {
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.transmitMode = mode;
|
this.transmitMode = mode;
|
||||||
|
|||||||
+28
-13
@@ -7,6 +7,7 @@ import Keybind, {
|
|||||||
type KeyCombo,
|
type KeyCombo,
|
||||||
} from '@app/features/input/state/InputKeybind';
|
} from '@app/features/input/state/InputKeybind';
|
||||||
import {remFromPx} from '@app/features/theme/layout/RemFromPx';
|
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 {MoreOptionsVerticalIcon} from '@app/features/ui/action_menu/ContextMenuIcons';
|
||||||
import {MenuGroup} from '@app/features/ui/action_menu/MenuGroup';
|
import {MenuGroup} from '@app/features/ui/action_menu/MenuGroup';
|
||||||
import {MenuItem} from '@app/features/ui/action_menu/MenuItem';
|
import {MenuItem} from '@app/features/ui/action_menu/MenuItem';
|
||||||
@@ -68,19 +69,33 @@ export const CustomKeybindRow = observer(
|
|||||||
};
|
};
|
||||||
const openRowMenu = (event: React.MouseEvent<HTMLButtonElement>) => {
|
const openRowMenu = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
ContextMenuCommands.openFromEvent(event, ({onClose}) => (
|
ContextMenuCommands.openFromEvent(event, ({onClose}) => (
|
||||||
<MenuGroup data-flx="user.keybinds-tab.open-row-menu.menu-group">
|
<>
|
||||||
<MenuItem
|
{entry.action && Keybind.isActionGlobalCapable(entry.action) ? (
|
||||||
icon={<TrashIcon size={remFromPx(16)} data-flx="user.keybinds-tab.open-row-menu.trash-icon" />}
|
<MenuGroup data-flx="user.keybinds-tab.open-row-menu.global-menu-group">
|
||||||
danger
|
<CheckboxItem
|
||||||
onClick={() => {
|
checked={entry.combo.global === true}
|
||||||
onClose();
|
onCheckedChange={(value) => Keybind.updateCustomKeybindCombo(entry.id, {...entry.combo, global: value})}
|
||||||
onRemove();
|
closeOnChange
|
||||||
}}
|
data-flx="user.keybinds-tab.open-row-menu.checkbox-item.global"
|
||||||
data-flx="user.keybinds-tab.open-row-menu.menu-item.close--2"
|
>
|
||||||
>
|
<Trans>Global shortcut</Trans>
|
||||||
{i18n._(DELETE_SHORTCUT_DESCRIPTOR)}
|
</CheckboxItem>
|
||||||
</MenuItem>
|
</MenuGroup>
|
||||||
</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 (
|
return (
|
||||||
|
|||||||
+88
-61
@@ -1,6 +1,7 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
import Keybind, {type KeybindCommand} from '@app/features/input/state/InputKeybind';
|
import Keybind, {type KeybindCommand} from '@app/features/input/state/InputKeybind';
|
||||||
|
import {CheckboxItem} from '@app/features/ui/action_menu/ContextMenu';
|
||||||
import {
|
import {
|
||||||
EditSimpleIcon,
|
EditSimpleIcon,
|
||||||
HideIcon,
|
HideIcon,
|
||||||
@@ -53,6 +54,10 @@ const MORE_SHORTCUT_OPTIONS_DESCRIPTOR = msg({
|
|||||||
message: 'More shortcut options',
|
message: 'More shortcut options',
|
||||||
comment: 'Short label in the keybinds tab. Keep it concise.',
|
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>}> = ({
|
export const DefaultShortcutRow: React.FC<{row: ShortcutRowModel; overriddenActions: ReadonlySet<KeybindCommand>}> = ({
|
||||||
row,
|
row,
|
||||||
overriddenActions,
|
overriddenActions,
|
||||||
@@ -74,97 +79,119 @@ export const DefaultShortcutRow: React.FC<{row: ShortcutRowModel; overriddenActi
|
|||||||
});
|
});
|
||||||
const allHaveCustom = perAction.every((p) => p.customBindings.length > 0);
|
const allHaveCustom = perAction.every((p) => p.customBindings.length > 0);
|
||||||
const noneHaveCustom = perAction.every((p) => p.customBindings.length === 0);
|
const noneHaveCustom = perAction.every((p) => p.customBindings.length === 0);
|
||||||
if (noneHaveCustom) {
|
const globalAction =
|
||||||
return (
|
actions.length === 1 && Keybind.isActionGlobalCapable(actions[0].action) ? actions[0].action : null;
|
||||||
<MenuGroup data-flx="user.keybinds-tab.open-row-menu.menu-group--2">
|
const globalGroup = globalAction ? (
|
||||||
<MenuItem
|
<MenuGroup data-flx="user.keybinds-tab.open-row-menu.global-menu-group">
|
||||||
icon={<EditSimpleIcon size={16} data-flx="user.keybinds-tab.open-row-menu.edit-simple-icon--2" />}
|
<CheckboxItem
|
||||||
onClick={() => {
|
checked={Keybind.isActionGlobal(globalAction)}
|
||||||
onClose();
|
onCheckedChange={(value) => Keybind.setActionGlobal(globalAction, value)}
|
||||||
for (const {action} of perAction) {
|
closeOnChange
|
||||||
Keybind.addCustomKeybindForAction(action.action);
|
data-flx="user.keybinds-tab.open-row-menu.checkbox-item.global"
|
||||||
}
|
>
|
||||||
}}
|
{i18n._(GLOBAL_SHORTCUT_DESCRIPTOR)}
|
||||||
data-flx="user.keybinds-tab.open-row-menu.menu-item.close--3"
|
</CheckboxItem>
|
||||||
>
|
</MenuGroup>
|
||||||
{i18n._(SET_CUSTOM_SHORTCUT_DESCRIPTOR)}
|
) : null;
|
||||||
</MenuItem>
|
const renderBody = () => {
|
||||||
<MenuItem
|
if (noneHaveCustom) {
|
||||||
icon={<HideIcon size={16} data-flx="user.keybinds-tab.open-row-menu.hide-icon--2" />}
|
return (
|
||||||
onClick={() => {
|
<MenuGroup data-flx="user.keybinds-tab.open-row-menu.menu-group--2">
|
||||||
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 ? (
|
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon={<RetryIcon size={16} data-flx="user.keybinds-tab.open-row-menu.retry-icon" />}
|
icon={<EditSimpleIcon size={16} data-flx="user.keybinds-tab.open-row-menu.edit-simple-icon--2" />}
|
||||||
danger
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onClose();
|
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>
|
||||||
) : (
|
|
||||||
<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={() => {
|
onClick={() => {
|
||||||
onClose();
|
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>
|
</MenuItem>
|
||||||
)}
|
</MenuGroup>
|
||||||
</MenuGroup>
|
);
|
||||||
);
|
}
|
||||||
}
|
if (allHaveCustom && perAction.length === 1) {
|
||||||
return (
|
const {action, hasEnabledCustomBinding} = perAction[0];
|
||||||
<>
|
return (
|
||||||
{perAction.map(({action, label, customBindings, hasEnabledCustomBinding}) => (
|
<MenuGroup data-flx="user.keybinds-tab.open-row-menu.menu-group--3">
|
||||||
<MenuGroup key={action.action} data-flx="user.keybinds-tab.open-row-menu.menu-group--4">
|
{hasEnabledCustomBinding ? (
|
||||||
{customBindings.length > 0 && hasEnabledCustomBinding ? (
|
|
||||||
<MenuItem
|
<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
|
danger
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onClose();
|
onClose();
|
||||||
Keybind.removeCustomKeybindsForAction(action.action);
|
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>
|
||||||
) : (
|
) : (
|
||||||
<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={() => {
|
onClick={() => {
|
||||||
onClose();
|
onClose();
|
||||||
Keybind.addCustomKeybindForAction(action.action);
|
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>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
</MenuGroup>
|
</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