浏览代码

feat: add keyboard shortcut in trigger block

Ahmad Kholid 3 年之前
父节点
当前提交
4f16674d5a

+ 31 - 0
.github/ISSUE_TEMPLATE/bug_report.md

@@ -0,0 +1,31 @@
+---
+name: Bug report
+about: Create a report to help us improve
+title: ''
+labels: bug
+assignees: ''
+
+---
+
+**Describe the bug**
+A clear and concise description of what the bug is.
+
+**To Reproduce**
+Steps to reproduce the behavior:
+1. Go to '...'
+2. Click on '....'
+3. Scroll down to '....'
+4. See error
+
+**Expected behavior**
+A clear and concise description of what you expected to happen.
+
+**Screenshots**
+If applicable, add screenshots to help explain your problem.
+
+**Desktop (please complete the following information):**
+ - OS: [e.g. iOS]
+ - Version [e.g. 22]
+
+**Additional context**
+Add any other context about the problem here.

+ 20 - 0
.github/ISSUE_TEMPLATE/feature_request.md

@@ -0,0 +1,20 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+title: ''
+labels: enhancement
+assignees: ''
+
+---
+
+**Is your feature request related to a problem? Please describe.**
+A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+
+**Describe the solution you'd like**
+A clear and concise description of what you want to happen.
+
+**Describe alternatives you've considered**
+A clear and concise description of any alternative solutions or features you've considered.
+
+**Additional context**
+Add any other context or screenshots about the feature request here.

+ 1 - 0
package.json

@@ -26,6 +26,7 @@
     "@webcomponents/custom-elements": "^1.5.0",
     "dayjs": "^1.10.7",
     "drawflow": "^0.0.51",
+    "mousetrap": "^1.6.5",
     "nanoid": "3.1.28",
     "papaparse": "^5.3.1",
     "prismjs": "^1.25.0",

+ 88 - 1
src/components/newtab/workflow/edit/EditTrigger.vue

@@ -10,7 +10,7 @@
     :model-value="data.type || 'manual'"
     placeholder="Trigger workflow"
     class="w-full"
-    @change="updateData({ type: $event })"
+    @change="handleSelectChange"
   >
     <option v-for="trigger in triggers" :key="trigger.id" :value="trigger.id">
       {{ trigger.name }}
@@ -74,9 +74,36 @@
         Use regex
       </ui-checkbox>
     </div>
+    <div v-else-if="data.type === 'keyboard-shortcut'" class="mt-2">
+      <div class="flex items-center mb-2">
+        <ui-input
+          :model-value="recordKeys.keys"
+          readonly
+          class="flex-1 mr-2"
+          placeholder="Shortcut"
+        />
+        <ui-button v-tooltip="'Record keys'" icon @click="toggleRecordKeys">
+          <v-remixicon
+            :name="recordKeys.isRecording ? 'riStopLine' : 'riRecordCircleLine'"
+          />
+        </ui-button>
+      </div>
+      <ui-checkbox
+        :model-value="data.activeInInput"
+        class="mb-1"
+        title="Execute shortcut even in an input element"
+        @change="updateData({ activeInInput: $event })"
+      >
+        Active while in input
+      </ui-checkbox>
+      <p class="mt-4 leading-tight text-gray-600 dark:text-gray-200">
+        Note: keyboard shortcut only executed when you're on a webpage
+      </p>
+    </div>
   </transition-expand>
 </template>
 <script setup>
