Ahmad Kholid 3 years ago
parent
commit
04f2d44436

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "automa",
-  "version": "0.7.0",
+  "version": "0.7.2",
   "description": "An extension for automating your browser by connecting blocks",
   "license": "MIT",
   "repository": {

+ 4 - 0
src/background/index.js

@@ -110,6 +110,10 @@ chrome.runtime.onInstalled.addListener((details) => {
 
 const message = new MessageListener('background');
 
+message.on('fetch:text', (url) => {
+  return fetch(url).then((response) => response.text());
+});
+
 message.on('get:sender', (_, sender) => {
   return sender;
 });

+ 7 - 2
src/components/block/BlockBasic.vue

@@ -14,8 +14,11 @@
       <v-remixicon :name="block.details.icon || 'riGlobalLine'" />
     </span>
     <div style="max-width: 200px">
-      <p class="font-semibold leading-none whitespace-nowrap">
-        {{ block.details.name }}
+      <p
+        v-if="block.details.id"
+        class="font-semibold leading-none whitespace-nowrap"
+      >
+        {{ t(`workflow.blocks.${block.details.id}.name`) }}
       </p>
       <p class="text-gray-600 text-overflow leading-tight">
         {{ block.data.description }}
@@ -31,6 +34,7 @@
 </template>
 <script setup>
 import emitter from 'tiny-emitter/instance';
+import { useI18n } from 'vue-i18n';
 import { useEditorBlock } from '@/composable/editorBlock';
 import { useComponentId } from '@/composable/componentId';
 import BlockBase from './BlockBase.vue';
@@ -42,6 +46,7 @@ const props = defineProps({
   },
 });
 
+const { t } = useI18n();
 const componentId = useComponentId('block-base');
 const block = useEditorBlock(`#${componentId}`, props.editor);
 

+ 18 - 11
src/components/block/BlockConditions.vue

@@ -6,7 +6,7 @@
         class="inline-block text-sm mr-4 p-2 rounded-lg"
       >
         <v-remixicon name="riAB" size="20" class="inline-block mr-1" />
-        <span>conditions</span>
+        <span>{{ t('workflow.blocks.conditions.name') }}</span>
       </div>
       <div class="flex-grow"></div>
       <v-remixicon
@@ -41,7 +41,7 @@
         <div class="flex items-center transition bg-input rounded-lg">
           <select
             v-model="block.data.conditions[index].type"
-            :title="conditions[block.data.conditions[index]?.type] || 'Equals'"
+            :title="getTitle(index)"
             class="
               bg-transparent
               font-mono
@@ -74,16 +74,17 @@
         v-if="block.data.conditions && block.data.conditions.length !== 0"
         class="text-right text-gray-600"
       >
-        <span title="Execute when all comparisons don't meet the requirement">
+        <span :title="t('workflow.blocks.conditions.fallbackTitle')">
           &#9432;
         </span>
-        Fallback
+        {{ t('common.fallback') }}
       </p>
     </div>
   </div>
 </template>
 <script setup>
 import { watch, toRaw } from 'vue';
+import { useI18n } from 'vue-i18n';
 import emitter from 'tiny-emitter/instance';
 import { debounce } from '@/utils/helper';
 import { useComponentId } from '@/composable/componentId';
@@ -96,20 +97,26 @@ const props = defineProps({
   },
 });
 
+const { t } = useI18n();
 const componentId = useComponentId('block-conditions');
 const block = useEditorBlock(`#${componentId}`, props.editor);
 
 const conditions = {
-  '==': 'Equals',
-  '>': 'Greater than',
-  '>=': 'Greater than or equal',
-  '<': 'Less than',
-  '<=': 'Less than or equal',
-  '()': 'Contains',
+  '==': 'equals',
+  '>': 'gt',
+  '>=': 'gte',
+  '<': 'lt',
+  '<=': 'lte',
+  '()': 'contains',
 };
 
+function getTitle(index) {
+  const type = conditions[block.data.conditions[index]?.type] || 'equals';
+
+  return t(`workflow.blocks.conditions.${type}`);
+}
 function addComparison() {
-  if (block.data.conditions.length >= 5) return;
+  if (block.data.conditions.length >= 10) return;
 
   block.data.conditions.push({ type: '==', value: '' });
 

+ 5 - 3
src/components/block/BlockDelay.vue

@@ -6,7 +6,7 @@
         class="inline-block text-sm mr-4 p-2 rounded-lg"
       >
         <v-remixicon name="riTimerLine" size="20" class="inline-block mr-1" />
-        <span>Delay</span>
+        <span>{{ t('workflow.blocks.delay.name') }}</span>
       </div>
       <div class="flex-grow"></div>
       <v-remixicon
@@ -18,9 +18,9 @@
     <input
       :value="block.data.time"
       min="0"
-      title="Delay in millisecond"
+      :title="t('workflow.blocks.delay.input.title')"
+      :placeholder="t('workflow.blocks.delay.input.placeholder')"
       class="px-4 py-2 rounded-lg w-36 bg-input"
-      placeholder="(millisecond)"
       type="number"
       required
       @input="handleInput"
@@ -28,6 +28,7 @@
   </div>
 </template>
 <script setup>
+import { useI18n } from 'vue-i18n';
 import emitter from 'tiny-emitter/instance';
 import { useComponentId } from '@/composable/componentId';
 import { useEditorBlock } from '@/composable/editorBlock';
@@ -39,6 +40,7 @@ const props = defineProps({
   },
 });
 
+const { t } = useI18n();
 const componentId = useComponentId('block-delay');
 const block = useEditorBlock(`#${componentId}`, props.editor);
 

+ 9 - 5
src/components/block/BlockElementExists.vue

@@ -10,10 +10,10 @@
       class="inline-block text-sm mb-2 p-2 rounded-lg"
     >
       <v-remixicon name="riFocus3Line" size="20" class="inline-block mr-1" />
-      <span>Element exists</span>
+      <span>{{ t('workflow.blocks.element-exists.name') }}</span>
     </div>
     <p
-      title="Element selector"
+      :title="t('workflow.blocks.element-exists.selector')"
       class="
         text-overflow
         p-2
@@ -26,11 +26,13 @@
       "
       style="max-width: 200px"
     >
-      {{ block.data.selector || 'Element selector' }}
+      {{ block.data.selector || t('workflow.blocks.element-exists.selector') }}
     </p>
     <p class="text-right text-gray-600">
-      <span title="Execute when element doesn't exists"> &#9432; </span>
-      Fallback
+      <span :title="t('workflow.blocks.element-exists.fallbackTitle')">
+        &#9432;
+      </span>
+      {{ t('common.fallback') }}
     </p>
     <input
       type="text"
@@ -41,6 +43,7 @@
   </block-base>
 </template>
 <script setup>
+import { useI18n } from 'vue-i18n';
 import emitter from 'tiny-emitter/instance';
 import BlockBase from './BlockBase.vue';
 import { useComponentId } from '@/composable/componentId';
@@ -53,6 +56,7 @@ const props = defineProps({
   },
 });
 
