Browse Source

feat: add browser event block

Ahmad Kholid 3 years ago
parent
commit
89c9437edf

+ 132 - 0
src/background/workflow-engine/blocks-handler/handler-browser-event.js

@@ -0,0 +1,132 @@
+import browser from 'webextension-polyfill';
+import { getBlockConnection } from '../helper';
+import { isWhitespace } from '@/utils/helper';
+
+function handleEventListener(target, validate) {
+  return (data, activeTab) => {
+    return new Promise((resolve) => {
+      let resolved = false;
+      const eventListener = (event) => {
+        if (resolved) return;
+        if (validate && !validate(event, { data, activeTab })) return;
+
+        target.removeListener(eventListener);
+        resolve(event);
+      };
+
+      setTimeout(() => {
+        resolved = true;
+        target.removeListener(eventListener);
+        resolve('');
+      }, data.timeout || 10000);
+
+      target.addListener(eventListener);
+    });
+  };
+}
+
+function onTabLoaded({ tabLoadedUrl, activeTabLoaded, timeout }, { id }) {
+  return new Promise((resolve, reject) => {
+    let resolved = false;
+
+    const checkActiveTabStatus = () => {
+      if (resolved) return;
+      if (!id) {
+        reject(new Error('no-tab'));
+        return;
+      }
+
+      browser.tabs
+        .get(id)
+        .then((tab) => {
+          if (tab.status === 'complete') {
+            resolve();
+            return;
+          }
+
+          setTimeout(checkActiveTabStatus, 1000);
+        })
+        .catch(reject);
+    };
+
+    const url = isWhitespace(tabLoadedUrl)
+      ? '<all_urls>'
+      : tabLoadedUrl.replace(/\s/g, '').split(',');
+    const checkTabsStatus = () => {
+      browser.tabs
+        .query({
+          url,
+          status: 'loading',
+        })
+        .then((tabs) => {
+          if (resolved) return;
+          if (tabs.length === 0) {
+            resolve();
+            return;
+          }
+
+          setTimeout(checkTabsStatus, 1000);
+        })
+        .catch(reject);
+    };
+
+    if (activeTabLoaded) checkActiveTabStatus();
+    else checkTabsStatus();
+
+    setTimeout(() => {
+      resolved = true;
+      resolve('');
+    }, timeout || 10000);
+  });
+}
+
+const validateCreatedTab = ({ url }, { data }) => {
+  if (!isWhitespace(data.tabUrl)) {
+    const regex = new RegExp(data.tabUrl, 'gi');
+
+    if (!regex.test(url)) return false;
+  }
+
+  return true;
+};
+const events = {
+  'tab:loaded': onTabLoaded,
+  'tab:close': handleEventListener(browser.tabs.onRemoved),
+  'tab:create': handleEventListener(
+    browser.webNavigation.onCreatedNavigationTarget,
+    validateCreatedTab
+  ),
+  'window:create': handleEventListener(
+    browser.webNavigation.onCreatedNavigationTarget,
+    validateCreatedTab
+  ),
+  'window:close': handleEventListener(browser.windows.onRemoved),
+};
+
+export default async function ({ data, outputs }) {
+  const nextBlockId = getBlockConnection({ outputs });
+
+  try {
+    const currentEvent = events[data.eventName];
+
+    if (!currentEvent) {
+      throw new Error(`Can't find ${data.eventName} event`);
+    }
+
+    const result = await currentEvent(data, this.activeTab);
+
+    if (data.eventName === 'tab:create' && data.setAsActiveTab) {
+      this.activeTab.id = result.tabId;
+      this.activeTab.url = result.url;
+    }
+
+    return {
+      nextBlockId,
+      data: result || '',
+    };
+  } catch (error) {
+    error.nextBlockId = nextBlockId;
+
+    throw error;
+  }
+}

+ 1 - 1
src/background/workflow-engine/blocks-handler/handler-new-tab.js

@@ -48,7 +48,7 @@ async function newTab(block) {
     }
 
     this.activeTab.frameId = 0;
-    this.activeTab.frames = await executeContentScript(this.activeTab.id);
+    await executeContentScript(this.activeTab.id);
 
     return {
       data: url,

+ 5 - 3
src/background/workflow-engine/blocks-handler/handler-switch-to.js

@@ -1,6 +1,6 @@
 import { objectHasKey } from '@/utils/helper';
 import { getBlockConnection } from '../helper';
-import executeContentScript from '../execute-content-script';
+import executeContentScript, { getFrames } from '../execute-content-script';
 
 async function switchTo(block) {
   const nextBlockId = getBlockConnection(block);
@@ -30,8 +30,10 @@ async function switchTo(block) {
       };
     }
 
-    if (objectHasKey(this.activeTab.frames, url)) {
-      this.activeTab.frameId = this.activeTab.frames[url];
+    const frames = await getFrames(this.activeTab.id);
+
+    if (objectHasKey(frames, url)) {
+      this.activeTab.frameId = frames[url];
 
       await executeContentScript(this.activeTab.id, this.activeTab.frameId);
       await new Promise((resolve) => setTimeout(resolve, 1000));

+ 1 - 1
src/background/workflow-engine/blocks-handler/handler-trigger.js

@@ -6,7 +6,7 @@ async function trigger(block) {
 
   try {
     if (block.data.type === 'visit-web' && this.activeTab.id) {
-      this.activeTab.frames = await executeContentScript(this.activeTab.id);
+      await executeContentScript(this.activeTab.id);
     }
 
     return { nextBlockId, data: '' };

+ 1 - 4
src/background/workflow-engine/engine.js

@@ -344,11 +344,8 @@ class WorkflowEngine {
       }
 
       await waitTabLoaded(this.activeTab.id);
+      await executeContentScript(this.activeTab.id, options.frameId || 0);
 
-      this.activeTab.frames = await executeContentScript(
-        this.activeTab.id,
-        options.frameId || 0
-      );
       const data = await browser.tabs.sendMessage(
         this.activeTab.id,
         { isBlock: true, ...payload },

+ 1 - 5
src/background/workflow-engine/execute-content-script.js

@@ -44,12 +44,8 @@ export default async function (tabId, frameId = 0) {
         file: './contentScript.bundle.js',
       });
     }
-
-    const frames = await getFrames(tabId);
-
-    return frames;
   } catch (error) {
     console.error(error);
-    return {};
+    throw error;
   }
 }

+ 1 - 1
src/components/newtab/workflow/WorkflowDetailsCard.vue

@@ -89,7 +89,7 @@
             <v-remixicon name="riInformationLine" size="18" />
           </a>
           <v-remixicon :name="block.icon" size="24" class="mb-2" />
-          <p class="leading-tight text-overflow">
+          <p class="leading-tight text-overflow capitalize">
             {{ block.name }}
           </p>
         </div>

+ 1 - 1
src/components/newtab/workflow/WorkflowEditBlock.vue

@@ -6,7 +6,7 @@
       <button class="mr-2" @click="$emit('close')">
         <v-remixicon name="riArrowLeftLine" />
       </button>
-      <p class="font-semibold inline-block flex-1">
+      <p class="font-semibold inline-block flex-1 capitalize">
         {{ t(`workflow.blocks.${data.id}.name`) }}
       </p>
       <a

+ 116 - 0
src/components/newtab/workflow/edit/EditBrowserEvent.vue

@@ -0,0 +1,116 @@
+<template>
+  <div>
+    <ui-textarea
+      :model-value="data.description"
+      class="w-full"
+      :placeholder="t('common.description')"
+      @change="updateData({ description: $event })"
+    />
+    <ui-input
+      :model-value="data.timeout"
+      :label="t('workflow.blocks.browser-event.timeout')"
+      type="number"
+      class="w-full"
+      @change="updateData({ timeout: +$event })"
+    />
+    <ui-select
+      :placeholder="t('workflow.blocks.browser-event.events')"
+      :model-value="data.eventName"
+      class="mt-2 w-full"
+      @change="updateData({ eventName: $event })"
+    >
+      <optgroup
+        v-for="(events, label) in browserEvents"
+        :key="label"
+        :label="label"
+      >
+        <option v-for="event in events" :key="event.id" :value="event.id">
+          {{ event.name }}
+        </option>
+      </optgroup>
+    </ui-select>
+    <template v-if="data.eventName === 'tab:loaded'">
+      <ui-input
+        v-if="!data.activeTabLoaded"
+        :model-value="data.tabLoadedUrl"
+        type="url"
+        class="w-full mt-1"
+        placeholder="*://*.example.org/*"
+        @change="updateData({ tabLoadedUrl: $event })"
+      >
+        <template #label>
+          <span>Match pattern</span>
+          <a
+            href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Match_patterns#examples"
+            target="_blank"
+            rel="noopener"
+            title="Examples"
+          >
+            <v-remixicon
+              class="inline-block ml-1"
+              name="riInformationLine"
+              size="18"
+            />
+          </a>
+        </template>
+      </ui-input>
+      <ui-checkbox
+        :model-value="data.activeTabLoaded"
+        class="mt-1"
+        @change="updateData({ activeTabLoaded: $event })"
+      >
+        {{ t('workflow.blocks.browser-event.activeTabLoaded') }}
+      </ui-checkbox>
+    </template>
+    <template v-if="['tab:create', 'window:create'].includes(data.eventName)">
+      <ui-input
+        :model-value="data.tabUrl"
+        type="url"
+        label="Filter"
+        class="w-full mt-1"
+        placeholder="URL or Regex"
+        @change="updateData({ tabUrl: $event })"
+      />
+      <ui-checkbox
+        :model-value="data.setAsActiveTab"
+        class="mt-1"
+        @change="updateData({ setAsActiveTab: $event })"
+      >
+        {{ t('workflow.blocks.browser-event.setAsActiveTab') }}
+      </ui-checkbox>
+    </template>
+  </div>
+</template>
+<script setup>
+import { useI18n } from 'vue-i18n';
+
+const props = defineProps({
+  data: {
+    type: Object,
+    default: () => ({}),
+  },
+});
+const emit = defineEmits(['update:data']);
+
+const { t } = useI18n();
+
+const browserEvents = {
+  Tab: [
+    { id: 'tab:close', name: 'Tab closed' },
+    { id: 'tab:loaded', name: 'Tab loaded' },
+    { id: 'tab:create', name: 'Tab created' },
+  ],
+  // 'Downloads': [
+  //   { id: 'download:start', name: 'Download started' },
+  //   { id: 'download:complete', name: 'Download complete' },
+  // ],
+  Window: [
+    { id: 'window:create', name: 'Window created' },
+    { id: 'window:close', name: 'Window closed' },
+  ],
+};
+
+function updateData(value) {
+  emit('update:data', { ...props.data, ...value });
+}
+</script>

+ 1 - 1
src/components/ui/UiPagination.vue

@@ -55,7 +55,7 @@ const emit = defineEmits(['update:modelValue', 'paginate']);
 const { t } = useI18n();
 
 const inputEl = ref(null);
-const maxPage = computed(() => Math.round(props.records / props.perPage) + 1);
+const maxPage = computed(() => Math.ceil(props.records / props.perPage));
 
 function emitEvent(page) {
   emit('update:modelValue', page);

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

@@ -1,6 +1,7 @@
 import vRemixicon from 'v-remixicon';
 import {
   riHome5Line,
+  riLightbulbLine,
   riSideBarLine,
   riSideBarFill,
   riFolderZipLine,
@@ -81,6 +82,7 @@ import {
 
 export const icons = {
   riHome5Line,
+  riLightbulbLine,
   riSideBarLine,
   riSideBarFill,
   riFolderZipLine,

+ 8 - 0
src/locales/en/blocks.json

@@ -27,6 +27,14 @@
           "text": "Multiple"
         }
       },
+      "browser-event": {
+        "name": "Browser event",
+        "description": "Execute the next block when the event is triggered",
+        "events": "Events",
+        "timeout": "Timeout (milliseconds)",
+        "activeTabLoaded": "Active tab",
+        "setAsActiveTab": "Set as active tab"
+      },
       "blocks-group": {
         "name": "Blocks group",
         "groupName": "Group name",

+ 1 - 0
src/manifest.json

@@ -31,6 +31,7 @@
     "proxy",
     "alarms",
     "storage",
+    "downloads",
     "webNavigation",
     "unlimitedStorage",
     "<all_urls>"

+ 22 - 0
src/utils/shared.js

@@ -181,6 +181,28 @@ export const tasks = {
       captureActiveTab: true,
     },
   },
+  'browser-event': {
+    name: 'Browser event',
+    description: 'Wait until the selected event is triggered',
+    icon: 'riLightbulbLine',
+    component: 'BlockBasic',
+    category: 'browser',
+    editComponent: 'EditBrowserEvent',
+    inputs: 1,
+    outputs: 1,
+    maxConnection: 1,
+    allowedInputs: true,
+    data: {
+      description: '',
+      timeout: 10000,
+      eventName: 'tab:loaded',
+      setAsActiveTab: true,
+      activeTabLoaded: true,
+      tabLoadedUrl: '',
+      tabUrl: '',
+      fileQuery: '',
+    },
+  },
   'event-click': {
     name: 'Click element',
     icon: 'riCursorLine',