+import { shallowReactive, onUnmounted } from 'vue';
 import dayjs from 'dayjs';
 
 const props = defineProps({
@@ -92,13 +119,69 @@ const triggers = [
   { id: 'interval', name: 'Interval' },
   { id: 'date', name: 'On specific date' },
   { id: 'visit-web', name: 'When visit a website' },
+  { id: 'keyboard-shortcut', name: 'Keyboard shortcut' },
 ];
 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 = shallowReactive({
+  isRecording: false,
+  keys: props.data.shortcut,
+});
 
 function updateData(value) {
   emit('update:data', { ...props.data, ...value });
 }
+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('alt');
+  if (shiftKey) keys.push('shift');
+
+  const isValidKey = !!allowedKeys[key] || /^[a-z0-9,./;'[\]\-=`]$/i.test(key);
+
+  if (isValidKey) {
+    keys.push(allowedKeys[key] || key.toLowerCase());
+
+    recordKeys.keys = keys.join('+');
+    updateData({ shortcut: recordKeys.keys });
+  }
+}
+function toggleRecordKeys() {
+  if (recordKeys.isRecording) {
+    window.removeEventListener('keydown', handleKeydownEvent);
+  } else {
+    window.addEventListener('keydown', handleKeydownEvent);
+  }
+
+  recordKeys.isRecording = !recordKeys.isRecording;
+}
+function handleSelectChange(type) {
+  if (recordKeys.isRecording) {
+    window.removeEventListener('keydown', handleKeydownEvent);
+    recordKeys.isRecording = false;
+  }
+
+  updateData({ type });
+}
 function updateIntervalInput(value, { key, min, max }) {
   let num = +value;
 
@@ -117,4 +200,8 @@ function updateDate(value) {
 
   updateData({ date });
 }
+
+onUnmounted(() => {
+  window.removeEventListener('keydown', handleKeydownEvent);
+});
 </script>

+ 0 - 33
src/components/shared/SharedTaskList.vue

@@ -1,33 +0,0 @@
-<template>
-  <ui-list class="space-y-1">
-    <ui-list-item
-      v-for="task in tasks"
-      :key="task.name"
-      :active="task.status === 'running'"
-      class="relative group"
-      color="bg-box-transparent"
-    >
-      <ui-spinner
-        v-if="task.status === 'running'"
-        color="text-accent dark:text-gray-200"
-        size="20"
-      ></ui-spinner>
-      <v-remixicon
-        v-else-if="task.status === 'success'"
-        name="riCheckboxCircleLine"
-        class="-ml-0.5"
-      />
-      <p class="ml-3 flex-1">{{ task.name }}</p>
-    </ui-list-item>
-  </ui-list>
-</template>
-<script>
-export default {
-  props: {
-    tasks: {
-      type: Array,
-      default: () => [],
-    },
-  },
-};
-</script>

+ 58 - 0
src/content/shortcut.js

@@ -0,0 +1,58 @@
+import Mousetrap from 'mousetrap';
+import browser from 'webextension-polyfill';
+import { sendMessage } from '@/utils/message';
+
+Mousetrap.prototype.stopCallback = function () {
+  return false;
+};
+
+function getTriggerBlock(workflow) {
+  const drawflow = JSON.parse(workflow?.drawflow || '{}');
+
+  if (!drawflow?.drawflow?.Home?.data) return null;
+
+  const blocks = Object.values(drawflow.drawflow.Home.data);
+  const trigger = blocks.find(({ name }) => name === 'trigger');
+
+  return trigger;
+}
+
+(async () => {
+  try {
+    const { shortcuts, workflows } = await browser.storage.local.get([
+      'shortcuts',
+      'workflows',
+    ]);
+    const shortcutsArr = Object.entries(shortcuts || {});
+
+    if (shortcutsArr.length === 0) return;
+
+    const keyboardShortcuts = shortcutsArr.reduce((acc, [id, value]) => {
+      const workflow = [...workflows].find((item) => item.id === id);
+
+      (acc[value] = acc[value] || []).push({
+        id,
+        workflow,
+        activeInInput: getTriggerBlock(workflow)?.data?.activeInInput,
+      });
+
+      return acc;
+    }, {});
+
+    Mousetrap.bind(Object.keys(keyboardShortcuts), ({ target }, command) => {
+      const isInputElement =
+        ['INPUT', 'SELECT', 'TEXTAREA'].includes(target.tagName) ||
+        target?.contentEditable === 'true';
+
+      keyboardShortcuts[command].forEach((item) => {
+        if (!item.activeInInput && isInputElement) return;
+
+        sendMessage('workflow:execute', item.workflow, 'background');
+      });
+
+      return false;
+    });
+  } catch (error) {
+    console.error(error);
+  }
+})();

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

@@ -2,6 +2,7 @@ import vRemixicon from 'v-remixicon';
 import {
   riHome5Line,
   riGithubFill,
+  riRecordCircleLine,
   riErrorWarningLine,
   riCalendarLine,
   riFileTextLine,
@@ -65,6 +66,7 @@ import {
 export const icons = {
   riHome5Line,
   riGithubFill,
+  riRecordCircleLine,
   riErrorWarningLine,
   riCalendarLine,
   riFileTextLine,

+ 12 - 0
src/manifest.json

@@ -14,6 +14,18 @@
   "icons": {
     "128": "icon-128.png"
   },
+  "content_scripts": [
+    {
+      "matches": [
+        "<all_urls>"
+      ],
+      "js": [
+        "shortcut.bundle.js"
+      ],
+      "run_at": "document_end",
+      "all_frames": false
+    }
+  ],
   "permissions": [
     "tabs",
     "alarms",

+ 14 - 7
src/models/workflow.js

@@ -38,17 +38,24 @@ class Workflow extends Model {
 
   static async afterDelete({ id }) {
     try {
-      const { visitWebTriggers } = await browser.storage.local.get(
-        'visitWebTriggers'
-      );
+      const { visitWebTriggers, shortcuts } = await browser.storage.local.get([
+        'visitWebTriggers',
+        'shortcuts',
+      ]);
       const index = visitWebTriggers.findIndex((item) => item.id === id);
 
-      await browser.alarms.clear(id);
+      if (index !== -1) {
+        visitWebTriggers.splice(index, 1);
+      }
 
-      if (index === -1) return;
+      const keyboardShortcuts = shortcuts || {};
+      delete keyboardShortcuts[id];
 
-      visitWebTriggers.splice(index, 1);
-      await browser.storage.local.set({ visitWebTriggers });
+      await browser.storage.local.set({
+        visitWebTriggers,
+        shortcuts: keyboardShortcuts,
+      });
+      await browser.alarms.clear(id);
     } catch (error) {
       console.error(error);
     }

+ 15 - 5
src/newtab/pages/workflows/[id].vue

@@ -198,22 +198,28 @@ function updateWorkflow(data) {
 async function handleWorkflowTrigger({ data }) {
   try {
     const workflowAlarm = await browser.alarms.get(workflowId);
-    const { visitWebTriggers } = await browser.storage.local.get(
-      'visitWebTriggers'
-    );
+    const { visitWebTriggers, shortcuts } = await browser.storage.local.get([
+      'visitWebTriggers',
+      'shortcuts',
+    ]);
     let visitWebTriggerIndex = visitWebTriggers.findIndex(
       (item) => item.id === workflowId
     );
+    const keyboardShortcuts = shortcuts || {};
+    delete keyboardShortcuts[workflowId];
 
     if (workflowAlarm) await browser.alarms.clear(workflowId);
     if (visitWebTriggerIndex !== -1) {
       visitWebTriggers.splice(visitWebTriggerIndex, 1);
 
       visitWebTriggerIndex = -1;
-
-      await browser.storage.local.set({ visitWebTriggers });
     }
 
+    await browser.storage.local.set({
+      visitWebTriggers,
+      shortcuts: keyboardShortcuts,
+    });
+
     if (['date', 'interval'].includes(data.type)) {
       let alarmInfo;
 
@@ -244,6 +250,10 @@ async function handleWorkflowTrigger({ data }) {
       }
 
       await browser.storage.local.set({ visitWebTriggers });
+    } else if (data.type === 'keyboard-shortcut') {
+      keyboardShortcuts[workflowId] = data.shortcut;
+
+      await browser.storage.local.set({ shortcuts: keyboardShortcuts });
     }
   } catch (error) {
     console.error(error);

+ 0 - 36
src/popup/pages/Workflow.vue

@@ -1,36 +0,0 @@
-<template>
-  <div class="bg-accent rounded-b-2xl h-36 p-5 w-full text-white dark">
-    <div class="flex items-center mb-4 text-gray-100 justify-between">
-      <router-link to="/" class="-ml-2">
-        <v-remixicon name="riArrowLeftSLine" size="28" />
-      </router-link>
-      <router-link :to="`/workflow/${$route.params.id}/edit`" title="Edit">
-        <v-remixicon name="riEditBoxLine" />
-      </router-link>
-    </div>
-    <h1 class="text-xl font-semibold">Workflow name</h1>
-  </div>
-  <div class="px-5 mb-2 -mt-10">
-    <ui-card class="mb-4 flex space-x-2">
-      <ui-button class="w-6/12">
-        <v-remixicon class="-ml-1 mr-1" name="riPauseLine" />
-        <span>Pause</span>
-      </ui-button>
-      <ui-button variant="accent" class="w-6/12">
-        <v-remixicon class="-ml-1 mr-1" name="riStopLine" />
-        <span>Stop</span>
-      </ui-button>
-    </ui-card>
-    <p class="mb-1">Tasks</p>
-    <shared-task-list :tasks="tasks" />
-  </div>
-</template>
-<script setup>
-import SharedTaskList from '@/components/shared/SharedTaskList.vue';
-
-const tasks = [
-  { name: 'Open website', status: 'success' },
-  { name: 'Get data', status: 'success' },
-  { name: 'Close web', status: 'running' },
-];
-</script>

+ 0 - 6
src/popup/router.js

@@ -1,6 +1,5 @@
 import { createRouter, createWebHashHistory } from 'vue-router';
 import Home from './pages/Home.vue';
-import Workflow from './pages/Workflow.vue';
 import WorkflowEdit from './pages/workflow/Edit.vue';
 
 const routes = [
@@ -9,11 +8,6 @@ const routes = [
     name: 'home',
     component: Home,
   },
-  {
-    path: '/workflow/:id',
-    name: 'workflow',
-    component: Workflow,
-  },
   {
     path: '/workflow/:id/edit',
     name: 'workflow-edit',

+ 0 - 0
src/utils/keyboard-shortcut.js


+ 2 - 0
src/utils/shared.js

@@ -21,6 +21,8 @@ export const tasks = {
       date: '',
       time: '00:00',
       url: '',
+      shortcut: '',
+      activeInInput: false,
       isUrlRegex: false,
     },
   },

+ 1 - 1
src/utils/workflow-data.js

@@ -20,7 +20,7 @@ export function importWorkflow() {
       try {
         const workflow = JSON.parse(target.result);
 
-        Workflow.insert({ data: workflow });
+        Workflow.insert({ data: workflow, createdAt: Date.now() });
       } catch (error) {
         console.error(error);
       }

+ 2 - 1
webpack.config.js

@@ -42,9 +42,10 @@ const options = {
     popup: path.join(__dirname, 'src', 'popup', 'index.js'),
     background: path.join(__dirname, 'src', 'background', 'index.js'),
     contentScript: path.join(__dirname, 'src', 'content', 'index.js'),
+    shortcut: path.join(__dirname, 'src', 'content', 'shortcut.js'),
   },
   chromeExtensionBoilerplate: {
-    notHotReload: ['contentScript'],
+    notHotReload: ['contentScript', 'shortcut'],
   },
   output: {
     path: path.resolve(__dirname, 'build'),

+ 5 - 0
yarn.lock

@@ -4586,6 +4586,11 @@ modern-normalize@^1.1.0:
   resolved "https://registry.yarnpkg.com/modern-normalize/-/modern-normalize-1.1.0.tgz#da8e80140d9221426bd4f725c6e11283d34f90b7"
   integrity sha512-2lMlY1Yc1+CUy0gw4H95uNN7vjbpoED7NNRSBHE25nWfLBdmMzFCsPshlzbxHz+gYMcBEUN8V4pU16prcdPSgA==
 
+mousetrap@^1.6.5:
+  version "1.6.5"
+  resolved "https://registry.yarnpkg.com/mousetrap/-/mousetrap-1.6.5.tgz#8a766d8c272b08393d5f56074e0b5ec183485bf9"
+  integrity sha512-QNo4kEepaIBwiT8CDhP98umTetp+JNfQYBWvC1pc6/OAibuXtRcxZ58Qz8skvEHYvURne/7R8T5VoOI7rDsEUA==
+
 ms@2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"