+const { t } = useI18n();
 const componentId = useComponentId('block-delay');
 const block = useEditorBlock(`#${componentId}`, props.editor);
 

+ 4 - 2
src/components/block/BlockExportData.vue

@@ -10,7 +10,7 @@
           size="20"
           class="inline-block mr-1"
         />
-        <span>Export data</span>
+        <span>{{ t('workflow.blocks.export-data.name') }}</span>
       </div>
       <div class="flex-grow"></div>
       <v-remixicon
@@ -21,8 +21,8 @@
     </div>
     <input
       v-model="block.data.name"
+      :placeholder="t('common.fileName')"
       class="bg-input rounded-lg transition w-40 mb-2 py-2 px-4 block"
-      placeholder="File name"
     />
     <ui-select v-model="block.data.type" class="w-40" placeholder="Export as">
       <option v-for="type in dataExportTypes" :key="type.id" :value="type.id">
@@ -32,6 +32,7 @@
   </div>
 </template>
 <script setup>
+import { useI18n } from 'vue-i18n';
 import { watch } from 'vue';
 import emitter from 'tiny-emitter/instance';
 import { dataExportTypes } from '@/utils/shared';
@@ -46,6 +47,7 @@ const props = defineProps({
   },
 });
 
+const { t } = useI18n();
 const componentId = useComponentId('block-delay');
 const block = useEditorBlock(`#${componentId}`, props.editor);
 

+ 3 - 1
src/components/block/BlockLoopBreakpoint.vue

@@ -6,7 +6,7 @@
         class="inline-block text-sm mr-4 p-2 rounded-lg"
       >
         <v-remixicon name="riStopLine" size="20" class="inline-block mr-1" />
-        <span>Loop breakpoint</span>
+        <span>{{ t('workflow.blocks.loop-breakpoint.name') }}</span>
       </div>
       <div class="flex-grow"></div>
       <v-remixicon
@@ -26,6 +26,7 @@
   </div>
 </template>
 <script setup>
+import { useI18n } from 'vue-i18n';
 import emitter from 'tiny-emitter/instance';
 import { useComponentId } from '@/composable/componentId';
 import { useEditorBlock } from '@/composable/editorBlock';
@@ -37,6 +38,7 @@ const props = defineProps({
   },
 });
 
+const { t } = useI18n();
 const componentId = useComponentId('block-delay');
 const block = useEditorBlock(`#${componentId}`, props.editor);
 

+ 9 - 3
src/components/block/BlockRepeatTask.vue

@@ -6,7 +6,7 @@
         class="inline-block text-sm mr-4 p-2 rounded-lg"
       >
         <v-remixicon name="riRepeat2Line" size="20" class="inline-block mr-1" />
-        <span>Repeat task</span>
+        <span>{{ t('workflow.blocks.repeat-task.name') }}</span>
       </div>
       <div class="flex-grow"></div>
       <v-remixicon
@@ -34,16 +34,22 @@
         required
         @input="handleInput"
       />
-      <span class="text-gray-600">Times</span>
+      <span class="text-gray-600">{{
+        t('workflow.blocks.repeat-task.times')
+      }}</span>
     </label>
-    <p class="text-right text-gray-600">Repeat from</p>
+    <p class="text-right text-gray-600">
+      {{ t('workflow.blocks.repeat-task.repeatFrom') }}
+    </p>
   </div>
 </template>
 <script setup>
+import { useI18n } from 'vue-i18n';
 import emitter from 'tiny-emitter/instance';
 import { useComponentId } from '@/composable/componentId';
 import { useEditorBlock } from '@/composable/editorBlock';
 
