fix(desktop): macOS 단축키 표기 (Win/Ctrl/Alt → ⌘/⌃/⌥/⇧)
기존: HotkeyRecordModal에서 키 녹화 시 'Win+Shift+1' 같은 정적 displayLabel 저장
- KEY_DISPLAY_MAP이 Windows 전용 (91/92 → 'Win')
- SettingsModal HotkeyDisplay가 binding.displayLabel.split(' + ')로 chip 분리
- 그런데 displayLabel은 join('+') (space 없음) — split이 작동 안 해서 단일 chip 'Win+Shift+1'
- macOS에서도 'Win'으로 표시되어 사용자 혼동
수정:
- preload/index.ts: process.platform 노출 (electronAPI.platform)
- renderer/utils/format-hotkey.ts 신규
- formatHotkeySegments(binding) → 플랫폼별 segment 배열
- macOS: ['⌘', '⇧', '1'] (⌃ ⌥ ⇧ ⌘ 순, Apple HIG)
- Win/Linux: ['Ctrl', 'Win', 'Alt', 'Shift', '1']
- formatHotkeyLabel(binding) → 단일 문자열
- macOS: '⌘⇧1' (구분자 없음, Apple 표준)
- Win/Linux: 'Ctrl + Shift + 1'
- 사용 위치 일괄 교체 (binding.displayLabel → format 헬퍼):
- SettingsModal HotkeyDisplay (chip 표시)
- SettingsModal Agent 모드 설명 ({{key}} 치환)
- OnboardingModal 단축키 chip + 완료 화면
- DashboardPage CrtDisplay 'PRESS X TO RECORD'
- HotkeyRecordModal currentBinding 표시
displayLabel 필드는 ConfigService 호환을 위해 유지 (저장된 값은 그대로)
This commit is contained in:
parent
07c6d13c99
commit
0d17b71866
6 changed files with 135 additions and 8 deletions
|
|
@ -187,6 +187,10 @@ function on<T>(channel: string, callback: (data: T) => void): Unsubscribe {
|
||||||
}
|
}
|
||||||
|
|
||||||
const electronAPI = {
|
const electronAPI = {
|
||||||
|
// ── Platform ───────────────────────────────────────────
|
||||||
|
// 'darwin' | 'win32' | 'linux' — 렌더러에서 키바인딩/단축키 표기 분기
|
||||||
|
platform: process.platform as NodeJS.Platform,
|
||||||
|
|
||||||
// ── Audio ──────────────────────────────────────────────
|
// ── Audio ──────────────────────────────────────────────
|
||||||
audio: {
|
audio: {
|
||||||
getDevices: () => invoke<AudioDevice[]>(IPC_CHANNELS.AUDIO.GET_DEVICES),
|
getDevices: () => invoke<AudioDevice[]>(IPC_CHANNELS.AUDIO.GET_DEVICES),
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import {
|
||||||
import { d3roPalette } from '@d3ro/ui/theme'
|
import { d3roPalette } from '@d3ro/ui/theme'
|
||||||
import { useI18n } from '@d3ro/i18n'
|
import { useI18n } from '@d3ro/i18n'
|
||||||
import type { HotkeyBinding } from '@d3ro/core/types'
|
import type { HotkeyBinding } from '@d3ro/core/types'
|
||||||
|
import { formatHotkeyLabel } from '../utils/format-hotkey'
|
||||||
|
|
||||||
// ── 키 이름 매핑 (Windows) ──────────────────────────────
|
// ── 키 이름 매핑 (Windows) ──────────────────────────────
|
||||||
const KEY_DISPLAY_MAP: Record<number, string> = {
|
const KEY_DISPLAY_MAP: Record<number, string> = {
|
||||||
|
|
@ -302,7 +303,7 @@ export function HotkeyRecordModal({
|
||||||
|
|
||||||
{currentBinding && (
|
{currentBinding && (
|
||||||
<Typography sx={{ color: d3roPalette.text.inactive, fontSize: '12px', mt: 2 }}>
|
<Typography sx={{ color: d3roPalette.text.inactive, fontSize: '12px', mt: 2 }}>
|
||||||
{t('hotkey.current', { keys: currentBinding.displayLabel })}
|
{t('hotkey.current', { keys: formatHotkeyLabel(currentBinding) })}
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import OpenInNewIcon from '@mui/icons-material/OpenInNew'
|
||||||
import { d3roPalette, d3roFontMono, d3roShadow } from '@d3ro/ui/theme'
|
import { d3roPalette, d3roFontMono, d3roShadow } from '@d3ro/ui/theme'
|
||||||
import { Led } from '@d3ro/ui/components/ds'
|
import { Led } from '@d3ro/ui/components/ds'
|
||||||
import { HotkeyRecordModal } from './HotkeyRecordModal'
|
import { HotkeyRecordModal } from './HotkeyRecordModal'
|
||||||
|
import { formatHotkeyLabel, formatHotkeySegments } from '../utils/format-hotkey'
|
||||||
import { useI18n } from '@d3ro/i18n'
|
import { useI18n } from '@d3ro/i18n'
|
||||||
import type { HotkeyBinding, AudioDevice } from '@d3ro/core/types'
|
import type { HotkeyBinding, AudioDevice } from '@d3ro/core/types'
|
||||||
|
|
||||||
|
|
@ -167,9 +168,9 @@ export function OnboardingModal({ open, onClose }: OnboardingModalProps): React.
|
||||||
{hotkeyBinding ? (
|
{hotkeyBinding ? (
|
||||||
<Stack direction="row" spacing={1} justifyContent="center" alignItems="center">
|
<Stack direction="row" spacing={1} justifyContent="center" alignItems="center">
|
||||||
<Led color="green" size={8} />
|
<Led color="green" size={8} />
|
||||||
{hotkeyBinding.displayLabel.split(' + ').map((key) => (
|
{formatHotkeySegments(hotkeyBinding).map((key, idx) => (
|
||||||
<Chip
|
<Chip
|
||||||
key={key}
|
key={`${key}-${idx}`}
|
||||||
label={key}
|
label={key}
|
||||||
sx={{
|
sx={{
|
||||||
fontFamily: d3roFontMono,
|
fontFamily: d3roFontMono,
|
||||||
|
|
@ -247,7 +248,7 @@ export function OnboardingModal({ open, onClose }: OnboardingModalProps): React.
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body2" sx={{ color: d3roPalette.text.secondary, mb: 4 }}>
|
<Typography variant="body2" sx={{ color: d3roPalette.text.secondary, mb: 4 }}>
|
||||||
{hotkeyBinding
|
{hotkeyBinding
|
||||||
? t('onboarding.done.descWithKey', { key: hotkeyBinding.displayLabel })
|
? t('onboarding.done.descWithKey', { key: formatHotkeyLabel(hotkeyBinding) })
|
||||||
: t('onboarding.done.descNoKey')}
|
: t('onboarding.done.descNoKey')}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Button variant="contained" onClick={handleFinish} fullWidth>
|
<Button variant="contained" onClick={handleFinish} fullWidth>
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ import CheckCircleIcon from '@mui/icons-material/CheckCircle'
|
||||||
import CancelIcon from '@mui/icons-material/Cancel'
|
import CancelIcon from '@mui/icons-material/Cancel'
|
||||||
import { d3roPalette, d3roFontMono, d3roShadow } from '@d3ro/ui/theme'
|
import { d3roPalette, d3roFontMono, d3roShadow } from '@d3ro/ui/theme'
|
||||||
import { HotkeyRecordModal } from './HotkeyRecordModal'
|
import { HotkeyRecordModal } from './HotkeyRecordModal'
|
||||||
|
import { formatHotkeyLabel, formatHotkeySegments } from '../utils/format-hotkey'
|
||||||
import { LicenseTab } from './LicenseTab'
|
import { LicenseTab } from './LicenseTab'
|
||||||
import { CloudSyncSection } from './CloudSyncSection'
|
import { CloudSyncSection } from './CloudSyncSection'
|
||||||
import { useI18n, LOCALE_META } from '@d3ro/i18n'
|
import { useI18n, LOCALE_META } from '@d3ro/i18n'
|
||||||
|
|
@ -76,9 +77,9 @@ function HotkeyDisplay({
|
||||||
</Typography>
|
</Typography>
|
||||||
{binding ? (
|
{binding ? (
|
||||||
<Stack direction="row" spacing={0.5} alignItems="center">
|
<Stack direction="row" spacing={0.5} alignItems="center">
|
||||||
{binding.displayLabel.split(' + ').map((key) => (
|
{formatHotkeySegments(binding).map((key, idx) => (
|
||||||
<Chip
|
<Chip
|
||||||
key={key}
|
key={`${key}-${idx}`}
|
||||||
label={key}
|
label={key}
|
||||||
size="small"
|
size="small"
|
||||||
sx={{
|
sx={{
|
||||||
|
|
@ -407,7 +408,7 @@ export function SettingsModal({ open, onClose }: SettingsModalProps): React.Reac
|
||||||
title={t('settings.agent')}
|
title={t('settings.agent')}
|
||||||
description={
|
description={
|
||||||
dictationBinding
|
dictationBinding
|
||||||
? t('settings.agent.descWithKey', { key: dictationBinding.displayLabel })
|
? t('settings.agent.descWithKey', { key: formatHotkeyLabel(dictationBinding) })
|
||||||
: t('settings.agent.descNoKey')
|
: t('settings.agent.descNoKey')
|
||||||
}
|
}
|
||||||
enabled={dictationEnabled}
|
enabled={dictationEnabled}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import { EmptyStateCard, HistoryEntryCard } from '../components/shared'
|
||||||
import { d3roPalette, d3roTypo } from '@d3ro/ui/theme'
|
import { d3roPalette, d3roTypo } from '@d3ro/ui/theme'
|
||||||
import { useI18n } from '@d3ro/i18n'
|
import { useI18n } from '@d3ro/i18n'
|
||||||
import { formatRecordingTime, formatRecordingTimeUnit, formatNumber, getDateKey } from '../utils/formatters'
|
import { formatRecordingTime, formatRecordingTimeUnit, formatNumber, getDateKey } from '../utils/formatters'
|
||||||
|
import { formatHotkeyLabel } from '../utils/format-hotkey'
|
||||||
import { FileDropZone } from '../components/FileDropZone'
|
import { FileDropZone } from '../components/FileDropZone'
|
||||||
import type { StatsSummary, HistoryEntry, HotkeyBinding, CaptionState, LicenseTier, UsageQuota } from '@d3ro/core/types'
|
import type { StatsSummary, HistoryEntry, HotkeyBinding, CaptionState, LicenseTier, UsageQuota } from '@d3ro/core/types'
|
||||||
|
|
||||||
|
|
@ -136,7 +137,7 @@ export function DashboardPage(): React.ReactElement {
|
||||||
</Box>
|
</Box>
|
||||||
<PhosphorText variant="label" sx={{ mt: 0.5, color: d3roPalette.text.dimLabel }}>
|
<PhosphorText variant="label" sx={{ mt: 0.5, color: d3roPalette.text.dimLabel }}>
|
||||||
{dictationBinding
|
{dictationBinding
|
||||||
? t('dashboard.pressToRecord', { key: dictationBinding.displayLabel.toUpperCase() }).toUpperCase()
|
? t('dashboard.pressToRecord', { key: formatHotkeyLabel(dictationBinding).toUpperCase() }).toUpperCase()
|
||||||
: t('dashboard.hotkeyNotSet').toUpperCase()}
|
: t('dashboard.hotkeyNotSet').toUpperCase()}
|
||||||
</PhosphorText>
|
</PhosphorText>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
119
apps/desktop/src/renderer/utils/format-hotkey.ts
Normal file
119
apps/desktop/src/renderer/utils/format-hotkey.ts
Normal file
|
|
@ -0,0 +1,119 @@
|
||||||
|
// apps/desktop/src/renderer/utils/format-hotkey.ts
|
||||||
|
// HotkeyBinding → 플랫폼별 표시 라벨 (segments + 결합 문자열).
|
||||||
|
// macOS: ⌘/⌥/⌃/⇧ + key, Windows/Linux: Ctrl/Alt/Win/Shift + key.
|
||||||
|
//
|
||||||
|
// 기존 binding.displayLabel은 키 녹화 시 만들어진 정적 문자열이라
|
||||||
|
// 플랫폼이 다르면 잘못 보임 — 이 헬퍼가 단일 진입점.
|
||||||
|
|
||||||
|
import type { HotkeyBinding } from '@d3ro/core/types'
|
||||||
|
|
||||||
|
type Platform = 'darwin' | 'win32' | 'linux'
|
||||||
|
|
||||||
|
function getPlatform(): Platform {
|
||||||
|
const p = (window as { electronAPI?: { platform?: string } }).electronAPI?.platform
|
||||||
|
if (p === 'darwin' || p === 'win32' || p === 'linux') return p
|
||||||
|
// 폴백: SSR/테스트 환경에서는 navigator.platform로 추정.
|
||||||
|
if (typeof navigator !== 'undefined' && /Mac/i.test(navigator.platform)) return 'darwin'
|
||||||
|
return 'win32'
|
||||||
|
}
|
||||||
|
|
||||||
|
// keyCode → 키명. Windows VK_* 기준 + 일부 macOS 분기.
|
||||||
|
function keyCodeToName(keyCode: number, platform: Platform): string {
|
||||||
|
// Modifier 표기 — 플랫폼별
|
||||||
|
if (platform === 'darwin') {
|
||||||
|
if (keyCode === 16 || keyCode === 160 || keyCode === 161) return '⇧'
|
||||||
|
if (keyCode === 17 || keyCode === 162 || keyCode === 163) return '⌃'
|
||||||
|
if (keyCode === 18 || keyCode === 164 || keyCode === 165) return '⌥'
|
||||||
|
if (keyCode === 91 || keyCode === 92) return '⌘'
|
||||||
|
} else {
|
||||||
|
if (keyCode === 16 || keyCode === 160 || keyCode === 161) return 'Shift'
|
||||||
|
if (keyCode === 17 || keyCode === 162 || keyCode === 163) return 'Ctrl'
|
||||||
|
if (keyCode === 18 || keyCode === 164 || keyCode === 165) return 'Alt'
|
||||||
|
if (keyCode === 91 || keyCode === 92) return 'Win'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 공통 키
|
||||||
|
switch (keyCode) {
|
||||||
|
case 8: return 'Backspace'
|
||||||
|
case 9: return 'Tab'
|
||||||
|
case 13: return platform === 'darwin' ? '↩' : 'Enter'
|
||||||
|
case 19: return 'Pause'
|
||||||
|
case 20: return 'CapsLock'
|
||||||
|
case 27: return 'Esc'
|
||||||
|
case 32: return 'Space'
|
||||||
|
case 33: return 'PgUp'
|
||||||
|
case 34: return 'PgDn'
|
||||||
|
case 35: return 'End'
|
||||||
|
case 36: return 'Home'
|
||||||
|
case 37: return '←'
|
||||||
|
case 38: return '↑'
|
||||||
|
case 39: return '→'
|
||||||
|
case 40: return '↓'
|
||||||
|
case 45: return 'Insert'
|
||||||
|
case 46: return platform === 'darwin' ? '⌫' : 'Delete'
|
||||||
|
case 186: return ';'
|
||||||
|
case 187: return '='
|
||||||
|
case 188: return ','
|
||||||
|
case 189: return '-'
|
||||||
|
case 190: return '.'
|
||||||
|
case 191: return '/'
|
||||||
|
case 192: return '`'
|
||||||
|
case 219: return '['
|
||||||
|
case 220: return '\\'
|
||||||
|
case 221: return ']'
|
||||||
|
case 222: return "'"
|
||||||
|
default: break
|
||||||
|
}
|
||||||
|
|
||||||
|
// F1~F24
|
||||||
|
if (keyCode >= 112 && keyCode <= 135) return `F${keyCode - 111}`
|
||||||
|
// Numpad
|
||||||
|
if (keyCode >= 96 && keyCode <= 105) return `Num${keyCode - 96}`
|
||||||
|
// 알파벳/숫자
|
||||||
|
if ((keyCode >= 48 && keyCode <= 57) || (keyCode >= 65 && keyCode <= 90)) {
|
||||||
|
return String.fromCharCode(keyCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
return `Key${keyCode}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* binding을 플랫폼별 segment 배열로 변환.
|
||||||
|
* 예) Mac: ['⌘', '⇧', '1'], Win: ['Win', 'Shift', '1']
|
||||||
|
*
|
||||||
|
* 순서:
|
||||||
|
* - macOS: ⌃ ⌥ ⇧ ⌘ + key (Apple 표준)
|
||||||
|
* - Win/Linux: Ctrl Win Alt Shift + key
|
||||||
|
*/
|
||||||
|
export function formatHotkeySegments(binding: HotkeyBinding | null): string[] {
|
||||||
|
if (!binding) return []
|
||||||
|
const platform = getPlatform()
|
||||||
|
const segments: string[] = []
|
||||||
|
|
||||||
|
if (platform === 'darwin') {
|
||||||
|
if (binding.ctrl) segments.push('⌃')
|
||||||
|
if (binding.alt) segments.push('⌥')
|
||||||
|
if (binding.shift) segments.push('⇧')
|
||||||
|
if (binding.meta) segments.push('⌘')
|
||||||
|
} else {
|
||||||
|
if (binding.ctrl) segments.push('Ctrl')
|
||||||
|
if (binding.meta) segments.push('Win')
|
||||||
|
if (binding.alt) segments.push('Alt')
|
||||||
|
if (binding.shift) segments.push('Shift')
|
||||||
|
}
|
||||||
|
|
||||||
|
segments.push(keyCodeToName(binding.keyCode, platform))
|
||||||
|
return segments
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 사람이 읽기 위한 단일 문자열 형태.
|
||||||
|
* macOS: '⌘⇧1' (구분자 없음 — Apple 표준)
|
||||||
|
* Win/Linux: 'Ctrl + Shift + 1'
|
||||||
|
*/
|
||||||
|
export function formatHotkeyLabel(binding: HotkeyBinding | null): string {
|
||||||
|
const segments = formatHotkeySegments(binding)
|
||||||
|
if (segments.length === 0) return ''
|
||||||
|
const platform = getPlatform()
|
||||||
|
return platform === 'darwin' ? segments.join('') : segments.join(' + ')
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue