Преглед изворни кода

feat: add take-screenshot block

Ahmad Kholid пре 3 година
родитељ
комит
52f32138a9

+ 84 - 9
src/background/blocks-handler.js

@@ -1,6 +1,6 @@
 /* eslint-disable no-underscore-dangle */
 import browser from 'webextension-polyfill';
-import { objectHasKey } from '@/utils/helper';
+import { objectHasKey, fileSaver } from '@/utils/helper';
 import { tasks } from '@/utils/shared';
 import dataExporter from '@/utils/data-exporter';
 import compareBlockValue from '@/utils/compare-block-value';
@@ -34,12 +34,25 @@ function generateBlockError(block) {
   return error;
 }
 
-export function trigger(block) {
-  return new Promise((resolve) => {
-    const nextBlockId = getBlockConnection(block);
+export async function trigger(block) {
+  const nextBlockId = getBlockConnection(block);
+  try {
+    console.log(this.tabId);
+    if (block.data.type === 'visit-web' && this.tabId) {
+      await browser.tabs.executeScript(this.tabId, {
+        file: './contentScript.bundle.js',
+      });
 
-    resolve({ nextBlockId, data: '' });
-  });
+      this._connectTab(this.tabId);
+    }
+
+    return { nextBlockId, data: '' };
+  } catch (error) {
+    const errorInstance = new Error(error);
+    errorInstance.nextBlockId = nextBlockId;
+
+    throw errorInstance;
+  }
 }
 
 export function goBack(block) {
@@ -135,13 +148,18 @@ export async function activeTab(block) {
   const nextBlockId = getBlockConnection(block);
 
   try {
-    const [tab] = await browser.tabs.query({ active: true });
     const data = {
       nextBlockId,
-      data: tab.url,
+      data: '',
     };
 
-    if (tab.id === this.tabId) return data;
+    if (this.tabId) {
+      await browser.tabs.update(this.tabId, { active: true });
+
+      return data;
+    }
+
+    const [tab] = await browser.tabs.query({ active: true });
 
     await browser.tabs.executeScript(tab.id, {
       file: './contentScript.bundle.js',
@@ -160,6 +178,63 @@ export async function activeTab(block) {
   }
 }
 
+export async function takeScreenshot(block) {
+  const nextBlockId = getBlockConnection(block);
+  const { ext, quality, captureActiveTab, fileName } = block.data;
+
+  function saveImage(uri) {
+    const image = new Image();
+
+    image.onload = () => {
+      const name = `${fileName || 'Screenshot'}.${ext || 'png'}`;
+      const canvas = document.createElement('canvas');
+      canvas.width = image.width;
+      canvas.height = image.height;
+
+      const context = canvas.getContext('2d');
+      context.drawImage(image, 0, 0);
+
+      fileSaver(name, canvas.toDataURL());
+    };
+
+    image.src = uri;
+  }
+
+  try {
+    const options = {
+      quality,
+      format: ext || 'png',
+    };
+
+    if (captureActiveTab) {
+      if (!this.tabId) {
+        throw new Error(errorMessage('no-tab', block));
+      }
+
+      const [tab] = await browser.tabs.query({ active: true });
+
+      await browser.tabs.update(this.tabId, { active: true });
+
+      setTimeout(() => {
+        browser.tabs.captureVisibleTab(options).then((uri) => {
+          browser.tabs.update(tab.id, { active: true });
+          saveImage(uri);
+        });
+      }, 500);
+    } else {
+      const uri = await browser.tabs.captureVisibleTab(options);
+
+      saveImage(uri);
+    }
+
+    return { data: '', nextBlockId };
+  } catch (error) {
+    error.nextBlockId = nextBlockId;
+
+    throw error;
+  }
+}
+
 export function interactionHandler(block) {
   return new Promise((resolve, reject) => {
     const nextBlockId = getBlockConnection(block);

+ 5 - 9
src/background/index.js

@@ -12,13 +12,9 @@ function getWorkflow(workflowId) {
     });
   });
 }
-async function executeWorkflow(workflow) {
+async function executeWorkflow(workflow, tabId) {
   try {
-    const state = await workflowState.get(({ id }) => id === workflow.id);
-
-    if (state.length !== 0) return false;
-
-    const engine = new WorkflowEngine(workflow);
+    const engine = new WorkflowEngine(workflow, tabId);
 
     engine.init();
     engine.on('destroyed', () => {
@@ -45,11 +41,11 @@ browser.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
     const runningWorkflow = await workflowState.get(
       (item) => item.state.tabId === tabId
     );
-
+    console.log(runningWorkflow.length, runningWorkflow);
     if (trigger && runningWorkflow.length === 0) {
       const workflow = await getWorkflow(trigger.id);
 
-      executeWorkflow(workflow);
+      executeWorkflow(workflow, tabId);
     }
   }
 });
@@ -74,6 +70,6 @@ browser.runtime.onInstalled.addListener((details) => {
 
 const message = new MessageListener('background');
 
-message.on('workflow:execute', executeWorkflow);
+message.on('workflow:execute', (workflow) => executeWorkflow(workflow));
 
 browser.runtime.onMessage.addListener(message.listener());

+ 3 - 2
src/background/workflow-engine.js

@@ -74,8 +74,9 @@ function tabUpdatedHandler(tabId, changeInfo) {
 }
 
 class WorkflowEngine {
-  constructor(workflow) {
+  constructor(workflow, tabId = null) {
     this.id = nanoid();
+    this.tabId = tabId;
     this.workflow = workflow;
     this.data = {};
     this.blocks = {};
@@ -277,7 +278,7 @@ class WorkflowEngine {
             message: error.message,
             name: tasks[block.name].name,
           });
-
+          console.dir(error);
           if (
             this.workflow.settings.onError === 'keep-running' &&
             error.nextBlockId

+ 2 - 0
src/components/newtab/workflow/WorkflowEditBlock.vue

@@ -24,6 +24,7 @@ import EditTriggerEvent from './edit/EditTriggerEvent.vue';
 import EditElementExists from './edit/EditElementExists.vue';
 import EditScrollElement from './edit/EditScrollElement.vue';
 import EditAttributeValue from './edit/EditAttributeValue.vue';
+import EditTakeScreenshot from './edit/EditTakeScreenshot.vue';
 import EditInteractionBase from './edit/EditInteractionBase.vue';
 
 export default {
@@ -35,6 +36,7 @@ export default {
     EditElementExists,
     EditScrollElement,
     EditAttributeValue,
+    EditTakeScreenshot,
     EditInteractionBase,
   },
 };

+ 60 - 0
src/components/newtab/workflow/edit/EditTakeScreenshot.vue

@@ -0,0 +1,60 @@
+<template>
+  <div class="flex items-center mb-2 mt-8">
+    <ui-input
+      :model-value="data.fileName"
+      placeholder="File name"
+      class="flex-1 mr-2"
+      title="File name"
+      @change="updateData({ fileName: $event })"
+    />
+    <ui-select
+      :model-value="data.ext || 'png'"
+      placeholder="Type"
+      @change="updateData({ ext: $event })"
+    >
+      <option value="png">PNG</option>
+      <option value="jpeg">JPEG</option>
+    </ui-select>
+  </div>
+  <p class="text-sm text-gray-600 ml-2">Image quality:</p>
+  <div class="bg-box-transparent px-4 mb-4 py-2 rounded-lg flex items-center">
+    <input
+      :value="data.quality"
+      title="Image quality"
+      class="focus:outline-none flex-1"
+      type="range"
+      min="0"
+      max="100"
+      @change="updateQuality"
+    />
+    <span class="w-8 text-right">{{ data.quality }}</span>
+  </div>
+  <ui-checkbox
+    v-if="false"
+    :model-value="data.captureActiveTab"
+    @change="updateData({ captureActiveTab: $event })"
+  >
+    Take screenshoot of active tab
+  </ui-checkbox>
+</template>
+<script setup>
+const props = defineProps({
+  data: {
+    type: Object,
+    default: () => ({}),
+  },
+});
+const emit = defineEmits(['update:data']);
+
+function updateData(value) {
+  emit('update:data', { ...props.data, ...value });
+}
+function updateQuality({ target }) {
+  let quality = +target.value;
+
+  if (quality <= 0) quality = 0;
+  if (quality >= 100) quality = 100;
+
+  updateData({ quality });
+}
+</script>

+ 1 - 2
src/manifest.json

@@ -19,8 +19,7 @@
     "alarms",
     "storage",
     "unlimitedStorage",
-    "http://*/*",
-    "https://*/*"
+    "<all_urls>"
   ],
   "web_accessible_resources": ["content.styles.css", "icon-128.png", "icon-34.png"]
 }

+ 1 - 1
src/models/workflow.js

@@ -21,7 +21,7 @@ class Workflow extends Model {
       createdAt: this.number(),
       settings: this.attr({
         timeout: 120000,
-        onError: 'stop-workflow',
+        onError: 'keep-running',
       }),
       logs: this.hasMany(Log, 'workflowId'),
     };

+ 1 - 1
src/popup/pages/Home.vue

@@ -57,7 +57,7 @@ const workflows = computed(() =>
     .where(({ name }) =>
       name.toLocaleLowerCase().includes(query.value.toLocaleLowerCase())
     )
-    .orderBy('createdAt', 'asc')
+    .orderBy('createdAt', 'desc')
     .get()
 );
 

+ 2 - 5
src/utils/data-exporter.js

@@ -1,4 +1,5 @@
 import Papa from 'papaparse';
+import { fileSaver } from './helper';
 
 const files = {
   'plain-text': {
@@ -52,9 +53,5 @@ export default function (data, { name, type }, converted) {
     type: mime,
   });
 
-  const anchor = document.createElement('a');
-  anchor.download = `${name || 'unnamed'}${ext}`;
-  anchor.href = URL.createObjectURL(blob);
-
-  anchor.dispatchEvent(new MouseEvent('click'));
+  fileSaver(`${name || 'unnamed'}${ext}`, URL.createObjectURL(blob));
 }

+ 9 - 0
src/utils/helper.js

@@ -1,3 +1,12 @@
+export function fileSaver(fileName, data) {
+  const anchor = document.createElement('a');
+  anchor.download = fileName;
+  anchor.href = data;
+
+  anchor.dispatchEvent(new MouseEvent('click'));
+  anchor.remove();
+}
+
 export function countDuration(started, ended) {
   const duration = Math.round((ended - started) / 1000);
   const minutes = parseInt((duration / 60) % 60, 10);

+ 0 - 1
src/utils/message.js

@@ -25,7 +25,6 @@ export class MessageListener {
   listen(message, sender) {
     try {
       const listener = this.listeners[message.name];
-      console.log(listener, this.listeners);
       const response =
         listener && listener.call({ message, sender }, message.data, sender);
 

+ 19 - 1
src/utils/shared.js

@@ -26,7 +26,7 @@ export const tasks = {
   },
   'active-tab': {
     name: 'Active tab',
-    description: 'Execute the next block on the current active tab',
+    description: 'Set as active tab',
     icon: 'riWindowLine',
     component: 'BlockBasic',
     category: 'browser',
@@ -79,6 +79,24 @@ export const tasks = {
     allowedInputs: true,
     data: {},
   },
+  'take-screenshot': {
+    name: 'Take screenshot',
+    description: 'Take a screenshot of current active tab',
+    icon: 'riImageLine',
+    component: 'BlockBasic',
+    category: 'browser',
+    editComponent: 'EditTakeScreenshot',
+    inputs: 1,
+    outputs: 1,
+    maxConnection: 1,
+    allowedInputs: true,
+    data: {
+      fileName: '',
+      ext: 'png',
+      quality: 100,
+      captureActiveTab: true,
+    },
+  },
   'event-click': {
     name: 'Click element',
     icon: 'riCursorLine',