1
0
Эх сурвалжийг харах

feat: add shortcuts setting

Ahmad Kholid 3 жил өмнө
parent
commit
dffff9d78c

+ 3 - 29
src/components/newtab/workflow/edit/EditTrigger.vue

@@ -202,6 +202,7 @@ import { useI18n } from 'vue-i18n';
 import { useToast } from 'vue-toastification';
 import dayjs from 'dayjs';
 import { isObject } from '@/utils/helper';
+import recordShortcut from '@/utils/record-shortcut';
 
 const props = defineProps({
   data: {
@@ -234,17 +235,6 @@ const days = {
 };
 const maxDate = dayjs().add(30, 'day').format('YYYY-MM-DD');
 const minDate = dayjs().format('YYYY-MM-DD');
-const allowedKeys = {
-  '+': 'plus',
-  Delete: 'del',
-  Insert: 'ins',
-  ArrowDown: 'down',
-  ArrowLeft: 'left',
-  ArrowUp: 'up',
-  ArrowRight: 'right',
-  Escape: 'escape',
-  Enter: 'enter',
-};
 
 const recordKeys = reactive({
   isRecording: false,
@@ -312,26 +302,10 @@ function addTime() {
   });
 }
 function handleKeydownEvent(event) {
-  event.preventDefault();
-  event.stopPropagation();
-
-  if (event.repeat) return;
-
-  const keys = [];
-  const { ctrlKey, altKey, metaKey, shiftKey, key } = event;
-
-  if (ctrlKey || metaKey) keys.push('mod');
-  if (altKey) keys.push('option');
-  if (shiftKey) keys.push('shift');
-
-  const isValidKey = !!allowedKeys[key] || /^[a-z0-9,./;'[\]\-=`]$/i.test(key);
-
-  if (isValidKey) {
-    keys.push(allowedKeys[key] || key.toLowerCase());
-
+  recordShortcut(event, (keys) => {
     recordKeys.keys = keys.join('+');
     updateData({ shortcut: recordKeys.keys });
-  }
+  });
 }
 function toggleRecordKeys() {
   if (recordKeys.isRecording) {

+ 6 - 2
src/composable/shortcut.js

@@ -1,8 +1,9 @@
 import { onUnmounted, onMounted } from 'vue';
+import defu from 'defu';
 import Mousetrap from 'mousetrap';
-import { isObject } from '@/utils/helper';
+import { isObject, parseJSON } from '@/utils/helper';
 
-export const mapShortcuts = {
+const defaultShortcut = {
   'page:dashboard': {
     id: 'page:dashboard',
     combo: 'option+1',
@@ -48,6 +49,9 @@ export const mapShortcuts = {
     combo: 'mod+[',
   },
 };
+const customShortcut = parseJSON(localStorage.getItem('shortcuts', {})) || {};
+
+export const mapShortcuts = defu(customShortcut, defaultShortcut);
 
 const os = navigator.appVersion.indexOf('Win') !== -1 ? 'win' : 'mac';
 export function getReadableShortcut(str) {

+ 2 - 0
src/lib/v-remixicon.js

@@ -4,6 +4,7 @@ import {
   riH2,
   riLinkM,
   riLock2Line,
+  riKeyboardLine,
   riLinkUnlinkM,
   riFileEditLine,
   riBold,
@@ -100,6 +101,7 @@ export const icons = {
   riH2,
   riLinkM,
   riLock2Line,
+  riKeyboardLine,
   riLinkUnlinkM,
   riFileEditLine,
   riBold,

+ 1 - 1
src/locales/en/blocks.json

@@ -69,7 +69,7 @@
         "useRegex": "Use regex",
         "shortcut": {
           "tooltip": "Record shortcut",
-          "stopRecord": "Stop record",
+          "stopRecord": "Stop recording",
           "checkboxTitle": "Execute shortcut even when you're in an input element",
           "checkbox": "Active while in input",
           "note": "Note: keyboard shortcut only working when you're on a webpage"

+ 4 - 0
src/locales/en/newtab.json

@@ -18,6 +18,9 @@
   },
   "settings": {
     "theme": "Theme",
+    "shortcuts": {
+      "duplicate": "Shortcut already use by \"{name}\""
+    },
     "language": {
       "label": "Language",
       "helpTranslate": "Can't find your language? Help translate.",
@@ -25,6 +28,7 @@
     },
     "menu": {
       "general": "General",
+      "shortcuts": "Shortcuts",
       "about": "About"
     },
     "backupWorkflows": {

+ 1 - 0
src/newtab/pages/Settings.vue

@@ -38,6 +38,7 @@ const { t } = useI18n();
 
 const menus = [
   { id: 'general', path: '/settings', icon: 'riSettings3Line' },
+  { id: 'shortcuts', path: '/shortcuts', icon: 'riKeyboardLine' },
   { id: 'about', path: '/about', icon: 'riInformationLine' },
 ];
 </script>

+ 150 - 0
src/newtab/pages/settings/Shortcuts.vue

@@ -0,0 +1,150 @@
+<template>
+  <p v-if="recording.isChanged" class="text-gray-600 dark:text-gray-200 mb-4">
+    {{ t('settings.language.reloadPage') }}
+  </p>
+  <div
+    v-for="(items, category) in shortcutsCats"
+    :key="category"
+    class="mb-8 border p-4 rounded-lg dark:border-gray-800 border-gray-200"
+  >
+    <p class="font-semibold mb-2 capitalize">{{ category }}</p>
+    <ui-list class="space-y-1 text-gray-600 dark:text-gray-200">
+      <ui-list-item
+        v-for="shortcut in items"
+        :key="shortcut.id"
+        class="group h-12"
+      >
+        <p class="flex-1 mr-4 capitalize">
+          {{ shortcut.name }}
+        </p>
+        <template v-if="recording.id === shortcut.id">
+          <kbd v-for="key in recording.keys" :key="key">
+            {{ key }}
+          </kbd>
+          <button
+            v-tooltip="t('common.cancel')"
+            class="mr-2 ml-4"
+            @click="cleanUp"
+          >
+            <v-remixicon name="riCloseLine" />
+          </button>
+          <button
+            v-tooltip="t('workflow.blocks.trigger.shortcut.stopRecord')"
+            @click="stopRecording"
+          >
+            <v-remixicon name="riStopLine" />
+          </button>
+        </template>
+        <template v-else>
+          <button
+            v-tooltip="t('workflow.blocks.trigger.shortcut.tooltip')"
+            class="group-hover:visible invisible"
+            @click="startRecording(shortcut)"
+          >
+            <v-remixicon name="riRecordCircleLine" />
+          </button>
+          <kbd v-for="key in shortcut.keys" :key="key">
+            {{ key }}
+          </kbd>
+        </template>
+      </ui-list-item>
+    </ui-list>
+  </div>
+</template>
+<script setup>
+import { ref, reactive, computed, onBeforeUnmount } from 'vue';
+import { useI18n } from 'vue-i18n';
+import { useToast } from 'vue-toastification';
+import { mapShortcuts, getReadableShortcut } from '@/composable/shortcut';
+import recordShortcut from '@/utils/record-shortcut';
+
+const { t } = useI18n();
+const toast = useToast();
+
+const shortcuts = ref(mapShortcuts);
+const recording = reactive({
+  id: '',
+  keys: [],
+  isChanged: false,
+});
+
+const shortcutsCats = computed(() => {
+  const arr = Object.values(shortcuts.value);
+  const result = {};
+
+  arr.forEach((item) => {
+    const [category, shortcutName] = item.id.split(':');
+    const readableKey = getReadableShortcut(item.combo);
+    const name = shortcutName.replace('-', ' ');
+
+    (result[category] = result[category] || []).push({
+      ...item,
+      name,
+      keys: readableKey.split('+'),
+    });
+  });
+
+  return result;
+});
+
+function keydownListener(event) {
+  event.preventDefault();
+  event.stopPropagation();
+
+  if (!recording.id) {
+    document.removeEventListener('keydown', keydownListener, true);
+    return;
+  }
+
+  recordShortcut(event, (keys) => {
+    recording.keys = keys;
+  });
+}
+function cleanUp() {
+  recording.id = '';
+  recording.keys = [];
+
+  document.removeEventListener('keydown', keydownListener, true);
+}
+function startRecording({ id }) {
+  if (!recording.id) {
+    document.addEventListener('keydown', keydownListener, true);
+  }
+
+  recording.keys = [];
+  recording.id = id;
+}
+function stopRecording() {
+  if (recording.keys.length === 0) return;
+
+  const newCombo = recording.keys.join('+');
+  const isDuplicate = Object.keys(shortcuts.value).find((key) => {
+    return shortcuts.value[key].combo === newCombo && key !== recording.id;
+  });
+
+  if (isDuplicate) {
+    toast.error(t('settings.shortcuts.duplicate', { name: isDuplicate }));
+
+    return;
+  }
+
+  shortcuts.value[recording.id].combo = newCombo;
+  cleanUp();
+
+  recording.isChanged = true;
+
+  localStorage.setItem('shortcuts', JSON.stringify(shortcuts.value));
+}
+
+onBeforeUnmount(() => {
+  document.removeEventListener('keydown', keydownListener, true);
+});
+</script>
+<style scoped>
+kbd {
+  min-width: 30px;
+  text-align: center;
+  text-transform: uppercase;
+  @apply p-1 px-2 rounded-lg border text-sm shadow ml-1;
+}
+</style>

+ 2 - 0
src/newtab/router.js

@@ -10,6 +10,7 @@ import LogsDetails from './pages/logs/[id].vue';
 import Settings from './pages/Settings.vue';
 import SettingsIndex from './pages/settings/index.vue';
 import SettingsAbout from './pages/settings/About.vue';
+import SettingsShortcuts from './pages/settings/Shortcuts.vue';
 
 const routes = [
   {
@@ -58,6 +59,7 @@ const routes = [
     children: [
       { path: '', component: SettingsIndex },
       { path: '/about', component: SettingsAbout },
+      { path: '/shortcuts', component: SettingsShortcuts },
     ],
   },
 ];

+ 33 - 0
src/utils/record-shortcut.js

@@ -0,0 +1,33 @@
+const allowedKeys = {
+  '+': 'plus',
+  Delete: 'del',
+  Insert: 'ins',
+  ArrowDown: 'down',
+  ArrowLeft: 'left',
+  ArrowUp: 'up',
+  ArrowRight: 'right',
+  Escape: 'escape',
+  Enter: 'enter',
+};
+
+export default function (event, callback) {
+  event.preventDefault();
+  event.stopPropagation();
+
+  if (event.repeat) return;
+
+  const keys = [];
+  const { ctrlKey, altKey, metaKey, shiftKey, key } = event;
+
+  if (ctrlKey || metaKey) keys.push('mod');
+  if (altKey) keys.push('option');
+  if (shiftKey) keys.push('shift');
+
+  const isValidKey = !!allowedKeys[key] || /^[a-z0-9,./;'[\]\-=`]$/i.test(key);
+
+  if (isValidKey) {
+    keys.push(allowedKeys[key] || key.toLowerCase());
+
+    callback(keys);
+  }
+}