+const { t } = useI18n();
 const props = defineProps({
   editor: {
     type: Object,

+ 14 - 11
src/components/newtab/workflow/WorkflowDetailsCard.vue

@@ -32,7 +32,7 @@
     class="px-4 mt-4 mb-2"
   />
   <div class="scroll bg-scroll overflow-auto px-4 flex-1 overflow-auto">
-    <template v-for="(items, catId) in taskList" :key="catId">
+    <template v-for="(items, catId) in blocks" :key="catId">
       <div class="flex items-center top-0 space-x-2 mb-2">
         <span
           :class="categories[catId].color"
@@ -78,7 +78,7 @@
           </a>
           <v-remixicon :name="block.icon" size="24" class="mb-2" />
           <p class="leading-tight text-overflow">
-            {{ t(`workflow.blocks.${block.id}.name`) }}
+            {{ block.name }}
           </p>
         </div>
       </div>
@@ -119,16 +119,19 @@ const icons = [
   'riCommandLine',
 ];
 
-const query = ref('');
-const taskList = computed(() =>
-  Object.keys(tasks).reduce((arr, key) => {
-    const task = tasks[key];
+const blocksArr = Object.entries(tasks).map(([key, block]) => ({
+  ...block,
+  id: key,
+  name: t(`workflow.blocks.${key}.name`),
+}));
 
-    if (tasks[key].name.toLowerCase().includes(query.value.toLowerCase())) {
-      (arr[task.category] = arr[task.category] || []).push({
-        id: key,
-        ...task,
-      });
+const query = ref('');
+const blocks = computed(() =>
+  blocksArr.reduce((arr, block) => {
+    if (
+      block.name.toLocaleLowerCase().includes(query.value.toLocaleLowerCase())
+    ) {
+      (arr[block.category] = arr[block.category] || []).push(block);
     }
 
     return arr;

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

@@ -5,7 +5,7 @@
         <v-remixicon name="riArrowLeftLine" />
       </button>
       <p class="font-semibold inline-block align-middle">
-        {{ data.name }}
+        {{ t(`workflow.blocks.${data.id}.name`) }}
       </p>
     </div>
     <component

+ 92 - 37
src/components/newtab/workflow/edit/EditJavascriptCode.vue

@@ -16,47 +16,85 @@
       @change="updateData({ timeout: +$event })"
     />
     <prism-editor
-      v-if="!showCodeModal"
+      v-if="!state.showCodeModal"
       :model-value="data.code"
       :highlight="highlighter('javascript')"
       readonly
       class="p-4 max-h-80"
-      @click="showCodeModal = true"
+      @click="state.showCodeModal = true"
     />
-    <ui-modal
-      v-model="showCodeModal"
-      :title="t('workflow.blocks.javascript-code.modal')"
-      content-class="max-w-3xl"
-    >
-      <prism-editor
-        v-model="code"
-        class="py-4"
-        :highlight="highlighter('javascript')"
-        line-numbers
-        style="height: calc(100vh - 12rem)"
-      />
-      <p class="mt-1">
-        {{ t('workflow.blocks.javascript-code.availabeFuns') }}
-      </p>
-      <p class="space-x-1">
-        <a
-          v-for="func in availableFuncs"
-          :key="func.id"
-          :href="`https://github.com/Kholid060/automa/wiki/Blocks#${func.id}`"
-          target="_blank"
-          rel="noopener"
-          class="inline-block"
-        >
-          <code>
-            {{ func.name }}
-          </code>
-        </a>
-      </p>
+    <ui-modal v-model="state.showCodeModal" content-class="max-w-3xl">
+      <template #header>
+        <ui-tabs v-model="state.activeTab" class="border-none">
+          <ui-tab value="code">
+            {{ t('workflow.blocks.javascript-code.modal.tabs.code') }}
+          </ui-tab>
+          <ui-tab value="preloadScript">
+            {{ t('workflow.blocks.javascript-code.modal.tabs.preloadScript') }}
+          </ui-tab>
+        </ui-tabs>
+      </template>
+      <ui-tab-panels
+        v-model="state.activeTab"
+        class="overflow-auto"
+        style="height: calc(100vh - 9rem)"
+      >
+        <ui-tab-panel value="code" class="h-full">
+          <prism-editor
+            v-model="state.code"
+            :highlight="highlighter('javascript')"
+            line-numbers
+            class="py-4 overflow-auto"
+            style="height: 87%"
+          />
+          <p class="mt-1">
+            {{ t('workflow.blocks.javascript-code.availabeFuncs') }}
+          </p>
+          <p class="space-x-1">
+            <a
+              v-for="func in availableFuncs"
+              :key="func.id"
+              :href="`https://github.com/Kholid060/automa/wiki/Blocks#${func.id}`"
+              target="_blank"
+              rel="noopener"
+              class="inline-block"
+            >
+              <code>
+                {{ func.name }}
+              </code>
+            </a>
+          </p>
+        </ui-tab-panel>
+        <ui-tab-panel value="preloadScript">
+          <div
+            v-for="(script, index) in state.preloadScripts"
+            :key="index"
+            class="flex items-center mt-4"
+          >
+            <v-remixicon
+              name="riDeleteBin7Line"
+              class="mr-2 cursor-pointer"
+              @click="state.preloadScripts.splice(index, 1)"
+            />
+            <ui-input
+              v-model="state.preloadScripts[index].src"
+              placeholder="http://example.com/script.js"
+              class="flex-1 mr-4"
+            />
+            <ui-checkbox v-model="state.preloadScripts[index].removeAfterExec">
+              {{ t('workflow.blocks.javascript-code.removeAfterExec') }}
+            </ui-checkbox>
+          </div>
+          <ui-button variant="accent" class="w-20 mt-4" @click="addScript">
+            {{ t('common.add') }}
+          </ui-button>
+        </ui-tab-panel>
+      </ui-tab-panels>
     </ui-modal>
   </div>
 </template>
 <script setup>
-import { ref, watch } from 'vue';
+import { watch, reactive } from 'vue';
 import { PrismEditor } from 'vue-prism-editor';
 import { useI18n } from 'vue-i18n';
 import { highlighter } from '@/lib/prism';
@@ -77,16 +115,33 @@ const availableFuncs = [
   { name: 'automaResetTimeout', id: 'automaresettimeout' },
 ];
 
-const code = ref(props.data.code);
-const showCodeModal = ref(false);
+const state = reactive({
+  activeTab: 'code',
+  code: `${props.data.code}`,
+  preloadScripts: [...(props.data.preloadScripts || [])],
+  showCodeModal: false,
+});
 
 function updateData(value) {
   emit('update:data', { ...props.data, ...value });
 }
+function addScript() {
+  state.preloadScripts.push({ src: '', removeAfterExec: true });
+}
 
-watch(code, (value) => {
-  updateData({ code: value });
-});
+watch(
+  () => state.code,
+  (value) => {
+    updateData({ code: value });
+  }
+);
+watch(
+  () => state.preloadScripts,
+  (value) => {
+    updateData({ preloadScripts: value });
+  },
+  { deep: true }
+);
 </script>
 <style scoped>
 code {

+ 62 - 20
src/content/blocks-handler.js

@@ -2,6 +2,7 @@
 import simulateEvent from '@/utils/simulate-event';
 import handleFormElement from '@/utils/handle-form-element';
 import { generateJSON } from '@/utils/data-exporter';
+import { sendMessage } from '@/utils/message';
 
 function markElement(el, { id, data }) {
   if (data.markEl) {
@@ -142,32 +143,73 @@ function automaRefData(keyword, path = '') {
       return;
     }
 
-    const script = document.createElement('script');
-    let timeout;
+    const promisePreloadScripts =
+      block.data?.preloadScripts.map(async (item) => {
+        try {
+          const { protocol, pathname } = new URL(item.src);
+          const isValidUrl = /https?/.test(protocol) && /\.js$/.test(pathname);
+
+          if (!isValidUrl) return null;
+
+          const script = await sendMessage(
+            'fetch:text',
+            item.src,
+            'background'
+          );
+          const scriptEl = document.createElement('script');
+
+          scriptEl.type = 'text/javascript';
+          scriptEl.innerHTML = script;
+
+          return {
+            ...item,
+            script: scriptEl,
+          };
+        } catch (error) {
+          return null;
+        }
+      }, []) || [];
 
-    script.setAttribute(scriptAttr, '');
-    script.id = 'automa-custom-js';
-    script.innerHTML = `(() => {\n${automaScript} ${block.data.code}\n})()`;
+    Promise.allSettled(promisePreloadScripts).then((result) => {
+      const preloadScripts = result.reduce((acc, { status, value }) => {
+        if (status !== 'fulfilled' || !value) return acc;
 
-    const cleanUp = (data = '') => {
-      script.remove();
-      sessionStorage.removeItem(`automa--${block.id}`);
-      resolve(data);
-    };
+        acc.push(value);
+        document.body.appendChild(value.script);
 
-    window.addEventListener('__automa-next-block__', ({ detail }) => {
-      clearTimeout(timeout);
-      cleanUp(detail || {});
-    });
-    window.addEventListener('__automa-reset-timeout__', () => {
-      clearTimeout(timeout);
+        return acc;
+      }, []);
 
-      timeout = setTimeout(cleanUp, block.data.timeout);
-    });
+      const script = document.createElement('script');
+      let timeout;
+
+      script.setAttribute(scriptAttr, '');
+      script.id = 'automa-custom-js';
+      script.innerHTML = `(() => {\n${automaScript} ${block.data.code}\n})()`;
+
+      const cleanUp = (data = '') => {
+        script.remove();
+        preloadScripts.forEach((item) => {
+          if (item.removeAfterExec) item.script.remove();
+        });
+        sessionStorage.removeItem(`automa--${block.id}`);
+        resolve(data);
+      };
+
+      window.addEventListener('__automa-next-block__', ({ detail }) => {
+        clearTimeout(timeout);
+        cleanUp(detail || {});
+      });
+      window.addEventListener('__automa-reset-timeout__', () => {
+        clearTimeout(timeout);
 
-    document.body.appendChild(script);
+        timeout = setTimeout(cleanUp, block.data.timeout);
+      });
 
-    timeout = setTimeout(cleanUp, block.data.timeout);
+      document.body.appendChild(script);
+
+      timeout = setTimeout(cleanUp, block.data.timeout);
+    });
   });
 }
 

+ 1 - 0
src/lib/dayjs.js

@@ -1,5 +1,6 @@
 import dayjs from 'dayjs';
 import relativeTime from 'dayjs/plugin/relativeTime';
+import 'dayjs/locale/zh';
 
 dayjs.extend(relativeTime);
 

+ 4 - 1
src/lib/vue-i18n.js

@@ -4,7 +4,6 @@ import { supportLocales } from '@/utils/shared';
 import dayjs from './dayjs';
 
 const i18n = createI18n({
-  locale: 'en',
   legacy: false,
   fallbackLocale: 'en',
 });
@@ -42,6 +41,10 @@ export async function loadLocaleMessages(locale, location) {
 
   dayjs.locale(locale);
 
+  if (locale !== 'en' && !i18n.global.availableLocales.includes('en')) {
+    await loadLocaleMessages('en', location);
+  }
+
   await importLocale('common.json');
   await importLocale(`${location}.json`, true);
   await importLocale('blocks.json', true);

+ 41 - 13
src/locales/en/blocks.json

@@ -23,13 +23,21 @@
       "trigger": {
         "name": "Trigger",
         "description": "Block where the workflow will start executing",
-        "days": ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"],
+        "days": [
+          "Sunday",
+          "Monday",
+          "Tuesday",
+          "Wednesday",
+          "Thursday",
+          "Friday",
+          "Saturday"
+        ],
         "useRegex": "Use regex",
         "shortcut": {
           "tootlip": "Record shortcut",
           "checkboxTitle": "Execute shortcut even when you're in an input element",
           "checkbox": "Active while in input",
-          "note": "Note: keyboard shortcut only working when you're on a webpage",
+          "note": "Note: keyboard shortcut only working when you're on a webpage"
         },
         "forms": {
           "triggerWorkflow": "Trigger workflow",
@@ -47,7 +55,7 @@
           "specific-day": "On a specific day",
           "visit-web": "When visit a website",
           "keyboard-shortcut": "Keyboard shortcut"
-        },
+        }
       },
       "active-tab": {
         "name": "Active tab",
@@ -68,7 +76,7 @@
         "incognito": {
           "text": "Set as incognito window",
           "note": "You must enable 'Allow in incognito' for this extension to use the option"
-        },
+        }
       },
       "go-back": {
         "name": "Go back",
@@ -90,7 +98,11 @@
       },
       "delay": {
         "name": "Delay",
-        "description": "Add delay before executing the next block"
+        "description": "Add delay before executing the next block",
+        "input": {
+          "title": "Delay in millisecond",
+          "placeholder": "(millisecond)"
+        }
       },
       "get-text": {
         "name": "Get text",
@@ -116,7 +128,7 @@
         "intoView": "Scroll into view",
         "smooth": "Smooth scroll",
         "incScrollX": "Increment horizontal scroll",
-        "incScrollY": "Increment vertical scroll",
+        "incScrollY": "Increment vertical scroll"
       },
       "new-tab": {
         "name": "New tab",
@@ -162,12 +174,20 @@
       "repeat-task": {
         "name": "Repeat task",
         "description": "",
+        "times": "times",
+        "repeatFrom": "Repeat from"
       },
       "javascript-code": {
         "name": "JavaScript code",
         "description": "Execute your javascript code in the web page",
-        "modal": "JavaScript code",
-        "availabeFuns": "Available functions:",
+        "availabeFuncs": "Available functions:",
+        "removeAfterExec": "Remove after block executed",
+        "modal": {
+          "tabs": {
+            "code": "JavaScript code",
+            "preloadScript": "Preload script"
+          }
+        },
         "timeout": {
           "placeholder": "Timeout",
           "title": "Javascript code execution timeout"
@@ -180,12 +200,20 @@
       },
       "conditions": {
         "name": "Conditions",
-        "description": "Conditional block"
+        "description": "Conditional block",
+        "fallbackTitle": "Execute when all comparisons don't meet the requirement",
+        "equals": "Equals",
+        "gt": "Greater than",
+        "gte": "Greater than or equal",
+        "lt": "Less than",
+        "lte": "Less than or equal",
+        "contains": "Contains"
       },
       "element-exists": {
         "name": "Element exists",
         "description": "Check if an element is exists",
         "selector": "Element selector",
+        "fallbackTitle": "Execute when element doesn't exists",
         "tryFor": {
           "title": "Try to check if element exist",
           "label": "Try for"
@@ -221,12 +249,12 @@
           "maxFile": "Max file size is 1MB",
           "options": {
             "firstRow": "Use the first row as keys"
-          },
+          }
         },
         "buttons": {
           "clear": "Clear data",
           "insert": "Insert data",
-          "import": "Import file",
+          "import": "Import file"
         },
         "maxLoop": {
           "title": "Max numbers of data to loop",
@@ -237,7 +265,7 @@
           "options": {
             "data-columns": "Data columns",
             "custom-data": "Custom data"
-          },
+          }
         }
       },
       "loop-breakpoint": {
@@ -256,7 +284,7 @@
         "windowTypes": {
           "main": "Main window",
           "iframe": "Iframe"
-        },
+        }
       }
     }
   }

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

@@ -30,6 +30,7 @@
     "disable": "Disable",
     "disabled": "Disabled",
     "enable": "Enable",
+    "fallback": "Fallback"
   },
   "message": {
     "noBlock": "No block",

+ 276 - 0
src/locales/zh/blocks.json

@@ -0,0 +1,276 @@
+{
+  "collection": {
+    "blocks": {
+      "export-result": {
+        "name": "导出结果",
+        "description": "将集合结果导出为JSON"
+      }
+    }
+  },
+  "workflow": {
+    "blocks": {
+      "base": {
+        "selector": "元素选择器",
+        "markElement": {
+          "title": "一个元素如果之前被标记过,则不再标记",
+          "text": "标记元素"
+        },
+        "multiple": {
+          "title": "选择多个元素",
+          "text": "选择多个元素"
+        }
+      },
+      "trigger": {
+        "name": "触发器",
+        "description": "工作流从这里开始执行",
+        "days": [
+          "星期日",
+          "星期一",
+          "星期二",
+          "星期三",
+          "星期四",
+          "星期五",
+          "星期六"
+        ],
+        "useRegex": "使用正则表达式",
+        "shortcut": {
+          "tootlip": "录制快捷键",
+          "checkboxTitle": "即使在输入框中也执行快捷键",
+          "checkbox": "在输入框中执行快捷键",
+          "note": "提示: 键盘快捷键仅在你访问网页时有效"
+        },
+        "forms": {
+          "triggerWorkflow": "触发工作流",
+          "interval": "间隔 (分钟)",
+          "delay": "延迟 (分钟)",
+          "date": "日期",
+          "time": "时间",
+          "url": "网址或正则表达式",
+          "shortcut": "快捷键"
+        },
+        "items": {
+          "manual": "手动",
+          "interval": "一定时间间隔",
+          "date": "在特定日期",
+          "specific-day": "在特定的星期几",
+          "visit-web": "当你访问一个网页",
+          "keyboard-shortcut": "键盘快捷键"
+        }
+      },
+      "active-tab": {
+        "name": "活动选项卡",
+        "description": "将您所在的当前选项卡设置为活动选项卡"
+      },
+      "new-window": {
+        "name": "新窗口",
+        "description": "创建一个新窗口",
+        "windowState": {
+          "placeholder": "窗口状态",
+          "options": {
+            "normal": "标准",
+            "minimized": "最小化",
+            "maximized": "最大化",
+            "fullscreen": "全屏"
+          }
+        },
+        "incognito": {
+          "text": "设为无痕窗口",
+          "note": "您必须为此扩展程序启用“在无痕模式下启用”才能使用该选项"
+        }
+      },
+      "go-back": {
+        "name": "返回",
+        "description": "返回到上一页"
+      },
+      "forward-page": {
+        "name": "前进",
+        "description": "前进到下一页"
+      },
+      "close-tab": {
+        "name": "关闭选项卡",
+        "description": "",
+        "activeTab": "关闭活动选项卡",
+        "url": "网址或匹配模式"
+      },
+      "event-click": {
+        "name": "点击元素",
+        "description": ""
+      },
+      "delay": {
+        "name": "延迟",
+        "description": "在执行下一个块之前添加延迟"
+      },
+      "get-text": {
+        "name": "获取文本",
+        "description": "从元素中获取文本",
+        "prefixText": {
+          "placeholder": "文本前缀",
+          "title": "为文本添加前缀"
+        },
+        "suffixText": {
+          "placeholder": "文本后缀",
+          "title": "为文本添加后缀"
+        }
+      },
+      "export-data": {
+        "name": "导出数据",
+        "description": "导出工作流数据列"
+      },
+      "element-scroll": {
+        "name": "滚动元素",
+        "description": "",
+        "scrollY": "垂直滚动",
+        "scrollX": "水平滚动",
+        "intoView": "滚动到视图中",
+        "smooth": "平滑滚动",
+        "incScrollX": "递增水平滚动",
+        "incScrollY": "递增垂直滚动"
+      },
+      "new-tab": {
+        "name": "新建选项卡",
+        "description": "",
+        "activeTab": "设置为活动选项卡",
+        "tabToGroup": "添加到选项卡组",
+        "updatePrevTab": {
+          "title": "使用之前打开的新选项卡而不是创建一个选项卡",
+          "text": "更新之前打开的选项卡"
+        }
+      },
+      "link": {
+        "name": "链接",
+        "description": "打开链接元素"
+      },
+      "attribute-value": {
+        "name": "属性值",
+        "description": "获取一个元素属性的值",
+        "forms": {
+          "name": "属性名称",
+          "checkbox": "保存数据",
+          "column": "选择列"
+        }
+      },
+      "forms": {
+        "name": "表单",
+        "description": "",
+        "selected": "选中",
+        "type": "表单类型",
+        "text-field": {
+          "name": "文本框",
+          "value": "值",
+          "clearValue": "清除表单值",
+          "delay": {
+            "placeholder": "延迟",
+            "label": "输入延迟(ms)(0为禁用)"
+          }
+        },
+        "select": { "name": "下拉列表" },
+        "radio": { "name": "单选框" },
+        "checkbox": { "name": "复选框" }
+      },
+      "repeat-task": {
+        "name": "重复任务",
+        "description": ""
+      },
+      "javascript-code": {
+        "name": "JavaScript 代码",
+        "description": "在网页中执行你的 javascript 代码",
+        "modal": {
+          "tabs": {
+            "code": "JavaScript 代码",
+            "preloadScript": "Preload script"
+          }
+        },
+        "availabeFuncs": "可用的函数:",
+        "timeout": {
+          "placeholder": "超时",
+          "title": "Javascript代码执行超时"
+        }
+      },
+      "trigger-event": {
+        "name": "触发事件",
+        "description": "",
+        "selectEvent": "选择事件"
+      },
+      "conditions": {
+        "name": "条件",
+        "description": "条件单元"
+      },
+      "element-exists": {
+        "name": "元素存在",
+        "description": "检查元素是否存在",
+        "selector": "元素选择器",
+        "tryFor": {
+          "title": "检查元素是否存在",
+          "label": "检查"
+        },
+        "timeout": {
+          "label": "超时 (毫秒)",
+          "title": "每次检查元素的超时时间"
+        }
+      },
+      "webhook": {
+        "name": "Webhook",
+        "description": "Webhook 允许通知外部服务",
+        "url": "接收POST请求的URL",
+        "contentType": "选择content type",
+        "buttons": {
+          "header": "添加请求头"
+        },
+        "timeout": {
+          "placeholder": "超时",
+          "title": "Http请求执行超时(ms)"
+        },
+        "tabs": {
+          "headers": "Headers",
+          "body": "Content body"
+        }
+      },
+      "loop-data": {
+        "name": "循环数据",
+        "description": "遍历数据列或你的自定义数据",
+        "loopId": "循环 ID",
+        "modal": {
+          "fileTooLarge": "文件太大无法编辑",
+          "maxFile": "最大文件大小为 1MB",
+          "options": {
+            "firstRow": "使用第一个 row 作为 keys"
+          }
+        },
+        "buttons": {
+          "clear": "清除数据",
+          "insert": "插入数据",
+          "import": "导出文件"
+        },
+        "maxLoop": {
+          "title": "循环数据的最大值",
+          "label": "循环数据的最大值(0为禁用)"
+        },
+        "loopThrough": {
+          "placeholder": "循环数据源",
+          "options": {
+            "data-columns": "数据列",
+            "custom-data": "自定义数据"
+          }
+        }
+      },
+      "loop-breakpoint": {
+        "name": "循环断点",
+        "description": "告诉循环数据单元在哪里停止"
+      },
+      "take-screenshot": {
+        "name": "截屏",
+        "description": "对当前活动选项卡截屏",
+        "imageQuality": "图像质量"
+      },
+      "switch-to": {
+        "name": "切换到",
+        "description": "在主窗口和iframe之间切换",
+        "iframeSelector": "iframe元素选择器",
+        "windowTypes": {
+          "main": "主窗口",
+          "iframe": "iframe"
+        }
+      }
+    }
+  }
+}

+ 54 - 0
src/locales/zh/common.json

@@ -0,0 +1,54 @@
+{
+  "common": {
+    "dashboard": "主面板",
+    "workflow": "工作流 | 工作流",
+    "collection": "集合 | 集合",
+    "log": "日志 | 日志",
+    "block": "单元 | 单元",
+    "docs": "文档",
+    "search": "搜索",
+    "import": "导入",
+    "export": "导出",
+    "rename": "重命名",
+    "execute": "执行",
+    "delete": "删除",
+    "cancel": "取消",
+    "settings": "设置",
+    "options": "选项",
+    "confirm": "确认",
+    "name": "名称",
+    "all": "全部",
+    "add": "添加",
+    "save": "保存",
+    "data": "数据",
+    "stop": "停止",
+    "editor": "编辑器",
+    "running": "运行",
+    "globalData": "全局数据",
+    "fileName": "文件名",
+    "description": "描述",
+    "disable": "禁用",
+    "disabled": "已禁用",
+    "enable": "启用"
+  },
+  "message": {
+    "noBlock": "没有工作单元",
+    "noData": "没有数据可以展示",
+    "noTriggerBlock": "没有触发器",
+    "useDynamicData": "了解如何添加动态数据",
+    "delete": "确定要删除\"{name}\"?",
+    "empty": "哎呀……你好像没有任何项目",
+    "notSaved": "你真的要退出吗? 你有未保存的更改!",
+    "maxSizeExceeded": "文件大小超出了允许的最大值"
+  },
+  "sort": {
+    "sortBy": "排序方式",
+    "name": "名称",
+    "createdAt": "创建时间"
+  },
+  "logStatus": {
+    "stopped": "停止",
+    "error": "错误",
+    "success": "成功"
+  }
+}

+ 128 - 0
src/locales/zh/newtab.json

@@ -0,0 +1,128 @@
+{
+  "home": {
+    "viewAll": "查看全部"
+  },
+  "settings": {
+    "language": {
+      "label": "语言",
+      "helpTranslate": "找不到你的语言? 帮忙翻译。",
+      "reloadPage": "重新加载页面生效"
+    }
+  },
+  "workflow": {
+    "import": "导入工作流",
+    "new": "新建工作流",
+    "delete": "删除工作流",
+    "name": "工作流名称",
+    "rename": "重命名工作流",
+    "add": "添加工作流",
+    "clickToEnable": "点击启用",
+    "dataColumns": {
+      "title": "数据列",
+      "placeholder": "搜索或者添加数据列",
+      "column": {
+        "name": "列名称",
+        "type": "数据类型"
+      }
+    },
+    "sidebar": {
+      "workflowIcon": "工作流图标"
+    },
+    "editor": {
+      "zoomIn": "缩小",
+      "zoomOut": "放大",
+      "resetZoom": "重置缩放",
+      "duplicate": "创建副本"
+    },
+    "settings": {
+      "onError": {
+        "title": "工作流出错",
+        "items": {
+          "keepRunning": "继续运行",
+          "stopWorkflow": "停止工作流"
+        }
+      },
+      "timeout": {
+        "title": "工作流超时 (毫秒)"
+      }
+    }
+  },
+  "collection": {
+    "description": "按顺序执行工作流",
+    "new": "新建集合",
+    "delete": "删除集合",
+    "add": "添加集合",
+    "rename": "重命名集合",
+    "flow": "流程",
+    "dragDropText": "拖放工作流或者单元到这里",
+    "options": {
+      "atOnce": {
+        "title": "一次执行集合中的所有工作流",
+        "description": "使用此选项时不会执行单元下的项目"
+      }
+    },
+    "globalData": {
+      "note": "这将覆盖工作流的全局数据"
+    }
+  },
+  "log": {
+    "goBack": "返回到\"{name}\"的日志",
+    "startedDate": "开始日期",
+    "duration": "持续时间",
+    "selectAll": "全选",
+    "deselectAll": "取消全选",
+    "deleteSelected": "删除选定的日志",
+    "types": {
+      "stop": "工作流已停止",
+      "finish": "完成"
+    },
+    "messages": {
+      "workflow-disabled": "工作流被禁用",
+      "stop-timeout": "工作流因超时而停止",
+      "no-iframe-id": "找不到\"{selector}\"选择的iframe",
+      "no-tab": "无法连接到选项卡,请在使用 \"{name}\" 单元之前使用 \"新建选项卡\" 或 \"活动选项卡\" 单元。"
+    },
+    "description": {
+      "text": "在{date}用了{duration}执行{status} ",
+      "status": {
+        "success": "成功",
+        "error": "失败",
+        "stopped": "停止"
+      }
+    },
+    "delete": {
+      "title": "删除日志",
+      "description": "你确定要删除选定的日志吗?"
+    },
+    "exportData": {
+      "title": "导出数据",
+      "types": {
+        "json": "JSON",
+        "csv": "CSV",
+        "plain-text": "Plain text"
+      }
+    },
+    "filter": {
+      "title": "筛选",
+      "byStatus": "按状态",
+      "byDate": {
+        "title": "按日期",
+        "items": {
+          "lastDay": "最近一天",
+          "last7Days": "最近7天",
+          "last30Days": "最近30天"
+        }
+      }
+    }
+  },
+  "components": {
+    "pagination": {
+      "text1": "显示",
+      "text2": "条 共{count}条",
+      "nextPage": "下一页",
+      "currentPage": "当前页",
+      "prevPage": "上一页",
+      "of": "共{page}页"
+    }
+  }
+}

+ 13 - 0
src/locales/zh/popup.json

@@ -0,0 +1,13 @@
+{
+  "home": {
+    "elementSelector": {
+      "name": "元素选择器",
+      "noAccess": "您没有权限访问此站点"
+    },
+    "workflow": {
+      "new": "新建工作流",
+      "rename": "重命名工作流",
+      "delete": "删除工作流"
+    }
+  }
+}

+ 32 - 30
src/newtab/pages/logs.vue

@@ -7,37 +7,39 @@
       @updateSorts="sortsBuilder[$event.key] = $event.value"
       @updateFilters="filtersBuilder[$event.key] = $event.value"
     />
-    <shared-logs-table :logs="logs" class="w-full" style="min-height: 320px">
-      <template #item-prepend="{ log }">
-        <td class="w-8">
-          <ui-checkbox
-            :model-value="selectedLogs.includes(log.id)"
-            class="align-text-bottom"
-            @change="toggleSelectedLog($event, log.id)"
-          />
-        </td>
-      </template>
-      <template #item-append="{ log }">
-        <td class="ml-4">
-          <div class="flex items-center justify-end space-x-4">
-            <v-remixicon
-              v-if="Object.keys(log.data).length !== 0"
-              name="riFileTextLine"
-              class="cursor-pointer"
-              @click="
-                exportDataModal.show = true;
-                exportDataModal.log = log;
-              "
-            />
-            <v-remixicon
-              name="riDeleteBin7Line"
-              class="text-red-500 cursor-pointer"
-              @click="deleteLog(log.id)"
+    <div style="min-height: 320px">
+      <shared-logs-table :logs="logs" class="w-full">
+        <template #item-prepend="{ log }">
+          <td class="w-8">
+            <ui-checkbox
+              :model-value="selectedLogs.includes(log.id)"
+              class="align-text-bottom"
+              @change="toggleSelectedLog($event, log.id)"
             />
-          </div>
-        </td>
-      </template>
-    </shared-logs-table>
+          </td>
+        </template>
+        <template #item-append="{ log }">
+          <td class="ml-4">
+            <div class="flex items-center justify-end space-x-4">
+              <v-remixicon
+                v-if="Object.keys(log.data).length !== 0"
+                name="riFileTextLine"
+                class="cursor-pointer"
+                @click="
+                  exportDataModal.show = true;
+                  exportDataModal.log = log;
+                "
+              />
+              <v-remixicon
+                name="riDeleteBin7Line"
+                class="text-red-500 cursor-pointer"
+                @click="deleteLog(log.id)"
+              />
+            </div>
+          </td>
+        </template>
+      </shared-logs-table>
+    </div>
     <div class="flex items-center justify-between mt-4">
       <div>
         {{ t('components.pagination.text1') }}

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

@@ -302,7 +302,7 @@ provide('workflow', {
 onBeforeRouteLeave(() => {
   if (!state.isDataChanged) return;
 
-  const answer = window.confirm(t('common.notSaved'));
+  const answer = window.confirm(t('message.notSaved'));
 
   if (!answer) return false;
 });
@@ -315,7 +315,7 @@ onMounted(() => {
 
   window.onbeforeunload = () => {
     if (state.isDataChanged) {
-      return t('common.notSaved');
+      return t('message.notSaved');
     }
   };
 

+ 5 - 1
src/utils/shared.js

@@ -334,6 +334,7 @@ export const tasks = {
       description: '',
       timeout: 10000,
       code: 'console.log("Hello world!")',
+      preloadScripts: [],
     },
   },
   'trigger-event': {
@@ -527,4 +528,7 @@ export const contentTypes = [
   { name: 'application/x-www-form-urlencoded', value: 'form' },
 ];
 
-export const supportLocales = [{ id: 'en', name: 'English' }];
+export const supportLocales = [
+  { id: 'en', name: 'English' },
+  { id: 'zh', name: '简体中文' },
+];