Browse Source

feat: add save assets block

Ahmad Kholid 3 years ago
parent
commit
cfc4fe9dee

+ 4 - 17
src/background/index.js

@@ -362,27 +362,11 @@ chrome.runtime.onStartup.addListener(async () => {
 if (chrome.downloads) {
   const getFileExtension = (str) => /(?:\.([^.]+))?$/.exec(str)[1];
   chrome.downloads.onDeterminingFilename.addListener((item, suggest) => {
-    if (item.byExtensionId === chrome.runtime.id) {
-      const filesname =
-        JSON.parse(sessionStorage.getItem('export-filesname')) || {};
-      const blobId = item.url.replace('blob:chrome-extension://', '');
-      const suggestion = filesname[blobId];
-
-      if (suggestion) {
-        delete filesname[blobId];
-
-        suggest(suggestion);
-        sessionStorage.setItem('export-filesname', JSON.stringify(filesname));
-      }
-
-      return;
-    }
-
     const filesname =
       JSON.parse(sessionStorage.getItem('rename-downloaded-files')) || {};
     const suggestion = filesname[item.id];
 
-    if (!suggestion) return;
+    if (!suggestion) return true;
 
     const hasFileExt = getFileExtension(suggestion.filename);
 
@@ -392,6 +376,7 @@ if (chrome.downloads) {
     }
 
     if (!suggestion.waitForDownload) delete filesname[item.id];
+
     sessionStorage.setItem(
       'rename-downloaded-files',
       JSON.stringify(filesname)
@@ -401,6 +386,8 @@ if (chrome.downloads) {
       filename: suggestion.filename,
       conflictAction: suggestion.onConflict,
     });
+
+    return false;
   });
 }
 

+ 0 - 7
src/background/workflow-engine/blocks-handler/handler-export-data.js

@@ -23,18 +23,11 @@ async function exportData({ data, outputs }) {
 
     if (hasDownloadAccess) {
       const filename = `${data.name}${files[data.type].ext}`;
-      const blobId = blobUrl.replace('blob:chrome-extension://', '');
-      const filesname =
-        JSON.parse(sessionStorage.getItem('export-filesname')) || {};
-
       const options = {
         filename,
         conflictAction: data.onConflict || 'uniquify',
       };
 
-      filesname[blobId] = options;
-      sessionStorage.setItem('export-filesname', JSON.stringify(filesname));
-
       await browser.downloads.download({
         ...options,
         url: blobUrl,

+ 69 - 0
src/background/workflow-engine/blocks-handler/handler-save-assets.js

@@ -0,0 +1,69 @@
+import browser from 'webextension-polyfill';
+import { getBlockConnection } from '../helper';
+
+function getFilename(url) {
+  try {
+    const filename = new URL(url).pathname.split('/').pop();
+    const hasExtension = /\.[0-9a-z]+$/i.test(filename);
+
+    if (!hasExtension) return null;
+
+    return filename;
+  } catch (e) {
+    return null;
+  }
+}
+
+export default async function ({ data, id, name, outputs }) {
+  const nextBlockId = getBlockConnection({ outputs });
+
+  try {
+    const hasPermission = await browser.permissions.contains({
+      permissions: ['downloads'],
+    });
+
+    if (!hasPermission) {
+      throw new Error('no-permission');
+    }
+
+    let sources = [data.url];
+    let index = 0;
+    const downloadFile = (url) => {
+      const options = { url, conflictAction: data.onConflict };
+      let filename = data.filename || getFilename(url);
+
+      if (filename) {
+        if (data.onConflict === 'overwrite' && index !== 0) {
+          filename = `(${index}) ${filename}`;
+        }
+
+        options.filename = filename;
+        index += 1;
+      }
+
+      return browser.downloads.download(options);
+    };
+
+    if (data.type === 'element') {
+      sources = await this._sendMessageToTab({
+        id,
+        name,
+        data,
+        tabId: this.activeTab.id,
+      });
+
+      await Promise.allSettled(sources.map((url) => downloadFile(url)));
+    } else if (data.type === 'url') {
+      await downloadFile(data.url);
+    }
+
+    return {
+      nextBlockId,
+      data: sources,
+    };
+  } catch (error) {
+    error.nextBlockId = nextBlockId;
+
+    throw error;
+  }
+}

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

@@ -545,7 +545,7 @@ class WorkflowEngine {
       const data = await browser.tabs.sendMessage(
         this.activeTab.id,
         messagePayload,
-        { ...options, frameId: this.activeTab.frameId }
+        { frameId: this.activeTab.frameId, ...options }
       );
 
       return data;

+ 13 - 7
src/components/newtab/workflow/WorkflowDetailsCard.vue

@@ -86,13 +86,7 @@
         <div
           v-for="block in items"
           :key="block.id"
-          :title="
-            t(
-              `workflow.blocks.${block.id}.${
-                block.description ? 'description' : 'name'
-              }`
-            )
-          "
+          :title="getBlockTitle(block)"
           draggable="true"
           class="transform select-none cursor-move relative p-4 rounded-lg bg-input transition group"
           @dragstart="
@@ -195,4 +189,16 @@ function updateWorkflowIcon(value) {
 
   emit('update', { icon: iconUrl });
 }
+function getBlockTitle({ description, id }) {
+  const blockPath = `workflow.blocks.${id}`;
+  let blockDescription = t(
+    `${blockPath}.${description ? 'description' : 'name'}`
+  );
+
+  if (description) {
+    blockDescription = `[${t(`${blockPath}.name`)}]\n${blockDescription}`;
+  }
+
+  return blockDescription;
+}
 </script>

+ 2 - 0
src/components/newtab/workflow/edit/EditInteractionBase.vue

@@ -8,7 +8,9 @@
         class="w-full mb-2"
         @change="updateData({ description: $event })"
       />
+      <slot name="prepend:selector" />
       <ui-select
+        v-if="!hideSelector"
         :model-value="data.findBy || 'cssSelector'"
         :placeholder="t('workflow.blocks.base.findElement.placeholder')"
         class="w-full mb-2"

+ 81 - 0
src/components/newtab/workflow/edit/EditSaveAssets.vue

@@ -0,0 +1,81 @@
+<template>
+  <edit-interaction-base
+    :data="data"
+    :hide="!permission.has.downloads"
+    :hide-selector="data.type !== 'element'"
+    @change="updateData"
+  >
+    <template #prepend:selector>
+      <ui-select
+        class="mb-4"
+        :model-value="data.type"
+        :label="t('workflow.blocks.save-assets.contentTypes.title')"
+        @change="updateData({ type: $event })"
+      >
+        <option v-for="type in types" :key="type" :value="type">
+          {{ t(`workflow.blocks.save-assets.contentTypes.${type}`) }}
+        </option>
+      </ui-select>
+    </template>
+    <template #prepend>
+      <template v-if="!permission.has.downloads">
+        <p class="mt-4">
+          {{ t('workflow.blocks.handle-download.noPermission') }}
+        </p>
+        <ui-button variant="accent" class="mt-2" @click="permission.request">
+          {{ t('workflow.blocks.clipboard.grantPermission') }}
+        </ui-button>
+      </template>
+    </template>
+    <ui-input
+      v-if="data.type === 'url'"
+      :model-value="data.url"
+      label="URL"
+      class="w-full"
+      placeholder="https://example.com/picture.png"
+      @change="updateData({ url: $event })"
+    />
+    <template v-if="true">
+      <ui-input
+        :model-value="data.filename"
+        :label="t('workflow.blocks.save-assets.filename')"
+        class="w-full mt-4"
+        placeholder="image.jpeg"
+        @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>
+    </template>
+  </edit-interaction-base>
+</template>
+<script setup>
+import { useI18n } from 'vue-i18n';
+import { useHasPermissions } from '@/composable/hasPermissions';
+import EditInteractionBase from './EditInteractionBase.vue';
+
+const props = defineProps({
+  data: {
+    type: Object,
+    default: () => ({}),
+  },
+});
+const emit = defineEmits(['update:data']);
+
+const { t } = useI18n();
+const permission = useHasPermissions(['downloads']);
+
+const types = ['element', 'url'];
+const onConflict = ['uniquify', 'overwrite', 'prompt'];
+
+function updateData(value) {
+  emit('update:data', { ...props.data, ...value });
+}
+</script>

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

@@ -3,7 +3,7 @@
     <label
       v-if="label || $slots.label"
       :for="selectId"
-      class="text-gray-600 dark:text-gray-200 text-sm ml-2"
+      class="text-gray-600 dark:text-gray-200 text-sm ml-1"
     >
       <slot name="label">
         {{ label }}

+ 29 - 0
src/content/blocks-handler/handler-save-assets.js

@@ -0,0 +1,29 @@
+import handleSelector from '../handle-selector';
+
+async function saveAssets(block) {
+  let elements = await handleSelector(block, { returnElement: true });
+
+  if (!elements) {
+    throw new Error('element-not-found');
+  }
+
+  elements = block.data.multiple ? Array.from(elements) : [elements];
+
+  const srcs = elements.reduce((acc, element) => {
+    const tag = element.tagName;
+
+    if ((tag === 'AUDIO' || tag === 'VIDEO') && !tag.src) {
+      const sourceEl = element.querySelector('source');
+
+      if (sourceEl && sourceEl.src) acc.push(sourceEl.src);
+    } else if (element.src) {
+      acc.push(element.src);
+    }
+
+    return acc;
+  }, []);
+
+  return srcs;
+}
+
+export default saveAssets;

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

@@ -48,6 +48,16 @@
           }
         }
       },
+      "save-assets": {
+        "name": "Save assets",
+        "description": "Save assets (image, video, audio, or file) from an element or URL",
+        "filename": "Filename (optional)",
+        "contentTypes": {
+          "title": "Type",
+          "element": "Media element (image, audio, or video)",
+          "url": "URL"
+        }
+      },
       "handle-dialog": {
         "name": "Handle dialog",
         "description": "Accepts or dismisses a JavaScript initiated dialog (alert, confirm, prompt, or onbeforeunload).",

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

@@ -222,6 +222,7 @@
     "messages": {
       "url-empty": "URL is empty",
       "invalid-url": "URL is not valid",
+      "no-permission": "Don't have permission",
       "conditions-empty": "Conditions is empty",
       "invalid-proxy-host": "Invalid proxy host",
       "workflow-disabled": "Workflow is disabled",

+ 28 - 1
src/utils/shared.js

@@ -527,7 +527,7 @@ export const tasks = {
       description: '',
       spreadsheetId: '',
       firstRowAsKey: false,
-      keysAsFirstRow: false,
+      keysAsFirstRow: true,
       valueInputOption: 'RAW',
       dataFrom: 'data-columns',
     },
@@ -764,6 +764,33 @@ export const tasks = {
       multiple: false,
     },
   },
+  'save-assets': {
+    name: 'Save assets',
+    description:
+      'Save assets (image, video, audio, or file) from an element or URL',
+    icon: 'riImageLine',
+    component: 'BlockBasic',
+    editComponent: 'EditSaveAssets',
+    category: 'interaction',
+    inputs: 1,
+    outputs: 1,
+    allowedInputs: true,
+    maxConnection: 1,
+    refDataKeys: ['selector', 'url'],
+    data: {
+      description: '',
+      findBy: 'cssSelector',
+      waitForSelector: false,
+      waitSelectorTimeout: 5000,
+      selector: '',
+      markEl: false,
+      multiple: false,
+      type: 'element',
+      url: '',
+      filename: '',
+      onConflict: 'uniquify',
+    },
+  },
   'handle-dialog': {
     name: 'Handle dialog',
     description: