Browse Source

feat: firefox compatibility

Ahmad Kholid 3 years ago
parent
commit
b0a4e9f00c
35 changed files with 316 additions and 97 deletions
  1. 18 3
      README.md
  2. 11 3
      package.json
  3. 2 2
      src/background/index.js
  4. 7 0
      src/background/workflowEngine/blocksHandler/handlerHandleDialog.js
  5. 3 3
      src/background/workflowEngine/blocksHandler/handlerHandleDownload.js
  6. 6 0
      src/background/workflowEngine/blocksHandler/handlerHoverElement.js
  7. 1 3
      src/background/workflowEngine/blocksHandler/handlerInteractionBlock.js
  8. 8 5
      src/background/workflowEngine/blocksHandler/handlerNewTab.js
  9. 3 2
      src/background/workflowEngine/blocksHandler/handlerProxy.js
  10. 18 7
      src/background/workflowEngine/blocksHandler/handlerTakeScreenshot.js
  11. 5 2
      src/background/workflowEngine/engine.js
  12. 3 1
      src/background/workflowEngine/helper.js
  13. 1 0
      src/background/workflowEngine/worker.js
  14. 2 1
      src/components/newtab/app/AppSidebar.vue
  15. 2 1
      src/components/newtab/workflow/WorkflowBuilder.vue
  16. 19 2
      src/components/newtab/workflow/WorkflowSettings.vue
  17. 27 18
      src/components/newtab/workflow/edit/EditHandleDialog.vue
  18. 17 14
      src/components/newtab/workflow/edit/EditNewTab.vue
  19. 1 1
      src/components/newtab/workflow/edit/EditProxy.vue
  20. 21 4
      src/components/newtab/workflow/edit/EditUploadFile.vue
  21. 4 2
      src/content/elementSelector/index.js
  22. 1 0
      src/locales/en/blocks.json
  23. 1 0
      src/locales/en/common.json
  24. 1 0
      src/locales/en/newtab.json
  25. 0 0
      src/manifest.chrome.json
  26. 72 0
      src/manifest.firefox.json
  27. 2 2
      src/newtab/App.vue
  28. 2 1
      src/newtab/pages/settings/SettingsAbout.vue
  29. 2 1
      src/newtab/pages/workflows/[id].vue
  30. 8 1
      src/utils/message.js
  31. 2 1
      src/utils/workflowData.js
  32. 4 2
      utils/build-zip.js
  33. 1 0
      utils/env.js
  34. 29 10
      webpack.config.js
  35. 12 5
      yarn.lock

+ 18 - 3
README.md

@@ -12,22 +12,37 @@ Auto-fill forms, do a repetitive task, take a screenshot, or scrape website data
 Browse the Automa marketplace where you can share and download workflows with others. [Go to the marketplace »](https://automa.vercel.app/workflows)
 
 ## Project setup
+Before running the `yarn dev` or `yarn build` script, you need to create the `getPassKey.js` file in the `src/utils` directory.  Inside the file write
+
+```js
+export default function() {
+  return 'anything-you-want';
+}
+```
+
 ```bash
 # Install dependencies
 yarn install
 
-# Compiles and hot-reloads for development
+# Compiles and hot-reloads for development for the chrome browser
 yarn dev
 
-# Compiles and minifies for production
+# Compiles and minifies for production for the chrome browser
 yarn build
 
-# Create a zip file from the build folder
+# Create a zip file from the build folder for the chrome browser
 yarn build:zip
 
+# Compiles and hot-reloads for development for the firefox browser
+yarn dev:firefox
+
+# Compiles and minifies for production for the firefox browser
+yarn build:firefox
+
 # Lints and fixes files
 yarn lint
 ```
+
 ## Contributors
 Thanks to everyone who has submitted issues, made suggestions, and generally helped make this a better project.
 

+ 11 - 3
package.json

@@ -1,6 +1,6 @@
 {
   "name": "automa",
-  "version": "1.9.4",
+  "version": "1.9.4-beta",
   "description": "An extension for automating your browser by connecting blocks",
   "license": "MIT",
   "repository": {
@@ -9,12 +9,19 @@
   },
   "scripts": {
     "build": "node utils/build.js",
+    "build:firefox": "cross-env BROWSER=firefox yarn build",
     "build:zip": "node utils/build-zip.js",
-    "build:prod": "yarn build && yarn build:zip",
+    "build:prod": "yarn build:prod-chrome && yarn build:prod-firefox",
+    "build:prod-chrome": "yarn build && yarn build:zip",
+    "build:prod-firefox": "yarn build:firefox && cross-env BROWSER=firefox yarn build:zip",
     "dev": "node utils/webserver.js",
+    "dev:firefox": "cross-env BROWSER=firefox yarn dev",
     "prettier": "prettier --write '**/*.{js,jsx,css,html}'",
     "lint": "eslint --ext .js,.vue --ignore-path .gitignore ."
   },
+  "engines": {
+    "node": ">=14.18.1"
+  },
   "simple-git-hooks": {
     "pre-commit": "npx lint-staged"
   },
@@ -57,7 +64,7 @@
     "vue-toastification": "^2.0.0-rc.5",
     "vuedraggable": "^4.1.0",
     "vuex": "^4.0.2",
-    "webextension-polyfill": "^0.8.0"
+    "webextension-polyfill": "^0.9.0"
   },
   "devDependencies": {
     "@babel/core": "7.15.5",
@@ -72,6 +79,7 @@
     "clean-webpack-plugin": "4.0.0",
     "copy-webpack-plugin": "9.0.1",
     "core-js": "3",
+    "cross-env": "^7.0.3",
     "css-loader": "5.2.7",
     "eslint": "7.32.0",
     "eslint-config-airbnb-base": "^14.2.1",

+ 2 - 2
src/background/index.js

@@ -306,7 +306,7 @@ browser.alarms.onAlarm.addListener(async ({ name }) => {
   }
 });
 
-chrome.runtime.onInstalled.addListener(async ({ reason }) => {
+browser.runtime.onInstalled.addListener(async ({ reason }) => {
   try {
     if (reason === 'install') {
       await browser.storage.local.set({
@@ -347,7 +347,7 @@ chrome.runtime.onInstalled.addListener(async ({ reason }) => {
     console.error(error);
   }
 });
-chrome.runtime.onStartup.addListener(async () => {
+browser.runtime.onStartup.addListener(async () => {
   const { onStartupTriggers, workflows } = await browser.storage.local.get([
     'onStartupTriggers',
     'workflows',

+ 7 - 0
src/background/workflowEngine/blocksHandler/handlerHandleDialog.js

@@ -4,6 +4,13 @@ function handleDialog({ data, outputs }) {
   const nextBlockId = getBlockConnection({ outputs });
 
   return new Promise((resolve, reject) => {
+    if (BROWSER_TYPE !== 'chrome') {
+      const error = new Error('log.messages.browser-not-supported');
+      error.data = { browser: BROWSER_TYPE };
+
+      reject(error);
+      return;
+    }
     if (!this.settings.debugMode) {
       const error = new Error('not-debug-mode');
       error.nextBlockId = nextBlockId;

+ 3 - 3
src/background/workflowEngine/blocksHandler/handlerHandleDownload.js

@@ -36,9 +36,9 @@ function handleDownload({ data, outputs }) {
   return new Promise((resolve) => {
     if (!this.activeTab.id) throw new Error('no-tab');
 
-    const hasListener = chrome.downloads.onDeterminingFilename.hasListeners(
-      () => {}
-    );
+    const hasListener =
+      BROWSER_TYPE === 'chrome' &&
+      chrome.downloads.onDeterminingFilename.hasListeners(() => {});
     if (!hasListener) {
       chrome.downloads.onDeterminingFilename.addListener(
         determineFilenameListener

+ 6 - 0
src/background/workflowEngine/blocksHandler/handlerHoverElement.js

@@ -5,6 +5,12 @@ export async function hoverElement(block) {
 
   try {
     if (!this.activeTab.id) throw new Error('no-tab');
+    if (BROWSER_TYPE !== 'chrome') {
+      const error = new Error('browser-not-supported');
+      error.data = { browser: BROWSER_TYPE };
+
+      throw error;
+    }
 
     const { debugMode, executedBlockOnWeb } = this.settings;
 

+ 1 - 3
src/background/workflowEngine/blocksHandler/handlerInteractionBlock.js

@@ -4,9 +4,7 @@ import { getBlockConnection } from '../helper';
 
 async function checkAccess(blockName) {
   if (blockName === 'upload-file') {
-    const hasFileAccess = await new Promise((resolve) =>
-      chrome.extension.isAllowedFileSchemeAccess(resolve)
-    );
+    const hasFileAccess = await browser.extension.isAllowedFileSchemeAccess();
 
     if (hasFileAccess) return true;
 

+ 8 - 5
src/background/workflowEngine/blocksHandler/handlerNewTab.js

@@ -32,6 +32,7 @@ async function newTab(block) {
     }
 
     let tab = null;
+    const isChrome = BROWSER_TYPE === 'chrome';
 
     if (updatePrevTab && this.activeTab.id) {
       tab = await browser.tabs.update(this.activeTab.id, { url, active });
@@ -49,7 +50,7 @@ async function newTab(block) {
         await attachDebugger(tab.id, this.activeTab.id);
         this.debugAttached = true;
 
-        if (customUserAgent) {
+        if (customUserAgent && isChrome) {
           await sendDebugCommand(tab.id, 'Network.setUserAgentOverride', {
             userAgent,
           });
@@ -74,14 +75,16 @@ async function newTab(block) {
         };
       }
 
-      chrome.tabs.group(options, (tabGroupId) => {
-        this.activeTab.groupId = tabGroupId;
-      });
+      if (isChrome) {
+        chrome.tabs.group(options, (tabGroupId) => {
+          this.activeTab.groupId = tabGroupId;
+        });
+      }
     }
 
     this.activeTab.frameId = 0;
 
-    if (!this.settings.debugMode && customUserAgent) {
+    if (isChrome && !this.settings.debugMode && customUserAgent) {
       chrome.debugger.detach({ tabId: tab.id });
     }
 

+ 3 - 2
src/background/workflowEngine/blocksHandler/handlerProxy.js

@@ -1,3 +1,4 @@
+import browser from 'webextension-polyfill';
 import { isWhitespace } from '@/utils/helper';
 import { getBlockConnection } from '../helper';
 
@@ -6,7 +7,7 @@ function setProxy({ data, outputs }) {
 
   return new Promise((resolve, reject) => {
     if (data.clearProxy) {
-      chrome.proxy.settings.clear({});
+      browser.proxy.settings.clear({});
     }
 
     const config = {
@@ -46,7 +47,7 @@ function setProxy({ data, outputs }) {
       config.rules.singleProxy.port = data.port;
     }
 
-    chrome.proxy.settings.set({ value: config, scope: 'regular' }, () => {
+    browser.proxy.settings.set({ value: config, scope: 'regular' }).then(() => {
       this.engine.isUsingProxy = true;
 
       resolve({

+ 18 - 7
src/background/workflowEngine/blocksHandler/handlerTakeScreenshot.js

@@ -60,13 +60,24 @@ async function takeScreenshot({ data, outputs, name }) {
         throw new Error('no-tab');
       }
 
-      const [tab] = await browser.tabs.query({
-        active: true,
-        currentWindow: true,
-      });
+      let tab = null;
+      const isChrome = BROWSER_TYPE === 'chrome';
+      const captureTab = () => {
+        console.log(isChrome, BROWSER_TYPE);
+        if (isChrome) return browser.tabs.captureVisibleTab(options);
+
+        return browser.tabs.captureTab(this.activeTab.id, options);
+      };
+
+      if (isChrome) {
+        [tab] = await browser.tabs.query({
+          active: true,
+          currentWindow: true,
+        });
 
-      if (this.windowId) {
-        await browser.windows.update(this.windowId, { focused: true });
+        if (this.windowId) {
+          await browser.windows.update(this.windowId, { focused: true });
+        }
       }
 
       await browser.tabs.update(this.activeTab.id, { active: true });
@@ -81,7 +92,7 @@ async function takeScreenshot({ data, outputs, name }) {
             selector: data.selector,
             tabId: this.activeTab.id,
           })
-        : browser.tabs.captureVisibleTab(options));
+        : captureTab());
 
       if (tab) {
         await browser.windows.update(tab.windowId, { focused: true });

+ 5 - 2
src/background/workflowEngine/engine.js

@@ -111,6 +111,9 @@ class WorkflowEngine {
       this.columns[columnId] = { index: 0, name, type };
     });
 
+    if (BROWSER_TYPE !== 'chrome') {
+      this.workflow.settings.debugMode = false;
+    }
     if (this.workflow.settings.debugMode) {
       chrome.debugger.onEvent.addListener(this.onDebugEvent);
     }
@@ -240,8 +243,8 @@ class WorkflowEngine {
   async destroy(status, message) {
     try {
       if (this.isDestroyed) return;
-      if (this.isUsingProxy) chrome.proxy.settings.clear({});
-      if (this.workflow.settings.debugMode) {
+      if (this.isUsingProxy) browser.proxy.settings.clear({});
+      if (this.workflow.settings.debugMode && BROWSER_TYPE === 'chrome') {
         chrome.debugger.onEvent.removeListener(this.onDebugEvent);
 
         await sleep(1000);

+ 3 - 1
src/background/workflowEngine/helper.js

@@ -1,3 +1,5 @@
+import browser from 'webextension-polyfill';
+
 export function sendDebugCommand(tabId, method, params = {}) {
   return new Promise((resolve) => {
     chrome.debugger.sendCommand({ tabId }, method, params, resolve);
@@ -18,7 +20,7 @@ export function attachDebugger(tabId, prevTab) {
 export function waitTabLoaded(tabId) {
   return new Promise((resolve, reject) => {
     const activeTabStatus = () => {
-      chrome.tabs.get(tabId, (tab) => {
+      browser.tabs.get(tabId).then((tab) => {
         if (!tab) {
           reject(new Error('no-tab'));
           return;

+ 1 - 0
src/background/workflowEngine/worker.js

@@ -199,6 +199,7 @@ class Worker {
         this.engine.destroyWorker(this.id);
       }
     } catch (error) {
+      console.error(error);
       const { onError: blockOnError } = replacedBlock.data;
       if (blockOnError && blockOnError.enable) {
         if (blockOnError.retry && blockOnError.retryTimes) {

+ 2 - 1
src/components/newtab/app/AppSidebar.vue

@@ -84,6 +84,7 @@ import { ref } from 'vue';
 import { useStore } from 'vuex';
 import { useI18n } from 'vue-i18n';
 import { useRouter } from 'vue-router';
+import browser from 'webextension-polyfill';
 import { useShortcut, getShortcut } from '@/composable/shortcut';
 import { useGroupTooltip } from '@/composable/groupTooltip';
 import { communities } from '@/utils/shared';
@@ -94,7 +95,7 @@ const { t } = useI18n();
 const store = useStore();
 const router = useRouter();
 
-const extensionVersion = chrome.runtime.getManifest().version;
+const extensionVersion = browser.runtime.getManifest().version;
 const tabs = [
   {
     id: 'dashboard',

+ 2 - 1
src/components/newtab/workflow/WorkflowBuilder.vue

@@ -80,6 +80,7 @@ import { useI18n } from 'vue-i18n';
 import { compare } from 'compare-versions';
 import defu from 'defu';
 import SelectionArea from '@viselect/vanilla';
+import browser from 'webextension-polyfill';
 import emitter from '@/lib/mitt';
 import {
   useShortcut,
@@ -668,7 +669,7 @@ export default {
 
         if (!data || !data?.drawflow?.Home) return;
 
-        const currentExtVersion = chrome.runtime.getManifest().version;
+        const currentExtVersion = browser.runtime.getManifest().version;
         const isOldWorkflow = compare(
           currentExtVersion,
           props.version || '0.0.0',

+ 19 - 2
src/components/newtab/workflow/WorkflowSettings.vue

@@ -51,11 +51,26 @@
         <p>
           {{ item.name }}
         </p>
-        <p class="text-gray-600 dark:text-gray-200 text-sm leading-tight">
+        <p
+          v-if="item.notSupport?.includes(browserType)"
+          class="text-sm leading-tight text-red-400 dark:text-red-300"
+        >
+          {{
+            t('log.messages.browser-not-supported', { browser: browserType })
+          }}
+        </p>
+        <p
+          v-else
+          class="text-gray-600 dark:text-gray-200 text-sm leading-tight"
+        >
           {{ item.description }}
         </p>
       </div>
-      <ui-switch v-model="settings[item.id]" class="mr-4" />
+      <ui-switch
+        v-if="!item.notSupport?.includes(browserType)"
+        v-model="settings[item.id]"
+        class="mr-4"
+      />
     </div>
     <div class="flex items-center pt-4">
       <div class="mr-4 flex-1">
@@ -89,6 +104,7 @@ const emit = defineEmits(['update']);
 const { t } = useI18n();
 const toast = useToast();
 
+const browserType = BROWSER_TYPE;
 const onError = [
   {
     id: 'keep-running',
@@ -106,6 +122,7 @@ const onError = [
 const settingItems = [
   {
     id: 'debugMode',
+    notSupport: ['firefox'],
     name: t('workflow.settings.debugMode.title'),
     description: t('workflow.settings.debugMode.description'),
   },

+ 27 - 18
src/components/newtab/workflow/edit/EditHandleDialog.vue

@@ -6,25 +6,33 @@
       :placeholder="t('common.description')"
       @change="updateData({ description: $event })"
     />
-    <ui-checkbox
-      :model-value="data.accept"
-      block
-      class="mt-4"
-      @change="updateData({ accept: $event })"
+    <p
+      v-if="browserType !== 'chrome'"
+      class="text-sm leading-tight text-red-400 dark:text-red-300 mt-4"
     >
-      {{ t('workflow.blocks.handle-dialog.accept') }}
-    </ui-checkbox>
-    <edit-autocomplete v-if="data.accept" class="mt-1">
-      <ui-input
-        :model-value="data.promptText"
-        :label="t('workflow.blocks.handle-dialog.promptText.label')"
-        :title="t('workflow.blocks.handle-dialog.promptText.description')"
-        autocomplete="off"
-        placeholder="Text"
-        class="w-full"
-        @change="updateData({ promptText: $event })"
-      />
-    </edit-autocomplete>
+      {{ t('log.messages.browser-not-supported', { browser: browserType }) }}
+    </p>
+    <template v-else>
+      <ui-checkbox
+        :model-value="data.accept"
+        block
+        class="mt-4"
+        @change="updateData({ accept: $event })"
+      >
+        {{ t('workflow.blocks.handle-dialog.accept') }}
+      </ui-checkbox>
+      <edit-autocomplete v-if="data.accept" class="mt-1">
+        <ui-input
+          :model-value="data.promptText"
+          :label="t('workflow.blocks.handle-dialog.promptText.label')"
+          :title="t('workflow.blocks.handle-dialog.promptText.description')"
+          autocomplete="off"
+          placeholder="Text"
+          class="w-full"
+          @change="updateData({ promptText: $event })"
+        />
+      </edit-autocomplete>
+    </template>
   </div>
 </template>
 <script setup>
@@ -40,6 +48,7 @@ const props = defineProps({
 const emit = defineEmits(['update:data']);
 
 const { t } = useI18n();
+const browserType = BROWSER_TYPE;
 
 function updateData(value) {
   emit('update:data', { ...props.data, ...value });

+ 17 - 14
src/components/newtab/workflow/edit/EditNewTab.vue

@@ -31,20 +31,22 @@
     >
       {{ t('workflow.blocks.new-tab.activeTab') }}
     </ui-checkbox>
-    <ui-checkbox
-      :model-value="data.inGroup"
-      @change="updateData({ inGroup: $event })"
-    >
-      {{ t('workflow.blocks.new-tab.tabToGroup') }}
-    </ui-checkbox>
-    <ui-checkbox
-      :model-value="data.customUserAgent"
-      block
-      class="mt-2"
-      @change="updateData({ customUserAgent: $event })"
-    >
-      {{ t('workflow.blocks.new-tab.customUserAgent') }}
-    </ui-checkbox>
+    <template v-if="browserType === 'chrome'">
+      <ui-checkbox
+        :model-value="data.inGroup"
+        @change="updateData({ inGroup: $event })"
+      >
+        {{ t('workflow.blocks.new-tab.tabToGroup') }}
+      </ui-checkbox>
+      <ui-checkbox
+        :model-value="data.customUserAgent"
+        block
+        class="mt-2"
+        @change="updateData({ customUserAgent: $event })"
+      >
+        {{ t('workflow.blocks.new-tab.customUserAgent') }}
+      </ui-checkbox>
+    </template>
     <ui-input
       v-if="data.customUserAgent"
       :model-value="data.userAgent"
@@ -67,6 +69,7 @@ const props = defineProps({
 const emit = defineEmits(['update:data']);
 
 const { t } = useI18n();
+const browserType = BROWSER_TYPE;
 
 function updateData(value) {
   emit('update:data', { ...props.data, ...value });

+ 1 - 1
src/components/newtab/workflow/edit/EditProxy.vue

@@ -69,7 +69,7 @@ const emit = defineEmits(['update:data']);
 
 const { t } = useI18n();
 
-const schemes = Object.values(chrome.proxy.Scheme);
+const schemes = ['http', 'https', 'socks4', 'socks5'];
 
 function updateData(value) {
   emit('update:data', { ...props.data, ...value });

+ 21 - 4
src/components/newtab/workflow/edit/EditUploadFile.vue

@@ -1,6 +1,19 @@
 <template>
-  <edit-interaction-base v-bind="{ data, hide: hideBase }" @change="updateData">
-    <template v-if="hasFileAccess">
+  <edit-interaction-base
+    class="mb-8"
+    v-bind="{ data, hide: hideBase }"
+    @change="updateData"
+  >
+    <template v-if="hasFileAccess || browserType === 'firefox'">
+      <div
+        v-if="browserType === 'firefox'"
+        class="mt-4 p-2 rounded-lg bg-primary mt-4 flex text-white items-start"
+      >
+        <v-remixicon name="riErrorWarningLine" size="20" />
+        <div class="ml-2 flex-1 leading-tight text-sm">
+          <p>{{ t('workflow.blocks.upload-file.onlyURL') }}</p>
+        </div>
+      </div>
       <div class="mt-4 space-y-2">
         <div
           v-for="(path, index) in filePaths"
@@ -27,7 +40,9 @@
       </ui-button>
     </template>
     <template v-else>
-      <div class="mt-4 p-2 rounded-lg bg-red-200 flex items-start">
+      <div
+        class="mt-4 p-2 rounded-lg bg-red-200 dark:bg-red-400 flex items-start"
+      >
         <v-remixicon name="riErrorWarningLine" />
         <div class="ml-2 flex-1 leading-tight">
           <p>{{ t('workflow.blocks.upload-file.noFileAccess') }}</p>
@@ -47,6 +62,7 @@
 <script setup>
 import { useI18n } from 'vue-i18n';
 import { ref, watch } from 'vue';
+import browser from 'webextension-polyfill';
 import EditInteractionBase from './EditInteractionBase.vue';
 import EditAutocomplete from './EditAutocomplete.vue';
 
@@ -63,6 +79,7 @@ const props = defineProps({
 const emit = defineEmits(['update:data']);
 
 const { t } = useI18n();
+const browserType = BROWSER_TYPE;
 
 const filePaths = ref([...props.data.filePaths]);
 const hasFileAccess = ref(true);
@@ -71,7 +88,7 @@ function updateData(value) {
   emit('update:data', { ...props.data, ...value });
 }
 
-chrome.extension.isAllowedFileSchemeAccess((value) => {
+browser.extension.isAllowedFileSchemeAccess().then((value) => {
   hasFileAccess.value = value;
 });
 

+ 4 - 2
src/content/elementSelector/index.js

@@ -3,7 +3,9 @@ import initElementSelector from './main';
 
 async function getStyles() {
   try {
-    const response = await fetch(chrome.runtime.getURL('/elementSelector.css'));
+    const response = await fetch(
+      browser.runtime.getURL('/elementSelector.css')
+    );
     const mainCSS = await response.text();
 
     const fontCSS = `
@@ -14,7 +16,7 @@ async function getStyles() {
         font-display: swap;
         font-style: normal;
         font-named-instance: "Regular";
-        src: url('${chrome.runtime.getURL(
+        src: url('${browser.runtime.getURL(
           '/Inter-roman-latin.var.woff2'
         )}') format("woff2");
       }

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

@@ -138,6 +138,7 @@
         "description": "Upload file into <input type=\"file\"> element",
         "filePath": "URL or File path",
         "addFile": "Add file",
+        "onlyURL": "Only support upload files from an URL in the Firefox browser",
         "requirement": "See the requirement before using this block",
         "noFileAccess": "Automa doesn't have file access"
       },

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

@@ -33,6 +33,7 @@
     "enable": "Enable",
     "fallback": "Fallback",
     "update": "Update",
+    "feature": "Feature",
     "duplicate": "Duplicate",
     "password": "Password",
     "category": "Category",

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

@@ -287,6 +287,7 @@
       "no-workflow": "Can't find workflow with \"{workflowId}\" ID",
       "no-match-tab": "Can't find a tab with \"{pattern}\" patterns",
       "no-clipboard-acces": "Don't have permission to access clipboard",
+      "browser-not-supported": "This feature not supported in {browser} browser",
       "element-not-found": "Can't find an element with \"{selector}\" selector.",
       "not-iframe": "Element with \"{selector}\" selector is not an Iframe element",
       "iframe-not-found": "Can't find an Iframe element with \"{selector}\" selector.",

+ 0 - 0
src/manifest.json → src/manifest.chrome.json


+ 72 - 0
src/manifest.firefox.json

@@ -0,0 +1,72 @@
+{
+  "manifest_version": 2,
+  "name": "Automa",
+  "browser_specific_settings": {
+    "gecko": {
+      "strict_min_version": "91.1.0"
+    }
+  },
+  "background": {
+    "scripts": [
+      "background.bundle.js"
+    ],
+    "persistent": false
+  },
+  "browser_action": {
+    "default_popup": "popup.html",
+    "default_icon": "icon-128.png"
+  },
+  "icons": {
+    "128": "icon-128.png"
+  },
+  "commands": {
+    "open-dashboard": {
+      "suggested_key": {
+        "default": "Ctrl+Shift+A",
+        "mac": "MacCtrl+Shift+A"
+      },
+      "description": "Open the Automa dashboard"
+    }
+  },
+  "content_scripts": [
+    {
+      "matches": [
+        "<all_urls>"
+      ],
+      "js": [
+        "shortcutListener.bundle.js"
+      ],
+      "run_at": "document_end",
+      "all_frames": false
+    },
+    {
+      "matches": [
+        "*://*.automa.site/*",
+        "*://automa.vercel.app/*"
+      ],
+      "js": [
+        "webService.bundle.js"
+      ],
+      "all_frames": false
+    }
+  ],
+  "optional_permissions": [
+    "clipboardRead",
+    "downloads"
+  ],
+  "permissions": [
+    "tabs",
+    "proxy",
+    "alarms",
+    "storage",
+    "webNavigation",
+    "unlimitedStorage",
+    "<all_urls>"
+  ],
+  "web_accessible_resources": [
+    "/elementSelector.css",
+    "/Inter-roman-latin.var.woff2",
+    "/locales/*",
+    "elementSelector.bundle.js"
+  ]
+}

+ 2 - 2
src/newtab/App.vue

@@ -214,10 +214,10 @@ function handleStorageChanged(change) {
   }
 }
 
-browser.storage.local.onChanged.addListener(handleStorageChanged);
+browser.storage.onChanged.addListener(handleStorageChanged);
 
 window.addEventListener('beforeunload', () => {
-  browser.storage.local.onChanged.removeListener(handleStorageChanged);
+  browser.storage.onChanged.removeListener(handleStorageChanged);
 });
 
 (async () => {

+ 2 - 1
src/newtab/pages/settings/SettingsAbout.vue

@@ -50,13 +50,14 @@
 /* eslint-disable camelcase */
 import { onMounted } from 'vue';
 import { useStore } from 'vuex';
+import browser from 'webextension-polyfill';
 import { useGroupTooltip } from '@/composable/groupTooltip';
 import { communities } from '@/utils/shared';
 
 useGroupTooltip();
 const store = useStore();
 
-const extensionVersion = chrome.runtime.getManifest().version;
+const extensionVersion = browser.runtime.getManifest().version;
 const links = [
   ...communities,
   { name: 'Website', icon: 'riGlobalLine', url: 'https://www.automa.site' },

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

@@ -312,6 +312,7 @@ const workflowId = route.params.id;
 const workflowModals = {
   table: {
     icon: 'riKey2Line',
+    width: 'max-w-2xl',
     component: WorkflowDataTable,
     title: t('workflow.table.title'),
     docs: 'https://docs.automa.site/api-reference/table.html',
@@ -589,7 +590,7 @@ function insertToLocal() {
   const copy = {
     ...workflow.value,
     createdAt: Date.now(),
-    version: chrome.runtime.getManifest().version,
+    version: browser.runtime.getManifest().version,
   };
 
   Workflow.insert({

+ 8 - 1
src/utils/message.js

@@ -2,6 +2,7 @@ import browser from 'webextension-polyfill';
 import { objectHasKey } from './helper';
 
 const nameBuilder = (prefix, name) => (prefix ? `${prefix}--${name}` : name);
+const isFirefox = BROWSER_TYPE === 'firefox';
 
 export class MessageListener {
   constructor(prefix = '') {
@@ -26,6 +27,8 @@ export class MessageListener {
 
   listen(message, sender) {
     try {
+      if (isFirefox) message = JSON.parse(message);
+
       const listener = this.listeners[message.name];
       const response =
         listener && listener.call({ message, sender }, message.data, sender);
@@ -46,10 +49,14 @@ export class MessageListener {
 }
 
 export function sendMessage(name = '', data = {}, prefix = '') {
-  const payload = {
+  let payload = {
     name: nameBuilder(prefix, name),
     data,
   };
 
+  if (isFirefox) {
+    payload = JSON.stringify(payload);
+  }
+
   return browser.runtime.sendMessage(payload);
 }

+ 2 - 1
src/utils/workflowData.js

@@ -1,3 +1,4 @@
+import browser from 'webextension-polyfill';
 import Workflow from '@/models/workflow';
 import { parseJSON, fileSaver, openFilePicker, isObject } from './helper';
 
@@ -77,7 +78,7 @@ export function convertWorkflow(workflow, additionalKeys = []) {
     ...additionalKeys,
   ];
   const content = {
-    extVersion: chrome.runtime.getManifest().version,
+    extVersion: browser.runtime.getManifest().version,
   };
 
   keys.forEach((key) => {

+ 4 - 2
utils/build-zip.js

@@ -4,10 +4,12 @@ const path = require('path');
 const archiver = require('archiver');
 const packageJSON = require('../package.json');
 
-const fileName = `${packageJSON.name}-v${packageJSON.version}.zip`;
+const browser = process.env.BROWSER || 'chrome';
+const appVersion = packageJSON.version;
+const fileName = `${packageJSON.name}-${browser}-v${appVersion}.zip`;
 
 const destDir = path.join(__dirname, '../build');
-const zipDir = path.join(__dirname, '../build-zip');
+const zipDir = path.join(__dirname, '../build-zip', appVersion);
 
 if (!fs.existsSync(zipDir)) {
   fs.mkdirSync(zipDir);

+ 1 - 0
utils/env.js

@@ -2,4 +2,5 @@
 module.exports = {
   NODE_ENV: process.env.NODE_ENV || 'development',
   PORT: process.env.PORT || 3001,
+  BROWSER: process.env.BROWSER || 'chrome',
 };

+ 29 - 10
webpack.config.js

@@ -143,6 +143,9 @@ const options = {
   plugins: [
     new MiniCssExtractPlugin(),
     new VueLoaderPlugin(),
+    new webpack.DefinePlugin({
+      BROWSER_TYPE: JSON.stringify(env.BROWSER),
+    }),
     new webpack.ProgressPlugin(),
     // clean the build folder
     new CleanWebpackPlugin({
@@ -154,18 +157,34 @@ const options = {
     new CopyWebpackPlugin({
       patterns: [
         {
-          from: 'src/manifest.json',
-          to: path.join(__dirname, 'build'),
+          from: `src/manifest.${env.BROWSER}.json`,
+          to: path.join(__dirname, 'build', 'manifest.json'),
           force: true,
+          toType: 'file',
           transform(content) {
-            // generates the manifest file using the package.json informations
-            return Buffer.from(
-              JSON.stringify({
-                description: process.env.npm_package_description,
-                version: process.env.npm_package_version,
-                ...JSON.parse(content.toString()),
-              })
-            );
+            const manifestObj = {
+              description: process.env.npm_package_description,
+              version: process.env.npm_package_version,
+              ...JSON.parse(content.toString()),
+            };
+            const isChrome = env.BROWSER === 'chrome';
+
+            if (env.NODE_ENV === 'development' && !isChrome) {
+              manifestObj.content_security_policy =
+                "script-src 'self' 'unsafe-eval'; object-src 'self'";
+            }
+            if (manifestObj.version.includes('-')) {
+              const [version, preRelease] = manifestObj.version.split('-');
+
+              if (isChrome) {
+                manifestObj.version = version;
+                manifestObj.version_name = `${version} ${preRelease}`;
+              } else {
+                manifestObj.version = `${version}${preRelease}`;
+              }
+            }
+
+            return Buffer.from(JSON.stringify(manifestObj));
           },
         },
         {

+ 12 - 5
yarn.lock

@@ -2911,6 +2911,13 @@ crelt@^1.0.5:
   resolved "https://registry.yarnpkg.com/crelt/-/crelt-1.0.5.tgz#57c0d52af8c859e354bace1883eb2e1eb182bb94"
   integrity sha512-+BO9wPPi+DWTDcNYhr/W90myha8ptzftZT+LwcmUbbok0rcP/fequmFYCw8NMoH7pkAZQzU78b3kYrlua5a9eA==
 
+cross-env@^7.0.3:
+  version "7.0.3"
+  resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf"
+  integrity sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==
+  dependencies:
+    cross-spawn "^7.0.1"
+
 cross-spawn@^6.0.0:
   version "6.0.5"
   resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
@@ -2922,7 +2929,7 @@ cross-spawn@^6.0.0:
     shebang-command "^1.2.0"
     which "^1.2.9"
 
-cross-spawn@^7.0.2, cross-spawn@^7.0.3:
+cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3:
   version "7.0.3"
   resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
   integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
@@ -7280,10 +7287,10 @@ wbuf@^1.1.0, wbuf@^1.7.3:
   dependencies:
     minimalistic-assert "^1.0.0"
 
-webextension-polyfill@^0.8.0:
-  version "0.8.0"
-  resolved "https://registry.yarnpkg.com/webextension-polyfill/-/webextension-polyfill-0.8.0.tgz#f80e9f4b7f81820c420abd6ffbebfa838c60e041"
-  integrity sha512-a19+DzlT6Kp9/UI+mF9XQopeZ+n2ussjhxHJ4/pmIGge9ijCDz7Gn93mNnjpZAk95T4Tae8iHZ6sSf869txqiQ==
+webextension-polyfill@^0.9.0:
+  version "0.9.0"
+  resolved "https://registry.yarnpkg.com/webextension-polyfill/-/webextension-polyfill-0.9.0.tgz#de6c1941d0ef1b0858b20e9c7b46bbc042c5a960"
+  integrity sha512-LTtHb0yR49xa9irkstDxba4GATDAcDw3ncnFH9RImoFwDlW47U95ME5sn5IiQX2ghfaECaf6xyXM8yvClIBkkw==
 
 webpack-cli@4.8.0:
   version "4.8.0"