Selaa lähdekoodia

feat: add handle download block

Ahmad Kholid 3 vuotta sitten
vanhempi
commit
2006a52ad4

+ 31 - 0
src/background/index.js

@@ -339,6 +339,37 @@ chrome.runtime.onStartup.addListener(async () => {
   await browser.storage.local.set({ onStartupTriggers });
 });
 
+if (chrome.downloads) {
+  const getFileExtension = (str) => /(?:\.([^.]+))?$/.exec(str)[1];
+  chrome.downloads.onDeterminingFilename.addListener((item, suggest) => {
+    const filesname =
+      JSON.parse(sessionStorage.getItem('rename-downloaded-files')) || {};
+    const referrer = new URL(item.referrer).origin.replace(/\/$/, '');
+    const suggestion = filesname[referrer];
+
+    if (!suggestion) return;
+
+    const hasFileExt = getFileExtension(suggestion.filename);
+
+    if (!hasFileExt) {
+      const filExtension = getFileExtension(item.filename);
+      suggestion.filename += `.${filExtension}`;
+    }
+
+    suggestion.id = item.id;
+    if (!suggestion.waitForDownload) delete filesname[referrer];
+    sessionStorage.setItem(
+      'rename-downloaded-files',
+      JSON.stringify(filesname)
+    );
+
+    suggest({
+      filename: suggestion.filename,
+      conflictAction: suggestion.onConflict,
+    });
+  });
+}
+
 const message = new MessageListener('background');
 
 message.on('fetch:text', (url) => {

+ 89 - 0
src/background/workflow-engine/blocks-handler/handler-handle-download.js

@@ -0,0 +1,89 @@
+import browser from 'webextension-polyfill';
+import { getBlockConnection } from '../helper';
+
+function handleDownload({ data, outputs }) {
+  const nextBlockId = getBlockConnection({ outputs });
+  const getFilesname = () =>
+    JSON.parse(sessionStorage.getItem('rename-downloaded-files')) || {};
+
+  return new Promise((resolve) => {
+    if (!this.activeTab.id) throw new Error('no-tab');
+
+    const names = getFilesname();
+    const { origin } = new URL(this.activeTab.url);
+
+    names[origin] = data;
+    sessionStorage.setItem('rename-downloaded-files', JSON.stringify(names));
+
+    if (!data.waitForDownload) {
+      resolve({
+        nextBlockId,
+        data: data.filename,
+      });
+
+      return;
+    }
+
+    let isResolved = false;
+    let currentFilename = data.filename;
+    const timeout = setTimeout(() => {
+      if (isResolved) return;
+
+      isResolved = true;
+
+      resolve({
+        nextBlockId,
+        data: currentFilename,
+      });
+    }, data.timeout);
+
+    const resolvePromise = () => {
+      if (data.saveData) {
+        this.addDataToColumn(data.dataColumn, currentFilename);
+      }
+      if (data.assignVariable) {
+        this.referenceData.variables[data.variableName] = currentFilename;
+      }
+
+      clearTimeout(timeout);
+      isResolved = true;
+
+      const filesname = getFilesname();
+      delete filesname[origin];
+      sessionStorage.setItem(
+        'rename-downloaded-files',
+        JSON.stringify(filesname)
+      );
+
+      resolve({
+        nextBlockId,
+        data: currentFilename,
+      });
+    };
+
+    const handleChanged = ({ state, id, filename }) => {
+      if (this.isDestroyed || isResolved) {
+        browser.downloads.onChanged.removeListener(handleChanged);
+        return;
+      }
+
+      const file = getFilesname()[origin];
+      if (!file || file.id !== id) return;
+
+      if (filename) currentFilename = filename.current;
+
+      if (state && state.current === 'complete') {
+        resolvePromise();
+      } else {
+        browser.downloads.search({ id }).then(([download]) => {
+          if (!download || !download.endTime) return;
+
+          resolvePromise();
+        });
+      }
+    };
+    browser.downloads.onChanged.addListener(handleChanged);
+  });
+}
+
+export default handleDownload;

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

@@ -100,10 +100,6 @@ const browserEvents = {
     { 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' },

+ 95 - 0
src/components/newtab/workflow/edit/EditHandleDownload.vue

@@ -0,0 +1,95 @@
+<template>
+  <div>
+    <template v-if="hasPermission">
+      <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.handle-download.timeout')"
+        placeholder="1000"
+        type="number"
+        class="w-full mt-2"
+        @change="updateData({ timeout: +$event || 1000 })"
+      />
+      <ui-input
+        :model-value="data.filename"
+        :label="`${t('common.fileName')} (${t('common.optional')})`"
+        placeholder="file"
+        class="mt-2 w-full"
+        @change="updateData({ filename: $event })"
+      />
+      <ui-select
+        :model-value="data.onConflict"
+        :label="t('workflow.blocks.handle-download.onConflict')"
+        class="mt-2 w-full"
+        @change="updateData({ onConflict: $event })"
+      >
+        <option v-for="item in onConflict" :key="item" :value="item">
+          {{ t(`workflow.blocks.base.downloads.onConflict.${item}`) }}
+        </option>
+      </ui-select>
+      <ui-checkbox
+        :model-value="data.waitForDownload"
+        class="mt-4"
+        @change="updateData({ waitForDownload: $event })"
+      >
+        {{ t('workflow.blocks.handle-download.waitFile') }}
+      </ui-checkbox>
+      <insert-workflow-data
+        v-if="data.waitForDownload"
+        :data="data"
+        variables
+        @update="updateData"
+      />
+    </template>
+    <template v-else>
+      <p class="mt-4">
+        {{ t('workflow.blocks.handle-download.noPermission') }}
+      </p>
+      <ui-button variant="accent" class="mt-2" @click="requestPermission">
+        {{ t('workflow.blocks.clipboard.grantPermission') }}
+      </ui-button>
+    </template>
+  </div>
+</template>
+<script setup>
+import { ref, onMounted } from 'vue';
+import { useI18n } from 'vue-i18n';
+import browser from 'webextension-polyfill';
+import InsertWorkflowData from './InsertWorkflowData.vue';
+
+const props = defineProps({
+  data: {
+    type: Object,
+    default: () => ({}),
+  },
+});
+const emit = defineEmits(['update:data']);
+
+const permission = { permissions: ['downloads'] };
+const onConflict = ['uniquify', 'overwrite', 'prompt'];
+
+const { t } = useI18n();
+
+const hasPermission = ref(false);
+
+function handlePermission(status) {
+  hasPermission.value = status;
+}
+function requestPermission() {
+  browser.permissions.request(permission).then((isGranted) => {
+    if (isGranted) chrome.runtime.reload();
+  });
+}
+function updateData(value) {
+  emit('update:data', { ...props.data, ...value });
+}
+
+onMounted(() => {
+  browser.permissions.contains(permission).then(handlePermission);
+});
+</script>

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

@@ -87,6 +87,7 @@ import {
   riFocusLine,
   riCursorLine,
   riDownloadLine,
+  riFileDownloadLine,
   riUploadLine,
   riCommandLine,
   riParagraph,
@@ -190,6 +191,7 @@ export const icons = {
   riFocusLine,
   riCursorLine,
   riDownloadLine,
+  riFileDownloadLine,
   riUploadLine,
   riCommandLine,
   riParagraph,

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

@@ -39,6 +39,13 @@
         "waitSelector": {
           "title": "Wait for selector",
           "timeout": "Selector timeout (ms)"
+        },
+        "downloads": {
+          "onConflict": {
+            "uniquify": "Uniquify",
+            "overwrite": "Overwrite",
+            "prompt": "Prompt"
+          }
         }
       },
       "handle-dialog": {
@@ -50,6 +57,14 @@
           "description": "The text to enter into the dialog prompt before accepting"
         }
       },
+      "handle-download": {
+        "name": "Handle download",
+        "description": "Handle downloaded file",
+        "timeout": "Timeout (milliseconds)",
+        "noPermission": "Don't have permission to access the downloads",
+        "onConflict": "On conflict",
+        "waitFile": "Wait for the file to be downloaded"
+      },
       "insert-data": {
         "name": "Insert data",
         "description": "Insert data into table or variable"

+ 2 - 1
src/locales/en/common.json

@@ -35,7 +35,8 @@
     "update": "Update",
     "duplicate": "Duplicate",
     "password": "Password",
-    "category": "Category"
+    "category": "Category",
+    "optional": "Optional"
   },
   "message": {
     "noBlock": "No block",

+ 1 - 1
src/manifest.json

@@ -47,7 +47,7 @@
   ],
   "optional_permissions": [
     "clipboardRead",
-    "debugger"
+    "downloads"
   ],
   "permissions": [
     "tabs",

+ 24 - 0
src/utils/shared.js

@@ -765,6 +765,30 @@ export const tasks = {
       promptText: '',
     },
   },
+  'handle-download': {
+    name: 'Handle download',
+    description: 'Handle downloaded file',
+    icon: 'riFileDownloadLine',
+    component: 'BlockBasic',
+    editComponent: 'EditHandleDownload',
+    category: 'browser',
+    inputs: 1,
+    outputs: 1,
+    allowedInputs: true,
+    maxConnection: 1,
+    refDataKeys: ['promptText'],
+    data: {
+      description: '',
+      filename: '',
+      timeout: 20000,
+      onConflict: 'uniquify',
+      waitForDownload: true,
+      dataColumn: '',
+      saveData: true,
+      assignVariable: false,
+      variableName: '',
+    },
+  },
 };
 
 export const categories = {