├── business
└── dev
│ ├── index.js
│ ├── blocks
│ ├── index.js
│ ├── backgroundHandler
│ │ └── index.js
│ ├── contentHandler
│ │ └── index.js
│ └── editComponents
│ │ └── index.js
│ └── parameters
│ └── index.js
├── src
├── offscreen
│ ├── index.js
│ ├── index.html
│ └── message-listener.js
├── common
│ └── utils
│ │ └── constant.js
├── assets
│ ├── images
│ │ ├── step.png
│ │ ├── tile.png
│ │ ├── default.png
│ │ ├── icon-128.png
│ │ ├── straight.png
│ │ ├── theme-dark.png
│ │ ├── tile-white.png
│ │ ├── icon-dev-128.png
│ │ ├── smooth-step.png
│ │ ├── theme-light.png
│ │ └── theme-system.png
│ ├── fonts
│ │ ├── inter-v3-latin-600.woff
│ │ ├── inter-v3-latin-600.woff2
│ │ ├── Inter-roman-latin.var.woff2
│ │ ├── inter-v3-latin-regular.woff
│ │ ├── inter-v3-latin-regular.woff2
│ │ ├── source-code-pro-v21-latin-600.woff
│ │ ├── source-code-pro-v21-latin-600.woff2
│ │ ├── source-code-pro-v21-latin-regular.woff
│ │ └── source-code-pro-v21-latin-regular.woff2
│ ├── css
│ │ ├── style.css
│ │ ├── fonts.css
│ │ └── flow.css
│ └── svg
│ │ ├── logo.svg
│ │ └── logoFirefox.svg
├── lib
│ ├── mitt.js
│ ├── tmpl.js
│ ├── vue-toastification.js
│ ├── dayjs.js
│ ├── tippy.js
│ ├── findSelector.js
│ ├── cronstrue.js
│ ├── pinia.js
│ ├── compsUi.js
│ └── vueI18n.js
├── directives
│ ├── VAutofocus.js
│ ├── VClosePopover.js
│ └── VTooltip.js
├── components
│ ├── newtab
│ │ ├── settings
│ │ │ └── jsBlockWrap.js
│ │ ├── workflow
│ │ │ ├── settings
│ │ │ │ ├── event
│ │ │ │ │ └── EventCodeAction.vue
│ │ │ │ ├── SettingsTable.vue
│ │ │ │ └── SettingsBlocks.vue
│ │ │ ├── edit
│ │ │ │ ├── EditDelay.vue
│ │ │ │ ├── Parameter
│ │ │ │ │ ├── ParameterCheckboxValue.vue
│ │ │ │ │ ├── ParameterJsonValue.vue
│ │ │ │ │ └── ParameterInputValue.vue
│ │ │ │ ├── EditLink.vue
│ │ │ │ ├── Trigger
│ │ │ │ │ ├── TriggerVisitWeb.vue
│ │ │ │ │ ├── TriggerDate.vue
│ │ │ │ │ ├── TriggerInterval.vue
│ │ │ │ │ └── TriggerCronJob.vue
│ │ │ │ ├── TriggerEvent
│ │ │ │ │ ├── TriggerEventInput.vue
│ │ │ │ │ ├── TriggerEventTouch.vue
│ │ │ │ │ └── TriggerEventWheel.vue
│ │ │ │ ├── EditIncreaseVariable.vue
│ │ │ │ ├── EditHandleDialog.vue
│ │ │ │ ├── EditAutocomplete.vue
│ │ │ │ ├── EditParameterPrompt.vue
│ │ │ │ ├── EditLogData.vue
│ │ │ │ ├── EditSliceVariable.vue
│ │ │ │ ├── EditWaitConnections.vue
│ │ │ │ └── EditSwitchTo.vue
│ │ │ ├── WorkflowGlobalData.vue
│ │ │ └── editor
│ │ │ │ └── EditorLogs.vue
│ │ ├── app
│ │ │ └── AppLogs.vue
│ │ ├── shared
│ │ │ └── SharedElSelectorActions.vue
│ │ ├── package
│ │ │ └── PackageDetails.vue
│ │ └── workflows
│ │ │ └── WorkflowsShared.vue
│ ├── ui
│ │ ├── UiList.vue
│ │ ├── UiTabPanels.vue
│ │ ├── UiCard.vue
│ │ ├── UiListItem.vue
│ │ ├── UiSpinner.vue
│ │ ├── UiTabPanel.vue
│ │ ├── UiTab.vue
│ │ ├── UiRadio.vue
│ │ └── UiTextarea.vue
│ ├── content
│ │ ├── selector
│ │ │ └── SelectorElementList.vue
│ │ └── shared
│ │ │ └── SharedElementHighlighter.vue
│ └── transitions
│ │ ├── TransitionSlide.vue
│ │ └── TransitionExpand.vue
├── composable
│ ├── componentId.js
│ ├── liveQuery.js
│ ├── dialog.js
│ ├── editorBlock.js
│ ├── commandManager.js
│ ├── groupTooltip.js
│ ├── blockValidation.js
│ ├── theme.js
│ └── hasPermissions.js
├── utils
│ ├── getSharedData.js
│ ├── constants
│ │ └── table.js
│ ├── getBlockMessage.js
│ ├── compareBlockValue.js
│ ├── decryptFlow.js
│ ├── credentialUtil.js
│ ├── triggerText.js
│ ├── simulateEvent
│ │ ├── mouseEvent.js
│ │ └── index.js
│ ├── serialization.js
│ ├── recordKeys.js
│ ├── dataMigration.js
│ └── editor
│ │ └── EditorCommands.js
├── content
│ ├── blocksHandler
│ │ ├── handlerClipboard.js
│ │ ├── handlerLink.js
│ │ ├── handlerSwitchTo.js
│ │ ├── handlerSaveAssets.js
│ │ ├── handlerElementExists.js
│ │ ├── handlerHoverElement.js
│ │ ├── handlerLoopData.js
│ │ ├── handlerJavascriptCode.js
│ │ ├── handlerAttributeValue.js
│ │ ├── handlerGetText.js
│ │ ├── handlerEventClick.js
│ │ ├── handlerVerifySelector.js
│ │ └── handlerUploadFile.js
│ ├── services
│ │ └── recordWorkflow
│ │ │ ├── icons.js
│ │ │ ├── addBlock.js
│ │ │ ├── main.js
│ │ │ └── index.js
│ ├── elementSelector
│ │ ├── vueI18n.js
│ │ ├── getSelectorOptions.js
│ │ ├── icons.js
│ │ ├── main.js
│ │ ├── index.js
│ │ ├── compsUi.js
│ │ └── generateElementsSelector.js
│ ├── blocksHandler.js
│ ├── commandPalette
│ │ ├── main.js
│ │ ├── icons.js
│ │ ├── compsUi.js
│ │ └── index.js
│ ├── synchronizedLock.js
│ └── injectAppStyles.js
├── params
│ ├── index.html
│ └── index.js
├── popup
│ ├── index.html
│ ├── router.js
│ ├── index.js
│ └── App.vue
├── sandbox
│ ├── index.html
│ ├── utils
│ │ ├── handleBlockExpression.js
│ │ └── handleConditionCode.js
│ └── index.js
├── execute
│ ├── index.html
│ └── index.js
├── workflowEngine
│ ├── blocksHandler
│ │ ├── handlerTrigger.js
│ │ ├── handlerGoogleSheetsDrive.js
│ │ ├── handlerDelay.js
│ │ ├── handlerGoBack.js
│ │ ├── handlerForwardPage.js
│ │ ├── handlerReloadTab.js
│ │ ├── handlerLink.js
│ │ ├── handlerElementExists.js
│ │ ├── handlerRepeatTask.js
│ │ ├── handlerIncreaseVariable.js
│ │ ├── handlerSliceVariable.js
│ │ ├── handlerHoverElement.js
│ │ ├── handlerWhileLoop.js
│ │ ├── handlerTabUrl.js
│ │ ├── handlerNotification.js
│ │ ├── handlerLogData.js
│ │ ├── handlerNewWindow.js
│ │ ├── handlerRegexVariable.js
│ │ ├── handlerWorkflowState.js
│ │ ├── handlerBlocksGroup.js
│ │ ├── handlerDeleteData.js
│ │ ├── handlerCloseTab.js
│ │ ├── handlerSwitchTo.js
│ │ ├── handlerDataMapping.js
│ │ ├── handlerExportData.js
│ │ └── handlerSortData.js
│ ├── WorkflowLogger.js
│ ├── blocksHandler.js
│ ├── templating
│ │ ├── renderString.js
│ │ └── index.js
│ ├── workflowEvent.js
│ ├── utils
│ │ └── conditionCode.js
│ └── injectContentScript.js
├── newtab
│ ├── index.html
│ ├── pages
│ │ └── logs
│ │ │ └── [id].vue
│ ├── index.js
│ └── utils
│ │ └── startRecordWorkflow.js
├── db
│ ├── storage.js
│ └── logs.js
├── locales
│ ├── fr
│ │ └── popup.json
│ ├── zh
│ │ ├── popup.json
│ │ └── common.json
│ ├── zh-TW
│ │ ├── popup.json
│ │ └── common.json
│ ├── en
│ │ └── popup.json
│ ├── tr
│ │ └── popup.json
│ ├── uk
│ │ └── popup.json
│ ├── pt-BR
│ │ └── popup.json
│ ├── it
│ │ └── popup.json
│ ├── vi
│ │ └── popup.json
│ └── es
│ │ └── popup.json
├── service
│ └── renderer
│ │ └── RendererWorkflowService.js
├── stores
│ └── folder.js
├── background
│ └── BackgroundUtils.js
└── manifest.firefox.json
├── secrets.blank.js
├── .github
├── FUNDING.yml
├── dependabot.yml
└── ISSUE_TEMPLATE
│ ├── feature_request.md
│ └── bug_report.md
├── .prettierrc
├── .vscode
└── settings.json
├── postcss.config.js
├── utils
├── env.js
├── build.js
├── build-zip.js
└── webserver.js
├── .babelrc
├── jsconfig.json
├── .editorconfig
├── .gitignore
└── LICENSE.txt
/business/dev/index.js:
--------------------------------------------------------------------------------
1 | export default function () {}
2 |
--------------------------------------------------------------------------------
/src/offscreen/index.js:
--------------------------------------------------------------------------------
1 | import './message-listener';
2 |
--------------------------------------------------------------------------------
/secrets.blank.js:
--------------------------------------------------------------------------------
1 | export default {
2 | baseApiUrl: '',
3 | };
4 |
--------------------------------------------------------------------------------
/business/dev/blocks/index.js:
--------------------------------------------------------------------------------
1 | export default function () {
2 | return {};
3 | }
4 |
--------------------------------------------------------------------------------
/business/dev/parameters/index.js:
--------------------------------------------------------------------------------
1 | export default function () {
2 | return {};
3 | }
4 |
--------------------------------------------------------------------------------
/src/common/utils/constant.js:
--------------------------------------------------------------------------------
1 | export const IS_FIREFOX = BROWSER_TYPE === 'firefox';
2 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: AutomaApp
4 |
5 |
--------------------------------------------------------------------------------
/business/dev/blocks/backgroundHandler/index.js:
--------------------------------------------------------------------------------
1 | export default function () {
2 | return {};
3 | }
4 |
--------------------------------------------------------------------------------
/business/dev/blocks/contentHandler/index.js:
--------------------------------------------------------------------------------
1 | export default function () {
2 | return {};
3 | }
4 |
--------------------------------------------------------------------------------
/business/dev/blocks/editComponents/index.js:
--------------------------------------------------------------------------------
1 | export default function () {
2 | return {};
3 | }
4 |
--------------------------------------------------------------------------------
/src/assets/images/step.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mthli/automa/main/src/assets/images/step.png
--------------------------------------------------------------------------------
/src/assets/images/tile.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mthli/automa/main/src/assets/images/tile.png
--------------------------------------------------------------------------------
/src/assets/images/default.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mthli/automa/main/src/assets/images/default.png
--------------------------------------------------------------------------------
/src/assets/images/icon-128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mthli/automa/main/src/assets/images/icon-128.png
--------------------------------------------------------------------------------
/src/assets/images/straight.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mthli/automa/main/src/assets/images/straight.png
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "trailingComma": "es5",
4 | "arrowParens": "always"
5 | }
6 |
--------------------------------------------------------------------------------
/src/assets/images/theme-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mthli/automa/main/src/assets/images/theme-dark.png
--------------------------------------------------------------------------------
/src/assets/images/tile-white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mthli/automa/main/src/assets/images/tile-white.png
--------------------------------------------------------------------------------
/src/lib/mitt.js:
--------------------------------------------------------------------------------
1 | import mitt from 'mitt';
2 |
3 | const emitter = mitt();
4 |
5 | export default emitter;
6 |
--------------------------------------------------------------------------------
/src/assets/images/icon-dev-128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mthli/automa/main/src/assets/images/icon-dev-128.png
--------------------------------------------------------------------------------
/src/assets/images/smooth-step.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mthli/automa/main/src/assets/images/smooth-step.png
--------------------------------------------------------------------------------
/src/assets/images/theme-light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mthli/automa/main/src/assets/images/theme-light.png
--------------------------------------------------------------------------------
/src/assets/images/theme-system.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mthli/automa/main/src/assets/images/theme-system.png
--------------------------------------------------------------------------------
/src/assets/fonts/inter-v3-latin-600.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mthli/automa/main/src/assets/fonts/inter-v3-latin-600.woff
--------------------------------------------------------------------------------
/src/assets/fonts/inter-v3-latin-600.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mthli/automa/main/src/assets/fonts/inter-v3-latin-600.woff2
--------------------------------------------------------------------------------
/src/lib/tmpl.js:
--------------------------------------------------------------------------------
1 | import * as tmpl from '@n8n_io/riot-tmpl';
2 |
3 | tmpl.brackets.set('{{ }}');
4 |
5 | export default tmpl;
6 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "i18n-ally.localesPaths": [
3 | "src/locales"
4 | ],
5 | "i18n-ally.keystyle": "nested"
6 | }
7 |
--------------------------------------------------------------------------------
/src/assets/fonts/Inter-roman-latin.var.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mthli/automa/main/src/assets/fonts/Inter-roman-latin.var.woff2
--------------------------------------------------------------------------------
/src/assets/fonts/inter-v3-latin-regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mthli/automa/main/src/assets/fonts/inter-v3-latin-regular.woff
--------------------------------------------------------------------------------
/src/assets/fonts/inter-v3-latin-regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mthli/automa/main/src/assets/fonts/inter-v3-latin-regular.woff2
--------------------------------------------------------------------------------
/src/directives/VAutofocus.js:
--------------------------------------------------------------------------------
1 | export default {
2 | mounted(el, { value = true }) {
3 | if (value) el.focus();
4 | },
5 | };
6 |
--------------------------------------------------------------------------------
/src/assets/fonts/source-code-pro-v21-latin-600.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mthli/automa/main/src/assets/fonts/source-code-pro-v21-latin-600.woff
--------------------------------------------------------------------------------
/src/assets/fonts/source-code-pro-v21-latin-600.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mthli/automa/main/src/assets/fonts/source-code-pro-v21-latin-600.woff2
--------------------------------------------------------------------------------
/src/lib/vue-toastification.js:
--------------------------------------------------------------------------------
1 | import Toast from 'vue-toastification';
2 | import 'vue-toastification/dist/index.css';
3 |
4 | export default Toast;
5 |
--------------------------------------------------------------------------------
/src/assets/fonts/source-code-pro-v21-latin-regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mthli/automa/main/src/assets/fonts/source-code-pro-v21-latin-regular.woff
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | 'tailwindcss/nesting': {},
4 | tailwindcss: {},
5 | autoprefixer: {},
6 | },
7 | };
8 |
--------------------------------------------------------------------------------
/src/assets/fonts/source-code-pro-v21-latin-regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mthli/automa/main/src/assets/fonts/source-code-pro-v21-latin-regular.woff2
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "npm"
4 | directory: "/"
5 | target-branch: "dev"
6 | schedule:
7 | interval: "daily"
8 |
9 |
--------------------------------------------------------------------------------
/src/components/newtab/settings/jsBlockWrap.js:
--------------------------------------------------------------------------------
1 | import { reactive } from 'vue';
2 |
3 | export const store = reactive({
4 | whiteSpace: 'pre',
5 | statePrettier: Math.random(),
6 | });
7 |
--------------------------------------------------------------------------------
/src/composable/componentId.js:
--------------------------------------------------------------------------------
1 | let id = 0;
2 |
3 | export function useComponentId(prefix) {
4 | id += 1;
5 |
6 | if (!prefix) return id;
7 |
8 | return `${prefix}--${id}`;
9 | }
10 |
--------------------------------------------------------------------------------
/src/utils/getSharedData.js:
--------------------------------------------------------------------------------
1 | import customBlocks from '@business/blocks';
2 | import { tasks } from './shared';
3 |
4 | export function getBlocks() {
5 | return { ...tasks, ...customBlocks() };
6 | }
7 |
--------------------------------------------------------------------------------
/src/composable/liveQuery.js:
--------------------------------------------------------------------------------
1 | import { liveQuery } from 'dexie';
2 | import { useObservable } from '@vueuse/rxjs';
3 |
4 | export function useLiveQuery(querier) {
5 | return useObservable(liveQuery(querier));
6 | }
7 |
--------------------------------------------------------------------------------
/utils/env.js:
--------------------------------------------------------------------------------
1 | // tiny wrapper with default env vars
2 | module.exports = {
3 | NODE_ENV: process.env.NODE_ENV || 'development',
4 | PORT: process.env.PORT || 3001,
5 | BROWSER: process.env.BROWSER || 'chrome',
6 | };
7 |
--------------------------------------------------------------------------------
/src/content/blocksHandler/handlerClipboard.js:
--------------------------------------------------------------------------------
1 | function clipboard() {
2 | return new Promise((resolve) => {
3 | const text = window.getSelection().toString();
4 | resolve(text);
5 | });
6 | }
7 |
8 | export default clipboard;
9 |
--------------------------------------------------------------------------------
/src/params/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/popup/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/sandbox/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Sandbox
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/utils/constants/table.js:
--------------------------------------------------------------------------------
1 | export const dataTypes = [
2 | { id: 'any', name: 'Any' },
3 | { id: 'string', name: 'Text' },
4 | { id: 'integer', name: 'Number' },
5 | { id: 'boolean', name: 'Boolean' },
6 | { id: 'array', name: 'Array' },
7 | ];
8 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": [],
3 | "presets": [
4 | ["@babel/preset-env", {
5 | "useBuiltIns": "usage",
6 | "corejs": 3,
7 | "targets": {
8 | "browsers": "last 2 Chrome versions"
9 | }
10 | }]
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/src/execute/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Execute
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/src/directives/VClosePopover.js:
--------------------------------------------------------------------------------
1 | import { hideAll } from 'tippy.js';
2 |
3 | export default {
4 | mounted(el) {
5 | el.addEventListener('click', hideAll);
6 | },
7 | beforeUnmount(el) {
8 | el.removeEventListener('click', hideAll);
9 | },
10 | };
11 |
--------------------------------------------------------------------------------
/src/workflowEngine/blocksHandler/handlerTrigger.js:
--------------------------------------------------------------------------------
1 | async function trigger(block) {
2 | return new Promise((resolve) => {
3 | resolve({
4 | data: '',
5 | nextBlockId: this.getBlockConnections(block.id),
6 | });
7 | });
8 | }
9 |
10 | export default trigger;
11 |
--------------------------------------------------------------------------------
/src/workflowEngine/blocksHandler/handlerGoogleSheetsDrive.js:
--------------------------------------------------------------------------------
1 | import handlerGoogleSheets from './handlerGoogleSheets';
2 |
3 | export default function (blockData, additionalData) {
4 | blockData.data.isDriveSheet = true;
5 | return handlerGoogleSheets.call(this, blockData, additionalData);
6 | }
7 |
--------------------------------------------------------------------------------
/src/newtab/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Automa
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "./",
4 | "paths": {
5 | "@/*": ["src/*"],
6 | "@business": ["business/dev/*"]
7 | },
8 | "lib": ["ESNext", "DOM"],
9 | "module": "ESNext",
10 | "target": "ES2020"
11 | },
12 | "include": ["src/**/*", "utils/**/*"]
13 | }
14 |
--------------------------------------------------------------------------------
/src/components/ui/UiList.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
10 |
17 |
--------------------------------------------------------------------------------
/src/db/storage.js:
--------------------------------------------------------------------------------
1 | import Dexie from 'dexie';
2 |
3 | const dbStorage = new Dexie('storage');
4 | dbStorage.version(2).stores({
5 | tablesData: '++id, tableId',
6 | tablesItems: '++id, name, createdAt, modifiedAt',
7 | variables: '++id, &name',
8 | credentials: '++id, &name',
9 | });
10 |
11 | export default dbStorage;
12 |
--------------------------------------------------------------------------------
/src/offscreen/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Offscreen
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/assets/css/style.css:
--------------------------------------------------------------------------------
1 | .list-item-transition {
2 | transition: all 0.4s ease;
3 | }
4 |
5 | .list-leave-active {
6 | position: absolute;
7 | width: 100%;
8 | }
9 |
10 | .list-enter-from,
11 | .list-leave-to {
12 | opacity: 0;
13 | }
14 |
15 | .list-enter-from,
16 | .list-enter-from {
17 | transform: translateY(30px);
18 | }
19 |
--------------------------------------------------------------------------------
/src/popup/router.js:
--------------------------------------------------------------------------------
1 | import { createRouter, createWebHashHistory } from 'vue-router';
2 | import Home from './pages/Home.vue';
3 |
4 | const routes = [
5 | {
6 | path: '/',
7 | name: 'home',
8 | component: Home,
9 | },
10 | ];
11 |
12 | export default createRouter({
13 | routes,
14 | history: createWebHashHistory(),
15 | });
16 |
--------------------------------------------------------------------------------
/src/workflowEngine/blocksHandler/handlerDelay.js:
--------------------------------------------------------------------------------
1 | function delay(block) {
2 | return new Promise((resolve) => {
3 | const delayTime = +block.data.time || 500;
4 | setTimeout(() => {
5 | resolve({
6 | data: '',
7 | nextBlockId: this.getBlockConnections(block.id),
8 | });
9 | }, delayTime);
10 | });
11 | }
12 |
13 | export default delay;
14 |
--------------------------------------------------------------------------------
/src/content/services/recordWorkflow/icons.js:
--------------------------------------------------------------------------------
1 | import { riRecordCircleLine, riArrowLeftLine } from 'v-remixicon/icons';
2 |
3 | export default {
4 | riRecordCircleLine,
5 | riArrowLeftLine,
6 | mdiDragHorizontal:
7 | 'M3,15V13H5V15H3M3,11V9H5V11H3M7,15V13H9V15H7M7,11V9H9V11H7M11,15V13H13V15H11M11,11V9H13V11H11M15,15V13H17V15H15M15,11V9H17V11H15M19,15V13H21V15H19M19,11V9H21V11H19Z',
8 | };
9 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig is awesome: http://EditorConfig.org
2 |
3 | # https://github.com/jokeyrhyme/standard-editorconfig
4 |
5 | # top-most EditorConfig file
6 | root = true
7 |
8 | # defaults
9 | [*]
10 | charset = utf-8
11 | end_of_line = lf
12 | insert_final_newline = true
13 | trim_trailing_whitespace = true
14 | indent_size = 2
15 | indent_style = space
16 |
17 | [*.md]
18 | trim_trailing_whitespace = false
19 |
--------------------------------------------------------------------------------
/src/params/index.js:
--------------------------------------------------------------------------------
1 | import { createApp } from 'vue';
2 | import App from './App.vue';
3 | import compsUi from '../lib/compsUi';
4 | import vRemixicon, { icons } from '../lib/vRemixicon';
5 | import '../assets/css/tailwind.css';
6 | import '../assets/css/fonts.css';
7 | import '../assets/css/flow.css';
8 |
9 | createApp(App).use(compsUi).use(vRemixicon, icons).mount('#app');
10 |
11 | if (module.hot) module.hot.accept();
12 |
--------------------------------------------------------------------------------
/src/content/elementSelector/vueI18n.js:
--------------------------------------------------------------------------------
1 | import { createI18n } from 'vue-i18n/dist/vue-i18n.esm-bundler';
2 | import enCommon from '@/locales/en/common.json';
3 | import enBlocks from '@/locales/en/blocks.json';
4 |
5 | const i18n = createI18n({
6 | locale: 'en',
7 | legacy: false,
8 | });
9 |
10 | i18n.global.mergeLocaleMessage('en', enCommon);
11 | i18n.global.mergeLocaleMessage('en', enBlocks);
12 |
13 | export default i18n;
14 |
--------------------------------------------------------------------------------
/src/workflowEngine/blocksHandler/handlerGoBack.js:
--------------------------------------------------------------------------------
1 | import BrowserAPIService from '@/service/browser-api/BrowserAPIService';
2 |
3 | export async function goBack({ id }) {
4 | if (!this.activeTab.id) throw new Error('no-tab');
5 |
6 | await BrowserAPIService.tabs.goBack(this.activeTab.id);
7 |
8 | return {
9 | data: '',
10 | nextBlockId: this.getBlockConnections(id),
11 | };
12 | }
13 |
14 | export default goBack;
15 |
--------------------------------------------------------------------------------
/src/content/elementSelector/getSelectorOptions.js:
--------------------------------------------------------------------------------
1 | export default function ({ idName, tagName, className, attr, attrNames }) {
2 | const attrs = attr
3 | ? attrNames.split(',').map((item) => item.trim())
4 | : ['data-testid'];
5 |
6 | return {
7 | idName: () => idName ?? true,
8 | tagName: () => tagName ?? true,
9 | className: () => className ?? true,
10 | attr: (name) => attrs.includes(name),
11 | };
12 | }
13 |
--------------------------------------------------------------------------------
/src/workflowEngine/blocksHandler/handlerForwardPage.js:
--------------------------------------------------------------------------------
1 | import BrowserAPIService from '@/service/browser-api/BrowserAPIService';
2 |
3 | export async function goBack({ id }) {
4 | if (!this.activeTab.id) throw new Error('no-tab');
5 |
6 | await BrowserAPIService.tabs.goForward(this.activeTab.id);
7 |
8 | return {
9 | data: '',
10 | nextBlockId: this.getBlockConnections(id),
11 | };
12 | }
13 |
14 | export default goBack;
15 |
--------------------------------------------------------------------------------
/src/workflowEngine/blocksHandler/handlerReloadTab.js:
--------------------------------------------------------------------------------
1 | import BrowserAPIService from '@/service/browser-api/BrowserAPIService';
2 |
3 | export async function reloadTab({ id }) {
4 | if (!this.activeTab.id) throw new Error('no-tab');
5 |
6 | await BrowserAPIService.tabs.reload(this.activeTab.id);
7 |
8 | return {
9 | data: '',
10 | nextBlockId: this.getBlockConnections(id),
11 | };
12 | }
13 |
14 | export default reloadTab;
15 |
--------------------------------------------------------------------------------
/src/lib/dayjs.js:
--------------------------------------------------------------------------------
1 | import dayjs from 'dayjs';
2 | import relativeTime from 'dayjs/plugin/relativeTime';
3 | import 'dayjs/locale/zh';
4 | import 'dayjs/locale/zh-tw';
5 | import 'dayjs/locale/vi';
6 | import 'dayjs/locale/fr';
7 | import 'dayjs/locale/it';
8 | import 'dayjs/locale/uk';
9 | import 'dayjs/locale/tr';
10 | import 'dayjs/locale/es';
11 | import 'dayjs/locale/pt'
12 |
13 | dayjs.extend(relativeTime);
14 |
15 | export default dayjs;
16 |
--------------------------------------------------------------------------------
/src/utils/getBlockMessage.js:
--------------------------------------------------------------------------------
1 | import locale from '../locales/en/newtab.json';
2 |
3 | export default function ({ message, ...data }) {
4 | const normalize = (value) => value.join('');
5 | const interpolate = (key) => data[key];
6 | const named = (key) => key;
7 |
8 | const localeMessage = locale.log.messages[message];
9 | if (localeMessage) return localeMessage({ normalize, interpolate, named });
10 |
11 | return message;
12 | }
13 |
--------------------------------------------------------------------------------
/src/lib/tippy.js:
--------------------------------------------------------------------------------
1 | import tippy from 'tippy.js';
2 | import 'tippy.js/animations/shift-toward-subtle.css';
3 |
4 | export const defaultOptions = {
5 | animation: 'shift-toward-subtle',
6 | theme: 'my-theme',
7 | };
8 |
9 | export default function (el, options = {}) {
10 | el?.setAttribute('vtooltip', '');
11 |
12 | const instance = tippy(el, {
13 | ...defaultOptions,
14 | ...options,
15 | });
16 |
17 | return instance;
18 | }
19 |
--------------------------------------------------------------------------------
/src/utils/compareBlockValue.js:
--------------------------------------------------------------------------------
1 | const handlers = {
2 | '==': (a, b) => a === b,
3 | '!=': (a, b) => a !== b,
4 | '>': (a, b) => a > b,
5 | '>=': (a, b) => a >= b,
6 | '<': (a, b) => a < b,
7 | '<=': (a, b) => a <= b,
8 | '()': (a, b) => a?.includes(b) ?? false,
9 | };
10 |
11 | export default function (type, valueA, valueB) {
12 | const handler = handlers[type];
13 |
14 | if (handler) return handler(valueA, valueB);
15 |
16 | return false;
17 | }
18 |
--------------------------------------------------------------------------------
/utils/build.js:
--------------------------------------------------------------------------------
1 | // Do this as the first thing so that any code reading it knows the right env.
2 | process.env.BABEL_ENV = 'production';
3 | process.env.NODE_ENV = 'production';
4 | process.env.ASSET_PATH = '/';
5 |
6 | const webpack = require('webpack');
7 | const config = require('../webpack.config');
8 |
9 | delete config.chromeExtensionBoilerplate;
10 |
11 | config.mode = 'production';
12 |
13 | webpack(config, function (err) {
14 | if (err) throw err;
15 | });
16 |
--------------------------------------------------------------------------------
/src/newtab/pages/logs/[id].vue:
--------------------------------------------------------------------------------
1 |
2 | Hello :)
3 |
4 |
21 |
--------------------------------------------------------------------------------
/src/components/ui/UiTabPanels.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
23 |
--------------------------------------------------------------------------------
/src/workflowEngine/WorkflowLogger.js:
--------------------------------------------------------------------------------
1 | import dbLogs, { defaultLogItem } from '@/db/logs';
2 | /* eslint-disable class-methods-use-this */
3 | class WorkflowLogger {
4 | async add({ detail, history, ctxData, data }) {
5 | const logDetail = { ...defaultLogItem, ...detail };
6 |
7 | await Promise.all([
8 | dbLogs.logsData.add(data),
9 | dbLogs.ctxData.add(ctxData),
10 | dbLogs.items.add(logDetail),
11 | dbLogs.histories.add(history),
12 | ]);
13 | }
14 | }
15 |
16 | export default WorkflowLogger;
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 | /build-zip
12 |
13 | # misc
14 | .DS_Store
15 | .eslintcache
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 | .history
21 | *.log
22 |
23 | # secrets
24 | secrets.production.js
25 | secrets.development.js
26 | get-pass-key.js
27 | getPassKey.js
28 |
29 | /business/prod
30 | /business/test
31 |
32 | .idea
--------------------------------------------------------------------------------
/src/db/logs.js:
--------------------------------------------------------------------------------
1 | import Dexie from 'dexie';
2 |
3 | const dbLogs = new Dexie('logs');
4 | dbLogs.version(1).stores({
5 | ctxData: '++id, logId',
6 | logsData: '++id, logId',
7 | histories: '++id, logId',
8 | items: '++id, name, endedAt, workflowId, status, collectionId',
9 | });
10 |
11 | export const defaultLogItem = {
12 | name: '',
13 | endedAt: 0,
14 | message: '',
15 | startedAt: 0,
16 | parentLog: null,
17 | workflowId: null,
18 | status: 'success',
19 | collectionId: null,
20 | };
21 |
22 | export default dbLogs;
23 |
--------------------------------------------------------------------------------
/src/sandbox/utils/handleBlockExpression.js:
--------------------------------------------------------------------------------
1 | import tmpl from '@/lib/tmpl';
2 | import functions from '@/workflowEngine/templating/templatingFunctions';
3 |
4 | const templatingFunctions = Object.keys(functions).reduce((acc, funcName) => {
5 | acc[`$${funcName}`] = functions[funcName];
6 |
7 | return acc;
8 | }, {});
9 |
10 | export default function ({ str, data }, sendResponse) {
11 | const value = tmpl.tmpl(str, { ...data, ...templatingFunctions });
12 |
13 | sendResponse({
14 | list: {},
15 | value: value.slice(2),
16 | });
17 | }
18 |
--------------------------------------------------------------------------------
/src/content/blocksHandler.js:
--------------------------------------------------------------------------------
1 | import customHandlers from '@business/blocks/contentHandler';
2 | import { toCamelCase } from '@/utils/helper';
3 |
4 | const blocksHandler = require.context('./blocksHandler', false, /\.js$/);
5 | const handlers = blocksHandler.keys().reduce((acc, key) => {
6 | const name = key.replace(/^\.\/handler|\.js/g, '');
7 |
8 | acc[toCamelCase(name)] = blocksHandler(key).default;
9 |
10 | return acc;
11 | }, {});
12 |
13 | export default function () {
14 | return {
15 | ...(customHandlers() || {}),
16 | ...handlers,
17 | };
18 | }
19 |
--------------------------------------------------------------------------------
/src/workflowEngine/blocksHandler.js:
--------------------------------------------------------------------------------
1 | import { toCamelCase } from '@/utils/helper';
2 | import customHandlers from '@business/blocks/backgroundHandler';
3 |
4 | const blocksHandler = require.context('./blocksHandler', false, /\.js$/);
5 | const handlers = blocksHandler.keys().reduce((acc, key) => {
6 | const name = key.replace(/^\.\/handler|\.js/g, '');
7 |
8 | acc[toCamelCase(name)] = blocksHandler(key).default;
9 |
10 | return acc;
11 | }, {});
12 |
13 | export default function () {
14 | return {
15 | ...handlers,
16 | ...customHandlers(),
17 | };
18 | }
19 |
--------------------------------------------------------------------------------
/src/composable/dialog.js:
--------------------------------------------------------------------------------
1 | import emitter from '@/lib/mitt';
2 |
3 | export function useDialog() {
4 | const emitDialog = (type, options = {}) => {
5 | emitter.emit('show-dialog', { type, options });
6 | };
7 |
8 | function confirm(options = {}) {
9 | emitDialog('confirm', options);
10 | }
11 | function prompt(options = {}) {
12 | emitDialog('prompt', options);
13 | }
14 | function custom(type, options = {}) {
15 | emitDialog(type, { ...options, custom: true });
16 | }
17 |
18 | return {
19 | custom,
20 | prompt,
21 | confirm,
22 | };
23 | }
24 |
--------------------------------------------------------------------------------
/src/composable/editorBlock.js:
--------------------------------------------------------------------------------
1 | import { reactive, onMounted } from 'vue';
2 | import { getBlocks } from '@/utils/getSharedData';
3 | import { categories } from '@/utils/shared';
4 |
5 | export function useEditorBlock(label) {
6 | const blocks = getBlocks();
7 | const block = reactive({
8 | details: {},
9 | category: {},
10 | });
11 |
12 | onMounted(() => {
13 | if (!label) return;
14 |
15 | const details = blocks[label];
16 |
17 | block.details = { id: label, ...details };
18 | block.category = categories[details.category];
19 | });
20 |
21 | return block;
22 | }
23 |
--------------------------------------------------------------------------------
/src/components/newtab/workflow/settings/event/EventCodeAction.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
23 |
--------------------------------------------------------------------------------
/src/content/blocksHandler/handlerLink.js:
--------------------------------------------------------------------------------
1 | import handleSelector, { markElement } from '../handleSelector';
2 |
3 | async function link(block) {
4 | const element = await handleSelector(block, { returnElement: true });
5 |
6 | if (!element) {
7 | throw new Error('element-not-found');
8 | }
9 | if (element.tagName !== 'A') {
10 | throw new Error('Element is not a link');
11 | }
12 |
13 | markElement(element, block);
14 |
15 | const url = element.href;
16 | if (url && !block.data.openInNewTab) window.open(url, '_self');
17 |
18 | return url;
19 | }
20 |
21 | export default link;
22 |
--------------------------------------------------------------------------------
/src/locales/fr/popup.json:
--------------------------------------------------------------------------------
1 | {
2 | "recording": {
3 | "stop": "Arrêter d'enregistrer",
4 | "title": "Enregistrer"
5 | },
6 | "home": {
7 | "record": {
8 | "title": "Enregistrer un workflow",
9 | "button": "Enregistrer",
10 | "name": "Nom du workflow"
11 | },
12 | "elementSelector": {
13 | "name": "Sélecteur d'éléments",
14 | "noAccess": "L'extension n'a pas accès à ce site"
15 | },
16 | "workflow": {
17 | "new": "Nouveau workflow",
18 | "rename": "Renommer le workflow",
19 | "delete": "Supprimer le workflow"
20 | }
21 | }
22 | }
--------------------------------------------------------------------------------
/src/lib/findSelector.js:
--------------------------------------------------------------------------------
1 | import { finder as finderLib } from '@medv/finder';
2 |
3 | const ariaAttrs = ['data-testid'];
4 |
5 | export const finder = finderLib;
6 |
7 | export default function (element, options = {}) {
8 | let selector = finder(element, {
9 | tagName: () => true,
10 | attr: (name, value) => name === 'id' || (ariaAttrs.includes(name) && value),
11 | ...options,
12 | });
13 |
14 | const tag = element.tagName.toLowerCase();
15 | if (!selector.startsWith(tag) && !selector.includes(' ')) {
16 | selector = `${tag}${selector}`;
17 | }
18 |
19 | return selector;
20 | }
21 |
--------------------------------------------------------------------------------
/src/content/services/recordWorkflow/addBlock.js:
--------------------------------------------------------------------------------
1 | import browser from 'webextension-polyfill';
2 |
3 | export default async function (detail, save = true) {
4 | const { isRecording, recording } = await browser.storage.local.get([
5 | 'isRecording',
6 | 'recording',
7 | ]);
8 |
9 | if (!isRecording || !recording) return null;
10 |
11 | let addedBlock = detail;
12 |
13 | if (typeof detail === 'function') addedBlock = detail(recording);
14 | else recording.flows.push(detail);
15 |
16 | if (save) await browser.storage.local.set({ recording });
17 |
18 | return { recording, addedBlock };
19 | }
20 |
--------------------------------------------------------------------------------
/src/popup/index.js:
--------------------------------------------------------------------------------
1 | import { createApp } from 'vue';
2 | import App from './App.vue';
3 | import router from './router';
4 | import pinia from '../lib/pinia';
5 | import compsUi from '../lib/compsUi';
6 | import vueI18n from '../lib/vueI18n';
7 | import vRemixicon, { icons } from '../lib/vRemixicon';
8 | import '../assets/css/tailwind.css';
9 | import '../assets/css/fonts.css';
10 | import '../assets/css/flow.css';
11 |
12 | createApp(App)
13 | .use(router)
14 | .use(compsUi)
15 | .use(vueI18n)
16 | .use(pinia)
17 | .use(vRemixicon, icons)
18 | .mount('#app');
19 |
20 | if (module.hot) module.hot.accept();
21 |
--------------------------------------------------------------------------------
/src/components/newtab/workflow/edit/EditDelay.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
11 |
12 |
25 |
--------------------------------------------------------------------------------
/src/components/ui/UiCard.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
11 |
29 |
--------------------------------------------------------------------------------
/src/assets/svg/logo.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/content/elementSelector/icons.js:
--------------------------------------------------------------------------------
1 | import {
2 | riEyeLine,
3 | riCheckLine,
4 | riCloseLine,
5 | riEyeOffLine,
6 | riFileCopyLine,
7 | riDragMoveLine,
8 | riSettings3Line,
9 | riListUnordered,
10 | riArrowLeftLine,
11 | riArrowLeftSLine,
12 | riInformationLine,
13 | riArrowDropDownLine,
14 | } from 'v-remixicon/icons';
15 |
16 | export default {
17 | riEyeLine,
18 | riCheckLine,
19 | riCloseLine,
20 | riEyeOffLine,
21 | riFileCopyLine,
22 | riDragMoveLine,
23 | riSettings3Line,
24 | riListUnordered,
25 | riArrowLeftLine,
26 | riArrowLeftSLine,
27 | riInformationLine,
28 | riArrowDropDownLine,
29 | };
30 |
--------------------------------------------------------------------------------
/src/lib/cronstrue.js:
--------------------------------------------------------------------------------
1 | import cronstrue from 'cronstrue';
2 | import 'cronstrue/locales/fr';
3 | import 'cronstrue/locales/zh_TW';
4 | import 'cronstrue/locales/zh_CN';
5 |
6 | const supportedLocales = ['en', 'zh', 'zh-tw', 'fr'];
7 | const altLocaleId = {
8 | zh: 'zh_CN',
9 | 'zh-TW': 'zh_TW',
10 | };
11 |
12 | export function readableCron(expression) {
13 | const currentLang = document.documentElement.lang;
14 | const locale = supportedLocales.includes(currentLang)
15 | ? altLocaleId[currentLang] || currentLang
16 | : 'en';
17 |
18 | return cronstrue.toString(expression, { locale });
19 | }
20 |
21 | export default cronstrue;
22 |
--------------------------------------------------------------------------------
/src/lib/pinia.js:
--------------------------------------------------------------------------------
1 | import { markRaw } from 'vue';
2 | import { createPinia } from 'pinia';
3 | import browser from 'webextension-polyfill';
4 |
5 | function saveToStoragePlugin({ store, options }) {
6 | const newBrowser = markRaw(browser);
7 |
8 | store.saveToStorage = (key) => {
9 | const storageKey = options.storageMap[key];
10 | if (!storageKey || !store.retrieved) return null;
11 |
12 | const value = JSON.parse(JSON.stringify(store[key]));
13 |
14 | return newBrowser.storage.local.set({ [storageKey]: value });
15 | };
16 | }
17 |
18 | const pinia = createPinia();
19 | pinia.use(saveToStoragePlugin);
20 |
21 | export default pinia;
22 |
--------------------------------------------------------------------------------
/src/components/newtab/workflow/edit/Parameter/ParameterCheckboxValue.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 | {{ paramData.placeholder || paramData.name }}
10 |
11 |
12 |
26 |
--------------------------------------------------------------------------------
/src/content/elementSelector/main.js:
--------------------------------------------------------------------------------
1 | import { createApp } from 'vue';
2 | import vRemixicon from 'v-remixicon';
3 | import App from './App.vue';
4 | import compsUi from './compsUi';
5 | import icons from './icons';
6 | import vueI18n from './vueI18n';
7 | import '@/assets/css/tailwind.css';
8 |
9 | export default function (rootElement) {
10 | const appRoot = document.createElement('div');
11 | appRoot.setAttribute('id', 'app');
12 |
13 | rootElement.shadowRoot.appendChild(appRoot);
14 |
15 | createApp(App)
16 | .provide('rootElement', rootElement)
17 | .use(vueI18n)
18 | .use(vRemixicon, icons)
19 | .use(compsUi)
20 | .mount(appRoot);
21 | }
22 |
--------------------------------------------------------------------------------
/src/workflowEngine/blocksHandler/handlerLink.js:
--------------------------------------------------------------------------------
1 | import BrowserAPIService from '@/service/browser-api/BrowserAPIService';
2 |
3 | export default async function ({ data, id, label }) {
4 | const url = await this._sendMessageToTab({
5 | id,
6 | data,
7 | label,
8 | });
9 |
10 | if (data.openInNewTab) {
11 | const tab = await BrowserAPIService.tabs.create({
12 | url,
13 | windowId: this.activeTab.windowId,
14 | });
15 |
16 | this.activeTab.url = url;
17 | this.activeTab.frameId = 0;
18 | this.activeTab.id = tab.id;
19 | }
20 |
21 | return {
22 | data: url,
23 | nextBlockId: this.getBlockConnections(id),
24 | };
25 | }
26 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/src/locales/zh/popup.json:
--------------------------------------------------------------------------------
1 | {
2 | "recording": {
3 | "stop": "停止录制",
4 | "title": "录制中"
5 | },
6 | "home": {
7 | "record": {
8 | "title": "录制工作流",
9 | "button": "录制",
10 | "name": "工作流名称",
11 | "selectBlock": "选择一个启动模块",
12 | "anotherBlock": "无法从此模块启动",
13 | "tabs": {
14 | "new": "新建模块",
15 | "existing": "已存在工作流"
16 | }
17 | },
18 | "elementSelector": {
19 | "name": "元素选择器",
20 | "noAccess": "您没有权限访问此站点"
21 | },
22 | "workflow": {
23 | "new": "新建工作流",
24 | "rename": "重命名工作流",
25 | "delete": "删除工作流",
26 | "type": {
27 | "host": "主机",
28 | "local": "本地"
29 | }
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/workflowEngine/blocksHandler/handlerElementExists.js:
--------------------------------------------------------------------------------
1 | function elementExists(block) {
2 | return new Promise((resolve, reject) => {
3 | this._sendMessageToTab(block)
4 | .then((data) => {
5 | if (!data && block.data.throwError) {
6 | const error = new Error('element-not-found');
7 | error.data = { selector: block.data.selector };
8 |
9 | reject(error);
10 | return;
11 | }
12 |
13 | resolve({
14 | data,
15 | nextBlockId: this.getBlockConnections(block.id, data ? 1 : 2),
16 | });
17 | })
18 | .catch((error) => {
19 | reject(error);
20 | });
21 | });
22 | }
23 |
24 | export default elementExists;
25 |
--------------------------------------------------------------------------------
/src/workflowEngine/blocksHandler/handlerRepeatTask.js:
--------------------------------------------------------------------------------
1 | function repeatTask({ data, id }) {
2 | return new Promise((resolve) => {
3 | const repeat = Number.isNaN(+data.repeatFor) ? 0 : +data.repeatFor;
4 |
5 | if (this.repeatedTasks[id] > repeat || !this.getBlockConnections(id, 2)) {
6 | delete this.repeatedTasks[id];
7 |
8 | resolve({
9 | data: repeat,
10 | nextBlockId: this.getBlockConnections(id),
11 | });
12 | } else {
13 | this.repeatedTasks[id] = (this.repeatedTasks[id] || 1) + 1;
14 |
15 | resolve({
16 | data: repeat,
17 | nextBlockId: this.getBlockConnections(id, 2),
18 | });
19 | }
20 | });
21 | }
22 |
23 | export default repeatTask;
24 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Source code in this repository is variously licensed under the GNU Affero General Public License (AGPL), or the Automa Commercial License (https://extension.automa.site/license/commercial/).
2 |
3 | * Outside of the top-level "business" directory, source code in a given file is licensed under the AGPL.
4 |
5 | * Within the the top-level "business" directory, source code in a given file is licensed under the Automa Commercial License, unless otherwise noted.
6 |
7 | When built, binary files are generated for the AGPL source code and the Automa Commercial License source code. Binaries located at business.automa.site are released under the Automa Commercial License. Binaries located at all non-business paths are released under the AGPL.
8 |
--------------------------------------------------------------------------------
/src/locales/zh-TW/popup.json:
--------------------------------------------------------------------------------
1 | {
2 | "recording": {
3 | "stop": "停止錄製",
4 | "title": "錄製中"
5 | },
6 | "home": {
7 | "record": {
8 | "title": "錄製工作流程",
9 | "button": "錄製",
10 | "name": "工作流程名稱",
11 | "selectBlock": "選擇一個區塊開始",
12 | "anotherBlock": "無法從此區塊開始",
13 | "tabs": {
14 | "new": "新增工作流程",
15 | "existing": "現有工作流程"
16 | }
17 | },
18 | "elementSelector": {
19 | "name": "元素選擇器",
20 | "noAccess": "無法存取此網站"
21 | },
22 | "workflow": {
23 | "new": "新增工作流程",
24 | "rename": "重新命名工作流程",
25 | "delete": "刪除工作流程",
26 | "type": {
27 | "host": "主機",
28 | "local": "本機"
29 | }
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/components/newtab/workflow/edit/Parameter/ParameterJsonValue.vue:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
29 |
--------------------------------------------------------------------------------
/src/components/content/selector/SelectorElementList.vue:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
27 |
--------------------------------------------------------------------------------
/src/assets/svg/logoFirefox.svg:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/src/content/commandPalette/main.js:
--------------------------------------------------------------------------------
1 | import { createApp } from 'vue';
2 | import vRemixicon from 'v-remixicon';
3 | import App from './App.vue';
4 | import compsUi from './compsUi';
5 | import icons from './icons';
6 |
7 | const additionalStyle = `.list-item-active svg { visibility: visible }`;
8 |
9 | export default function (rootElement) {
10 | const appRoot = document.createElement('div');
11 | appRoot.setAttribute('id', 'app');
12 |
13 | const style = document.createElement('style');
14 | style.textContent = additionalStyle;
15 |
16 | rootElement.shadowRoot.appendChild(style);
17 | rootElement.shadowRoot.appendChild(appRoot);
18 |
19 | createApp(App)
20 | .use(compsUi)
21 | .use(vRemixicon, icons)
22 | .provide('rootElement', rootElement)
23 | .mount(appRoot);
24 | }
25 |
--------------------------------------------------------------------------------
/src/content/commandPalette/icons.js:
--------------------------------------------------------------------------------
1 | import {
2 | riArrowGoForwardLine,
3 | riGlobalLine,
4 | riFileTextLine,
5 | riEqualizerLine,
6 | riTimerLine,
7 | riCalendarLine,
8 | riFlashlightLine,
9 | riLightbulbFlashLine,
10 | riDatabase2Line,
11 | riWindowLine,
12 | riCursorLine,
13 | riDownloadLine,
14 | riCommandLine,
15 | riExternalLinkLine,
16 | riArrowDropDownLine,
17 | } from 'v-remixicon/icons';
18 |
19 | export default {
20 | riArrowGoForwardLine,
21 | riGlobalLine,
22 | riFileTextLine,
23 | riEqualizerLine,
24 | riTimerLine,
25 | riCalendarLine,
26 | riFlashlightLine,
27 | riLightbulbFlashLine,
28 | riDatabase2Line,
29 | riWindowLine,
30 | riCursorLine,
31 | riDownloadLine,
32 | riCommandLine,
33 | riExternalLinkLine,
34 | riArrowDropDownLine,
35 | };
36 |
--------------------------------------------------------------------------------
/src/content/blocksHandler/handlerSwitchTo.js:
--------------------------------------------------------------------------------
1 | import { isXPath } from '@/utils/helper';
2 | import handleSelector from '../handleSelector';
3 |
4 | const framesEl = ['IFRAME', 'FRAME'];
5 |
6 | function switchTo(block) {
7 | return new Promise((resolve, reject) => {
8 | block.data.findBy = isXPath(block.data.selector) ? 'xpath' : 'cssSelector';
9 |
10 | handleSelector(block, {
11 | onSelected(element) {
12 | if (!framesEl.includes(element.tagName)) {
13 | reject(new Error('not-iframe'));
14 | return;
15 | }
16 |
17 | const isSameOrigin = element.contentDocument !== null;
18 |
19 | resolve({ url: element.src, isSameOrigin });
20 | },
21 | onError(error) {
22 | reject(error);
23 | },
24 | });
25 | });
26 | }
27 |
28 | export default switchTo;
29 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Desktop (please complete the following information):**
27 | - OS: [e.g. Windows]
28 | - Browser: [e.g. Google Chrome]
29 | - Extension Version: [e.g. v0.12.0]
30 |
31 | **Additional context**
32 | Add any other context about the problem here.
33 |
--------------------------------------------------------------------------------
/src/components/newtab/workflow/edit/EditLink.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 | {{ t('workflow.blocks.link.openInNewTab') }}
9 |
10 |
11 |
12 |
30 |
--------------------------------------------------------------------------------
/src/components/ui/UiListItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 |
15 |
33 |
--------------------------------------------------------------------------------
/src/newtab/index.js:
--------------------------------------------------------------------------------
1 | import { createApp } from 'vue';
2 | import { createHead } from '@vueuse/head';
3 | import App from './App.vue';
4 | import router from './router';
5 | import pinia from '../lib/pinia';
6 | import compsUi from '../lib/compsUi';
7 | import vueI18n from '../lib/vueI18n';
8 | import vRemixicon, { icons } from '../lib/vRemixicon';
9 | import vueToastification from '../lib/vue-toastification';
10 | import '../assets/css/tailwind.css';
11 | import '../assets/css/fonts.css';
12 | import '../assets/css/style.css';
13 | import '../assets/css/flow.css';
14 |
15 | const head = createHead();
16 |
17 | createApp(App)
18 | .use(head)
19 | .use(router)
20 | .use(compsUi)
21 | .use(pinia)
22 | .use(vueI18n)
23 | .use(vueToastification)
24 | .use(vRemixicon, icons)
25 | .mount('#app');
26 |
27 | if (module.hot) module.hot.accept();
28 |
--------------------------------------------------------------------------------
/src/content/blocksHandler/handlerSaveAssets.js:
--------------------------------------------------------------------------------
1 | import handleSelector from '../handleSelector';
2 |
3 | async function saveAssets(block) {
4 | let elements = await handleSelector(block, { returnElement: true });
5 |
6 | if (!elements) {
7 | throw new Error('element-not-found');
8 | }
9 |
10 | elements = block.data.multiple ? Array.from(elements) : [elements];
11 |
12 | const srcList = elements.reduce((acc, element) => {
13 | const tag = element.tagName;
14 |
15 | if ((tag === 'AUDIO' || tag === 'VIDEO') && !tag.src) {
16 | const sourceEl = element.querySelector('source');
17 |
18 | if (sourceEl && sourceEl.src) acc.push(sourceEl.src);
19 | } else if (element.src) {
20 | acc.push(element.src);
21 | }
22 |
23 | return acc;
24 | }, []);
25 |
26 | return srcList;
27 | }
28 |
29 | export default saveAssets;
30 |
--------------------------------------------------------------------------------
/src/locales/en/popup.json:
--------------------------------------------------------------------------------
1 | {
2 | "recording": {
3 | "stop": "Stop recording",
4 | "title": "Recording"
5 | },
6 | "home": {
7 | "record": {
8 | "title": "Record workflow",
9 | "button": "Record",
10 | "name": "Workflow name",
11 | "selectBlock": "Select a block to start from",
12 | "anotherBlock": "Can't start from this block",
13 | "tabs": {
14 | "new": "New workflow",
15 | "existing": "Existing workflow"
16 | }
17 | },
18 | "elementSelector": {
19 | "name": "Element selector",
20 | "noAccess": "Don't have access to this site"
21 | },
22 | "workflow": {
23 | "new": "New workflow",
24 | "rename": "Rename workflow",
25 | "delete": "Delete workflow",
26 | "type": {
27 | "host": "Host",
28 | "local": "Local"
29 | }
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/locales/tr/popup.json:
--------------------------------------------------------------------------------
1 | {
2 | "recording": {
3 | "stop": "Kaydı Durdur",
4 | "title": "Kayıt"
5 | },
6 | "home": {
7 | "record": {
8 | "title": "İş Akışı Kaydı",
9 | "button": "Kayıt",
10 | "name": "İş Akışı Adı",
11 | "selectBlock": "Başlamak için bir blok seçin",
12 | "anotherBlock": "Bu bloktan başlatılamaz",
13 | "tabs": {
14 | "new": "Yeni İş Akışı",
15 | "existing": "Mevcut İş Akışı"
16 | }
17 | },
18 | "elementSelector": {
19 | "name": "EÖğe Seçici",
20 | "noAccess": "Bu siteye erişim izniniz yok"
21 | },
22 | "workflow": {
23 | "new": "Yeni İş Akışı",
24 | "rename": "İş Akışını Yeniden Adlandır",
25 | "delete": "İş Akışını Sil",
26 | "type": {
27 | "host": "Ana Bilgisayar",
28 | "local": "Yerel"
29 | }
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/content/synchronizedLock.js:
--------------------------------------------------------------------------------
1 | class SynchronizedLock {
2 | constructor() {
3 | this.lock = false;
4 | this.queue = [];
5 | }
6 |
7 | async getLock(timeout = 10000) {
8 | while (this.lock) {
9 | await new Promise((resolve) => {
10 | this.queue.push(resolve);
11 | setTimeout(() => {
12 | const index = this.queue.indexOf(resolve);
13 | if (index !== -1) {
14 | this.queue.splice(index, 1);
15 | console.warn('SynchronizedLock timeout');
16 | resolve();
17 | }
18 | }, timeout);
19 | });
20 | }
21 |
22 | this.lock = true;
23 | }
24 |
25 | releaseLock() {
26 | this.lock = false;
27 | const resolve = this.queue.shift();
28 | if (resolve) resolve();
29 | }
30 | }
31 |
32 | const synchronizedLock = new SynchronizedLock();
33 |
34 | export default synchronizedLock;
35 |
--------------------------------------------------------------------------------
/src/locales/uk/popup.json:
--------------------------------------------------------------------------------
1 | {
2 | "recording": {
3 | "stop": "Зупинити запис",
4 | "title": "Запис"
5 | },
6 | "home": {
7 | "record": {
8 | "title": "Запис workflow",
9 | "button": "Запис",
10 | "name": "Назва workflow",
11 | "selectBlock": "Виберіть блок, з якого потрібно розпочати",
12 | "anotherBlock": "Неможливо почати з цього блоку",
13 | "tabs": {
14 | "new": "Новий workflow",
15 | "existing": "Існуючий workflow"
16 | }
17 | },
18 | "elementSelector": {
19 | "name": "Вибір елемента",
20 | "noAccess": "Немає доступу до цього сайту"
21 | },
22 | "workflow": {
23 | "new": "Новий workflow",
24 | "rename": "Перейменувати workflow",
25 | "delete": "Видалити workflow",
26 | "type": {
27 | "host": "Хост",
28 | "local": "Локальний"
29 | }
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/locales/pt-BR/popup.json:
--------------------------------------------------------------------------------
1 | {
2 | "recording": {
3 | "stop": "Parar gravação",
4 | "title": "Gravando"
5 | },
6 | "home": {
7 | "record": {
8 | "title": "Gravar Workflow",
9 | "button": "Gravar",
10 | "name": "Nome do Workflow",
11 | "selectBlock": "Selecione um bloco para iniciar",
12 | "anotherBlock": "Não é possível iniciar a partir deste bloco",
13 | "tabs": {
14 | "new": "Novo Workflow",
15 | "existing": "Workflow existente"
16 | }
17 | },
18 | "elementSelector": {
19 | "name": "Seletor de elemento",
20 | "noAccess": "Não tem acesso a este site"
21 | },
22 | "workflow": {
23 | "new": "Novo Workflow",
24 | "rename": "Renomear Workflow",
25 | "delete": "Excluir Workflow",
26 | "type": {
27 | "host": "Host",
28 | "local": "Local"
29 | }
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/locales/it/popup.json:
--------------------------------------------------------------------------------
1 | {
2 | "recording": {
3 | "stop": "Termina registrazione",
4 | "title": "Registrando"
5 | },
6 | "home": {
7 | "record": {
8 | "title": "Registra workflow",
9 | "button": "Registra",
10 | "name": "Nome workflow",
11 | "selectBlock": "Seleziona un blocco da cui iniziare",
12 | "anotherBlock": "Impossibile iniziare da questo blocco",
13 | "tabs": {
14 | "new": "Nuovo workflow",
15 | "existing": "Workflow esistente"
16 | }
17 | },
18 | "elementSelector": {
19 | "name": "Selettore elemento",
20 | "noAccess": "Automa non ha accesso a questo sito"
21 | },
22 | "workflow": {
23 | "new": "Nuovo workflow",
24 | "rename": "Rinomina workflow",
25 | "delete": "Elimina workflow",
26 | "type": {
27 | "host": "Host",
28 | "local": "Locale"
29 | }
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/locales/vi/popup.json:
--------------------------------------------------------------------------------
1 | {
2 | "recording": {
3 | "stop": "Dừng ghi",
4 | "title": "Ghi âm"
5 | },
6 | "home": {
7 | "record": {
8 | "title": "Ghi lại quy trình làm việc",
9 | "button": "Ghi lại",
10 | "name": "Tên quy trình làm việc",
11 | "selectBlock": "Chọn một khối để bắt đầu",
12 | "anotherBlock": "Không thể bắt đầu từ khối này",
13 | "tabs": {
14 | "new": "Quy trình làm việc mới",
15 | "existing": "Quy trình làm việc hiện tại"
16 | }
17 | },
18 | "elementSelector": {
19 | "name": "Bộ chọn phần tử",
20 | "noAccess": "Không có quyền truy cập vào trang web này"
21 | },
22 | "workflow": {
23 | "new": "Tạo quy trình mới",
24 | "rename": "Đổi tên quy trình",
25 | "delete": "Xóa quy trình",
26 | "type": {
27 | "host": "Máy chủ",
28 | "local": "Cục bộ",
29 | }
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/components/ui/UiSpinner.vue:
--------------------------------------------------------------------------------
1 |
2 |
25 |
26 |
40 |
--------------------------------------------------------------------------------
/src/service/renderer/RendererWorkflowService.js:
--------------------------------------------------------------------------------
1 | import { MessageListener } from '@/utils/message';
2 | import { toRaw } from 'vue';
3 |
4 | class RendererWorkflowService {
5 | static executeWorkflow(workflowData, options) {
6 | /**
7 | * Convert Vue-created proxy into plain object.
8 | * It will throw error if there a proxy inside the object.
9 | */
10 | const clonedWorkflowData = {};
11 | Object.keys(workflowData).forEach((key) => {
12 | clonedWorkflowData[key] = toRaw(workflowData[key]);
13 | });
14 |
15 | return MessageListener.sendMessage(
16 | 'workflow:execute',
17 | { ...workflowData, options },
18 | 'background'
19 | );
20 | }
21 |
22 | static stopWorkflowExecution(executionId) {
23 | return MessageListener.sendMessage(
24 | 'workflow:stop',
25 | executionId,
26 | 'background'
27 | );
28 | }
29 | }
30 |
31 | export default RendererWorkflowService;
32 |
--------------------------------------------------------------------------------
/src/lib/compsUi.js:
--------------------------------------------------------------------------------
1 | import VTooltip from '../directives/VTooltip';
2 | import VAutofocus from '../directives/VAutofocus';
3 | import VClosePopover from '../directives/VClosePopover';
4 |
5 | const uiComponents = require.context('../components/ui', false, /\.vue$/);
6 | const transitionComponents = require.context(
7 | '../components/transitions',
8 | false,
9 | /\.vue$/
10 | );
11 |
12 | function componentsExtractor(app, components) {
13 | components.keys().forEach((key) => {
14 | const componentName = key.replace(/(.\/)|\.vue$/g, '');
15 | const component = components(key)?.default ?? {};
16 |
17 | app.component(componentName, component);
18 | });
19 | }
20 |
21 | export default function (app) {
22 | app.directive('tooltip', VTooltip);
23 | app.directive('autofocus', VAutofocus);
24 | app.directive('close-popover', VClosePopover);
25 |
26 | componentsExtractor(app, uiComponents);
27 | componentsExtractor(app, transitionComponents);
28 | }
29 |
--------------------------------------------------------------------------------
/src/workflowEngine/blocksHandler/handlerIncreaseVariable.js:
--------------------------------------------------------------------------------
1 | import objectPath from 'object-path';
2 |
3 | export async function increaseVariable({ id, data }) {
4 | const refVariables = this.engine.referenceData.variables;
5 | const variableExist = objectPath.has(refVariables, data.variableName);
6 |
7 | if (!variableExist) {
8 | throw new Error(`Cant find "${data.variableName}" variable`);
9 | }
10 |
11 | const currentVar = +objectPath.get(refVariables, data.variableName);
12 | if (Number.isNaN(currentVar)) {
13 | throw new Error(
14 | `The "${data.variableName}" variable value is not a number`
15 | );
16 | }
17 |
18 | objectPath.set(
19 | this.engine.referenceData.variables,
20 | data.variableName,
21 | currentVar + data.increaseBy
22 | );
23 |
24 | return {
25 | data: refVariables[data.variableName],
26 | nextBlockId: this.getBlockConnections(id),
27 | };
28 | }
29 |
30 | export default increaseVariable;
31 |
--------------------------------------------------------------------------------
/utils/build-zip.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | const fs = require('fs');
3 | const path = require('path');
4 | const archiver = require('archiver');
5 | const packageJSON = require('../package.json');
6 |
7 | const browser = process.env.BROWSER || 'chrome';
8 | const appVersion = packageJSON.version;
9 | const fileName = `${packageJSON.name}-${browser}-v${appVersion}.zip`;
10 |
11 | const destDir = path.join(__dirname, '../build');
12 | const zipDir = path.join(__dirname, '../build-zip', appVersion);
13 |
14 | if (!fs.existsSync(zipDir)) {
15 | fs.mkdirSync(zipDir, { recursive: true });
16 | }
17 |
18 | const archive = archiver('zip', { zlib: { level: 9 } });
19 | const stream = fs.createWriteStream(path.join(zipDir, fileName));
20 |
21 | archive
22 | .directory(destDir, false)
23 | .on('error', (error) => {
24 | console.error(error);
25 | })
26 | .pipe(stream);
27 |
28 | stream.on('close', () => console.log('Success'));
29 | archive.finalize();
30 |
--------------------------------------------------------------------------------
/src/workflowEngine/blocksHandler/handlerSliceVariable.js:
--------------------------------------------------------------------------------
1 | import objectPath from 'object-path';
2 |
3 | export async function sliceData({ id, data }) {
4 | const variable = objectPath.get(
5 | this.engine.referenceData.variables,
6 | data.variableName
7 | );
8 | const payload = {
9 | data: variable,
10 | nextBlockId: this.getBlockConnections(id),
11 | };
12 |
13 | if (!variable || !variable?.slice) return payload;
14 |
15 | let startIndex = 0;
16 | let endIndex = variable.length;
17 |
18 | if (data.startIdxEnabled) {
19 | startIndex = data.startIndex;
20 | }
21 | if (data.endIdxEnabled) {
22 | endIndex = data.endIndex;
23 | }
24 |
25 | const slicedVariable = variable.slice(startIndex, endIndex);
26 | payload.data = slicedVariable;
27 | objectPath.set(
28 | this.engine.referenceData.variables,
29 | data.variableName,
30 | slicedVariable
31 | );
32 |
33 | return payload;
34 | }
35 |
36 | export default sliceData;
37 |
--------------------------------------------------------------------------------
/src/directives/VTooltip.js:
--------------------------------------------------------------------------------
1 | import createTippy from '@/lib/tippy';
2 |
3 | function getContent(content) {
4 | if (typeof content === 'string') {
5 | return { content };
6 | }
7 |
8 | if (typeof content === 'object' && content !== null) {
9 | return content;
10 | }
11 |
12 | return {};
13 | }
14 |
15 | export default {
16 | mounted(el, { value, arg = 'top', instance, modifiers }) {
17 | const content = getContent(value);
18 |
19 | const tooltip = createTippy(el, {
20 | ...content,
21 | theme: 'tooltip-theme',
22 | placement: arg,
23 | });
24 |
25 | if (modifiers.group) {
26 | if (!Array.isArray(instance._tooltipGroup)) instance._tooltipGroup = [];
27 |
28 | instance._tooltipGroup.push(tooltip);
29 | }
30 | },
31 | updated(el, { value, arg = 'top' }) {
32 | const content = getContent(value);
33 |
34 | el._tippy.setProps({
35 | placement: arg,
36 | ...content,
37 | });
38 | },
39 | };
40 |
--------------------------------------------------------------------------------
/src/locales/es/popup.json:
--------------------------------------------------------------------------------
1 | {
2 | "recording": {
3 | "stop": "Detener la grabación",
4 | "title": "Grabación"
5 | },
6 | "home": {
7 | "record": {
8 | "title": "Registro del flujo de trabajo",
9 | "button": "Registro",
10 | "name": "Nombre del flujo de trabajo",
11 | "selectBlock": "Seleccione un bloque para empezar",
12 | "anotherBlock": "No se puede empezar desde este bloque",
13 | "tabs": {
14 | "new": "Nuevo flujo de trabajo",
15 | "existing": "Flujo de trabajo existente"
16 | }
17 | },
18 | "elementSelector": {
19 | "name": "Selector de elementos",
20 | "noAccess": "No tiene acceso a este sitio"
21 | },
22 | "workflow": {
23 | "new": "Nuevo flujo de trabajo",
24 | "rename": "Renombrar flujo de trabajo",
25 | "delete": "Eliminar flujo de trabajo",
26 | "type": {
27 | "host": "Host",
28 | "local": "Local"
29 | }
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/utils/decryptFlow.js:
--------------------------------------------------------------------------------
1 | import { nanoid } from 'nanoid';
2 | import hmacSHA256 from 'crypto-js/hmac-sha256';
3 | import AES from 'crypto-js/aes';
4 | import encUtf8 from 'crypto-js/enc-utf8';
5 | import { parseJSON } from './helper';
6 | import getPassKey from './getPassKey';
7 |
8 | export function getWorkflowPass(pass) {
9 | const key = getPassKey(nanoid());
10 | const decryptedPass = AES.decrypt(pass.substring(64), key).toString(encUtf8);
11 |
12 | return decryptedPass;
13 | }
14 |
15 | export default function ({ pass, drawflow }, password) {
16 | const hmac = pass.substring(0, 64);
17 | const decryptedHmac = hmacSHA256(pass.substring(64), password).toString();
18 |
19 | if (hmac !== decryptedHmac)
20 | return {
21 | isError: true,
22 | message: 'incorrect-password',
23 | };
24 |
25 | const isDecrypted = parseJSON(drawflow, null);
26 | if (isDecrypted) return isDecrypted;
27 |
28 | return AES.decrypt(drawflow, password).toString(encUtf8);
29 | }
30 |
--------------------------------------------------------------------------------
/src/components/newtab/workflow/edit/Trigger/TriggerVisitWeb.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
14 | {{ t('workflow.blocks.trigger.useRegex') }}
15 |
16 |
21 | Support SPA website
22 |
23 |
24 |
25 |
38 |
--------------------------------------------------------------------------------
/src/components/ui/UiTabPanel.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
42 |
--------------------------------------------------------------------------------
/src/components/newtab/workflow/edit/TriggerEvent/TriggerEventInput.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
38 |
--------------------------------------------------------------------------------
/src/content/blocksHandler/handlerElementExists.js:
--------------------------------------------------------------------------------
1 | import handleSelector from '../handleSelector';
2 |
3 | function elementExists(block) {
4 | return new Promise((resolve) => {
5 | let trying = 0;
6 |
7 | const isExists = async () => {
8 | try {
9 | const element = await handleSelector(block, { returnElement: true });
10 |
11 | if (!element) throw new Error('element-not-found');
12 |
13 | return true;
14 | } catch (error) {
15 | return false;
16 | }
17 | };
18 |
19 | async function checkElement() {
20 | if (trying > (block.data.tryCount || 1)) {
21 | resolve(false);
22 | return;
23 | }
24 |
25 | const isElementExist = await isExists();
26 |
27 | if (isElementExist) {
28 | resolve(true);
29 | } else {
30 | trying += 1;
31 |
32 | setTimeout(checkElement, block.data.timeout || 500);
33 | }
34 | }
35 |
36 | checkElement();
37 | });
38 | }
39 |
40 | export default elementExists;
41 |
--------------------------------------------------------------------------------
/src/content/blocksHandler/handlerHoverElement.js:
--------------------------------------------------------------------------------
1 | import { sendMessage } from '@/utils/message';
2 | import { getElementPosition } from '../utils';
3 | import handleSelector from '../handleSelector';
4 |
5 | function eventClick(block) {
6 | return new Promise((resolve, reject) => {
7 | handleSelector(block, {
8 | async onSelected(element) {
9 | const { x, y } = await getElementPosition(element);
10 | const payload = {
11 | tabId: block.activeTabId,
12 | method: 'Input.dispatchMouseEvent',
13 | params: {
14 | x,
15 | y,
16 | clickCount: 1,
17 | button: 'left',
18 | type: 'mousePressed',
19 | },
20 | };
21 |
22 | await sendMessage('debugger:send-command', payload, 'background');
23 | },
24 | onError(error) {
25 | reject(error);
26 | },
27 | onSuccess() {
28 | resolve('');
29 | },
30 | });
31 | });
32 | }
33 |
34 | export default eventClick;
35 |
--------------------------------------------------------------------------------
/src/content/blocksHandler/handlerLoopData.js:
--------------------------------------------------------------------------------
1 | import { nanoid } from 'nanoid';
2 | import handleSelector from '../handleSelector';
3 | import { generateLoopSelectors } from '../utils';
4 |
5 | export default async function loopElements(block) {
6 | const elements = await handleSelector(block);
7 | if (!elements) throw new Error('element-not-found');
8 |
9 | let frameSelector = '';
10 | if (block.data.$frameSelector) {
11 | frameSelector = `${block.data.$frameSelector} |> `;
12 | }
13 |
14 | if (block.onlyGenerate) {
15 | generateLoopSelectors(elements, {
16 | ...block.data,
17 | frameSelector,
18 | attrId: block.data.loopId,
19 | });
20 |
21 | return {};
22 | }
23 |
24 | const attrId = `${block.id}-${nanoid(5)}`;
25 | const selectors = generateLoopSelectors(elements, {
26 | ...block.data,
27 | frameSelector,
28 | attrId,
29 | });
30 | const { origin, pathname } = window.location;
31 |
32 | return {
33 | loopId: attrId,
34 | elements: selectors,
35 | url: origin + pathname,
36 | };
37 | }
38 |
--------------------------------------------------------------------------------
/src/content/services/recordWorkflow/main.js:
--------------------------------------------------------------------------------
1 | import { createApp } from 'vue';
2 | import vRemixicon from 'v-remixicon';
3 | import App from './App.vue';
4 | import icons from './icons';
5 | import injectAppStyles from '../../injectAppStyles';
6 |
7 | const customCSS = `
8 | #app {
9 | font-family: 'Inter var';
10 | line-height: 1.5;
11 | }
12 | .content {
13 | width: 250px;
14 | }
15 | `;
16 |
17 | export default function () {
18 | const rootElement = document.createElement('div');
19 | rootElement.attachShadow({ mode: 'open' });
20 | rootElement.setAttribute('id', 'automa-recording');
21 | rootElement.classList.add('automa-element-selector');
22 | document.body.appendChild(rootElement);
23 |
24 | return injectAppStyles(rootElement.shadowRoot, customCSS).then(() => {
25 | const appRoot = document.createElement('div');
26 | appRoot.setAttribute('id', 'app');
27 | rootElement.shadowRoot.appendChild(appRoot);
28 |
29 | const app = createApp(App).use(vRemixicon, icons);
30 | app.mount(appRoot);
31 |
32 | return app;
33 | });
34 | }
35 |
--------------------------------------------------------------------------------
/src/workflowEngine/blocksHandler/handlerHoverElement.js:
--------------------------------------------------------------------------------
1 | import BrowserAPIService from '@/service/browser-api/BrowserAPIService';
2 | import { attachDebugger } from '../helper';
3 |
4 | export async function hoverElement(block) {
5 | if (!this.activeTab.id) throw new Error('no-tab');
6 | if (BROWSER_TYPE !== 'chrome') {
7 | const error = new Error('browser-not-supported');
8 | error.data = { browser: BROWSER_TYPE };
9 |
10 | throw error;
11 | }
12 |
13 | const { debugMode, executedBlockOnWeb } = this.settings;
14 |
15 | if (!debugMode) {
16 | await attachDebugger(this.activeTab.id);
17 | }
18 |
19 | await this._sendMessageToTab({
20 | ...block,
21 | debugMode,
22 | executedBlockOnWeb,
23 | activeTabId: this.activeTab.id,
24 | frameSelector: this.frameSelector,
25 | });
26 |
27 | if (!debugMode) {
28 | BrowserAPIService.debugger.detach({ tabId: this.activeTab.id });
29 | }
30 |
31 | return {
32 | data: '',
33 | nextBlockId: this.getBlockConnections(block.id),
34 | };
35 | }
36 |
37 | export default hoverElement;
38 |
--------------------------------------------------------------------------------
/src/workflowEngine/blocksHandler/handlerWhileLoop.js:
--------------------------------------------------------------------------------
1 | import testConditions from '../utils/testConditions';
2 | import checkCodeCondition from '../utils/conditionCode';
3 |
4 | async function whileLoop({ data, id }, { refData }) {
5 | const { debugMode } = this.engine.workflow?.settings || {};
6 | const conditionPayload = {
7 | refData,
8 | isMV2: this.engine.isMV2,
9 | isPopup: this.engine.isPopup,
10 | activeTab: this.activeTab.id,
11 | checkCodeCondition: (payload) => {
12 | payload.debugMode = debugMode;
13 | return checkCodeCondition(this.activeTab, payload);
14 | },
15 | sendMessage: (payload) =>
16 | this._sendMessageToTab({ ...payload.data, label: 'conditions', id }),
17 | };
18 | const result = await testConditions(data.conditions, conditionPayload);
19 | const nextBlockId = this.getBlockConnections(
20 | id,
21 | result.isMatch ? 1 : 'fallback'
22 | );
23 |
24 | return {
25 | data: '',
26 | nextBlockId,
27 | replacedValue: result?.replacedValue || {},
28 | };
29 | }
30 |
31 | export default whileLoop;
32 |
--------------------------------------------------------------------------------
/src/workflowEngine/blocksHandler/handlerTabUrl.js:
--------------------------------------------------------------------------------
1 | import BrowserAPIService from '@/service/browser-api/BrowserAPIService';
2 |
3 | export async function logData({ id, data }) {
4 | let urls = [];
5 |
6 | if (data.type === 'active-tab') {
7 | if (!this.activeTab.id) throw new Error('no-tab');
8 |
9 | const tab = await BrowserAPIService.tabs.get(this.activeTab.id);
10 | urls = tab.url || tab.pendingUrl || '';
11 | } else {
12 | const query = {};
13 |
14 | if (data.qMatchPatterns) {
15 | query.url = data.qMatchPatterns;
16 | }
17 | if (data.qTitle) {
18 | query.title = data.qTitle;
19 | }
20 |
21 | const tabs = await BrowserAPIService.tabs.query(query);
22 | urls = tabs.map((tab) => tab.url);
23 | }
24 |
25 | if (data.assignVariable) {
26 | await this.setVariable(data.variableName, urls);
27 | }
28 | if (data.saveData) {
29 | this.addDataToColumn(data.dataColumn, urls);
30 | }
31 |
32 | return {
33 | data: urls,
34 | nextBlockId: this.getBlockConnections(id),
35 | };
36 | }
37 |
38 | export default logData;
39 |
--------------------------------------------------------------------------------
/src/assets/css/fonts.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: Inter var;
3 | font-weight: 100 900;
4 | font-display: swap;
5 | font-style: normal;
6 | font-named-instance: "Regular";
7 | src: url('../fonts/Inter-roman-latin.var.woff2') format("woff2");
8 | }
9 |
10 | /* source-code-pro-regular - latin */
11 | @font-face {
12 | font-family: 'Source Code Pro';
13 | font-style: normal;
14 | font-weight: 400;
15 | src: local(''),
16 | url('../fonts/source-code-pro-v21-latin-regular.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
17 | url('../fonts/source-code-pro-v21-latin-regular.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
18 | }
19 | /* source-code-pro-600 - latin */
20 | @font-face {
21 | font-family: 'Source Code Pro';
22 | font-style: normal;
23 | font-weight: 600;
24 | src: local(''),
25 | url('../fonts/source-code-pro-v21-latin-600.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
26 | url('../fonts/source-code-pro-v21-latin-600.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
27 | }
--------------------------------------------------------------------------------
/src/utils/credentialUtil.js:
--------------------------------------------------------------------------------
1 | import SHA256 from 'crypto-js/sha256';
2 | import HmacSHA256 from 'crypto-js/hmac-sha256';
3 | import AES from 'crypto-js/aes';
4 | import encUtf8 from 'crypto-js/enc-utf8';
5 | import getPassKey from './getPassKey';
6 | import { parseJSON } from './helper';
7 |
8 | function encryptValue(value) {
9 | const pass = getPassKey('credential');
10 | const encryptedValue = AES.encrypt(value, pass).toString();
11 | const hmac = HmacSHA256(encryptedValue, SHA256(pass)).toString();
12 |
13 | return hmac + encryptedValue;
14 | }
15 |
16 | function decryptValue(value) {
17 | const pass = getPassKey('credential');
18 | const hmac = value.substring(0, 64);
19 | const encryptedValue = value.substring(64);
20 | const decryptedHmac = HmacSHA256(encryptedValue, SHA256(pass)).toString();
21 |
22 | if (hmac !== decryptedHmac) return '';
23 |
24 | const decryptedValue = AES.decrypt(encryptedValue, pass).toString(encUtf8);
25 |
26 | return parseJSON(decryptedValue, decryptedValue);
27 | }
28 |
29 | export default {
30 | encrypt: encryptValue,
31 | decrypt: decryptValue,
32 | };
33 |
--------------------------------------------------------------------------------
/src/sandbox/index.js:
--------------------------------------------------------------------------------
1 | import objectPath from 'object-path';
2 | import handleConditionCode from './utils/handleConditionCode';
3 | import handleJavascriptBlock from './utils/handleJavascriptBlock';
4 | import handleBlockExpression from './utils/handleBlockExpression';
5 |
6 | window.$getNestedProperties = objectPath.get;
7 |
8 | function fetchResponse({ id, data }) {
9 | window.dispatchEvent(
10 | new CustomEvent(`automa-fetch-response-${id}`, {
11 | detail: data,
12 | })
13 | );
14 | }
15 |
16 | const eventHandlers = {
17 | fetchResponse,
18 | conditionCode: handleConditionCode,
19 | blockExpression: handleBlockExpression,
20 | javascriptBlock: handleJavascriptBlock,
21 | };
22 |
23 | window.addEventListener('message', ({ data }) => {
24 | if (!data.id || !data.type || !eventHandlers[data.type]) return;
25 |
26 | function sendResponse(payload) {
27 | window.top.postMessage(
28 | {
29 | id: data.id,
30 | type: 'sandbox',
31 | result: payload,
32 | },
33 | '*'
34 | );
35 | }
36 |
37 | eventHandlers[data.type](data, sendResponse);
38 | });
39 |
--------------------------------------------------------------------------------
/src/workflowEngine/blocksHandler/handlerNotification.js:
--------------------------------------------------------------------------------
1 | import BrowserAPIService from '@/service/browser-api/BrowserAPIService';
2 | import { nanoid } from 'nanoid';
3 |
4 | export default async function ({ data, id }) {
5 | const hasPermission = await BrowserAPIService.permissions.contains({
6 | permissions: ['notifications'],
7 | });
8 |
9 | if (!hasPermission) {
10 | const error = new Error('no-permission');
11 | error.data = { permission: 'notifications' };
12 |
13 | throw error;
14 | }
15 |
16 | const options = {
17 | title: data.title,
18 | message: data.message,
19 | iconUrl: BrowserAPIService.runtime.getURL('icon-128.png'),
20 | };
21 |
22 | ['iconUrl', 'imageUrl'].forEach((key) => {
23 | const url = data[key];
24 | if (!url || !url.startsWith('http')) return;
25 |
26 | options[key] = url;
27 | });
28 |
29 | await BrowserAPIService.notifications.create(nanoid(), {
30 | ...options,
31 | type: options.imageUrl ? 'image' : 'basic',
32 | });
33 |
34 | return {
35 | data: '',
36 | nextBlockId: this.getBlockConnections(id),
37 | };
38 | }
39 |
--------------------------------------------------------------------------------
/src/offscreen/message-listener.js:
--------------------------------------------------------------------------------
1 | import BrowserAPIEventHandler from '@/service/browser-api/BrowserAPIEventHandler';
2 | import { MessageListener } from '@/utils/message';
3 | import WorkflowManager from '@/workflowEngine/WorkflowManager';
4 | import Browser from 'webextension-polyfill';
5 |
6 | const messageListener = new MessageListener('offscreen');
7 | Browser.runtime.onMessage.addListener(messageListener.listener);
8 |
9 | messageListener.on('workflow:execute', ({ workflow, options }) => {
10 | WorkflowManager.instance.execute(workflow, options);
11 | });
12 |
13 | messageListener.on('workflow:stop', (stateId) => {
14 | WorkflowManager.instance.stopExecution(stateId);
15 | });
16 |
17 | messageListener.on('workflow:resume', ({ id, nextBlock }) => {
18 | WorkflowManager.instance.resumeExecution(id, nextBlock);
19 | });
20 |
21 | messageListener.on('workflow:update', ({ id, data }) => {
22 | WorkflowManager.instance.updateExecution(id, data);
23 | });
24 |
25 | messageListener.on(BrowserAPIEventHandler.RuntimeEvents.ON_EVENT, (event) =>
26 | BrowserAPIEventHandler.instance.onBrowserEventListener(event)
27 | );
28 |
--------------------------------------------------------------------------------
/src/composable/commandManager.js:
--------------------------------------------------------------------------------
1 | import { shallowRef, computed } from 'vue';
2 |
3 | export function useCommandManager({ maxHistory = 100 } = {}) {
4 | const position = shallowRef(0);
5 | let history = [null];
6 |
7 | const state = computed(() => ({
8 | position: position.value,
9 | historyLen: history.length,
10 | canUndo: position.value > 0,
11 | canRedo: position.value < history.length - 1,
12 | }));
13 |
14 | return {
15 | state,
16 | add(command) {
17 | if (position.value < history.length - 1) {
18 | history = history.slice(0, position.value + 1);
19 | }
20 | if (history.length > maxHistory) {
21 | history.shift();
22 | }
23 |
24 | history.push(command);
25 | position.value += 1;
26 | },
27 | undo() {
28 | if (position.value > 0) {
29 | history[position.value].undo();
30 | position.value -= 1;
31 | }
32 | },
33 | redo() {
34 | if (position.value < history.length - 1) {
35 | position.value += 1;
36 | history[position.value].execute();
37 | }
38 | },
39 | };
40 | }
41 |
--------------------------------------------------------------------------------
/src/components/newtab/workflow/edit/TriggerEvent/TriggerEventTouch.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 | {{ item }}
9 |
10 |
11 |
12 |
45 |
--------------------------------------------------------------------------------
/src/workflowEngine/templating/renderString.js:
--------------------------------------------------------------------------------
1 | import { messageSandbox } from '../helper';
2 | import mustacheReplacer from './mustacheReplacer';
3 |
4 | const isFirefox = BROWSER_TYPE === 'firefox';
5 |
6 | export default async function (str, data, options = {}) {
7 | if (!str || typeof str !== 'string') return '';
8 |
9 | const hasMustacheTag = /\{\{(.*?)\}\}/.test(str);
10 | if (!hasMustacheTag) {
11 | return {
12 | list: {},
13 | value: str,
14 | };
15 | }
16 |
17 | let renderedValue = {};
18 | const evaluateJS = str.startsWith('!!');
19 |
20 | if (evaluateJS && !isFirefox) {
21 | const refKeysRegex =
22 | /(variables|table|secrets|loopData|workflow|googleSheets|globalData)@/g;
23 | const strToRender = str.replace(refKeysRegex, '$1.');
24 |
25 | renderedValue = await messageSandbox('blockExpression', {
26 | str: strToRender,
27 | data,
28 | });
29 | } else {
30 | let copyStr = `${str}`;
31 | if (evaluateJS) copyStr = copyStr.slice(2);
32 |
33 | renderedValue = mustacheReplacer(copyStr, data, options);
34 | }
35 |
36 | return renderedValue;
37 | }
38 |
--------------------------------------------------------------------------------
/src/workflowEngine/blocksHandler/handlerLogData.js:
--------------------------------------------------------------------------------
1 | import getTranslateLog from '@/utils/getTranslateLog';
2 |
3 | export async function logData({ id, data }) {
4 | if (!data.workflowId) {
5 | throw new Error('No workflow is selected');
6 | }
7 |
8 | // 工作流状态数组
9 | // block handler is inside WorkflowWorker scope. See WorkflowWorker.js:343
10 | const { states } = this.engine.states;
11 | let logs = [];
12 | if (states) {
13 | // 转换为数组
14 | const stateValues = Object.values(Object.fromEntries(states));
15 | // 当前工作流状态
16 | const curWorkflowState = stateValues.find(
17 | (item) => item.workflowId === data.workflowId
18 | )?.state;
19 |
20 | if (curWorkflowState) {
21 | // 当前工作流最新日志
22 | logs = getTranslateLog(curWorkflowState, 'json');
23 |
24 | if (data.assignVariable) {
25 | await this.setVariable(data.variableName, logs);
26 | }
27 | if (data.saveData) {
28 | this.addDataToColumn(data.dataColumn, logs);
29 | }
30 | }
31 | }
32 |
33 | return {
34 | data: logs,
35 | nextBlockId: this.getBlockConnections(id),
36 | };
37 | }
38 |
39 | export default logData;
40 |
--------------------------------------------------------------------------------
/src/utils/triggerText.js:
--------------------------------------------------------------------------------
1 | import browser from 'webextension-polyfill';
2 | import dayjs from '@/lib/dayjs';
3 | import { getReadableShortcut } from '@/composable/shortcut';
4 |
5 | export default async function (trigger, t, workflowId, includeManual = false) {
6 | if (!trigger || (trigger.type === 'manual' && !includeManual)) return null;
7 |
8 | const triggerLocale = t('workflow.blocks.trigger.name');
9 |
10 | if (trigger.type === 'manual') {
11 | return `${triggerLocale}: ${t('workflow.blocks.trigger.items.manual')}`;
12 | }
13 |
14 | const triggerName = t(`workflow.blocks.trigger.items.${trigger.type}`);
15 | let text = '';
16 |
17 | if (trigger.type === 'keyboard-shortcut') {
18 | text = getReadableShortcut(trigger.shortcut);
19 | } else if (trigger.type === 'visit-web') {
20 | text = trigger.url;
21 | } else if (['specific-day', 'date'].includes(trigger.type)) {
22 | const triggerTime = (await browser.alarms.get(workflowId))?.scheduledTime;
23 |
24 | text = dayjs(triggerTime || Date.now()).format('DD-MMM-YYYY, hh:mm A');
25 | }
26 |
27 | text = text && `: \n ${text}`;
28 |
29 | return `${triggerLocale} (${triggerName})${text}`;
30 | }
31 |
--------------------------------------------------------------------------------
/src/composable/groupTooltip.js:
--------------------------------------------------------------------------------
1 | import { getCurrentInstance, shallowRef, nextTick, onUnmounted } from 'vue';
2 | import { createSingleton } from 'tippy.js';
3 | import createTippy, { defaultOptions } from '@/lib/tippy';
4 |
5 | export function useGroupTooltip(elements, options = {}) {
6 | const singleton = shallowRef(null);
7 | const instance = getCurrentInstance();
8 | const context = instance && instance.ctx;
9 |
10 | nextTick(() => {
11 | let tippyInstances = [];
12 |
13 | if (Array.isArray(elements)) {
14 | tippyInstances = elements.map((el) => el._tippy || createTippy(el));
15 | } else {
16 | tippyInstances = context._tooltipGroup || [];
17 | }
18 |
19 | singleton.value = createSingleton(tippyInstances, {
20 | ...defaultOptions,
21 | ...options,
22 | theme: 'tooltip-theme',
23 | placement: 'right',
24 | moveTransition: 'transform 0.2s ease-out',
25 | overrides: ['placement', 'theme'],
26 | });
27 |
28 | if (!elements) {
29 | context.__tpSingleton = singleton.value;
30 | }
31 | });
32 | onUnmounted(() => {
33 | singleton.value?.destroy();
34 | });
35 |
36 | return singleton;
37 | }
38 |
--------------------------------------------------------------------------------
/src/content/elementSelector/index.js:
--------------------------------------------------------------------------------
1 | import { elementSelectorInstance } from '../utils';
2 | import initElementSelector from './main';
3 | import injectAppStyles from '../injectAppStyles';
4 | import selectorFrameContext from './selectorFrameContext';
5 |
6 | (async function () {
7 | try {
8 | const isMainFrame = window.self === window.top;
9 |
10 | if (isMainFrame) {
11 | const isAppExists = elementSelectorInstance();
12 | if (isAppExists) return;
13 |
14 | const rootElement = document.createElement('div');
15 | rootElement.setAttribute('id', 'app-container');
16 | rootElement.classList.add('automa-element-selector');
17 | rootElement.attachShadow({ mode: 'open' });
18 |
19 | initElementSelector(rootElement);
20 | await injectAppStyles(rootElement.shadowRoot);
21 |
22 | document.documentElement.appendChild(rootElement);
23 | } else {
24 | const style = document.createElement('style');
25 | style.textContent = '[automa-el-list] {outline: 2px dashed #6366f1;}';
26 |
27 | document.body.appendChild(style);
28 |
29 | selectorFrameContext();
30 | }
31 | } catch (error) {
32 | console.error(error);
33 | }
34 | })();
35 |
--------------------------------------------------------------------------------
/src/workflowEngine/blocksHandler/handlerNewWindow.js:
--------------------------------------------------------------------------------
1 | import BrowserAPIService from '@/service/browser-api/BrowserAPIService';
2 | import { attachDebugger } from '../helper';
3 |
4 | export async function newWindow({ data, id }) {
5 | const windowOptions = {
6 | state: data.windowState,
7 | incognito: data.incognito,
8 | type: data.type || 'normal',
9 | };
10 |
11 | if (data.windowState === 'normal') {
12 | ['top', 'left', 'height', 'width'].forEach((key) => {
13 | if (data[key] <= 0) return;
14 |
15 | windowOptions[key] = data[key];
16 | });
17 | }
18 | if (data.url) windowOptions.url = data.url;
19 |
20 | const newWindowInstance = await BrowserAPIService.windows.create(
21 | windowOptions
22 | );
23 | this.windowId = newWindowInstance.id;
24 |
25 | if (data.url) {
26 | const [tab] = newWindowInstance.tabs;
27 |
28 | if (this.settings.debugMode)
29 | await attachDebugger(tab.id, this.activeTab.id);
30 |
31 | this.activeTab.id = tab.id;
32 | this.activeTab.url = tab.url;
33 | }
34 |
35 | return {
36 | data: newWindowInstance.id,
37 | nextBlockId: this.getBlockConnections(id),
38 | };
39 | }
40 |
41 | export default newWindow;
42 |
--------------------------------------------------------------------------------
/src/content/commandPalette/compsUi.js:
--------------------------------------------------------------------------------
1 | import VAutofocus from '@/directives/VAutofocus';
2 | import UiCard from '@/components/ui/UiCard.vue';
3 | import UiInput from '@/components/ui/UiInput.vue';
4 | import UiList from '@/components/ui/UiList.vue';
5 | import UiListItem from '@/components/ui/UiListItem.vue';
6 | import UiButton from '@/components/ui/UiButton.vue';
7 | import UiSelect from '@/components/ui/UiSelect.vue';
8 | import UiSpinner from '@/components/ui/UiSpinner.vue';
9 | import UiTextarea from '@/components/ui/UiTextarea.vue';
10 | import UiPopover from '@/components/ui/UiPopover.vue';
11 | import TransitionExpand from '@/components/transitions/TransitionExpand.vue';
12 |
13 | export default function (app) {
14 | app.component('UiCard', UiCard);
15 | app.component('UiList', UiList);
16 | app.component('UiInput', UiInput);
17 | app.component('UiButton', UiButton);
18 | app.component('UiSelect', UiSelect);
19 | app.component('UiPopover', UiPopover);
20 | app.component('UiSpinner', UiSpinner);
21 | app.component('UiTextarea', UiTextarea);
22 | app.component('UiListItem', UiListItem);
23 | app.component('TransitionExpand', TransitionExpand);
24 |
25 | app.directive('autofocus', VAutofocus);
26 | }
27 |
--------------------------------------------------------------------------------
/src/composable/blockValidation.js:
--------------------------------------------------------------------------------
1 | import { onMounted, watch, shallowRef } from 'vue';
2 | import blocksValidation from '@/newtab/utils/blocksValidation';
3 |
4 | export function useBlockValidation(blockId, data) {
5 | const errors = shallowRef('');
6 |
7 | onMounted(() => {
8 | const blockValidation = blocksValidation[blockId];
9 | if (!blockValidation) return;
10 |
11 | const unwatch = watch(
12 | data,
13 | (newData) => {
14 | blockValidation
15 | .func(newData)
16 | .then((blockErrors) => {
17 | let errorsStr = '';
18 | blockErrors.forEach((error) => {
19 | errorsStr += `${error}\n`;
20 | });
21 |
22 | errors.value =
23 | errorsStr.trim() &&
24 | `Issues: ${errorsStr}
`;
25 | })
26 | .catch((error) => {
27 | console.error(error);
28 | })
29 | .finally(() => {
30 | if (blockValidation.once) {
31 | unwatch();
32 | }
33 | });
34 | },
35 | { deep: true, immediate: true }
36 | );
37 | });
38 |
39 | return { errors };
40 | }
41 |
--------------------------------------------------------------------------------
/src/components/newtab/workflow/WorkflowGlobalData.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ globalData.length }}/{{ maxLength.toLocaleString() }}
5 |
6 |
11 |
12 |
13 |
46 |
--------------------------------------------------------------------------------
/src/utils/simulateEvent/mouseEvent.js:
--------------------------------------------------------------------------------
1 | export default function ({ sendCommand, commandParams }) {
2 | async function mousedown() {
3 | commandParams.type = 'mousePressed';
4 | await sendCommand('Input.dispatchMouseEvent', commandParams);
5 | }
6 | async function mouseup() {
7 | commandParams.type = 'mouseReleased';
8 | await sendCommand('Input.dispatchMouseEvent', commandParams);
9 | }
10 | async function click() {
11 | if (!commandParams.clickCount) commandParams.clickCount = 1;
12 |
13 | await mousedown();
14 | await mouseup();
15 | }
16 | async function dblclick() {
17 | commandParams.clickCount = 2;
18 | await click();
19 | }
20 | async function mousemove() {
21 | commandParams.type = 'mouseMoved';
22 | await sendCommand('Input.dispatchMouseEvent', commandParams);
23 | }
24 | async function mouseenter() {
25 | await mousemove();
26 | }
27 | async function mouseleave() {
28 | await mousemove();
29 |
30 | commandParams.x = -100;
31 | commandParams.y = -100;
32 | await mousemove();
33 | }
34 |
35 | return {
36 | mousedown,
37 | mouseup,
38 | click,
39 | dblclick,
40 | mousemove,
41 | mouseenter,
42 | mouseleave,
43 | };
44 | }
45 |
--------------------------------------------------------------------------------
/src/components/newtab/workflow/edit/Parameter/ParameterInputValue.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
48 |
--------------------------------------------------------------------------------
/src/sandbox/utils/handleConditionCode.js:
--------------------------------------------------------------------------------
1 | export default function (data) {
2 | const propertyName = `automa${data.id}`;
3 |
4 | const script = document.createElement('script');
5 | script.textContent = `
6 | (async () => {
7 | function automaRefData(keyword, path = '') {
8 | if (!keyword) return null;
9 | if (!path) return ${propertyName}.refData[keyword];
10 |
11 | return window.$getNestedProperties(${propertyName}.refData, keyword + '.' + path);
12 | }
13 |
14 | try {
15 | ${data.data.code}
16 | } catch (error) {
17 | return {
18 | $isError: true,
19 | message: error.message,
20 | }
21 | }
22 | })()
23 | .then((result) => {
24 | ${propertyName}.done(result);
25 | });
26 | `;
27 |
28 | window[propertyName] = {
29 | refData: data.refData,
30 | done: (result) => {
31 | script.remove();
32 | delete window[propertyName];
33 |
34 | window.top.postMessage(
35 | {
36 | result,
37 | id: data.id,
38 | type: 'sandbox',
39 | },
40 | '*'
41 | );
42 | },
43 | };
44 |
45 | (document.body || document.documentElement).appendChild(script);
46 | }
47 |
--------------------------------------------------------------------------------
/src/utils/simulateEvent/index.js:
--------------------------------------------------------------------------------
1 | import { eventList } from '../shared';
2 |
3 | export function getEventObj(name, params) {
4 | const eventType = eventList.find(({ id }) => id === name)?.type ?? '';
5 | let event;
6 |
7 | switch (eventType) {
8 | case 'mouse-event':
9 | event = new MouseEvent(name, { ...params, view: window });
10 | break;
11 | case 'focus-event':
12 | event = new FocusEvent(name, params);
13 | break;
14 | case 'touch-event':
15 | event = new TouchEvent(name, params);
16 | break;
17 | case 'keyboard-event':
18 | event = new KeyboardEvent(name, params);
19 | break;
20 | case 'wheel-event':
21 | event = new WheelEvent(name, params);
22 | break;
23 | case 'input-event':
24 | event = new InputEvent(name, params);
25 | break;
26 | default:
27 | event = new Event(name, params);
28 | }
29 |
30 | return event;
31 | }
32 |
33 | export default function (element, name, params) {
34 | const event = getEventObj(name, params);
35 | const useNativeMethods = ['focus', 'submit', 'blur'];
36 |
37 | if (useNativeMethods.includes(name) && element[name]) {
38 | element[name]();
39 | } else {
40 | element.dispatchEvent(event);
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/utils/serialization.js:
--------------------------------------------------------------------------------
1 | export function serializeFunctions(obj) {
2 | if (typeof obj === 'function') {
3 | return {
4 | __type: 'function',
5 | __value: obj.toString(),
6 | };
7 | }
8 |
9 | if (Array.isArray(obj)) {
10 | return obj.map((item) => serializeFunctions(item));
11 | }
12 |
13 | if (obj && typeof obj === 'object') {
14 | const result = {};
15 | for (const key in obj) {
16 | if (Object.prototype.hasOwnProperty.call(obj, key)) {
17 | result[key] = serializeFunctions(obj[key]);
18 | }
19 | }
20 | return result;
21 | }
22 |
23 | return obj;
24 | }
25 |
26 | export function deserializeFunctions(obj) {
27 | if (obj && typeof obj === 'object') {
28 | if (obj.__type === 'function') {
29 | // eslint-disable-next-line no-new-func, prefer-template
30 | return new Function('return ' + obj.__value)();
31 | }
32 |
33 | if (Array.isArray(obj)) {
34 | return obj.map((item) => deserializeFunctions(item));
35 | }
36 |
37 | const result = {};
38 | for (const key in obj) {
39 | if (Object.prototype.hasOwnProperty.call(obj, key)) {
40 | result[key] = deserializeFunctions(obj[key]);
41 | }
42 | }
43 | return result;
44 | }
45 |
46 | return obj;
47 | }
48 |
--------------------------------------------------------------------------------
/src/components/newtab/workflow/edit/TriggerEvent/TriggerEventWheel.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
13 |
19 |
20 |
21 |
53 |
--------------------------------------------------------------------------------
/src/assets/css/flow.css:
--------------------------------------------------------------------------------
1 | .vue-flow__minimap {
2 | @apply rounded-lg dark:bg-gray-800;
3 | }
4 |
5 | .vue-flow__node {
6 | & > div {
7 | @apply rounded-lg transition;
8 | }
9 | &.selected .block-base__content {
10 | @apply ring-2 ring-accent;
11 | }
12 | &:hover {
13 | .block-menu-container {
14 | display: block;
15 | }
16 | }
17 |
18 | &.vue-flow__node-BlockGroup2 {
19 | z-index: 0 !important;
20 | }
21 |
22 | .vue-flow__handle {
23 | @apply h-4 w-4 rounded-full border-0;
24 | &.target {
25 | @apply bg-accent -ml-4;
26 | }
27 | &.source {
28 | border-width: 3px;
29 | @apply border-accent -mr-4 bg-white dark:bg-black;
30 | }
31 | }
32 | }
33 |
34 | .vue-flow {
35 | &.disabled {
36 | .vue-flow__handle {
37 | pointer-events: none;
38 | }
39 | }
40 | svg g.connected-edges path {
41 | stroke: theme('colors.primary');
42 | }
43 | }
44 |
45 | .vue-flow__edge {
46 | cursor: pointer;
47 | &.selected .vue-flow__edge-path {
48 | stroke: theme('colors.green.300');
49 | }
50 | }
51 |
52 | .dark .vue-flow__edge-path:hover {
53 | stroke: theme('colors.yellow.400');
54 | }
55 | .vue-flow__edge-path {
56 | stroke: theme('colors.accent');
57 | stroke-width: 4;
58 | transition: stroke 100ms ease;
59 | &:hover {
60 | stroke: theme('colors.yellow.500');
61 | }
62 | }
--------------------------------------------------------------------------------
/src/components/newtab/workflow/edit/EditIncreaseVariable.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
16 |
24 |
25 |
26 |
43 |
48 |
--------------------------------------------------------------------------------
/src/content/blocksHandler/handlerJavascriptCode.js:
--------------------------------------------------------------------------------
1 | import { jsContentHandler } from '@/workflowEngine/utils/javascriptBlockUtil';
2 | import { getDocumentCtx } from '../handleSelector';
3 |
4 | function javascriptCode({ data, isPreloadScripts, frameSelector }) {
5 | if (!isPreloadScripts && Array.isArray(data))
6 | return jsContentHandler(...data);
7 | if (!data.scripts) return Promise.resolve({ success: true });
8 |
9 | let $documentCtx = document;
10 |
11 | if (frameSelector) {
12 | const iframeCtx = getDocumentCtx(frameSelector);
13 | if (!iframeCtx) return Promise.resolve({ success: false });
14 |
15 | $documentCtx = iframeCtx;
16 | }
17 |
18 | data.scripts.forEach((script) => {
19 | const scriptAttr = `block--${script.id}`;
20 |
21 | const isScriptExists = $documentCtx.querySelector(
22 | `.automa-custom-js[${scriptAttr}]`
23 | );
24 |
25 | if (isScriptExists) return;
26 |
27 | const scriptEl = $documentCtx.createElement('script');
28 | scriptEl.textContent = script.data.code;
29 | scriptEl.setAttribute(scriptAttr, '');
30 | scriptEl.classList.add('automa-custom-js');
31 |
32 | $documentCtx.documentElement.appendChild(scriptEl);
33 | });
34 |
35 | return Promise.resolve({ success: true });
36 | }
37 |
38 | export default javascriptCode;
39 |
--------------------------------------------------------------------------------
/src/content/blocksHandler/handlerAttributeValue.js:
--------------------------------------------------------------------------------
1 | import handleSelector from '../handleSelector';
2 |
3 | function handleAttributeValue(block) {
4 | return new Promise((resolve, reject) => {
5 | let result = [];
6 | const { attributeName, multiple, attributeValue, action } = block.data;
7 | const isCheckboxOrRadio = (element) => {
8 | if (element.tagName !== 'INPUT') return false;
9 |
10 | return ['checkbox', 'radio'].includes(element.getAttribute('type'));
11 | };
12 |
13 | handleSelector(block, {
14 | onSelected(element) {
15 | if (action === 'set') {
16 | element.setAttribute(attributeName, attributeValue);
17 | return;
18 | }
19 |
20 | let value = element.getAttribute(attributeName);
21 |
22 | if (attributeName === 'checked' && isCheckboxOrRadio(element)) {
23 | value = element.checked;
24 | } else if (attributeName === 'href' && element.tagName === 'A') {
25 | value = element.href;
26 | }
27 |
28 | if (multiple) result.push(value);
29 | else result = value;
30 | },
31 | onError(error) {
32 | reject(error);
33 | },
34 | onSuccess() {
35 | resolve(result);
36 | },
37 | });
38 | });
39 | }
40 |
41 | export default handleAttributeValue;
42 |
--------------------------------------------------------------------------------
/src/workflowEngine/blocksHandler/handlerRegexVariable.js:
--------------------------------------------------------------------------------
1 | import objectPath from 'object-path';
2 |
3 | export async function regexVariable({ id, data }) {
4 | const refVariables = this.engine.referenceData.variables;
5 | const variableExist = objectPath.has(refVariables, data.variableName);
6 |
7 | if (!variableExist) {
8 | throw new Error(`Cant find "${data.variableName}" variable`);
9 | }
10 |
11 | const str = objectPath.get(refVariables, data.variableName);
12 | if (typeof str !== 'string') {
13 | throw new Error(
14 | `The value of the "${data.variableName}" variable is not a string/text`
15 | );
16 | }
17 |
18 | const method = data.method || 'match';
19 | const regex = new RegExp(data.expression, data.flag.join(''));
20 |
21 | let newValue = '';
22 |
23 | if (method === 'match') {
24 | const matches = str.match(regex);
25 | newValue = matches && !data.flag.includes('g') ? matches[0] : matches;
26 | } else if (method === 'replace') {
27 | newValue = str.replace(regex, data.replaceVal ?? '');
28 | }
29 |
30 | objectPath.set(
31 | this.engine.referenceData.variables,
32 | data.variableName,
33 | newValue
34 | );
35 |
36 | return {
37 | data: newValue,
38 | nextBlockId: this.getBlockConnections(id),
39 | };
40 | }
41 |
42 | export default regexVariable;
43 |
--------------------------------------------------------------------------------
/src/components/newtab/workflow/edit/Trigger/TriggerDate.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
12 |
20 |
21 |
22 |
49 |
--------------------------------------------------------------------------------
/src/workflowEngine/blocksHandler/handlerWorkflowState.js:
--------------------------------------------------------------------------------
1 | export default async function ({ data, id }) {
2 | try {
3 | let stopCurrent = false;
4 |
5 | if (data.type === 'stop-current') {
6 | // 如果需要抛出错误
7 | if (data.throwError) {
8 | throw new Error(data.errorMessage || 'Workflow stopped manually');
9 | } else {
10 | return {};
11 | }
12 | }
13 | if (['stop-specific', 'stop-all'].includes(data.type)) {
14 | const ids = [];
15 | const isSpecific = data.type === 'stop-specific';
16 | this.engine.states.getAll.forEach((state) => {
17 | const workflowNotIncluded =
18 | isSpecific && !data.workflowsToStop.includes(state.workflowId);
19 | if (workflowNotIncluded) return;
20 |
21 | ids.push(state.id);
22 | });
23 |
24 | for (const stateId of ids) {
25 | if (stateId === this.engine.id) {
26 | stopCurrent = isSpecific ? true : !data.exceptCurrent;
27 | } else {
28 | await this.engine.states.stop(stateId);
29 | }
30 | }
31 | }
32 |
33 | if (stopCurrent) return {};
34 |
35 | return {
36 | data: '',
37 | nextBlockId: this.getBlockConnections(id),
38 | };
39 | } catch (error) {
40 | error.data = error.data || {};
41 | console.error(error);
42 |
43 | throw error;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/content/blocksHandler/handlerGetText.js:
--------------------------------------------------------------------------------
1 | import handleSelector from '../handleSelector';
2 |
3 | function getText(block) {
4 | return new Promise((resolve, reject) => {
5 | let regex;
6 | let textResult = [];
7 | const {
8 | regex: regexData,
9 | regexExp,
10 | prefixText,
11 | suffixText,
12 | multiple,
13 | includeTags,
14 | useTextContent,
15 | } = block.data;
16 |
17 | if (regexData) {
18 | regex = new RegExp(regexData, [...new Set(regexExp)].join(''));
19 | }
20 |
21 | handleSelector(block, {
22 | onSelected(element) {
23 | let text = '';
24 |
25 | if (includeTags) {
26 | text = element.outerHTML;
27 | } else if (useTextContent) {
28 | text = element.textContent;
29 | } else {
30 | text = element.innerText;
31 | }
32 |
33 | if (regex) text = text.match(regex)?.join(' ') ?? text;
34 |
35 | text = (prefixText || '') + text + (suffixText || '');
36 |
37 | if (multiple) {
38 | textResult.push(text);
39 | } else {
40 | textResult = text;
41 | }
42 | },
43 | onError(error) {
44 | reject(error);
45 | },
46 | onSuccess() {
47 | resolve(textResult);
48 | },
49 | });
50 | });
51 | }
52 |
53 | export default getText;
54 |
--------------------------------------------------------------------------------
/src/content/services/recordWorkflow/index.js:
--------------------------------------------------------------------------------
1 | import browser from 'webextension-polyfill';
2 | import initElementSelector from './main';
3 | import initRecordEvents from './recordEvents';
4 | import selectorFrameContext from '../../elementSelector/selectorFrameContext';
5 |
6 | (async () => {
7 | try {
8 | let elementSelectorInstance = null;
9 | const isMainFrame = window.self === window.top;
10 | const destroyRecordEvents = await initRecordEvents(isMainFrame);
11 |
12 | if (isMainFrame) {
13 | const element = document.querySelector('#automa-recording');
14 | if (element) return;
15 |
16 | elementSelectorInstance = await initElementSelector();
17 | } else {
18 | const style = document.createElement('style');
19 | style.textContent = '[automa-el-list] {outline: 2px dashed #6366f1;}';
20 |
21 | document.body.appendChild(style);
22 |
23 | selectorFrameContext();
24 | }
25 |
26 | browser.runtime.onMessage.addListener(function messageListener({ type }) {
27 | if (type === 'recording:stop') {
28 | if (elementSelectorInstance) {
29 | elementSelectorInstance.unmount();
30 | }
31 |
32 | destroyRecordEvents();
33 | browser.runtime.onMessage.removeListener(messageListener);
34 | }
35 | });
36 | } catch (error) {
37 | console.error(error);
38 | }
39 | })();
40 |
--------------------------------------------------------------------------------
/src/components/newtab/workflow/settings/SettingsTable.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ t('workflow.settings.defaultColumn.title') }}
6 |
7 |
8 | {{ t('workflow.settings.defaultColumn.description') }}
9 |
10 |
11 |
15 |
16 |
17 |
18 |
19 | {{ t('workflow.settings.defaultColumn.name') }}
20 |
21 |
26 |
27 |
28 |
29 |
46 |
--------------------------------------------------------------------------------
/src/composable/theme.js:
--------------------------------------------------------------------------------
1 | import { ref, onMounted } from 'vue';
2 | import browser from 'webextension-polyfill';
3 |
4 | const themes = [
5 | { name: 'Light', id: 'light' },
6 | { name: 'Dark', id: 'dark' },
7 | { name: 'System', id: 'system' },
8 | ];
9 | const isPreferDark = () =>
10 | window.matchMedia('(prefers-color-scheme: dark)').matches;
11 |
12 | export function useTheme() {
13 | const activeTheme = ref('system');
14 |
15 | async function setTheme(theme) {
16 | const isValidTheme = themes.some(({ id }) => id === theme);
17 |
18 | if (!isValidTheme) return;
19 |
20 | let isDarkTheme = theme === 'dark';
21 |
22 | if (theme === 'system') isDarkTheme = isPreferDark();
23 |
24 | document.documentElement.classList.toggle('dark', isDarkTheme);
25 | activeTheme.value = theme;
26 |
27 | await browser.storage.local.set({ theme });
28 | }
29 | async function getTheme() {
30 | let { theme } = await browser.storage.local.get('theme');
31 |
32 | if (!theme) theme = 'system';
33 |
34 | return theme;
35 | }
36 | async function init() {
37 | const theme = await getTheme();
38 |
39 | await setTheme(theme);
40 | }
41 |
42 | onMounted(async () => {
43 | activeTheme.value = await getTheme();
44 | });
45 |
46 | return {
47 | init,
48 | themes,
49 | activeTheme,
50 | set: setTheme,
51 | get: getTheme,
52 | };
53 | }
54 |
--------------------------------------------------------------------------------
/src/stores/folder.js:
--------------------------------------------------------------------------------
1 | import { defineStore } from 'pinia';
2 | import { nanoid } from 'nanoid';
3 | import browser from 'webextension-polyfill';
4 |
5 | export const useFolderStore = defineStore('folder', {
6 | storageMap: {
7 | items: 'folders',
8 | },
9 | state: () => ({
10 | items: [],
11 | retrieved: false,
12 | }),
13 | actions: {
14 | async addFolder(name) {
15 | this.items.push({
16 | name,
17 | id: nanoid(),
18 | });
19 |
20 | await this.saveToStorage('items');
21 |
22 | return this.items.at(-1);
23 | },
24 | async deleteFolder(id) {
25 | const index = this.items.findIndex((folder) => folder.id === id);
26 | if (index === -1) return null;
27 |
28 | this.items.splice(index, 1);
29 | await this.saveToStorage('items');
30 |
31 | return index;
32 | },
33 | async updateFolder(id, data = {}) {
34 | const index = this.items.findIndex((folder) => folder.id === id);
35 | if (index === -1) return null;
36 |
37 | Object.assign(this.items[index], data);
38 | await this.saveToStorage('items');
39 |
40 | return this.items[index];
41 | },
42 | load() {
43 | return browser.storage.local.get('folders').then(({ folders }) => {
44 | this.items = folders || [];
45 | this.retrieved = true;
46 | return folders;
47 | });
48 | },
49 | },
50 | });
51 |
--------------------------------------------------------------------------------
/src/workflowEngine/blocksHandler/handlerBlocksGroup.js:
--------------------------------------------------------------------------------
1 | function blocksGroup({ data, id }, { prevBlockData }) {
2 | return new Promise((resolve) => {
3 | const nextBlockId = this.getBlockConnections(id);
4 |
5 | if (data.blocks.length === 0) {
6 | resolve({
7 | nextBlockId,
8 | data: prevBlockData,
9 | });
10 |
11 | return;
12 | }
13 |
14 | const { blocks, connections } = data.blocks.reduce(
15 | (acc, block, index) => {
16 | const nextBlock = data.blocks[index + 1]?.itemId;
17 |
18 | acc.blocks[block.itemId] = {
19 | label: block.id,
20 | data: block.data,
21 | id: nextBlock ? block.itemId : id,
22 | };
23 |
24 | if (nextBlock) {
25 | const outputId = `${block.itemId}-output-1`;
26 |
27 | if (!acc.connections[outputId]) {
28 | acc.connections[outputId] = new Map();
29 | }
30 | acc.connections[outputId].set(nextBlock, { id: nextBlock });
31 | }
32 |
33 | return acc;
34 | },
35 | { blocks: {}, connections: {} }
36 | );
37 |
38 | Object.assign(this.engine.blocks, blocks);
39 | Object.assign(this.engine.connectionsMap, connections);
40 |
41 | resolve({
42 | data: prevBlockData,
43 | nextBlockId: [{ id: data.blocks[0].itemId }],
44 | });
45 | });
46 | }
47 |
48 | export default blocksGroup;
49 |
--------------------------------------------------------------------------------
/src/components/content/shared/SharedElementHighlighter.vue:
--------------------------------------------------------------------------------
1 |
2 |
16 |
17 |
58 |
--------------------------------------------------------------------------------
/src/content/elementSelector/compsUi.js:
--------------------------------------------------------------------------------
1 | import VAutofocus from '@/directives/VAutofocus';
2 | import UiTab from '@/components/ui/UiTab.vue';
3 | import UiTabs from '@/components/ui/UiTabs.vue';
4 | import UiInput from '@/components/ui/UiInput.vue';
5 | import UiButton from '@/components/ui/UiButton.vue';
6 | import UiSelect from '@/components/ui/UiSelect.vue';
7 | import UiExpand from '@/components/ui/UiExpand.vue';
8 | import UiSwitch from '@/components/ui/UiSwitch.vue';
9 | import UiTextarea from '@/components/ui/UiTextarea.vue';
10 | import UiCheckbox from '@/components/ui/UiCheckbox.vue';
11 | import UiTabPanel from '@/components/ui/UiTabPanel.vue';
12 | import UiTabPanels from '@/components/ui/UiTabPanels.vue';
13 | import TransitionExpand from '@/components/transitions/TransitionExpand.vue';
14 |
15 | export default function (app) {
16 | app.component('UiTab', UiTab);
17 | app.component('UiTabs', UiTabs);
18 | app.component('UiInput', UiInput);
19 | app.component('UiButton', UiButton);
20 | app.component('UiSelect', UiSelect);
21 | app.component('UiSwitch', UiSwitch);
22 | app.component('UiExpand', UiExpand);
23 | app.component('UiTextarea', UiTextarea);
24 | app.component('UiCheckbox', UiCheckbox);
25 | app.component('UiTabPanel', UiTabPanel);
26 | app.component('UiTabPanels', UiTabPanels);
27 | app.component('TransitionExpand', TransitionExpand);
28 |
29 | app.directive('autofocus', VAutofocus);
30 | }
31 |
--------------------------------------------------------------------------------
/src/workflowEngine/blocksHandler/handlerDeleteData.js:
--------------------------------------------------------------------------------
1 | function deleteData({ data, id }) {
2 | return new Promise((resolve) => {
3 | let variableDeleted = false;
4 |
5 | data.deleteList.forEach((item) => {
6 | if (item.type === 'table') {
7 | if (item.columnId === '[all]') {
8 | this.engine.referenceData.table = [];
9 |
10 | Object.keys(this.engine.columns).forEach((key) => {
11 | this.engine.columns[key].index = 0;
12 | });
13 | } else {
14 | const columnName = this.engine.columns[item.columnId].name;
15 |
16 | this.engine.referenceData.table.forEach((_, index) => {
17 | const row = this.engine.referenceData.table[index];
18 | delete row[columnName];
19 |
20 | if (!row || Object.keys(row).length === 0) {
21 | this.engine.referenceData.table[index] = {};
22 | }
23 | });
24 |
25 | this.engine.columns[item.columnId].index = 0;
26 | }
27 | } else if (item.variableName) {
28 | delete this.engine.referenceData.variables[item.variableName];
29 | variableDeleted = true;
30 | }
31 | });
32 |
33 | if (variableDeleted) this.engine.addRefDataSnapshot('variables');
34 |
35 | resolve({
36 | data: '',
37 | nextBlockId: this.getBlockConnections(id),
38 | });
39 | });
40 | }
41 |
42 | export default deleteData;
43 |
--------------------------------------------------------------------------------
/src/content/commandPalette/index.js:
--------------------------------------------------------------------------------
1 | import browser from 'webextension-polyfill';
2 | import initApp from './main';
3 | import injectAppStyles from '../injectAppStyles';
4 |
5 | function pageLoaded() {
6 | return new Promise((resolve) => {
7 | const checkDocState = () => {
8 | if (document.readyState === 'loading') {
9 | setTimeout(checkDocState, 1000);
10 | return;
11 | }
12 |
13 | resolve();
14 | };
15 |
16 | checkDocState();
17 | });
18 | }
19 |
20 | export default async function () {
21 | try {
22 | const isMainFrame = window.self === window.top;
23 | if (!isMainFrame) return;
24 |
25 | const isInvalidURL = /.(json|xml)$/.test(window.location.pathname);
26 | if (isInvalidURL) return;
27 |
28 | const { automaShortcut } = await browser.storage.local.get(
29 | 'automaShortcut'
30 | );
31 | if (Array.isArray(automaShortcut) && automaShortcut.length === 0) return;
32 |
33 | await pageLoaded();
34 |
35 | const instanceExist = document.querySelector('automa-palette');
36 | if (instanceExist) return;
37 |
38 | const element = document.createElement('div');
39 | element.attachShadow({ mode: 'open' });
40 | element.id = 'automa-palette';
41 |
42 | await injectAppStyles(element.shadowRoot);
43 | initApp(element);
44 |
45 | document.body.appendChild(element);
46 | } catch (error) {
47 | console.error(error);
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/workflowEngine/workflowEvent.js:
--------------------------------------------------------------------------------
1 | import { nanoid } from 'nanoid';
2 | import { messageSandbox } from './helper';
3 | import renderString from './templating/renderString';
4 |
5 | class WorkflowEvent {
6 | static async #httpRequest({ url, method, headers, body }, refData) {
7 | if (!url.trim()) return;
8 |
9 | const reqHeaders = {
10 | 'Content-Type': 'application/json',
11 | };
12 | headers.forEach((header) => {
13 | reqHeaders[header.name] = header.value;
14 | });
15 |
16 | const renderedBody =
17 | method !== 'GET' ? (await renderString(body, refData)).value : undefined;
18 |
19 | await fetch(url, {
20 | method,
21 | body: renderedBody,
22 | headers: reqHeaders,
23 | });
24 | }
25 |
26 | static async #javascriptCode(event, refData) {
27 | const instanceId = `automa${nanoid()}`;
28 |
29 | await messageSandbox('javascriptBlock', {
30 | refData,
31 | instanceId,
32 | preloadScripts: [],
33 | blockData: {
34 | code: event.code,
35 | },
36 | });
37 | }
38 |
39 | static async handle(event, refData) {
40 | switch (event.type) {
41 | case 'http-request':
42 | await this.#httpRequest(event, refData);
43 | break;
44 | case 'js-code':
45 | await this.#javascriptCode(event, refData);
46 | break;
47 | default:
48 | }
49 | }
50 | }
51 |
52 | export default WorkflowEvent;
53 |
--------------------------------------------------------------------------------
/src/components/newtab/workflow/edit/EditHandleDialog.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
15 | {{ t('workflow.blocks.handle-dialog.accept') }}
16 |
17 |
18 |
27 |
28 |
29 |
30 |
48 |
--------------------------------------------------------------------------------
/src/components/newtab/workflow/edit/EditAutocomplete.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 |
13 |
51 |
--------------------------------------------------------------------------------
/src/components/newtab/workflow/settings/SettingsBlocks.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ t('workflow.settings.blockDelay.title') }}
6 |
7 |
8 | {{ t('workflow.settings.blockDelay.description') }}
9 |
10 |
11 |
16 |
17 |
18 |
19 |
20 | {{ t('workflow.settings.tabLoadTimeout.title') }}
21 |
22 |
23 | {{ t('workflow.settings.tabLoadTimeout.description') }}
24 |
25 |
26 |
33 |
34 |
35 |
52 |
--------------------------------------------------------------------------------
/src/content/injectAppStyles.js:
--------------------------------------------------------------------------------
1 | import browser from 'webextension-polyfill';
2 |
3 | export function generateStyleEl(css, classes = true) {
4 | const style = document.createElement('style');
5 | style.textContent = css;
6 |
7 | if (classes) {
8 | style.classList.add('automa-element-selector');
9 | }
10 |
11 | return style;
12 | }
13 |
14 | export default async function (appRoot, customCss = '') {
15 | try {
16 | const response = await fetch(
17 | browser.runtime.getURL('/elementSelector.css')
18 | );
19 | const mainCSS = await response.text();
20 | const appStyleEl = generateStyleEl(mainCSS + customCss, false);
21 | appRoot.appendChild(appStyleEl);
22 |
23 | const fontStyleExists = document.head.querySelector(
24 | '.automa-element-selector'
25 | );
26 |
27 | if (!fontStyleExists) {
28 | const commonCSS =
29 | '\n.automa-element-selector { direction: ltr } \n [automa-isDragging] { user-select: none } \n [automa-el-list] {outline: 2px dashed #6366f1;}';
30 |
31 | const fontURL = browser.runtime.getURL('/Inter-roman-latin.var.woff2');
32 | const fontCSS = `@font-face { font-family: "Inter var"; font-weight: 100 900; font-display: swap; font-style: normal; font-named-instance: "Regular"; src: url("${fontURL}") format("woff2") }`;
33 | const fontStyleEl = generateStyleEl(fontCSS + commonCSS);
34 |
35 | document.head.appendChild(fontStyleEl);
36 | }
37 | } catch (error) {
38 | console.error(error);
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/components/newtab/workflow/edit/EditParameterPrompt.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
16 |
21 | Insert Parameters
22 |
23 |
24 |
29 |
30 |
31 |
32 |
53 |
--------------------------------------------------------------------------------
/src/workflowEngine/templating/index.js:
--------------------------------------------------------------------------------
1 | import objectPath from 'object-path';
2 | import cloneDeep from 'lodash.clonedeep';
3 | import renderString from './renderString';
4 |
5 | export default async function ({ block, refKeys, data, isPopup }) {
6 | if (!refKeys || refKeys.length === 0) return block;
7 |
8 | const copyBlock = cloneDeep(block);
9 | const addReplacedValue = (value) => {
10 | if (!copyBlock.replacedValue) copyBlock.replacedValue = {};
11 | copyBlock.replacedValue = { ...copyBlock.replacedValue, ...value };
12 | };
13 |
14 | for (const blockDataKey of refKeys) {
15 | const currentData = objectPath.get(copyBlock.data, blockDataKey);
16 | /* eslint-disable-next-line */
17 | if (!currentData) continue;
18 |
19 | if (Array.isArray(currentData)) {
20 | for (let index = 0; index < currentData.length; index += 1) {
21 | const value = currentData[index];
22 | const renderedValue = await renderString(value, data, isPopup);
23 |
24 | addReplacedValue(renderedValue.list);
25 | objectPath.set(
26 | copyBlock.data,
27 | `${blockDataKey}.${index}`,
28 | renderedValue.value
29 | );
30 | }
31 | } else if (typeof currentData === 'string') {
32 | const renderedValue = await renderString(currentData, data, isPopup);
33 |
34 | addReplacedValue(renderedValue.list);
35 | objectPath.set(copyBlock.data, blockDataKey, renderedValue.value);
36 | }
37 | }
38 |
39 | return copyBlock;
40 | }
41 |
--------------------------------------------------------------------------------
/src/popup/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
46 |
53 |
--------------------------------------------------------------------------------
/src/utils/recordKeys.js:
--------------------------------------------------------------------------------
1 | import { toCamelCase } from './helper';
2 |
3 | const modifierKeys = ['Control', 'Alt', 'Shift', 'Meta'];
4 | export function recordPressedKey(
5 | { repeat, shiftKey, metaKey, altKey, ctrlKey, key },
6 | callback
7 | ) {
8 | if (repeat || modifierKeys.includes(key)) return;
9 |
10 | let pressedKey = key.length > 1 || shiftKey ? toCamelCase(key, true) : key;
11 |
12 | if (pressedKey === ' ') pressedKey = 'Space';
13 | else if (pressedKey === '+') pressedKey = 'NumpadAdd';
14 |
15 | const keys = [pressedKey];
16 |
17 | if (shiftKey) keys.unshift('Shift');
18 | if (metaKey) keys.unshift('Meta');
19 | if (altKey) keys.unshift('Alt');
20 | if (ctrlKey) keys.unshift('Control');
21 |
22 | if (callback) callback(keys);
23 | }
24 |
25 | const allowedKeys = {
26 | '+': 'plus',
27 | Delete: 'del',
28 | Insert: 'ins',
29 | ArrowDown: 'down',
30 | ArrowLeft: 'left',
31 | ArrowUp: 'up',
32 | ArrowRight: 'right',
33 | Escape: 'escape',
34 | Enter: 'enter',
35 | };
36 | export function recordShortcut(
37 | { ctrlKey, altKey, metaKey, shiftKey, key, repeat },
38 | callback
39 | ) {
40 | if (repeat) return;
41 |
42 | const keys = [];
43 |
44 | if (ctrlKey || metaKey) keys.push('mod');
45 | if (altKey) keys.push('option');
46 | if (shiftKey) keys.push('shift');
47 |
48 | const isValidKey = !!allowedKeys[key] || /^[a-z0-9,./;'[\]\-=`]$/i.test(key);
49 |
50 | if (isValidKey) {
51 | keys.push(allowedKeys[key] || key.toLowerCase());
52 |
53 | callback(keys);
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/components/newtab/workflow/edit/Trigger/TriggerInterval.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
15 |
26 |
27 |
33 | {{ t('workflow.blocks.trigger.fixedDelay') }}
34 |
35 |
36 |
58 |
--------------------------------------------------------------------------------
/src/workflowEngine/blocksHandler/handlerCloseTab.js:
--------------------------------------------------------------------------------
1 | import BrowserAPIService from '@/service/browser-api/BrowserAPIService';
2 |
3 | async function closeWindow(data, windowId) {
4 | const windowIds = [];
5 |
6 | if (data.allWindows) {
7 | const windows = await BrowserAPIService.windows.getAll();
8 |
9 | windows.forEach(({ id }) => {
10 | windowIds.push(id);
11 | });
12 | } else {
13 | let currentWindowId;
14 |
15 | if (windowId && typeof windowId === 'number') {
16 | currentWindowId = windowId;
17 | } else {
18 | currentWindowId = (await BrowserAPIService.windows.getCurrent()).id;
19 | }
20 |
21 | windowIds.push(currentWindowId);
22 | }
23 |
24 | await Promise.allSettled(
25 | windowIds.map((id) => BrowserAPIService.windows.remove(id))
26 | );
27 | }
28 |
29 | async function closeTab(data, tabId) {
30 | let tabIds;
31 |
32 | if (data.activeTab && tabId) {
33 | tabIds = tabId;
34 | } else if (data.url) {
35 | tabIds = (await BrowserAPIService.tabs.query({ url: data.url })).map(
36 | (tab) => tab.id
37 | );
38 | }
39 |
40 | if (tabIds) await BrowserAPIService.tabs.remove(tabIds);
41 | }
42 |
43 | export default async function ({ data, id }) {
44 | if (data.closeType === 'window') {
45 | await closeWindow(data, this.windowId);
46 |
47 | this.windowId = null;
48 | } else {
49 | await closeTab(data, this.activeTab.id);
50 |
51 | if (data.activeTab) {
52 | this.activeTab.id = null;
53 | }
54 | }
55 |
56 | return {
57 | data: '',
58 | nextBlockId: this.getBlockConnections(id),
59 | };
60 | }
61 |
--------------------------------------------------------------------------------
/src/components/newtab/app/AppLogs.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
15 |
20 |
21 |
22 |
23 |
24 |
59 |
--------------------------------------------------------------------------------
/src/components/ui/UiTab.vue:
--------------------------------------------------------------------------------
1 |
2 |
21 |
22 |
39 |
63 |
--------------------------------------------------------------------------------
/src/utils/dataMigration.js:
--------------------------------------------------------------------------------
1 | import browser from 'webextension-polyfill';
2 | import dbLogs from '@/db/logs';
3 |
4 | export default async function () {
5 | try {
6 | const { logs, logsCtxData, migration } = await browser.storage.local.get([
7 | 'logs',
8 | 'migration',
9 | 'logsCtxData',
10 | ]);
11 | const hasMigrated = migration || {};
12 | const backupData = {};
13 |
14 | if (!hasMigrated.logs && logs) {
15 | const ids = new Set();
16 |
17 | const items = [];
18 | const ctxData = [];
19 | const logsData = [];
20 | const histories = [];
21 |
22 | for (let index = logs.length - 1; index > 0; index -= 1) {
23 | const { data, history, ...item } = logs[index];
24 | const logId = item.id;
25 |
26 | if (!ids.has(logId) && ids.size < 500) {
27 | items.push(item);
28 | logsData.push({ logId, data });
29 | histories.push({ logId, data: history });
30 | ctxData.push({ logId, data: logsCtxData[logId] });
31 |
32 | ids.add(logId);
33 | }
34 | }
35 |
36 | await Promise.all([
37 | dbLogs.items.bulkAdd(items),
38 | dbLogs.ctxData.bulkAdd(ctxData),
39 | dbLogs.logsData.bulkAdd(logsData),
40 | dbLogs.histories.bulkAdd(histories),
41 | ]);
42 |
43 | backupData.logs = logs;
44 | hasMigrated.logs = true;
45 |
46 | await browser.storage.local.remove('logs');
47 | }
48 |
49 | await browser.storage.local.set({
50 | migration: hasMigrated,
51 | ...backupData,
52 | });
53 | } catch (error) {
54 | console.error(error);
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/content/blocksHandler/handlerEventClick.js:
--------------------------------------------------------------------------------
1 | import { sendMessage } from '@/utils/message';
2 | import { sleep } from '@/utils/helper';
3 | import { getElementPosition, simulateClickElement } from '../utils';
4 | import handleSelector from '../handleSelector';
5 |
6 | function eventClick(block) {
7 | return new Promise((resolve, reject) => {
8 | handleSelector(block, {
9 | async onSelected(element) {
10 | if (block.debugMode) {
11 | const { x, y } = await getElementPosition(element);
12 | const payload = {
13 | tabId: block.activeTabId,
14 | method: 'Input.dispatchMouseEvent',
15 | params: {
16 | x,
17 | y,
18 | button: 'left',
19 | },
20 | };
21 | const executeCommand = (type) => {
22 | payload.params.type = type;
23 |
24 | if (type === 'mousePressed') {
25 | payload.params.clickCount = 1;
26 | }
27 |
28 | return sendMessage('debugger:send-command', payload, 'background');
29 | };
30 |
31 | // bypass the bot detection.
32 | await executeCommand('mouseMoved');
33 | await sleep(100);
34 | await executeCommand('mousePressed');
35 | await sleep(100);
36 | await executeCommand('mouseReleased');
37 |
38 | return;
39 | }
40 |
41 | simulateClickElement(element);
42 | },
43 | onError(error) {
44 | reject(error);
45 | },
46 | onSuccess() {
47 | resolve('');
48 | },
49 | });
50 | });
51 | }
52 |
53 | export default eventClick;
54 |
--------------------------------------------------------------------------------
/src/workflowEngine/utils/conditionCode.js:
--------------------------------------------------------------------------------
1 | import { customAlphabet } from 'nanoid/non-secure';
2 | import { automaRefDataStr, checkCSPAndInject, messageSandbox } from '../helper';
3 |
4 | const nanoid = customAlphabet('1234567890abcdef', 5);
5 |
6 | export default async function (activeTab, payload) {
7 | const variableId = `automa${nanoid()}`;
8 |
9 | if (
10 | !payload.data.context ||
11 | payload.data.context === 'website' ||
12 | !payload.isPopup
13 | ) {
14 | if (!activeTab.id) throw new Error('no-tab');
15 |
16 | const refDataScriptStr = automaRefDataStr(variableId);
17 |
18 | // 构建一个完全自包含的函数字符串,其中所有变量都是硬编码的
19 | // 这确保在跨环境执行时不依赖闭包变量
20 | const callbackFunctionStr = `
21 | function() {
22 | // 直接返回一个自执行的异步函数字符串
23 | // 所有变量值都已内联到字符串中
24 | return \`
25 | (async () => {
26 | const automa${variableId} = ${JSON.stringify(payload.refData)};
27 | ${refDataScriptStr}
28 | try {
29 | ${payload.data.code}
30 | } catch (error) {
31 | return {
32 | $isError: true,
33 | message: error.message,
34 | }
35 | }
36 | })();
37 | \`;
38 | }
39 | `;
40 |
41 | const result = await checkCSPAndInject(
42 | {
43 | target: { tabId: activeTab.id },
44 | debugMode: payload.debugMode,
45 | },
46 | callbackFunctionStr
47 | );
48 |
49 | return result.value;
50 | }
51 |
52 | const result = await messageSandbox('conditionCode', payload);
53 | if (result && result.$isError) throw new Error(result.message);
54 |
55 | return result;
56 | }
57 |
--------------------------------------------------------------------------------
/src/lib/vueI18n.js:
--------------------------------------------------------------------------------
1 | import { nextTick } from 'vue';
2 | import { createI18n } from 'vue-i18n/dist/vue-i18n.esm-bundler';
3 | import { supportLocales } from '@/utils/shared';
4 | import dayjs from './dayjs';
5 |
6 | const i18n = createI18n({
7 | legacy: false,
8 | fallbackLocale: 'en',
9 | });
10 |
11 | export function setI18nLanguage(locale) {
12 | i18n.global.locale.value = locale;
13 |
14 | document.querySelector('html').setAttribute('lang', locale);
15 | }
16 |
17 | export async function loadLocaleMessages(locale, location) {
18 | const isLocaleSupported = supportLocales.some(({ id }) => id === locale);
19 |
20 | if (!isLocaleSupported) {
21 | console.error(`${locale} locale is not supported`);
22 |
23 | return null;
24 | }
25 |
26 | const importLocale = async (path, merge = false) => {
27 | try {
28 | const messages = await import(
29 | /* webpackChunkName: "locales/locale-[request]" */ `../locales/${locale}/${path}`
30 | );
31 |
32 | if (merge) {
33 | i18n.global.mergeLocaleMessage(locale, messages.default);
34 | } else {
35 | i18n.global.setLocaleMessage(locale, messages.default);
36 | }
37 | } catch (error) {
38 | console.error(error);
39 | }
40 | };
41 |
42 | if (locale !== 'en' && !i18n.global.availableLocales.includes('en')) {
43 | await loadLocaleMessages('en', location);
44 | }
45 |
46 | dayjs.locale(locale);
47 |
48 | await importLocale('common.json');
49 | await importLocale('popup.json', true);
50 | await importLocale(`${location}.json`, true);
51 | await importLocale('blocks.json', true);
52 |
53 | return nextTick();
54 | }
55 |
56 | export default i18n;
57 |
--------------------------------------------------------------------------------
/src/components/newtab/shared/SharedElSelectorActions.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
11 |
17 |
18 |
19 |
20 |
21 |
64 |
--------------------------------------------------------------------------------
/src/components/transitions/TransitionSlide.vue:
--------------------------------------------------------------------------------
1 |
58 |
64 |
--------------------------------------------------------------------------------
/src/composable/hasPermissions.js:
--------------------------------------------------------------------------------
1 | import { onMounted, shallowReactive } from 'vue';
2 | import browser from 'webextension-polyfill';
3 |
4 | const isMV2 = browser.runtime.getManifest().manifest_version === 2;
5 |
6 | export function useHasPermissions(permissions) {
7 | const hasPermissions = shallowReactive({});
8 |
9 | function handlePermission(name, status) {
10 | hasPermissions[name] = status;
11 | }
12 | function request(needReload = false) {
13 | const reqPermissions = permissions.filter(
14 | (permission) => !hasPermissions[permission]
15 | );
16 |
17 | browser.permissions
18 | .request({ permissions: reqPermissions })
19 | .then((status) => {
20 | if (!status) return;
21 |
22 | reqPermissions.forEach((permission) => {
23 | handlePermission(permission, true);
24 | });
25 |
26 | if (typeof needReload === 'boolean' && needReload) {
27 | alert('Automa needs to reload to make this feature work');
28 |
29 | if (isMV2) {
30 | browser.runtime.getBackgroundPage().then((background) => {
31 | background.location.reload();
32 | });
33 | } else {
34 | browser.runtime.reload();
35 | }
36 | }
37 | })
38 | .catch((error) => {
39 | console.error(error);
40 | });
41 | }
42 |
43 | onMounted(() => {
44 | permissions.forEach((permission) => {
45 | browser.permissions
46 | .contains({ permissions: [permission] })
47 | .then((status) => {
48 | handlePermission(permission, status);
49 | });
50 | });
51 | });
52 |
53 | return {
54 | request,
55 | has: hasPermissions,
56 | };
57 | }
58 |
--------------------------------------------------------------------------------
/src/utils/editor/EditorCommands.js:
--------------------------------------------------------------------------------
1 | class EditorCommands {
2 | constructor(editor, initialStates = {}) {
3 | this.editor = editor;
4 | this.state = initialStates;
5 | }
6 |
7 | nodeAdded(addedNodes) {
8 | const ids = [];
9 | addedNodes.forEach((node) => {
10 | ids.push(node.id);
11 | this.state.nodes[node.id] = node;
12 | });
13 |
14 | return {
15 | name: 'node-added',
16 | execute: () => {
17 | this.editor.addNodes(addedNodes);
18 | },
19 | undo: () => {
20 | this.editor.removeNodes(ids);
21 | },
22 | };
23 | }
24 |
25 | nodeRemoved(ids) {
26 | return {
27 | name: 'node-removed',
28 | execute: () => {
29 | this.editor.removeNodes(ids);
30 | },
31 | undo: () => {
32 | const nodes = ids.map((id) => this.state.nodes[id]);
33 | this.editor.addNodes(nodes);
34 | },
35 | };
36 | }
37 |
38 | edgeAdded(addedEdges) {
39 | const ids = [];
40 | addedEdges.forEach((edge) => {
41 | ids.push(edge.id);
42 | this.state.edges[edge.id] = edge;
43 | });
44 |
45 | return {
46 | name: 'edge-added',
47 | execute: () => {
48 | this.editor.addEdges(addedEdges);
49 | },
50 | undo: () => {
51 | this.editor.removeEdges(ids);
52 | },
53 | };
54 | }
55 |
56 | edgeRemoved(ids) {
57 | return {
58 | name: 'edge-removed',
59 | execute: () => {
60 | this.editor.removeEdges(ids);
61 | },
62 | undo: () => {
63 | const edges = ids.map((id) => this.state.edges[id]);
64 | this.editor.addEdges(edges);
65 | },
66 | };
67 | }
68 | }
69 |
70 | export default EditorCommands;
71 |
--------------------------------------------------------------------------------
/src/workflowEngine/injectContentScript.js:
--------------------------------------------------------------------------------
1 | import browser from 'webextension-polyfill';
2 |
3 | const isMV2 = browser.runtime.getManifest().manifest_version === 2;
4 |
5 | async function contentScriptExist(tabId, frameId = 0) {
6 | try {
7 | await browser.tabs.sendMessage(
8 | tabId,
9 | { type: 'content-script-exists' },
10 | { frameId }
11 | );
12 |
13 | return true;
14 | } catch (error) {
15 | return false;
16 | }
17 | }
18 |
19 | export default function (tabId, frameId = 0) {
20 | return new Promise((resolve) => {
21 | const currentFrameId = typeof frameId !== 'number' ? 0 : frameId;
22 | let tryCount = 0;
23 |
24 | (async function tryExecute() {
25 | try {
26 | if (tryCount > 3) {
27 | resolve(false);
28 | return;
29 | }
30 |
31 | tryCount += 1;
32 |
33 | if (isMV2) {
34 | await browser.tabs.executeScript(tabId, {
35 | allFrames: true,
36 | runAt: 'document_start',
37 | file: './contentScript.bundle.js',
38 | });
39 | } else {
40 | await browser.scripting.executeScript({
41 | target: {
42 | tabId,
43 | allFrames: true,
44 | },
45 | injectImmediately: true,
46 | files: ['./contentScript.bundle.js'],
47 | });
48 | }
49 |
50 | const isScriptExists = await contentScriptExist(tabId, currentFrameId);
51 |
52 | if (isScriptExists) {
53 | resolve(true);
54 | } else {
55 | setTimeout(tryExecute, 1000);
56 | }
57 | } catch (error) {
58 | console.error(error);
59 | setTimeout(tryExecute, 1000);
60 | }
61 | })();
62 | });
63 | }
64 |
--------------------------------------------------------------------------------
/src/components/newtab/workflow/edit/Trigger/TriggerCronJob.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
13 | {{ state.nextSchedule }}
14 |
15 |
16 |
60 |
--------------------------------------------------------------------------------
/utils/webserver.js:
--------------------------------------------------------------------------------
1 | // Do this as the first thing so that any code reading it knows the right env.
2 | process.env.BABEL_ENV = 'development';
3 | process.env.NODE_ENV = 'development';
4 | process.env.ASSET_PATH = '/';
5 |
6 | const WebpackDevServer = require('webpack-dev-server');
7 | const webpack = require('webpack');
8 | const path = require('path');
9 | const config = require('../webpack.config');
10 | const env = require('./env');
11 |
12 | const options = config.chromeExtensionBoilerplate || {};
13 | const excludeEntriesToHotReload = options.notHotReload || [];
14 |
15 | for (const entryName in config.entry) {
16 | if (excludeEntriesToHotReload.indexOf(entryName) === -1) {
17 | config.entry[entryName] = [
18 | 'webpack/hot/dev-server',
19 | `webpack-dev-server/client?hot=true&hostname=localhost&port=${env.PORT}`,
20 | ].concat(config.entry[entryName]);
21 | }
22 | }
23 |
24 | config.plugins = [new webpack.HotModuleReplacementPlugin()].concat(
25 | config.plugins || []
26 | );
27 |
28 | delete config.chromeExtensionBoilerplate;
29 |
30 | const compiler = webpack(config);
31 |
32 | const server = new WebpackDevServer(
33 | {
34 | https: false,
35 | hot: false,
36 | client: false,
37 | host: 'localhost',
38 | port: env.PORT,
39 | static: {
40 | directory: path.join(__dirname, '../build'),
41 | },
42 | devMiddleware: {
43 | publicPath: `http://localhost:${env.PORT}/`,
44 | writeToDisk: true,
45 | },
46 | headers: {
47 | 'Access-Control-Allow-Origin': '*',
48 | },
49 | allowedHosts: 'all',
50 | },
51 | compiler
52 | );
53 |
54 | if (process.env.NODE_ENV === 'development' && module.hot) {
55 | module.hot.accept();
56 | }
57 |
58 | (async () => {
59 | await server.start();
60 | })();
61 |
--------------------------------------------------------------------------------
/src/components/newtab/workflow/edit/EditLogData.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
15 |
22 |
23 |
24 |
25 |
26 | {{ t('workflow.blocks.log-data.data') }}
27 |
28 |
29 |
30 |
31 |
32 |
33 |
57 |
62 |
--------------------------------------------------------------------------------
/src/components/transitions/TransitionExpand.vue:
--------------------------------------------------------------------------------
1 |
56 |
68 |
--------------------------------------------------------------------------------
/src/components/ui/UiRadio.vue:
--------------------------------------------------------------------------------
1 |
2 |
21 |
22 |
52 |
68 |
--------------------------------------------------------------------------------
/src/components/newtab/package/PackageDetails.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
20 |
28 |
29 |
32 | {{ state.contentLength }}/5000
33 |
34 |
35 |
36 |
37 |
38 |
59 |
--------------------------------------------------------------------------------
/src/workflowEngine/blocksHandler/handlerSwitchTo.js:
--------------------------------------------------------------------------------
1 | import { sleep } from '@/utils/helper';
2 | import { getFrames } from '../helper';
3 |
4 | async function switchTo(block) {
5 | const nextBlockId = this.getBlockConnections(block.id);
6 |
7 | try {
8 | if (block.data.windowType === 'main-window') {
9 | this.activeTab.frameId = 0;
10 |
11 | delete this.frameSelector;
12 |
13 | return {
14 | data: '',
15 | nextBlockId,
16 | };
17 | }
18 |
19 | const { url, isSameOrigin } = await this._sendMessageToTab(block, {
20 | frameId: 0,
21 | });
22 |
23 | if (isSameOrigin) {
24 | this.frameSelector = block.data.selector;
25 |
26 | return {
27 | data: block.data.selector,
28 | nextBlockId,
29 | };
30 | }
31 |
32 | const frames = await getFrames(this.activeTab.id);
33 |
34 | let frameId = frames[url] ?? null;
35 | if (frameId === null) {
36 | // Incase the iframe is redirect
37 | frameId = Object.entries(frames).find(([frameURL]) => {
38 | try {
39 | const currFramePathName = new URL(url).pathname;
40 | const framePathName = new URL(frameURL).pathname;
41 |
42 | return currFramePathName === framePathName;
43 | } catch (error) {
44 | return false;
45 | }
46 | })?.[1];
47 | }
48 |
49 | if (frameId !== null) {
50 | this.activeTab.frameId = frameId;
51 |
52 | await sleep(1000);
53 |
54 | return {
55 | nextBlockId,
56 | data: this.activeTab.frameId,
57 | };
58 | }
59 |
60 | throw new Error('no-iframe-id');
61 | } catch (error) {
62 | error.data = { selector: block.data.selector };
63 | error.nextBlockId = nextBlockId;
64 |
65 | throw error;
66 | }
67 | }
68 |
69 | export default switchTo;
70 |
--------------------------------------------------------------------------------
/src/components/newtab/workflows/WorkflowsShared.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |

7 |
8 |
9 | {{ t('message.empty') }}
10 |
11 |
12 |
13 |
14 |
22 |
23 |
24 |
62 |
--------------------------------------------------------------------------------
/src/content/elementSelector/generateElementsSelector.js:
--------------------------------------------------------------------------------
1 | import findSelector from '@/lib/findSelector';
2 | import { generateXPath } from '../utils';
3 |
4 | export default function ({
5 | list,
6 | target,
7 | selectorType,
8 | frameElement,
9 | hoveredElements,
10 | selectorSettings,
11 | }) {
12 | let selector = '';
13 |
14 | const selectorOptions = selectorSettings || {};
15 | const [selectedElement] = hoveredElements;
16 | const finderOptions = { ...selectorOptions };
17 | let documentCtx = document;
18 |
19 | if (frameElement) {
20 | documentCtx = frameElement.contentDocument.body;
21 | finderOptions.root = documentCtx;
22 | }
23 |
24 | if (list) {
25 | const isInList = target.closest('[automa-el-list]');
26 |
27 | if (isInList) {
28 | const childSelector = findSelector(target, {
29 | root: isInList,
30 | ...selectorOptions,
31 | idName: () => false,
32 | });
33 | const listSelector = isInList.getAttribute('automa-el-list');
34 |
35 | selector = `${listSelector} ${childSelector}`;
36 | } else {
37 | const parentSelector = findSelector(
38 | selectedElement.parentElement,
39 | finderOptions
40 | );
41 | selector = `${parentSelector} > ${selectedElement.tagName.toLowerCase()}`;
42 |
43 | const prevSelectedList = documentCtx.querySelectorAll('[automa-el-list]');
44 | prevSelectedList.forEach((el) => {
45 | el.removeAttribute('automa-el-list');
46 | });
47 |
48 | hoveredElements.forEach((el) => {
49 | el.setAttribute('automa-el-list', selector);
50 | });
51 | }
52 | } else {
53 | selector =
54 | selectorType === 'css'
55 | ? findSelector(selectedElement, finderOptions)
56 | : generateXPath(selectedElement);
57 | }
58 |
59 | return selector;
60 | }
61 |
--------------------------------------------------------------------------------
/src/components/newtab/workflow/edit/EditSliceVariable.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
16 |
17 | -
18 |
22 | {{ t(`workflow.blocks.slice-variable.${param.text}`) }}
23 |
24 |
32 |
33 |
34 |
35 |
36 |
58 |
63 |
--------------------------------------------------------------------------------
/src/workflowEngine/blocksHandler/handlerDataMapping.js:
--------------------------------------------------------------------------------
1 | import objectPath from 'object-path';
2 | import { objectHasKey, isObject } from '@/utils/helper';
3 |
4 | function mapData(data, sources) {
5 | const mappedData = {};
6 |
7 | sources.forEach((source) => {
8 | const dataExist = objectPath.has(data, source.name);
9 | if (!dataExist) return;
10 |
11 | const value = objectPath.get(data, source.name);
12 |
13 | source.destinations.forEach(({ name }) => {
14 | objectPath.set(mappedData, name, value);
15 | });
16 | });
17 |
18 | return mappedData;
19 | }
20 |
21 | export async function dataMapping({ id, data }) {
22 | let dataToMap = null;
23 |
24 | if (data.dataSource === 'table') {
25 | dataToMap = this.engine.referenceData.table;
26 | } else if (data.dataSource === 'variable') {
27 | const { variables } = this.engine.referenceData;
28 |
29 | if (!objectHasKey(variables, data.varSourceName)) {
30 | throw new Error(`Cant find "${data.varSourceName}" variable`);
31 | }
32 |
33 | dataToMap = variables[data.varSourceName];
34 | }
35 |
36 | if (!isObject(dataToMap) && !Array.isArray(dataToMap)) {
37 | const dataType = dataToMap === null ? 'null' : typeof dataToMap;
38 |
39 | throw new Error(`Can't map data with "${dataType}" data type`);
40 | }
41 |
42 | if (isObject(dataToMap)) {
43 | dataToMap = mapData(dataToMap, data.sources);
44 | } else {
45 | dataToMap = dataToMap.map((item) => mapData(item, data.sources));
46 | }
47 |
48 | if (data.assignVariable) {
49 | await this.setVariable(data.variableName, dataToMap);
50 | }
51 | if (data.saveData) {
52 | this.addDataToColumn(data.dataColumn, dataToMap);
53 | }
54 |
55 | return {
56 | data: dataToMap,
57 | nextBlockId: this.getBlockConnections(id),
58 | };
59 | }
60 |
61 | export default dataMapping;
62 |
--------------------------------------------------------------------------------
/src/background/BackgroundUtils.js:
--------------------------------------------------------------------------------
1 | import browser from 'webextension-polyfill';
2 | import { waitTabLoaded } from '@/workflowEngine/helper';
3 |
4 | class BackgroundUtils {
5 | static async openDashboard(url, updateTab = true) {
6 | const tabUrl = browser.runtime.getURL(
7 | `/newtab.html#${typeof url === 'string' ? url : ''}`
8 | );
9 |
10 | try {
11 | const [tab] = await browser.tabs.query({
12 | url: browser.runtime.getURL('/newtab.html'),
13 | });
14 |
15 | if (tab) {
16 | const tabOptions = { active: true };
17 | if (updateTab) tabOptions.url = tabUrl;
18 |
19 | await browser.tabs.update(tab.id, tabOptions);
20 |
21 | if (updateTab) {
22 | await browser.windows.update(tab.windowId, {
23 | focused: true,
24 | state: 'maximized',
25 | });
26 | }
27 | } else {
28 | const curWin = await browser.windows.getCurrent();
29 | const windowOptions = {
30 | top: 0,
31 | left: 0,
32 | width: Math.min(curWin.width, 715),
33 | height: Math.min(curWin.height, 715),
34 | url: tabUrl,
35 | type: 'popup',
36 | };
37 |
38 | if (updateTab) {
39 | windowOptions.focused = true;
40 | }
41 |
42 | await browser.windows.create(windowOptions);
43 | }
44 | } catch (error) {
45 | console.error(error);
46 | throw error;
47 | }
48 | }
49 |
50 | static async sendMessageToDashboard(type, data) {
51 | const [tab] = await browser.tabs.query({
52 | url: browser.runtime.getURL('/newtab.html'),
53 | });
54 |
55 | await waitTabLoaded({ tabId: tab.id });
56 | const result = await browser.tabs.sendMessage(tab.id, { type, data });
57 |
58 | return result;
59 | }
60 | }
61 |
62 | export default BackgroundUtils;
63 |
--------------------------------------------------------------------------------
/src/content/blocksHandler/handlerVerifySelector.js:
--------------------------------------------------------------------------------
1 | import { sleep } from '@/utils/helper';
2 | import handleSelector from '../handleSelector';
3 |
4 | const SLEEP_TIME = 1700;
5 |
6 | async function verifySelector(block) {
7 | let elements = await handleSelector(block);
8 | if (!elements) {
9 | await sleep(SLEEP_TIME);
10 | return { notFound: true };
11 | }
12 |
13 | if (!block.data.multiple) elements = [elements];
14 |
15 | elements[0].scrollIntoView({
16 | block: 'center',
17 | inline: 'center',
18 | behavior: 'smooth',
19 | });
20 |
21 | await sleep(200);
22 |
23 | const divEl = document.createElement('div');
24 | divEl.style =
25 | 'height: 100%; width: 100%; top: 0; left: 0; background-color: rgb(0 0 0 / 0.3); pointer-events: none; position: fixed; z-index: 99999';
26 |
27 | const svgEl = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
28 | svgEl.style =
29 | 'height: 100%; width: 100%; top: 0; left: 0; pointer-events: none; position: relative;';
30 |
31 | divEl.appendChild(svgEl);
32 |
33 | elements.forEach((element) => {
34 | const { left, top, width, height } = element.getBoundingClientRect();
35 | const rectEl = document.createElementNS(
36 | 'http://www.w3.org/2000/svg',
37 | 'rect'
38 | );
39 |
40 | rectEl.setAttribute('y', top);
41 | rectEl.setAttribute('x', left);
42 | rectEl.setAttribute('width', width);
43 | rectEl.setAttribute('height', height);
44 | rectEl.setAttribute('stroke', '#2563EB');
45 | rectEl.setAttribute('stroke-width', '2');
46 | rectEl.setAttribute('fill', 'rgba(37, 99, 235, 0.4)');
47 |
48 | svgEl.appendChild(rectEl);
49 | });
50 |
51 | document.body.appendChild(divEl);
52 |
53 | await sleep(SLEEP_TIME);
54 |
55 | divEl.remove();
56 |
57 | return { notFound: false };
58 | }
59 |
60 | export default verifySelector;
61 |
--------------------------------------------------------------------------------
/src/execute/index.js:
--------------------------------------------------------------------------------
1 | import { parseJSON } from '@/utils/helper';
2 | import { sendMessage } from '@/utils/message';
3 | import Browser from 'webextension-polyfill';
4 |
5 | function getWorkflowDetail() {
6 | let hash = window.location.hash.slice(1);
7 | if (!hash.startsWith('/')) hash = `/${hash}`;
8 |
9 | const { pathname, searchParams } = new URL(window.location.origin + hash);
10 |
11 | const variables = {};
12 | const { 1: workflowId } = pathname.split('/');
13 |
14 | searchParams.forEach((key, value) => {
15 | const varValue = parseJSON(decodeURIComponent(value), '##_empty');
16 | if (varValue === '##_empty') return;
17 |
18 | variables[key] = varValue;
19 | });
20 |
21 | return { workflowId: workflowId ?? '', variables };
22 | }
23 |
24 | function writeResult(text) {
25 | document.body.innerText = text;
26 | }
27 |
28 | (async () => {
29 | try {
30 | const { workflowId, variables } = getWorkflowDetail();
31 | if (!workflowId) {
32 | writeResult('Invalid path');
33 | return;
34 | }
35 |
36 | const { workflows } = await Browser.storage.local.get('workflows');
37 |
38 | let workflow = workflows[workflowId];
39 | if (!workflow && Array.isArray(workflows)) {
40 | workflow = workflows.find((item) => item.id === workflowId);
41 | }
42 |
43 | if (!workflow) {
44 | writeResult('Workflow not found');
45 | return;
46 | }
47 |
48 | const hasVariables = Object.keys(variables).length > 0;
49 |
50 | writeResult('Executing workflow');
51 |
52 | sendMessage(
53 | 'workflow:execute',
54 | {
55 | ...workflow,
56 | options: { checkParam: !hasVariables, data: { variables } },
57 | },
58 | 'background'
59 | ).then(() => {
60 | setTimeout(window.close, 1000);
61 | });
62 | } catch (error) {
63 | console.error(error);
64 | }
65 | })();
66 |
--------------------------------------------------------------------------------
/src/manifest.firefox.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 2,
3 | "name": "Automa",
4 | "browser_specific_settings": {
5 | "gecko": {
6 | "strict_min_version": "91.1.0"
7 | }
8 | },
9 | "background": {
10 | "scripts": ["background.bundle.js"],
11 | "persistent": true
12 | },
13 | "browser_action": {
14 | "default_popup": "popup.html",
15 | "default_icon": "icon-128.png"
16 | },
17 | "icons": {
18 | "128": "icon-128.png"
19 | },
20 | "commands": {
21 | "open-dashboard": {
22 | "suggested_key": {
23 | "default": "Alt+A",
24 | "mac": "Alt+A"
25 | },
26 | "description": "Open the dashboard"
27 | },
28 | "element-picker": {
29 | "suggested_key": {
30 | "default": "Alt+P",
31 | "mac": "Alt+P"
32 | },
33 | "description": "Open element picker"
34 | }
35 | },
36 | "content_scripts": [
37 | {
38 | "matches": [""],
39 | "js": ["contentScript.bundle.js"],
40 | "run_at": "document_start",
41 | "all_frames": true
42 | },
43 | {
44 | "matches": ["*://*.automa.site/*", "*://automa.vercel.app/*"],
45 | "js": ["webService.bundle.js"],
46 | "run_at": "document_start",
47 | "all_frames": false
48 | }
49 | ],
50 | "optional_permissions": ["clipboardRead", "clipboardWrite", "downloads", "notifications", "cookies"],
51 | "permissions": [
52 | "tabs",
53 | "proxy",
54 | "menus",
55 | "alarms",
56 | "storage",
57 | "webNavigation",
58 | "unlimitedStorage",
59 | ""
60 | ],
61 | "web_accessible_resources": [
62 | "/elementSelector.css",
63 | "/icon-128.png",
64 | "/Inter-roman-latin.var.woff2",
65 | "/locales/*",
66 | "elementSelector.bundle.js"
67 | ],
68 | "content_security_policy": "script-src 'self' 'unsafe-inline' https:; object-src 'self'"
69 | }
70 |
--------------------------------------------------------------------------------
/src/components/newtab/workflow/edit/EditWaitConnections.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
17 |
22 | {{ t('workflow.blocks.wait-connections.specificFlow') }}
23 |
24 |
31 |
34 |
35 |
36 |
37 |
67 |
--------------------------------------------------------------------------------
/src/components/newtab/workflow/editor/EditorLogs.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |

7 |
{{ t('message.noData') }}
8 |
9 |
15 |
16 |
17 |
22 | |
23 |
24 |
25 |
26 |
62 |
--------------------------------------------------------------------------------
/src/newtab/utils/startRecordWorkflow.js:
--------------------------------------------------------------------------------
1 | import browser from 'webextension-polyfill';
2 |
3 | const isMV2 = browser.runtime.getManifest().manifest_version === 2;
4 |
5 | export default async function (options = {}) {
6 | try {
7 | const flows = [];
8 | const [activeTab] = await browser.tabs.query({
9 | active: true,
10 | url: '*://*/*',
11 | });
12 |
13 | if (activeTab && activeTab.url.startsWith('http')) {
14 | flows.push({
15 | id: 'new-tab',
16 | description: activeTab.url,
17 | data: { url: activeTab.url },
18 | });
19 |
20 | await browser.windows.update(activeTab.windowId, { focused: true });
21 | }
22 |
23 | await browser.storage.local.set({
24 | isRecording: true,
25 | recording: {
26 | flows,
27 | name: 'unnamed',
28 | activeTab: {
29 | id: activeTab?.id,
30 | url: activeTab?.url,
31 | },
32 | ...options,
33 | },
34 | });
35 |
36 | const action = browser.action || browser.browserAction;
37 | await action.setBadgeBackgroundColor({ color: '#ef4444' });
38 | await action.setBadgeText({ text: 'rec' });
39 |
40 | const tabs = await browser.tabs.query({});
41 | for (const tab of tabs) {
42 | if (
43 | tab.url.startsWith('http') &&
44 | !tab.url.includes('chrome.google.com')
45 | ) {
46 | if (isMV2) {
47 | await browser.tabs.executeScript(tab.id, {
48 | allFrames: true,
49 | runAt: 'document_start',
50 | file: './recordWorkflow.bundle.js',
51 | });
52 | } else {
53 | await browser.scripting.executeScript({
54 | target: {
55 | tabId: tab.id,
56 | allFrames: true,
57 | },
58 | files: ['recordWorkflow.bundle.js'],
59 | });
60 | }
61 | }
62 | }
63 | } catch (error) {
64 | console.error(error);
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/content/blocksHandler/handlerUploadFile.js:
--------------------------------------------------------------------------------
1 | import { sendMessage } from '@/utils/message';
2 | import handleSelector from '../handleSelector';
3 |
4 | function injectFiles(element, files) {
5 | const notFileTypeAttr = element.getAttribute('type') !== 'file';
6 |
7 | if (element.tagName !== 'INPUT' || notFileTypeAttr) return;
8 |
9 | element.files = files;
10 | element.dispatchEvent(new Event('change', { bubbles: true }));
11 | }
12 |
13 | export default async function (block) {
14 | const elements = await handleSelector(block, { returnElement: true });
15 |
16 | if (!elements) throw new Error('element-not-found');
17 |
18 | const getFile = async (path) => {
19 | let fileObject;
20 | if (
21 | path.includes('|') &&
22 | !path.startsWith('file') &&
23 | !path.startsWith('http')
24 | ) {
25 | const [filename, mime, base64] = path.split('|');
26 | const response = await fetch(base64);
27 | const arrayBuffer = await response.arrayBuffer();
28 |
29 | fileObject = new File([arrayBuffer], filename, { type: mime });
30 | } else {
31 | const file = await sendMessage('get:file', path, 'background');
32 | const name = file?.path?.replace(/^.*[\\/]/, '') || '';
33 | const blob = await fetch(file.objUrl).then((response) => response.blob());
34 |
35 | if (file.objUrl.startsWith('blob')) URL.revokeObjectURL(file.objUrl);
36 |
37 | fileObject = new File([blob], name, { type: file.type });
38 | }
39 |
40 | return fileObject;
41 | };
42 | const filesPromises = await Promise.all(block.data.filePaths.map(getFile));
43 | const dataTransfer = filesPromises.reduce((acc, file) => {
44 | acc.items.add(file);
45 |
46 | return acc;
47 | }, new DataTransfer());
48 |
49 | if (block.data.multiple) {
50 | elements.forEach((element) => {
51 | injectFiles(element, dataTransfer.files);
52 | });
53 | } else {
54 | injectFiles(elements, dataTransfer.files);
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/workflowEngine/blocksHandler/handlerExportData.js:
--------------------------------------------------------------------------------
1 | import { default as dataExporter, files } from '@/utils/dataExporter';
2 | import BrowserAPIService from '@/service/browser-api/BrowserAPIService';
3 |
4 | function blobToBase64(blob) {
5 | return new Promise((resolve) => {
6 | const reader = new FileReader();
7 | reader.onloadend = () => resolve(reader.result);
8 | reader.readAsDataURL(blob);
9 | });
10 | }
11 |
12 | async function exportData({ data, id }, { refData }) {
13 | const dataToExport = data.dataToExport || 'data-columns';
14 | let payload = refData.table;
15 |
16 | if (dataToExport === 'google-sheets') {
17 | payload = refData.googleSheets[data.refKey] || [];
18 | } else if (dataToExport === 'variable') {
19 | payload = refData.variables[data.variableName] || [];
20 |
21 | if (!Array.isArray(payload)) {
22 | payload = [payload];
23 |
24 | if (data.type === 'csv' && typeof payload[0] !== 'object')
25 | payload = [payload];
26 | }
27 | }
28 |
29 | const isDOMAvailable = typeof document !== 'undefined';
30 | let blobUrl = dataExporter(payload, {
31 | ...data,
32 | csvOptions: {
33 | delimiter: data.csvDelimiter || ',',
34 | },
35 | returnUrl: !isDOMAvailable,
36 | });
37 |
38 | const hasDownloadAccess =
39 | !isDOMAvailable &&
40 | (await BrowserAPIService.permissions.contains({
41 | permissions: ['downloads'],
42 | }));
43 | if (hasDownloadAccess) {
44 | blobUrl = await blobToBase64(blobUrl);
45 |
46 | const filename = `${data.name || 'unnamed'}${files[data.type].ext}`;
47 | const options = {
48 | filename,
49 | conflictAction: data.onConflict || 'uniquify',
50 | };
51 |
52 | await BrowserAPIService.downloads.download({
53 | ...options,
54 | url: blobUrl,
55 | });
56 | }
57 |
58 | return {
59 | data: '',
60 | nextBlockId: this.getBlockConnections(id),
61 | };
62 | }
63 |
64 | export default exportData;
65 |
--------------------------------------------------------------------------------
/src/locales/zh/common.json:
--------------------------------------------------------------------------------
1 | {
2 | "common": {
3 | "dashboard": "主面板",
4 | "workflow": "工作流 | 工作流",
5 | "collection": "集合 | 集合",
6 | "log": "日志 | 日志",
7 | "block": "单元 | 单元",
8 | "schedule": "计划",
9 | "folder": "文件夹 | 文件夹",
10 | "new": "新建",
11 | "docs": "文档",
12 | "search": "搜索",
13 | "example": "示例 | 示例",
14 | "import": "导入",
15 | "export": "导出",
16 | "rename": "重命名",
17 | "execute": "执行",
18 | "delete": "删除",
19 | "cancel": "取消",
20 | "settings": "设置",
21 | "options": "选项",
22 | "confirm": "确认",
23 | "name": "名称",
24 | "all": "全部",
25 | "add": "添加",
26 | "save": "保存",
27 | "data": "数据",
28 | "stop": "停止",
29 | "sheet": "工作簿",
30 | "pause": "暂停",
31 | "resume": "恢复",
32 | "action": "操作 | 操作",
33 | "packages": "包",
34 | "storage": "存储",
35 | "editor": "编辑器",
36 | "running": "运行",
37 | "globalData": "全局数据",
38 | "fileName": "文件名",
39 | "description": "描述",
40 | "disable": "禁用",
41 | "disabled": "已禁用",
42 | "enable": "启用",
43 | "fallback": "反馈",
44 | "update": "更新",
45 | "feature": "特点",
46 | "duplicate": "副本",
47 | "password": "密码",
48 | "category": "分类",
49 | "optional": "可选",
50 | "0disable": "0 到禁用",
51 | "millisecond": "毫秒 | 毫秒"
52 | },
53 | "message": {
54 | "noBlock": "没有模块",
55 | "noData": "没有数据可以展示",
56 | "noTriggerBlock": "没有找到触发器模块",
57 | "useDynamicData": "了解如何添加动态数据",
58 | "delete": "确定要删除\"{name}\"?",
59 | "empty": "哎呀……你好像没有任何项目",
60 | "maxSizeExceeded": "文件大小超出了允许的最大值",
61 | "notSaved": "你真的要退出吗? 你有未保存的更改!",
62 | "somethingWrong": "出现错误",
63 | "limitExceeded": "您已超出限制"
64 | },
65 | "sort": {
66 | "sortBy": "排序方式",
67 | "name": "名称",
68 | "createdAt": "创建日期",
69 | "updatedAt": "上次更新",
70 | "mostUsed": "最常用"
71 | },
72 | "logStatus": {
73 | "stopped": "停止",
74 | "error": "错误",
75 | "success": "成功"
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/locales/zh-TW/common.json:
--------------------------------------------------------------------------------
1 | {
2 | "common": {
3 | "dashboard": "儀表板",
4 | "workflow": "工作流程 | 工作流程",
5 | "collection": "集合 | 集合",
6 | "log": "日誌 | 日誌",
7 | "block": "區塊 | 區塊",
8 | "schedule": "排程",
9 | "folder": "資料夾 | 資料夾",
10 | "new": "新增",
11 | "docs": "文件",
12 | "search": "搜尋",
13 | "example": "範例 | 範例",
14 | "import": "匯入",
15 | "export": "匯出",
16 | "rename": "重新命名",
17 | "execute": "執行",
18 | "delete": "刪除",
19 | "cancel": "取消",
20 | "settings": "設定",
21 | "options": "選項",
22 | "confirm": "確認",
23 | "name": "名稱",
24 | "all": "全部",
25 | "add": "新增",
26 | "save": "儲存",
27 | "data": "資料",
28 | "stop": "停止",
29 | "sheet": "工作表",
30 | "pause": "暫停",
31 | "resume": "繼續",
32 | "action": "動作 | 動作",
33 | "packages": "套件",
34 | "storage": "儲存空間",
35 | "editor": "編輯器",
36 | "running": "執行中",
37 | "globalData": "全域資料",
38 | "fileName": "檔案名稱",
39 | "description": "描述",
40 | "disable": "停用",
41 | "disabled": "已停用",
42 | "enable": "啟用",
43 | "fallback": "備用",
44 | "update": "更新",
45 | "feature": "功能",
46 | "duplicate": "複製",
47 | "password": "密碼",
48 | "category": "分類",
49 | "optional": "選填",
50 | "0disable": "0 表示停用",
51 | "millisecond": "毫秒 | 毫秒"
52 | },
53 | "message": {
54 | "noBlock": "無區塊",
55 | "noData": "無資料可顯示",
56 | "noTriggerBlock": "找不到觸發區塊",
57 | "useDynamicData": "了解如何加入動態資料",
58 | "delete": "您確定要刪除 \"{name}\" 嗎?",
59 | "empty": "哎呀... 看起來您沒有任何項目",
60 | "maxSizeExceeded": "檔案大小已超過允許的最大值",
61 | "notSaved": "您確定要離開嗎?您有未儲存的變更!",
62 | "somethingWrong": "出了點問題",
63 | "limitExceeded": "您已超過限制"
64 | },
65 | "sort": {
66 | "sortBy": "排序依據",
67 | "name": "名稱",
68 | "createdAt": "建立日期",
69 | "updatedAt": "最後更新",
70 | "mostUsed": "最常使用"
71 | },
72 | "logStatus": {
73 | "stopped": "已停止",
74 | "error": "錯誤",
75 | "success": "成功"
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/components/newtab/workflow/edit/EditSwitchTo.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
15 |
18 |
21 |
22 |
30 |
38 |
42 |
43 |
44 |
45 |
64 |
--------------------------------------------------------------------------------
/src/workflowEngine/blocksHandler/handlerSortData.js:
--------------------------------------------------------------------------------
1 | import { objectHasKey } from '@/utils/helper';
2 |
3 | export async function sliceData({ id, data }) {
4 | let dataToSort = null;
5 |
6 | if (data.dataSource === 'table') {
7 | dataToSort = this.engine.referenceData.table;
8 | } else if (data.dataSource === 'variable') {
9 | const { variables } = this.engine.referenceData;
10 |
11 | if (!objectHasKey(variables, data.varSourceName)) {
12 | throw new Error(`Cant find "${data.varSourceName}" variable`);
13 | }
14 |
15 | dataToSort = variables[data.varSourceName];
16 | }
17 |
18 | if (!Array.isArray(dataToSort)) {
19 | const dataType = dataToSort === null ? 'null' : typeof dataToSort;
20 |
21 | throw new Error(`Can't sort data with "${dataType}" data type`);
22 | }
23 |
24 | const getComparisonValue = ({ itemA, itemB, order = 'asc' }) => {
25 | let comparison = 0;
26 |
27 | if (itemA > itemB) {
28 | comparison = 1;
29 | } else if (itemA < itemB) {
30 | comparison = -1;
31 | }
32 |
33 | return order === 'desc' ? comparison * -1 : comparison;
34 | };
35 | const sortedArray = dataToSort.sort((a, b) => {
36 | let comparison = 0;
37 |
38 | if (data.sortByProperty) {
39 | data.itemProperties.forEach(({ name, order }) => {
40 | comparison = getComparisonValue({
41 | order,
42 | itemA: a[name] ?? a,
43 | itemB: b[name] ?? b,
44 | });
45 | });
46 | } else {
47 | comparison = getComparisonValue({
48 | itemA: a,
49 | itemB: b,
50 | });
51 | }
52 |
53 | return comparison;
54 | });
55 |
56 | if (data.assignVariable) {
57 | await this.setVariable(data.variableName, sortedArray);
58 | }
59 | if (data.saveData) {
60 | this.addDataToColumn(data.dataColumn, sortedArray);
61 | }
62 |
63 | return {
64 | data: sortedArray,
65 | nextBlockId: this.getBlockConnections(id),
66 | };
67 | }
68 |
69 | export default sliceData;
70 |
--------------------------------------------------------------------------------
/src/components/ui/UiTextarea.vue:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
77 |
--------------------------------------------------------------------------------