diff --git a/src/components/App.tsx b/src/components/App.tsx index 57fa7788..a42f685f 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -45,7 +45,6 @@ const App = observer(() => { isPrefsOpen, isShortcutsOpen, isExplorer, - activeView, } = appState const cache = appState.getActiveCache() @@ -79,6 +78,7 @@ const App = observer(() => { filesLength: activeCache.files.length, clipboardLength: appState.clipboard.files.length, activeViewId: activeView.viewId, + viewMode: activeView.getVisibleCache().viewmode, // missing: about opened, tab: is it needed? } }, [appState]) diff --git a/src/components/dialogs/ShortcutsDialog.tsx b/src/components/dialogs/ShortcutsDialog.tsx index b874d256..e2acf1ed 100644 --- a/src/components/dialogs/ShortcutsDialog.tsx +++ b/src/components/dialogs/ShortcutsDialog.tsx @@ -1,10 +1,11 @@ -import * as React from 'react' +import React, { useEffect, useState } from 'react' import { Dialog, Classes, Button, KeyCombo, InputGroup, Callout } from '@blueprintjs/core' import type { TFunction } from 'i18next' import { useTranslation } from 'react-i18next' import { isMac } from '$src/utils/platform' import CONFIG from '$src/config/appConfig' +import { getKeyboardLayoutMap } from '$src/utils/keyboard' interface ShortcutsProps { isOpen: boolean @@ -16,49 +17,59 @@ interface Combo { label: string } -export const buildShortcuts = (t: TFunction<'translation', undefined>) => ({ - [t('SHORTCUT.GROUP.GLOBAL')]: [ - { combo: 'alt + mod + l', label: t('SHORTCUT.MAIN.DOWNLOADS_TAB') }, - { combo: 'alt + mod + e', label: t('SHORTCUT.MAIN.EXPLORER_TAB') }, - { combo: 'ctrl + shift + right', label: t('SHORTCUT.MAIN.NEXT_VIEW') }, - { combo: 'ctrl + shift + left', label: t('SHORTCUT.MAIN.PREVIOUS_VIEW') }, - { combo: 'mod + r', label: t('SHORTCUT.MAIN.RELOAD_VIEW') }, - { combo: 'escape', label: t('SHORTCUT.LOG.TOGGLE') }, - { combo: 'mod + s', label: t('SHORTCUT.MAIN.KEYBOARD_SHORTCUTS') }, - { combo: 'mod + ,', label: t('SHORTCUT.MAIN.PREFERENCES') }, - { combo: 'alt + mod + i', label: t('SHORTCUT.OPEN_DEVTOOLS') }, - { combo: 'mod + q', label: t('SHORTCUT.MAIN.QUIT') }, - { combo: 'mod + alt + shift + v', label: t('NAV.SPLITVIEW') }, - ], - [t('SHORTCUT.GROUP.ACTIVE_VIEW')]: [ - { combo: 'space', label: t('SHORTCUT.ACTIVE_VIEW.OPEN_PREVIEW') }, - { combo: (isMac && 'mod + left') || 'alt + left', label: t('SHORTCUT.ACTIVE_VIEW.BACKWARD_HISTORY') }, - { combo: (isMac && 'mod + right') || 'alt + right', label: t('SHORTCUT.ACTIVE_VIEW.FORWARD_HISTORY') }, - { combo: 'meta + c', label: t('SHORTCUT.ACTIVE_VIEW.COPY') }, - { combo: 'meta + v', label: t('SHORTCUT.ACTIVE_VIEW.PASTE') }, - { combo: 'mod + shift + c', label: t('SHORTCUT.ACTIVE_VIEW.COPY_PATH') }, - { combo: 'mod + shift + n', label: t('SHORTCUT.ACTIVE_VIEW.COPY_FILENAME') }, - { combo: 'mod + o', label: t('SHORTCUT.ACTIVE_VIEW.OPEN_FILE') }, - { - combo: isMac ? 'mod + alt + o' : 'mod + shift + o', - label: t('SHORTCUT.ACTIVE_VIEW.OPEN_FILE_INACTIVE_VIEW'), - }, - { combo: 'mod + a', label: t('SHORTCUT.ACTIVE_VIEW.SELECT_ALL') }, - { combo: 'mod + i', label: t('SHORTCUT.ACTIVE_VIEW.SELECT_INVERT') }, - { combo: 'mod + l', label: t('SHORTCUT.ACTIVE_VIEW.FOCUS_PATH') }, - { combo: 'mod + n', label: t('COMMON.MAKEDIR') }, - { combo: 'mod + d', label: t('SHORTCUT.ACTIVE_VIEW.DELETE') }, - { combo: 'mod + k', label: t('SHORTCUT.ACTIVE_VIEW.OPEN_TERMINAL') }, - { combo: 'backspace', label: t('SHORTCUT.ACTIVE_VIEW.PARENT_DIRECTORY') }, - { combo: 'mod + u', label: t('APP_MENUS.TOGGLE_HIDDEN_FILES') }, - ], - [t('SHORTCUT.GROUP.TABS')]: [ - { combo: 'ctrl + tab', label: t('APP_MENUS.SELECT_NEXT_TAB') }, - { combo: 'ctrl + shift + tab', label: t('APP_MENUS.SELECT_PREVIOUS_TAB') }, - { combo: 'mod + t', label: t('APP_MENUS.NEW_TAB') }, - { combo: 'mod + w', label: t('SHORTCUT.TABS.CLOSE_ACTIVE_TAB') }, - ], -}) +interface Shortcuts { + [key: string]: Combo[] +} + +export const buildShortcuts = async (t: TFunction<'translation', undefined>): Promise => { + const keyboardLayoutMap = await getKeyboardLayoutMap() + + return { + [t('SHORTCUT.GROUP.GLOBAL')]: [ + { combo: 'alt + mod + l', label: t('SHORTCUT.MAIN.DOWNLOADS_TAB') }, + { combo: 'alt + mod + e', label: t('SHORTCUT.MAIN.EXPLORER_TAB') }, + { combo: 'ctrl + shift + right', label: t('SHORTCUT.MAIN.NEXT_VIEW') }, + { combo: 'ctrl + shift + left', label: t('SHORTCUT.MAIN.PREVIOUS_VIEW') }, + { combo: 'mod + r', label: t('SHORTCUT.MAIN.RELOAD_VIEW') }, + { combo: 'escape', label: t('SHORTCUT.LOG.TOGGLE') }, + { combo: 'mod + s', label: t('SHORTCUT.MAIN.KEYBOARD_SHORTCUTS') }, + { combo: 'mod + ,', label: t('SHORTCUT.MAIN.PREFERENCES') }, + { combo: 'alt + mod + i', label: t('SHORTCUT.OPEN_DEVTOOLS') }, + { combo: 'mod + q', label: t('SHORTCUT.MAIN.QUIT') }, + { combo: 'mod + alt + shift + v', label: t('NAV.SPLITVIEW') }, + ], + [t('SHORTCUT.GROUP.ACTIVE_VIEW')]: [ + { combo: `mod + ${keyboardLayoutMap['Digit1']}`, label: t('SHORTCUT.ACTIVE_VIEW.ICON_MODE') }, + { combo: `mod + ${keyboardLayoutMap['Digit2']}`, label: t('SHORTCUT.ACTIVE_VIEW.TABLE_MODE') }, + { combo: 'space', label: t('SHORTCUT.ACTIVE_VIEW.OPEN_PREVIEW') }, + { combo: (isMac && 'mod + left') || 'alt + left', label: t('SHORTCUT.ACTIVE_VIEW.BACKWARD_HISTORY') }, + { combo: (isMac && 'mod + right') || 'alt + right', label: t('SHORTCUT.ACTIVE_VIEW.FORWARD_HISTORY') }, + { combo: 'meta + c', label: t('SHORTCUT.ACTIVE_VIEW.COPY') }, + { combo: 'meta + v', label: t('SHORTCUT.ACTIVE_VIEW.PASTE') }, + { combo: 'mod + shift + c', label: t('SHORTCUT.ACTIVE_VIEW.COPY_PATH') }, + { combo: 'mod + shift + n', label: t('SHORTCUT.ACTIVE_VIEW.COPY_FILENAME') }, + { combo: 'mod + o', label: t('SHORTCUT.ACTIVE_VIEW.OPEN_FILE') }, + { + combo: isMac ? 'mod + alt + o' : 'mod + shift + o', + label: t('SHORTCUT.ACTIVE_VIEW.OPEN_FILE_INACTIVE_VIEW'), + }, + { combo: 'mod + a', label: t('SHORTCUT.ACTIVE_VIEW.SELECT_ALL') }, + { combo: 'mod + i', label: t('SHORTCUT.ACTIVE_VIEW.SELECT_INVERT') }, + { combo: 'mod + l', label: t('SHORTCUT.ACTIVE_VIEW.FOCUS_PATH') }, + { combo: 'mod + n', label: t('COMMON.MAKEDIR') }, + { combo: 'mod + d', label: t('SHORTCUT.ACTIVE_VIEW.DELETE') }, + { combo: 'mod + k', label: t('SHORTCUT.ACTIVE_VIEW.OPEN_TERMINAL') }, + { combo: 'backspace', label: t('SHORTCUT.ACTIVE_VIEW.PARENT_DIRECTORY') }, + { combo: 'mod + u', label: t('APP_MENUS.TOGGLE_HIDDEN_FILES') }, + ], + [t('SHORTCUT.GROUP.TABS')]: [ + { combo: 'ctrl + tab', label: t('APP_MENUS.SELECT_NEXT_TAB') }, + { combo: 'ctrl + shift + tab', label: t('APP_MENUS.SELECT_PREVIOUS_TAB') }, + { combo: 'mod + t', label: t('APP_MENUS.NEW_TAB') }, + { combo: 'mod + w', label: t('SHORTCUT.TABS.CLOSE_ACTIVE_TAB') }, + ], + } +} const renderShortcuts = (shortcuts: Combo[]) => shortcuts.map((shortcut) => ( @@ -72,20 +83,30 @@ const renderTitle = (title: string) =>

{title} { const { t, i18n } = useTranslation() - const [shortcutsList, setShortcutsList] = React.useState(() => buildShortcuts(t)) - const [filter, setFilter] = React.useState('') - const labels = Object.keys(shortcutsList) - const shortcuts: { [x: string]: Combo[] } = {} + const [shortcutsList, setShortcutsList] = useState({}) + const [filter, setFilter] = useState('') + const sections = Object.keys(shortcutsList) const regex = new RegExp(filter, 'i') - for (const label of labels) { - shortcuts[label] = shortcutsList[label].filter((shortcut) => shortcut.label.match(regex)) + const visibleShortcuts: Shortcuts = {} + for (const section of sections) { + visibleShortcuts[section] = shortcutsList[section].filter((shortcut) => shortcut.label.match(regex)) } - const isEmpty = labels.every((label) => shortcuts[label].length === 0) - React.useEffect(() => { - setShortcutsList(() => buildShortcuts(t)) + const isEmpty = sections.every((label) => visibleShortcuts[label].length === 0) + + useEffect(() => { + ;(async () => { + const shortcutsList = await buildShortcuts(t) + setShortcutsList(shortcutsList) + })() }, [i18n.language]) + // useEffect(() => { + // (async () => { + // () => buildShortcuts(t) + // })() + // }, []) + return ( { {t('DIALOG.SHORTCUTS.NO_RESULTS')} ) : ( <> - {labels.map((label) => - shortcuts[label].length ? ( + {sections.map((label) => + visibleShortcuts[label].length ? ( {renderTitle(label)} - {renderShortcuts(shortcuts[label])} + {renderShortcuts(visibleShortcuts[label])} ) : null, )} diff --git a/src/components/shortcuts/MenuAccelerators.tsx b/src/components/shortcuts/MenuAccelerators.tsx index deb4a638..ce5cafd1 100644 --- a/src/components/shortcuts/MenuAccelerators.tsx +++ b/src/components/shortcuts/MenuAccelerators.tsx @@ -183,6 +183,16 @@ class MenuAcceleratorsClass extends React.Component { this.appState.startEditingFile(this.getActiveFileCache()) } + onToggleIconViewMode = (): void => { + const cache = this.getActiveFileCache() + cache.viewmode !== 'icons' && cache.setViewMode('icons') + } + + onToggleTableViewMode = (): void => { + const cache = this.getActiveFileCache() + cache.viewmode !== 'details' && cache.setViewMode('details') + } + renderMenuAccelerators(): React.ReactElement { return ( @@ -203,6 +213,8 @@ class MenuAcceleratorsClass extends React.Component { + + ) } diff --git a/src/electron/appMenus.ts b/src/electron/appMenus.ts index fa777182..9f5deb07 100644 --- a/src/electron/appMenus.ts +++ b/src/electron/appMenus.ts @@ -1,7 +1,7 @@ import { clipboard, Menu, BrowserWindow, MenuItemConstructorOptions, MenuItem, app, ipcMain, dialog } from 'electron' import { isMac, isLinux, VERSIONS } from '$src/electron/osSupport' -import { ReactiveProperties } from '$src/types' +import { KeyboardLayoutMap, ReactiveProperties } from '$src/types' const ACCELERATOR_EVENT = 'menu_accelerator' @@ -17,7 +17,6 @@ export class AppMenu { // eslint-disable-next-line @typescript-eslint/no-explicit-any sendComboEvent = (menuItem: MenuItem & { accelerator: string }) => { const accel = menuItem.accelerator || '' - console.log('sending', menuItem.label, accel) this.win.webContents.send(ACCELERATOR_EVENT, Object.assign({ combo: accel, data: undefined })) } @@ -95,10 +94,13 @@ export class AppMenu { clipboardLength, filesLength, status, + viewMode, }: ReactiveProperties): MenuItemConstructorOptions[] { const menuStrings = this.menuStrings const explorerWithoutOverlay = !isOverlayOpen && isExplorer const explorerWithoutOverlayCanWrite = explorerWithoutOverlay && !isReadonly && status === 'ok' + const isIconViewMode = viewMode === 'icons' + let windowMenuIndex = 4 const template = [ @@ -196,6 +198,23 @@ export class AppMenu { { label: menuStrings['TITLE_VIEW'], submenu: [ + { + type: 'checkbox', + label: menuStrings['TOGGLE_ICONVIEW_MODE'], + accelerator: 'CmdOrCtrl+1', + click: this.sendComboEvent, + enabled: explorerWithoutOverlay, + checked: isIconViewMode, + }, + { + type: 'checkbox', + label: menuStrings['TOGGLE_TABLEVIEW_MODE'], + accelerator: 'CmdOrCtrl+2', + click: this.sendComboEvent, + enabled: explorerWithoutOverlay, + checked: !isIconViewMode, + }, + { type: 'separator' }, { label: menuStrings['TOGGLE_SPLITVIEW'], accelerator: 'CmdOrCtrl+Shift+Alt+V', diff --git a/src/electron/main.ts b/src/electron/main.ts index 4d8f8300..42855a72 100644 --- a/src/electron/main.ts +++ b/src/electron/main.ts @@ -7,7 +7,7 @@ import { AppMenu } from '$src/electron/appMenus' import { isLinux } from '$src/electron/osSupport' import { WindowSettings } from '$src/electron//windowSettings' import { Remote } from '$src/electron/remote' -import { ReactiveProperties } from '$src/types' +import { KeyboardLayoutMap, ReactiveProperties } from '$src/types' const ENV_E2E = !!process.env.E2E const HTML_PATH = `${__dirname}/index.html` @@ -187,13 +187,21 @@ const ElectronApp = { }) }) - ipcMain.handle('updateMenus', (e: Event, strings: Record, props: ReactiveProperties) => { - if (this.appMenu) { - this.appMenu.createMenu(strings, props) - } else { - console.log('languageChanged but app not ready :(') - } - }) + ipcMain.handle( + 'updateMenus', + ( + e: Event, + strings: Record, + props: ReactiveProperties, + keyboardLayoutMap: KeyboardLayoutMap, + ) => { + if (this.appMenu) { + this.appMenu.createMenu(strings, props, keyboardLayoutMap) + } else { + console.log('languageChanged but app not ready :(') + } + }, + ) ipcMain.handle('selectAll', () => { if (this.mainWindow) { diff --git a/src/events/index.ts b/src/events/index.ts index dabfd240..647778ca 100644 --- a/src/events/index.ts +++ b/src/events/index.ts @@ -1,5 +1,6 @@ -import { ReactiveProperties } from '$src/types' +import { KeyboardLayoutMap, ReactiveProperties } from '$src/types' import { ipcRenderer } from 'electron' -export const triggerUpdateMenus = (strings: Record, props: ReactiveProperties) => +export const triggerUpdateMenus = async (strings: Record, props: ReactiveProperties) => { ipcRenderer.invoke('updateMenus', strings, props) +} diff --git a/src/locale/lang/en.json b/src/locale/lang/en.json index 56bba47c..bcc6ec60 100644 --- a/src/locale/lang/en.json +++ b/src/locale/lang/en.json @@ -96,6 +96,8 @@ "CLOSE_ACTIVE_TAB": "Close active tab" }, "ACTIVE_VIEW": { + "TABLE_MODE": "Table view", + "ICON_MODE": "Icons view", "COPY": "Copy selected items to clipboard", "PASTE": "Paste selected items into current folder", "VIEW_HISTORY": "Show nav history (debug)", @@ -266,7 +268,9 @@ "GO_PARENT": "Parent folder", "GO_BACK": "Back", "GO_FORWARD": "Forward", - "TOGGLE_HIDDEN_FILES": "Show/Hide Hidden Files" + "TOGGLE_HIDDEN_FILES": "Show/Hide Hidden Files", + "TOGGLE_ICONVIEW_MODE": "as Icons", + "TOGGLE_TABLEVIEW_MODE": "as List" } } } \ No newline at end of file diff --git a/src/locale/lang/fr.json b/src/locale/lang/fr.json index 42e914ce..03cf7d6b 100644 --- a/src/locale/lang/fr.json +++ b/src/locale/lang/fr.json @@ -96,6 +96,8 @@ "CLOSE_ACTIVE_TAB": "Fermer l'onglet actif" }, "ACTIVE_VIEW": { + "TABLE_MODE": "Vue liste", + "ICON_MODE": "Vue icônes", "COPY": "Copier les éléments sélectionnés dans le presse-papier", "PASTE": "Coller les éléments sélectionnés dans le presse-papier", "VIEW_HISTORY": "Afficher l'historique de navigation (debug)", @@ -252,7 +254,7 @@ "PASTE": "Coller", "RELOAD_VIEW": "Recharger Vue Active", "FORCE_RELOAD_APP": "Recharger l'App", - "KEYBOARD_SHORTCUTS": "List des Raccourcis", + "KEYBOARD_SHORTCUTS": "Liste des Raccourcis", "ABOUT_TITLE": "React-Explorer", "ABOUT_CONTENT": "Version: ${version}\nCommit: ${hash}\nDate: ${date}\nElectron: ${electron}\nChrome: ${chrome}\nNode: ${node}\nSE: ${platform} ${arch} ${release}", "ZOOM": "Zoom", @@ -266,7 +268,9 @@ "GO_PARENT": "Dossier parent", "GO_BACK": "Précédent", "GO_FORWARD": "Suivant", - "TOGGLE_HIDDEN_FILES": "Voir/Cacher Fichiers Cachés" + "TOGGLE_HIDDEN_FILES": "Voir/Cacher Fichiers Cachés", + "TOGGLE_ICONVIEW_MODE": "Par icônes", + "TOGGLE_TABLEVIEW_MODE": "Par liste" } } } \ No newline at end of file diff --git a/src/types/index.ts b/src/types/index.ts index 24c0b342..ee1656ec 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,3 +1,4 @@ +import { ViewModeName } from '$src/hooks/useViewMode' import { FileDescriptor } from '$src/services/Fs' import { FileState, TStatus } from '$src/state/fileState' import { IconName } from '@blueprintjs/icons' @@ -49,4 +50,7 @@ export interface ReactiveProperties { filesLength: number status: TStatus language: string + viewMode: ViewModeName } + +export type KeyboardLayoutMap = Record diff --git a/src/typings/index.d.ts b/src/typings/index.d.ts index 38ec6feb..4d1714d6 100644 --- a/src/typings/index.d.ts +++ b/src/typings/index.d.ts @@ -3,6 +3,15 @@ import { SettingsState } from '$src/state/settingsState' export {} +interface KeyboardIterator extends Iterator<[string, string]> { + length: number + [key: number]: [string, string] +} + +interface KeyboardMap { + entries: () => KeyboardIterator +} + declare global { interface Window { // debug @@ -18,4 +27,10 @@ declare global { BUILD_DATE: string } } + + interface Navigator { + keyboard: { + getLayoutMap: () => Promise + } + } } diff --git a/src/utils/keyboard.ts b/src/utils/keyboard.ts index b8d46e1c..a38a14b4 100644 --- a/src/utils/keyboard.ts +++ b/src/utils/keyboard.ts @@ -1,3 +1,4 @@ +import { KeyboardLayoutMap } from '$src/types' import { ipcRenderer } from 'electron' export const ACCELERATOR_EVENT = 'menu_accelerator' @@ -7,3 +8,11 @@ export async function sendFakeCombo(combo: string, data?: any): Promise { const id = await ipcRenderer.invoke('window:getId') ipcRenderer.sendTo(id, ACCELERATOR_EVENT, Object.assign({ combo: combo, data })) } + +export async function getKeyboardLayoutMap() { + const map = await navigator.keyboard.getLayoutMap() + return Array.from(map.entries()).reduce((acc, [key, value]) => { + acc[key] = value + return acc + }, {} as KeyboardLayoutMap) +}