├── .eslintignore ├── .dockerignore ├── .prettierrc ├── docs ├── .npmrc ├── app.vue ├── assets │ ├── css │ │ └── main.css │ └── images │ │ ├── app.png │ │ ├── bg.png │ │ └── icon.svg ├── .gitignore ├── tsconfig.json ├── app.config.ts ├── nuxt.config.ts ├── tailwind.config.js ├── package.json ├── README.md └── components │ └── LandingPage.vue ├── prettier.config.js ├── public ├── images │ ├── application │ │ ├── icon.png │ │ ├── favicon.ico │ │ ├── icon.svg │ │ ├── docker_not_found.svg │ │ └── ocr4all.svg │ └── repository │ │ ├── product_image.jpg │ │ ├── logo_with_text_light_mode.svg │ │ └── logo_with_text_dark_mode.svg └── assets │ └── nodeflow-custom.css ├── postcss.config.js ├── src ├── views │ ├── EditorPage.vue │ ├── TourPage.vue │ ├── CommandCenterPage.vue │ └── DashboardPage.vue ├── vite-env.d.ts ├── utils │ └── nodeEditor.ts ├── stores │ ├── editor.store.ts │ └── commandCenter.store.ts ├── App.vue ├── nodes │ └── InputNode.ts ├── components │ ├── Editor │ │ ├── Custom │ │ │ ├── viewNode.ts │ │ │ ├── PaletteEntry.vue │ │ │ ├── Toolbar.vue │ │ │ ├── NodeInterface.vue │ │ │ ├── Palette.vue │ │ │ └── Node.vue │ │ ├── UI │ │ │ ├── SpeedDial.vue │ │ │ ├── HintOverlay.vue │ │ │ └── EditorMenubar.vue │ │ ├── logic │ │ │ ├── NodeFactory.ts │ │ │ └── WorkflowExporter.js │ │ └── WorkflowEditor.vue │ ├── Layout │ │ ├── LanguageDropdown.vue │ │ ├── NotFound.vue │ │ └── Sidebar.vue │ ├── CommandCenter │ │ ├── Success.vue │ │ ├── Export.vue │ │ ├── ImageImport.vue │ │ ├── Docker.vue │ │ └── Workflow.vue │ ├── Tour │ │ ├── TourCard.vue │ │ └── Tour.vue │ └── Settings │ │ └── SettingsModal.vue ├── style.css ├── router │ └── index.ts ├── locales │ ├── en.json │ └── de.json ├── main.ts └── auto-imports.d.ts ├── .editorconfig ├── tailwind.config.js ├── tsconfig.node.json ├── electron ├── electron-env.d.ts ├── preload │ └── index.ts └── main │ └── index.ts ├── .gitignore ├── .prettierignore ├── index.html ├── tsconfig.json ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ ├── release.yml │ └── build_develop.yml ├── .eslintrc.js ├── electron-builder.json5 ├── License ├── README.md ├── package.json ├── vite.config.ts └── dist-electron └── main └── index.js /.eslintignore: -------------------------------------------------------------------------------- 1 | cypress 2 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /docs/.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | strict-peer-dependencies=false 3 | -------------------------------------------------------------------------------- /docs/app.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /docs/assets/css/main.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /docs/assets/images/app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxnth/NodeFlow/HEAD/docs/assets/images/app.png -------------------------------------------------------------------------------- /docs/assets/images/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxnth/NodeFlow/HEAD/docs/assets/images/bg.png -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log* 3 | .nuxt 4 | .nitro 5 | .cache 6 | .output 7 | .env 8 | dist 9 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [require('prettier-plugin-tailwindcss')], 3 | }; 4 | -------------------------------------------------------------------------------- /public/images/application/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxnth/NodeFlow/HEAD/public/images/application/icon.png -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // https://nuxt.com/docs/guide/concepts/typescript 3 | "extends": "./.nuxt/tsconfig.json" 4 | } 5 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /public/images/application/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxnth/NodeFlow/HEAD/public/images/application/favicon.ico -------------------------------------------------------------------------------- /public/images/repository/product_image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxnth/NodeFlow/HEAD/public/images/repository/product_image.jpg -------------------------------------------------------------------------------- /src/views/EditorPage.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module '*.vue' { 4 | import type { DefineComponent } from 'vue'; 5 | const component: DefineComponent<{}, {}, any>; 6 | export default component; 7 | } 8 | -------------------------------------------------------------------------------- /docs/app.config.ts: -------------------------------------------------------------------------------- 1 | export default defineAppConfig({ 2 | nuxtIcon: { 3 | size: '32px', // default size applied 4 | class: 'icon', // default class applied 5 | aliases: { 6 | nuxt: 'logos:nuxt-icon', 7 | }, 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | './index.html', 5 | './src/**/*.{vue,js,ts,jsx,tsx}', 6 | ], 7 | darkMode: 'class', 8 | theme: { 9 | extend: {}, 10 | }, 11 | plugins: [], 12 | }; 13 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "resolveJsonModule": true, 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts", "package.json", "electron"] 10 | } 11 | -------------------------------------------------------------------------------- /electron/electron-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare namespace NodeJS { 4 | interface ProcessEnv { 5 | VSCODE_DEBUG?: 'true'; 6 | DIST_ELECTRON: string; 7 | DIST: string; 8 | /** /dist/ or /public/ */ 9 | PUBLIC: string; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /docs/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | // https://nuxt.com/docs/api/configuration/nuxt-config 2 | export default defineNuxtConfig({ 3 | css: ['~/assets/css/main.css'], 4 | modules: ['@nuxt/content', 'nuxt-icon'], 5 | postcss: { 6 | plugins: { 7 | tailwindcss: {}, 8 | autoprefixer: {}, 9 | }, 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /src/utils/nodeEditor.ts: -------------------------------------------------------------------------------- 1 | import { IBaklavaViewModel } from "baklavajs"; 2 | 3 | export function addNodeWithCoordinates(baklava: IBaklavaViewModel, nodeType: any, x: number, y: number) { 4 | const n = new nodeType(); 5 | baklava.displayedGraph.addNode(n); 6 | n.position.x = x; 7 | n.position.y = y; 8 | return n; 9 | } 10 | -------------------------------------------------------------------------------- /src/stores/editor.store.ts: -------------------------------------------------------------------------------- 1 | import { acceptHMRUpdate, defineStore } from 'pinia'; 2 | 3 | export const useEditorStore = defineStore('editor', () => { 4 | const stack = ref(); 5 | 6 | return { 7 | stack 8 | }; 9 | }); 10 | 11 | if (import.meta.hot) 12 | import.meta.hot.accept(acceptHMRUpdate(useEditorStore, import.meta.hot)); 13 | -------------------------------------------------------------------------------- /docs/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | './components/**/*.{js,vue,ts}', 5 | './layouts/**/*.vue', 6 | './pages/**/*.vue', 7 | './plugins/**/*.{js,ts}', 8 | './nuxt.config.{js,ts}', 9 | './app.vue', 10 | ], 11 | theme: { 12 | extend: {}, 13 | }, 14 | plugins: [], 15 | }; 16 | -------------------------------------------------------------------------------- /src/stores/commandCenter.store.ts: -------------------------------------------------------------------------------- 1 | import { acceptHMRUpdate, defineStore } from 'pinia'; 2 | 3 | export const useCommandCenterStore = defineStore('command-center', () => { 4 | const workspace = ref(); 5 | 6 | return { 7 | workspace 8 | }; 9 | }); 10 | 11 | if (import.meta.hot) 12 | import.meta.hot.accept(acceptHMRUpdate(useCommandCenterStore, import.meta.hot)); 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | release 11 | node_modules 12 | dist 13 | dist-ssr 14 | *.local 15 | 16 | # Editor directories and files 17 | .vscode/* 18 | !.vscode/extensions.json 19 | !.vscode/launch.json 20 | .idea 21 | .DS_Store 22 | *.suo 23 | *.ntvs* 24 | *.njsproj 25 | *.sln 26 | *.sw? 27 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | .output 11 | dist-electron 12 | node_modules 13 | dist 14 | *.local 15 | public 16 | pnpm-lock.yaml 17 | auto-imports.d.ts 18 | 19 | # Editor directories and files 20 | .vscode/* 21 | !.vscode/extensions.json 22 | !.vscode/settings.json 23 | .idea 24 | .DS_Store 25 | *.suo 26 | *.ntvs* 27 | *.njsproj 28 | *.sln 29 | *.sw? 30 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 17 | -------------------------------------------------------------------------------- /src/views/TourPage.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 18 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "build": "nuxt build", 5 | "dev": "nuxt dev", 6 | "generate": "nuxt generate", 7 | "preview": "nuxt preview", 8 | "postinstall": "nuxt prepare" 9 | }, 10 | "devDependencies": { 11 | "@nuxt/content": "^2.3.0", 12 | "autoprefixer": "^10.4.13", 13 | "nuxt": "^3.0.0", 14 | "nuxt-icon": "^0.2.6", 15 | "postcss": "^8.4.21", 16 | "tailwindcss": "^3.2.4" 17 | }, 18 | "dependencies": {} 19 | } 20 | -------------------------------------------------------------------------------- /src/nodes/InputNode.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import {ButtonInterface, Node, NodeInterface } from "baklavajs"; 3 | export default class InputNode extends Node { 4 | public type = "InputNode"; 5 | public title = "Input" 6 | 7 | public inputs = { 8 | "description": new ButtonInterface("Description", () => console.log("Description")) 9 | }; 10 | 11 | public outputs = { 12 | "output": new NodeInterface("Output", 0), 13 | }; 14 | 15 | public constructor() { 16 | super(); 17 | this.initializeIo(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/components/Editor/Custom/viewNode.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { AbstractNode, INodeState } from "@baklavajs/core"; 3 | 4 | export interface IViewNodeState extends INodeState { 5 | position: { x: number; y: number }; 6 | width: number; 7 | twoColumn: boolean; 8 | } 9 | 10 | export function setViewNodeProperties(node: AbstractNode) { 11 | node.position = node.position ?? { x: 0, y: 0 }; 12 | node.disablePointerEvents = false; 13 | node.twoColumn = node.twoColumn ?? false; 14 | node.width = node.width ?? 200; 15 | } 16 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | NodeFlow 12 | 13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true, 4 | "target": "ESNext", 5 | "useDefineForClassFields": true, 6 | "module": "ESNext", 7 | "moduleResolution": "Node", 8 | "strict": true, 9 | "jsx": "preserve", 10 | "resolveJsonModule": true, 11 | "isolatedModules": true, 12 | "esModuleInterop": true, 13 | "lib": ["ESNext", "DOM"], 14 | "skipLibCheck": true, 15 | "noEmit": true, 16 | "allowJs": true 17 | }, 18 | "include": ["src"], 19 | "references": [{ "path": "./tsconfig.node.json" }] 20 | } 21 | -------------------------------------------------------------------------------- /src/style.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | code { 7 | @apply mx-2 rounded bg-gray-200 px-1 py-0.5 dark:bg-gray-700; 8 | } 9 | 10 | a { 11 | @apply text-[#42b983]; 12 | } 13 | } 14 | 15 | .border-trueblack { 16 | border-color: black; 17 | } 18 | 19 | ::-webkit-scrollbar { 20 | background-color: #2b2b2b; 21 | border-left: 1px solid #3e3e3e; 22 | width: 0.65rem; 23 | } 24 | 25 | ::-webkit-scrollbar-thumb { 26 | background: #6b6b6b; 27 | border-radius: 10px; 28 | } 29 | 30 | .router-link-exact-active { 31 | @apply bg-gray-200 dark:bg-gray-700; 32 | } 33 | 34 | -------------------------------------------------------------------------------- /.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 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /src/components/Layout/LanguageDropdown.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 24 | 25 | 31 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Nuxt 3 Minimal Starter 2 | 3 | Look at the [Nuxt 3 documentation](https://nuxt.com/docs/getting-started/introduction) to learn more. 4 | 5 | ## Setup 6 | 7 | Make sure to install the dependencies: 8 | 9 | ```bash 10 | # yarn 11 | yarn install 12 | 13 | # npm 14 | npm install 15 | 16 | # pnpm 17 | pnpm install 18 | ``` 19 | 20 | ## Development Server 21 | 22 | Start the development server on http://localhost:3000 23 | 24 | ```bash 25 | npm run dev 26 | ``` 27 | 28 | ## Production 29 | 30 | Build the application for production: 31 | 32 | ```bash 33 | npm run build 34 | ``` 35 | 36 | Locally preview production build: 37 | 38 | ```bash 39 | npm run preview 40 | ``` 41 | 42 | Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information. 43 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | es2021: true, 6 | node: true, 7 | }, 8 | ignorePatterns: [ 9 | 'dist-electron/', 10 | '.output/', 11 | 'dist/', 12 | 'public/', 13 | '**/*.min.js', 14 | '**/*.d.ts', 15 | ], 16 | extends: [ 17 | 'eslint:recommended', 18 | 'plugin:vue/vue3-recommended', 19 | '@vue/eslint-config-typescript/recommended', 20 | 'eslint-config-prettier', 21 | ], 22 | rules: { 23 | 'vue/singleline-html-element-content-newline': 0, 24 | 'vue/component-name-in-template-casing': ['error', 'PascalCase'], 25 | 'vue/no-v-model-argument': 0, 26 | '@typescript-eslint/no-var-requires': 0, 27 | 'vue/no-v-html': 0, 28 | 'vue/multi-word-component-names': 'off', 29 | }, 30 | globals: { 31 | _: true, 32 | }, 33 | }; 34 | -------------------------------------------------------------------------------- /.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 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 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 | 28 | - OS: [e.g. iOS] 29 | - Browser [e.g. chrome, safari] 30 | - Version [e.g. 22] 31 | 32 | **Smartphone (please complete the following information):** 33 | 34 | - Device: [e.g. iPhone6] 35 | - OS: [e.g. iOS8.1] 36 | - Browser [e.g. stock browser, safari] 37 | - Version [e.g. 22] 38 | 39 | **Additional context** 40 | Add any other context about the problem here. 41 | -------------------------------------------------------------------------------- /src/components/CommandCenter/Success.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 22 | -------------------------------------------------------------------------------- /electron-builder.json5: -------------------------------------------------------------------------------- 1 | /** 2 | * @see https://www.electron.build/configuration/configuration 3 | */ 4 | { 5 | productName: 'NodeFlow', 6 | appId: 'com.github.maxnth.NodeFlow', 7 | asar: true, 8 | icon: 'public/images/application/icon.png', 9 | directories: { 10 | output: 'release/${version}', 11 | }, 12 | files: ['dist-electron', 'dist'], 13 | mac: { 14 | artifactName: '${productName}_${version}.${ext}', 15 | target: ['dmg'], 16 | icon: 'public/images/application/icon.png', 17 | }, 18 | win: { 19 | target: [ 20 | { 21 | target: 'msi', 22 | arch: ['x64'], 23 | }, 24 | ], 25 | icon: 'public/images/application/favicon.ico', 26 | publisherName: "Maximilian Nöth", 27 | artifactName: '${productName}_${version}.${ext}', 28 | }, 29 | linux: { 30 | target: ['AppImage'], 31 | category: 'Productivity', 32 | icon: 'public/images/application/icon.png', 33 | }, 34 | nsis: { 35 | oneClick: false, 36 | perMachine: false, 37 | allowToChangeInstallationDirectory: true, 38 | deleteAppDataOnUninstall: false, 39 | }, 40 | } 41 | -------------------------------------------------------------------------------- /License: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Maximilian Nöth 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, RouteRecordRaw, createWebHashHistory } from 'vue-router'; 2 | 3 | import DashboardPage from '../views/DashboardPage.vue'; 4 | import EditorPage from '../views/EditorPage.vue'; 5 | import TourPage from '../views/TourPage.vue'; 6 | import NotFound from '../components/Layout/NotFound.vue'; 7 | import CommandCenterPage from "../views/CommandCenterPage.vue"; 8 | 9 | const routes: RouteRecordRaw[] = [ 10 | { 11 | path: '/', 12 | name: 'dashboard', 13 | component: DashboardPage, 14 | }, 15 | { 16 | path: '/editor', 17 | name: 'editor', 18 | component: EditorPage, 19 | }, 20 | { 21 | path: '/tour', 22 | name: 'tour', 23 | component: TourPage, 24 | }, 25 | { 26 | path: '/command-center', 27 | name: 'command-center', 28 | component: CommandCenterPage 29 | }, 30 | { 31 | path: '/404', 32 | name: 'NotFound', 33 | component: NotFound, 34 | }, 35 | { 36 | path: '/:catchAll(.*)', 37 | redirect: '404', 38 | }, 39 | ]; 40 | 41 | const router = createRouter({ 42 | history: createWebHashHistory(), 43 | routes, 44 | }); 45 | 46 | export default router; 47 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | workflow_dispatch: 5 | branches: [main] 6 | paths-ignore: 7 | - "**.md" 8 | - "**.spec.js" 9 | - ".idea" 10 | - ".vscode" 11 | - ".dockerignore" 12 | - "Dockerfile" 13 | - ".gitignore" 14 | - ".github/**" 15 | - "!.github/workflows/build.yml" 16 | 17 | jobs: 18 | build: 19 | runs-on: ${{ matrix.os }} 20 | 21 | strategy: 22 | matrix: 23 | os: [macos-latest, ubuntu-latest, windows-latest] 24 | 25 | steps: 26 | - name: Checkout Code 27 | uses: actions/checkout@v4 28 | 29 | - name: Setup Node.js 30 | uses: actions/setup-node@v4 31 | with: 32 | node-version: 20 33 | 34 | - name: Install Dependencies 35 | run: yarn 36 | 37 | - name: Build Release Files 38 | run: yarn app:build 39 | env: 40 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 41 | 42 | - name: Upload Artifact 43 | uses: actions/upload-artifact@v4 44 | with: 45 | name: release_on_${{ matrix. os }} 46 | path: release/ 47 | retention-days: 5 48 | -------------------------------------------------------------------------------- /.github/workflows/build_develop.yml: -------------------------------------------------------------------------------- 1 | name: Build Develop 2 | 3 | on: 4 | workflow_dispatch: 5 | branches: [develop] 6 | paths-ignore: 7 | - "**.md" 8 | - "**.spec.js" 9 | - ".idea" 10 | - ".vscode" 11 | - ".dockerignore" 12 | - "Dockerfile" 13 | - ".gitignore" 14 | - ".github/**" 15 | - "!.github/workflows/build.yml" 16 | 17 | jobs: 18 | build: 19 | runs-on: ${{ matrix.os }} 20 | 21 | strategy: 22 | matrix: 23 | os: [macos-latest, ubuntu-latest, windows-latest] 24 | 25 | steps: 26 | - name: Checkout Code 27 | uses: actions/checkout@v4 28 | 29 | - name: Setup Node.js 30 | uses: actions/setup-node@v4 31 | with: 32 | node-version: 20 33 | 34 | - name: Install Dependencies 35 | run: yarn 36 | 37 | - name: Build Release Files 38 | run: yarn app:build 39 | env: 40 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 41 | 42 | - name: Upload Artifact 43 | uses: actions/upload-artifact@v4 44 | with: 45 | name: release_on_${{ matrix. os }} 46 | path: release/ 47 | retention-days: 5 48 | -------------------------------------------------------------------------------- /src/components/Editor/UI/SpeedDial.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 45 | 46 | 51 | -------------------------------------------------------------------------------- /src/components/Layout/NotFound.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 31 | -------------------------------------------------------------------------------- /src/components/Tour/TourCard.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 34 | -------------------------------------------------------------------------------- /src/components/Editor/UI/HintOverlay.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 43 | 44 | 60 | -------------------------------------------------------------------------------- /src/locales/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "sidebar": { 3 | "cta": { 4 | "heading": "Alpha release", 5 | "content": "This version of NodeFlow is an early alpha release of the software and may still contain severe bugs and missing features." 6 | } 7 | }, 8 | "dashboard": { 9 | "heading": { 10 | "welcome": "Welcome to", 11 | "tool-name": "NodeFlow" 12 | }, 13 | "cta": { 14 | "create": { 15 | "heading": "Create", 16 | "content": "Create an OCR workflow from scratch", 17 | "button": "Open Editor" 18 | }, 19 | "edit": { 20 | "heading": "Edit", 21 | "content": "Upload and edit an existing workflow", 22 | "button": "Upload" 23 | }, 24 | "tour": { 25 | "heading": "Tour", 26 | "content": "Take a tour to the NodeFlow editor", 27 | "button": "Start Tour" 28 | }, 29 | "launch": { 30 | "heading": "Launch", 31 | "content": "Directly launch your workflows from NodeFlow", 32 | "button": "Open Command Center" 33 | } 34 | } 35 | }, 36 | "nodeflow": { 37 | "node": { 38 | "settings": "Settings" 39 | }, 40 | "palette": { 41 | "processors": "Processors" 42 | }, 43 | "toolbar": { 44 | "toggle-palette": { 45 | "tooltip": "Toggle Toolbar" 46 | } 47 | } 48 | }, 49 | "command-center": { 50 | "docker": { 51 | "inactive": { 52 | "not-responding": "Looks like Docker doesn't respond", 53 | "install": "Install Docker" 54 | } 55 | } 56 | }, 57 | "404": { 58 | "heading": "Something's missing.", 59 | "content": "Sorry, we can't find that page. You'll find lots to explore on the Dashboard.", 60 | "link": "Back to Dashboard" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 |

5 | 6 | NodeFlow is an open‑source, multi-platform desktop application for creating and managing OCR workflows. It allows beginner and advanced users to interactively craft everything from basic linear to complex branched workflows and export them for command line usage or for the usage in other applications like [OCR4all](https://github.com/OCR4all/OCR4all). 7 | 8 | 9 | | WARNING: This is still a very early proof of concept with major missing features and possibly severe bugs! | 10 | |------------------------------------------------------------------------------------------------------------| 11 | 12 | 13 | 14 | # (Planned) Features 15 | 16 | NodeFlow provides the following features: 17 | 18 | - Create and / or edit OCR workflows using a node editor (powered by [baklavajs](https://github.com/newcat/baklavajs)) 19 | - Leverage dozens of OCR processors (thanks to OCR-D) and configure all available parameters to your liking 20 | - Interactively explore the processor and parameter documentation of every implemente OCR processor 21 | - Export your workflows for command line usage or to use them in other OCR tools 22 | - Interactively explore NodeFlow with the internal tour 23 | 24 | ## Getting Started 25 | 26 | ### 1. Install packages 27 | 28 | ```bash 29 | yarn 30 | ``` 31 | 32 | ### 2. Run 33 | 34 | In development mode, 35 | 36 | ```bash 37 | yarn app:dev 38 | ``` 39 | 40 | In preview mode, 41 | 42 | ```bash 43 | yarn app:preview 44 | ``` 45 | 46 | To build for production, 47 | 48 | ```bash 49 | yarn app:build 50 | ``` 51 | -------------------------------------------------------------------------------- /src/components/Editor/Custom/PaletteEntry.vue: -------------------------------------------------------------------------------- 1 | 58 | 59 | 68 | -------------------------------------------------------------------------------- /src/locales/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "sidebar": { 3 | "cta": { 4 | "heading": "Alpha release", 5 | "content": "Diese Version von NodeFLow ist noch eine frühe Alpha-Version und kann noch schwerwiegende Fehler und fehlende Features beinhalten." 6 | } 7 | }, 8 | "dashboard": { 9 | "heading": { 10 | "welcome": "Willkommen bei", 11 | "tool-name": "NodeFlow" 12 | }, 13 | "cta": { 14 | "create": { 15 | "heading": "Erstellen", 16 | "content": "Benutze den Workflow Editor um einen neuen OCR-Workflow zu erstellen", 17 | "button": "Editor öffnen" 18 | }, 19 | "edit": { 20 | "heading": "Bearbeiten", 21 | "content": "Lade einen existierenden Workflow hoch und bearbeite ihn", 22 | "button": "Upload starten" 23 | }, 24 | "launch": { 25 | "heading": "Ausführen", 26 | "content": "Starte erstellte Workflows direkt aus NodeFlow heraus", 27 | "button": "Command Center öffnen" 28 | }, 29 | "tour": { 30 | "heading": "Tour", 31 | "content": "Nimm an einer Tour durch den NodeFlow-Editor teil", 32 | "button": "Tour starten" 33 | } 34 | } 35 | }, 36 | "nodeflow": { 37 | "node": { 38 | "settings": "Parameter" 39 | }, 40 | "palette": { 41 | "processors": "Prozessoren" 42 | }, 43 | "toolbar": { 44 | "toggle-palette": { 45 | "tooltip": "Toolbar umschalten" 46 | } 47 | } 48 | }, 49 | "command-center": { 50 | "docker": { 51 | "inactive": { 52 | "not-responding": "Docker ist nicht erreichbar", 53 | "install": "Installiere Docker" 54 | } 55 | } 56 | }, 57 | "404": { 58 | "heading": "Seite nicht gefunden", 59 | "content": "Entschuldigung, leider konnte diese Seite nicht gefunden werden. Versuche es doch einmal auf dem Dashboard.", 60 | "link": "Zurück zum Dashboard" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/components/CommandCenter/Export.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 41 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue'; 2 | import { createPinia } from 'pinia'; 3 | import { createI18n } from 'vue-i18n'; 4 | 5 | import App from './App.vue'; 6 | 7 | import router from './router'; 8 | 9 | import PrimeVue from 'primevue/config'; 10 | import 'primevue/resources/primevue.min.css'; 11 | import 'primevue/resources/themes/tailwind-light/theme.css'; 12 | import 'primeicons/primeicons.css'; 13 | import SpeedDial from 'primevue/speeddial'; 14 | import Dropdown from 'primevue/dropdown'; 15 | 16 | 17 | import Menubar from 'primevue/menubar'; 18 | import ToastService from 'primevue/toastservice'; 19 | 20 | // @ts-ignore 21 | import VueShepherd from 'vue-shepherd'; 22 | import 'shepherd.js/dist/css/shepherd.css'; 23 | 24 | import './style.css'; 25 | import Accordion from "primevue/accordion"; 26 | import AccordionTab from "primevue/accordiontab"; 27 | import Tooltip from 'primevue/tooltip' 28 | 29 | const app = createApp(App); 30 | 31 | const messages = Object.fromEntries( 32 | Object.entries( 33 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 34 | import.meta.glob<{ default: any }>('./locales/*.json', { 35 | eager: true, 36 | }) 37 | ).map(([key, value]) => { 38 | return [key.slice(10, -5), value.default]; 39 | }) 40 | ); 41 | 42 | const i18n = createI18n({ 43 | legacy: false, 44 | locale: 'en', 45 | fallbackLocale: 'en', 46 | warnHtmlMessage: false, 47 | globalInjection: true, 48 | messages, 49 | }); 50 | 51 | app.use(i18n); 52 | app.use(createPinia()); 53 | app.use(router); 54 | app.use(PrimeVue); 55 | app.use(ToastService); 56 | app.use(VueShepherd); 57 | 58 | // PrimeVue components 59 | app.component('SpeedDial', SpeedDial); 60 | app.component('Dropdown', Dropdown); 61 | app.component('Menubar', Menubar); 62 | app.component('Accordion', Accordion) 63 | app.component('AccordionTab', AccordionTab) 64 | app.directive('tooltip', Tooltip) 65 | 66 | app.mount('#app').$nextTick(() => { 67 | postMessage({ payload: 'removeLoading' }, '*'); 68 | }); 69 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-flow", 3 | "version": "0.0.3-dev", 4 | "description": "Desktop application for crafting OCR workflows", 5 | "author": "Maximilian Nöth", 6 | "license": "MIT", 7 | "private": true, 8 | "main": "dist-electron/main/index.js", 9 | "keywords": [ 10 | "electron", 11 | "rollup", 12 | "vite", 13 | "vue3", 14 | "vue" 15 | ], 16 | "debug": { 17 | "env": { 18 | "VITE_DEV_SERVER_URL": "http://127.0.0.1:3344/" 19 | } 20 | }, 21 | "scripts": { 22 | "app:dev": "vite", 23 | "app:build": "vue-tsc --noEmit && vite build && electron-builder", 24 | "app:preview": "vite preview", 25 | "build": "vue-tsc --noEmit && vite build", 26 | "lint": "eslint --ext .js,.vue,.ts --ignore-path .gitignore --fix src", 27 | "format": "prettier . --write", 28 | "deps:update": "taze major -I" 29 | }, 30 | "dependencies": { 31 | "@headlessui/vue": "^1.7.14", 32 | "@heroicons/vue": "^2.0.18", 33 | "archiver": "^5.3.1", 34 | "baklavajs": "^2.0.2-beta.4", 35 | "dockerode": "^3.3.5", 36 | "primeicons": "^6.0.1", 37 | "primevue": "^3.29.2", 38 | "vue-shepherd": "^3.0.0" 39 | }, 40 | "devDependencies": { 41 | "@vitejs/plugin-vue": "^4.2.3", 42 | "@vue/eslint-config-typescript": "^11.0.3", 43 | "@vueuse/core": "^10.2.1", 44 | "autoprefixer": "^10.4.14", 45 | "electron": "^24.6.1", 46 | "electron-builder": "^24.4.0", 47 | "eslint": "^8.44.0", 48 | "eslint-config-prettier": "^8.8.0", 49 | "eslint-plugin-prettier": "^4.2.1", 50 | "eslint-plugin-vue": "^9.15.1", 51 | "pinia": "^2.1.4", 52 | "postcss": "^8.4.24", 53 | "prettier": "^3.0.0", 54 | "prettier-plugin-tailwindcss": "^0.3.0", 55 | "tailwindcss": "^3.3.2", 56 | "taze": "^0.11.2", 57 | "typescript": "^5.1.6", 58 | "unplugin-auto-import": "^0.16.6", 59 | "vite": "^4.3.9", 60 | "vite-plugin-electron": "^0.12.0", 61 | "vite-plugin-electron-renderer": "^0.14.5", 62 | "vue": "^3.3.4", 63 | "vue-i18n": "^9.2.2", 64 | "vue-router": "^4.2.3", 65 | "vue-tsc": "^1.8.4" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/components/Tour/Tour.vue: -------------------------------------------------------------------------------- 1 | 89 | 90 | 93 | -------------------------------------------------------------------------------- /src/components/CommandCenter/ImageImport.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 54 | -------------------------------------------------------------------------------- /src/components/Editor/Custom/Toolbar.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 50 | 51 | 56 | -------------------------------------------------------------------------------- /src/components/Editor/UI/EditorMenubar.vue: -------------------------------------------------------------------------------- 1 | 105 | 112 | 113 | -------------------------------------------------------------------------------- /src/components/Editor/logic/NodeFactory.ts: -------------------------------------------------------------------------------- 1 | import { 2 | allowMultipleConnections, 3 | ButtonInterface, 4 | CheckboxInterface, 5 | IntegerInterface, 6 | defineNode, 7 | NodeInterface, 8 | NumberInterface, 9 | SelectInterface, 10 | TextInputInterface 11 | } from "baklavajs"; 12 | 13 | interface INode { 14 | categories: string[] 15 | parameters: IParameter[] 16 | } 17 | 18 | interface IParameter { 19 | type: string 20 | enum: string[] 21 | format: string 22 | default: any 23 | } 24 | 25 | export function constructNodesFromFile(): Promise { 26 | const url = `data/ocrd-all-tool.json` 27 | 28 | return fetch(url) 29 | .then( 30 | (response: Response) => response.json() 31 | ).then( 32 | (data) => constructNodes(data) 33 | ) 34 | } 35 | 36 | function constructNodes(nodeData: object){ 37 | const nodes: object[] = [] 38 | for(const [name, data] of Object.entries(nodeData)){ 39 | const node = createNodeType(name, data) 40 | const category = data.categories !== undefined && data.categories.length > 0 ? data.categories.at(0) : "Undefined" 41 | nodes.push({node: node, category: category}) 42 | } 43 | return nodes 44 | } 45 | function createNodeType(name: string, nodeData: INode) { 46 | const inputs: {[index: string]: any} = {} 47 | if(nodeData.parameters !== undefined) { 48 | for (const [paramName, paramData] of Object.entries(nodeData.parameters)) { 49 | switch (paramData.type) { 50 | case "string": 51 | if (paramData.enum !== undefined) { 52 | inputs[paramName] = () => new SelectInterface(paramName, paramData.default, paramData.enum).setPort(false).use(allowMultipleConnections) 53 | } else { 54 | inputs[paramName] = () => new TextInputInterface(paramName, paramData.default).setPort(false) 55 | } 56 | break; 57 | case "number": 58 | if (paramData.format === "integer") { 59 | inputs[paramName] = () => new IntegerInterface(paramName, paramData.default).setPort(false) 60 | } else if (paramData.format === "float") { 61 | inputs[paramName] = () => new NumberInterface(paramName, paramData.default).setPort(false) 62 | } 63 | break; 64 | case "boolean": 65 | inputs[paramName] = () => new CheckboxInterface(paramName, paramData.default).setPort(false) 66 | break; 67 | } 68 | } 69 | } 70 | inputs["Input"] = () => new NodeInterface("Input", 0) 71 | inputs["Description"] = () => new ButtonInterface("Description", () => console.log("Description")) 72 | 73 | const outputs = { 74 | "Output": () => new NodeInterface("Output", 0) 75 | } 76 | 77 | return defineNode({ 78 | type: name, 79 | inputs: inputs, 80 | outputs: outputs, 81 | }) 82 | } 83 | -------------------------------------------------------------------------------- /src/views/CommandCenterPage.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 80 | 81 | 84 | -------------------------------------------------------------------------------- /electron/preload/index.ts: -------------------------------------------------------------------------------- 1 | function domReady( 2 | condition: DocumentReadyState[] = ['complete', 'interactive'] 3 | ) { 4 | return new Promise((resolve) => { 5 | if (condition.includes(document.readyState)) { 6 | resolve(true); 7 | } else { 8 | document.addEventListener('readystatechange', () => { 9 | if (condition.includes(document.readyState)) { 10 | resolve(true); 11 | } 12 | }); 13 | } 14 | }); 15 | } 16 | 17 | const safeDOM = { 18 | append(parent: HTMLElement, child: HTMLElement) { 19 | if (!Array.from(parent.children).find((e) => e === child)) { 20 | return parent.appendChild(child); 21 | } 22 | }, 23 | remove(parent: HTMLElement, child: HTMLElement) { 24 | if (Array.from(parent.children).find((e) => e === child)) { 25 | return parent.removeChild(child); 26 | } 27 | }, 28 | }; 29 | 30 | /** 31 | * https://tobiasahlin.com/spinkit 32 | * https://connoratherton.com/loaders 33 | * https://projects.lukehaas.me/css-loaders 34 | * https://matejkustec.github.io/SpinThatShit 35 | */ 36 | function useLoading() { 37 | const className = `loaders-css__square-spin`; 38 | const styleContent = ` 39 | @keyframes square-spin { 40 | 25% { transform: perspective(100px) rotateX(180deg) rotateY(0); } 41 | 50% { transform: perspective(100px) rotateX(180deg) rotateY(180deg); } 42 | 75% { transform: perspective(100px) rotateX(0) rotateY(180deg); } 43 | 100% { transform: perspective(100px) rotateX(0) rotateY(0); } 44 | } 45 | .${className} > div { 46 | animation-fill-mode: both; 47 | width: 50px; 48 | height: 50px; 49 | background: #fff; 50 | animation: square-spin 3s 0s cubic-bezier(0.09, 0.57, 0.49, 0.9) infinite; 51 | } 52 | .app-loading-wrap { 53 | position: fixed; 54 | top: 0; 55 | left: 0; 56 | width: 100vw; 57 | height: 100vh; 58 | display: flex; 59 | align-items: center; 60 | justify-content: center; 61 | background: #282c34; 62 | z-index: 9; 63 | } 64 | `; 65 | const oStyle = document.createElement('style'); 66 | const oDiv = document.createElement('div'); 67 | 68 | oStyle.id = 'app-loading-style'; 69 | oStyle.innerHTML = styleContent; 70 | oDiv.className = 'app-loading-wrap'; 71 | oDiv.innerHTML = `
`; 72 | 73 | return { 74 | appendLoading() { 75 | safeDOM.append(document.head, oStyle); 76 | safeDOM.append(document.body, oDiv); 77 | }, 78 | removeLoading() { 79 | safeDOM.remove(document.head, oStyle); 80 | safeDOM.remove(document.body, oDiv); 81 | }, 82 | }; 83 | } 84 | 85 | // ---------------------------------------------------------------------- 86 | 87 | const { appendLoading, removeLoading } = useLoading(); 88 | domReady().then(appendLoading); 89 | 90 | window.onmessage = (ev) => { 91 | ev.data.payload === 'removeLoading' && removeLoading(); 92 | }; 93 | 94 | setTimeout(removeLoading, 4999); 95 | -------------------------------------------------------------------------------- /src/components/Editor/Custom/NodeInterface.vue: -------------------------------------------------------------------------------- 1 | 84 | 105 | -------------------------------------------------------------------------------- /src/components/CommandCenter/Docker.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 68 | -------------------------------------------------------------------------------- /src/components/Editor/WorkflowEditor.vue: -------------------------------------------------------------------------------- 1 | 72 | 73 | 91 | 92 | 93 | 96 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { rmSync } from 'node:fs'; 3 | import { defineConfig } from 'vite'; 4 | import vue from '@vitejs/plugin-vue'; 5 | import AutoImport from 'unplugin-auto-import/vite'; 6 | import electron from 'vite-plugin-electron'; 7 | import renderer from 'vite-plugin-electron-renderer'; 8 | import pkg from './package.json'; 9 | 10 | rmSync('dist-electron', { recursive: true, force: true }); 11 | 12 | const isDevelopment = 13 | process.env.NODE_ENV === 'development' || !!process.env.VSCODE_DEBUG; 14 | const isProduction = process.env.NODE_ENV === 'production'; 15 | 16 | // https://vitejs.dev/config/ 17 | export default defineConfig({ 18 | define: { 19 | __VUE_I18N_FULL_INSTALL__: true, 20 | __VUE_I18N_LEGACY_API__: false, 21 | __INTLIFY_PROD_DEVTOOLS__: false, 22 | }, 23 | 24 | resolve: { 25 | alias: { 26 | '~/': `${path.resolve(__dirname, 'src')}/`, 27 | }, 28 | }, 29 | 30 | plugins: [ 31 | vue(), 32 | 33 | // https://github.com/antfu/unplugin-auto-import 34 | AutoImport({ 35 | imports: ['vue', 'vue-router', 'vue-i18n', 'vue/macros', '@vueuse/core'], 36 | dts: 'src/auto-imports.d.ts', 37 | }), 38 | 39 | electron([ 40 | { 41 | // Main-Process entry file of the Electron App. 42 | entry: 'electron/main/index.ts', 43 | onstart(options) { 44 | if (process.env.VSCODE_DEBUG) { 45 | console.log( 46 | /* For `.vscode/.debug.script.mjs` */ '[startup] Electron App' 47 | ); 48 | } else { 49 | options.startup(); 50 | } 51 | }, 52 | vite: { 53 | build: { 54 | sourcemap: isDevelopment, 55 | minify: isProduction, 56 | outDir: 'dist-electron/main', 57 | rollupOptions: { 58 | external: Object.keys( 59 | 'dependencies' in pkg ? pkg.dependencies : {} 60 | ), 61 | }, 62 | }, 63 | }, 64 | }, 65 | { 66 | entry: 'electron/preload/index.ts', 67 | onstart(options) { 68 | // Notify the Renderer-Process to reload the page when the Preload-Scripts build is complete, 69 | // instead of restarting the entire Electron App. 70 | options.reload(); 71 | }, 72 | vite: { 73 | build: { 74 | sourcemap: isDevelopment, 75 | minify: isProduction, 76 | outDir: 'dist-electron/preload', 77 | rollupOptions: { 78 | external: Object.keys( 79 | 'dependencies' in pkg ? pkg.dependencies : {} 80 | ), 81 | }, 82 | }, 83 | }, 84 | }, 85 | ]), 86 | 87 | // Use Node.js API in the Renderer-process 88 | renderer({ 89 | nodeIntegration: true, 90 | }), 91 | ], 92 | 93 | server: process.env.VSCODE_DEBUG 94 | ? (() => { 95 | const url = new URL(pkg.debug.env.VITE_DEV_SERVER_URL); 96 | return { 97 | host: url.hostname, 98 | port: +url.port, 99 | }; 100 | })() 101 | : undefined, 102 | clearScreen: false, 103 | }); 104 | -------------------------------------------------------------------------------- /docs/assets/images/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 31 | 35 | 39 | 42 | 46 | 50 | 53 | 57 | 61 | 62 | -------------------------------------------------------------------------------- /public/images/application/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 31 | 35 | 39 | 42 | 46 | 50 | 53 | 57 | 61 | 62 | -------------------------------------------------------------------------------- /src/components/CommandCenter/Workflow.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 89 | -------------------------------------------------------------------------------- /src/components/Settings/SettingsModal.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 101 | 102 | -------------------------------------------------------------------------------- /dist-electron/main/index.js: -------------------------------------------------------------------------------- 1 | "use strict";const o=require("electron"),P=require("node:os"),w=require("node:path"),p=require("path"),m=require("dockerode"),{exec:g}=require("child_process"),E=require("archiver"),u=require("fs");process.env.DIST_ELECTRON=w.join(__dirname,"..");process.env.DIST=w.join(process.env.DIST_ELECTRON,"../dist");process.env.PUBLIC=process.env.VITE_DEV_SERVER_URL?w.join(process.env.DIST_ELECTRON,"../public"):process.env.DIST;P.release().startsWith("6.1")&&o.app.disableHardwareAcceleration();process.platform==="win32"&&o.app.setAppUserModelId(o.app.getName());o.app.requestSingleInstanceLock()||(o.app.quit(),process.exit(0));let a=null;const h=w.join(__dirname,"../preload/index.js"),v=process.env.VITE_DEV_SERVER_URL,k=w.join(process.env.DIST,"index.html");async function D(){a=new o.BrowserWindow({title:"NodeFlow",icon:w.join(process.env.PUBLIC,"favicon.ico"),width:1200,height:750,webPreferences:{preload:h,nodeIntegration:!0,contextIsolation:!1}}),a.setMenu(null),a.setOpacity(1),process.env.VITE_DEV_SERVER_URL?(a.loadURL(v),a.webContents.openDevTools()):a.loadFile(k),a.webContents.on("did-finish-load",()=>{a==null||a.webContents.send("main-process-message",new Date().toLocaleString())}),a.webContents.setWindowOpenHandler(({url:n})=>(n.startsWith("https:")&&o.shell.openExternal(n),{action:"deny"}))}o.app.whenReady().then(D);o.app.on("window-all-closed",()=>{a=null,process.platform!=="darwin"&&o.app.quit()});o.app.on("second-instance",()=>{a&&(a.isMinimized()&&a.restore(),a.focus())});o.app.on("activate",()=>{const n=o.BrowserWindow.getAllWindows();n.length?n[0].focus():D()});async function I(n){const{canceled:e,filePath:i}=await o.dialog.showSaveDialog(o.BrowserWindow.getFocusedWindow(),{properties:["createDirectory"]});return e?new Promise((t,r)=>{t(!1)}):new Promise((t,r)=>{u.writeFileSync(i,n),t(!0)})}o.ipcMain.on("save-file",async(n,e)=>await I(e));async function j(n){const{canceled:e,filePath:i}=await o.dialog.showSaveDialog(o.BrowserWindow.getFocusedWindow(),{properties:["createDirectory"]});if(e)return new Promise((t,r)=>{t(!1)});{const t=p.join(o.app.getAppPath(),n),r=E("zip",{zlib:{level:9}}),l=u.createWriteStream(i);return new Promise((s,d)=>{r.directory(t,!1).on("error",c=>d(c)).pipe(l),l.on("close",()=>s(!0)),r.finalize()})}}o.ipcMain.handle("export-results",async(n,e)=>await j(e));function R(n){return new Promise((e,i)=>{u.readFile(n,"utf8",(t,r)=>{t||e(r)})})}o.ipcMain.handle("load-workflow-from-file",async(n,e)=>await R(e));function _(){return new Promise((n,e)=>{new m().listContainers(function(t,r){n(!t)})})}o.ipcMain.handle("is-docker-available",async(n,...e)=>await _());function S(){return new Promise((n,e)=>{const i=new m;i.pull("ocrd/all:maximum",function(t,r){i.modem.followProgress(r,l,s);function l(d,c){d||n(!0)}function s(d){if(d.progressDetail!==void 0){const c=d.progressDetail;if(c.current!==void 0&&c.total!==void 0){const f=Math.round(c.total/c.current);console.log(f)}}}})})}o.ipcMain.handle("pull-ocrd-docker-image",async(n,e)=>await S());function W(n){return new Promise((e,i)=>{const t=`docker run --rm -v ${p.join(o.app.getAppPath(),n.workspace)}:/data -w /data -- ocrd/all:maximum ${n.processString}`;g(t,(r,l,s)=>{r&&(console.log(`error: ${r.message}`),e(!1)),s&&(console.log(`stderr: ${s}`),e(!1)),e(!0)})})}o.ipcMain.handle("launch-workflow",async(n,e)=>await W(e));o.ipcMain.on("open-docker-setup-guide",(n,...e)=>{require("electron").shell.openExternal("https://docs.docker.com/get-docker/")});async function M(){const{canceled:n,filePaths:e}=await o.dialog.showOpenDialog(o.BrowserWindow.getFocusedWindow(),{properties:["openDirectory"]});if(!n)return new Promise((i,t)=>{i(e[0])})}o.ipcMain.handle("get-directory",async(n,e)=>await M());function x(n){return new Promise((e,i)=>{u.mkdirSync(n),u.mkdirSync(p.join(n,"OCR-D-IMG"));const t=`docker run --rm -v ${n}:/data -w /data -- ocrd/all:maximum ocrd workspace -d /data init`;g(t,(r,l,s)=>{r&&(console.log(`error: ${r.message}`),e(!1)),s&&(console.log(`stderr: ${s}`),s.includes("Writing METS to /data/mets.xml")||e(!1)),console.log(`stdout: ${l}`),e(!0)})})}o.ipcMain.handle("init-ocrd-workspace",async(n,e)=>await x(p.join(o.app.getAppPath(),e)));function T(n,e){return new Promise((i,t)=>{const r=p.join(o.app.getAppPath(),e),l=p.join(r,"OCR-D-IMG");for(const s of n){const d=p.parse(s).base,c=p.parse(s).name,f=p.join(l,d);u.copyFileSync(s,f);const y=`docker run --rm -v ${r}:/data -w /data -- ocrd/all:maximum ocrd workspace add -G OCR-D-IMG -i OCR-D-IMG_${c} -g P_${c} -m image/tif OCR-D-IMG/${d}`;require("child_process").execSync(y)}i(!0)})}o.ipcMain.handle("upload-images-to-workspace",async(n,e)=>await T(e.paths,e.workspaceName));o.ipcMain.handle("open-win",(n,e)=>{const i=new o.BrowserWindow({webPreferences:{preload:h,nodeIntegration:!0,contextIsolation:!1}});process.env.VITE_DEV_SERVER_URL?i.loadURL(`${v}#${e}`):i.loadFile(k,{hash:e})}); 2 | -------------------------------------------------------------------------------- /src/components/Editor/logic/WorkflowExporter.js: -------------------------------------------------------------------------------- 1 | function hasInputNode(graph) { 2 | for(const node of graph.nodes){ 3 | if(node.type === "InputNode") return node.id 4 | } 5 | return "" 6 | } 7 | 8 | function flatMapToTree(flatMap){ 9 | const nodes = []; 10 | const toplevelNodes = []; 11 | const lookupList = {}; 12 | 13 | for (let i = 0; i < flatMap.length; i++) { 14 | const n = { 15 | id: flatMap[i].id, 16 | name: flatMap[i].name, 17 | parent_id: ((flatMap[i].parent === 0) ? null : flatMap[i].parent), 18 | children: [] 19 | }; 20 | lookupList[n.id] = n; 21 | nodes.push(n); 22 | if (n.parent_id == null) { 23 | toplevelNodes.push(n); 24 | } 25 | } 26 | 27 | for (let i = 0; i < nodes.length; i++) { 28 | const n = nodes[i]; 29 | if (!(n.parent_id == null)) { 30 | lookupList[n.parent_id].children = lookupList[n.parent_id].children.concat([n]); 31 | } 32 | } 33 | return toplevelNodes[0] 34 | } 35 | 36 | function traverseTree(tree, nodeMap){ 37 | function iter(o, p) { 38 | if(o.children){ 39 | o.children.forEach(function (k){ 40 | iter(k, p.concat(nodeMap.get(k.id))) 41 | }) 42 | } 43 | if(o.children.length === 0){ 44 | result.push(p) 45 | } 46 | } 47 | 48 | const result = []; 49 | iter(tree, []); 50 | return result; 51 | } 52 | 53 | function filterFlatMap(flatMap, idNameMap){ 54 | // Removes all connections which contain non input root nodes 55 | const nonInputRootNodes = new Map(idNameMap) 56 | for(const entry of flatMap){ 57 | nonInputRootNodes.delete(entry.id) 58 | } 59 | const filteredNodes = Array.from(nonInputRootNodes.keys()) 60 | const filteredFlatMap = [] 61 | for(const entry of flatMap){ 62 | if(filteredNodes.indexOf(entry.parent) === -1) filteredFlatMap.push(entry) 63 | } 64 | return filteredFlatMap 65 | } 66 | 67 | function buildExportString(processString, mode) { 68 | switch(mode){ 69 | case "external": 70 | return [ 71 | "docker", 72 | "run", 73 | "--rm", 74 | "-u", 75 | "$(id -u)", 76 | "-v", 77 | "$PWD:/data", 78 | "-w", 79 | "/data", 80 | "--", 81 | "ocrd/all:maximum"].concat(processString).join(" ") 82 | case "internal": 83 | return processString.join(" ") 84 | } 85 | } 86 | 87 | function buildProcessStringFromPaths(paths){ 88 | const reservedNames = ["Description", "Input", "Output"] 89 | const call = [ 90 | "ocrd", 91 | "process" 92 | ] 93 | for(const [pathIndex, path] of paths.entries()){ 94 | let latestOutput = "" 95 | for(const [processorIndex, processor] of path.entries()){ 96 | const processorCall = [] 97 | 98 | processorCall.push(`${processor.type}`.replace(/^(ocrd-)/,"")) 99 | 100 | processorCall.push("-I") 101 | latestOutput === "" ? processorCall.push("OCR-D-IMG") : processorCall.push(latestOutput) 102 | processorCall.push("-O") 103 | const outputName = `P${pathIndex}_${processor.type}_${processorIndex}` 104 | processorCall.push(outputName) 105 | latestOutput = outputName 106 | 107 | for(const [key, value] of Object.entries(processor.inputs)){ 108 | if(reservedNames.indexOf(key) === -1){ 109 | if(value.value){ 110 | processorCall.push("-P") 111 | processorCall.push(key) 112 | processorCall.push(value.value) 113 | } 114 | } 115 | } 116 | call.push("'" + processorCall.join(" ") + "'") 117 | } 118 | } 119 | return call 120 | } 121 | 122 | export function exportWorkflow(state, mode){ 123 | const graph = state.graph 124 | const inputNode = hasInputNode(graph) 125 | if(!inputNode){ 126 | return false 127 | } 128 | 129 | const connectionNodeMap = new Map() 130 | const nodeMap = new Map() 131 | 132 | for(const node of graph.nodes){ 133 | nodeMap.set(node.id, node) 134 | for(const [key, value] of Object.entries(node.inputs)){ 135 | connectionNodeMap.set(value.id, node.id) 136 | } 137 | for(const [key, value] of Object.entries(node.outputs)){ 138 | connectionNodeMap.set(value.id, node.id) 139 | } 140 | } 141 | 142 | const flatMap= [] 143 | flatMap.push({ 144 | id: inputNode, 145 | name: inputNode, 146 | parent: 0 147 | }) 148 | 149 | for(const connection of graph.connections){ 150 | const fromNode = connectionNodeMap.get(connection.from) 151 | const toNode = connectionNodeMap.get(connection.to) 152 | 153 | flatMap.push({ 154 | id: toNode, 155 | name: toNode, 156 | parent: fromNode 157 | }) 158 | 159 | } 160 | 161 | const filteredFlatMap = filterFlatMap(flatMap, nodeMap) 162 | 163 | const tree = flatMapToTree(filteredFlatMap) 164 | const paths = traverseTree(tree, nodeMap) 165 | const processString = buildProcessStringFromPaths(paths) 166 | return buildExportString(processString, mode) 167 | } 168 | -------------------------------------------------------------------------------- /src/components/Layout/Sidebar.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 140 | -------------------------------------------------------------------------------- /src/views/DashboardPage.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 122 | -------------------------------------------------------------------------------- /public/images/repository/logo_with_text_light_mode.svg: -------------------------------------------------------------------------------- 1 | 2 | 22 | 24 | 27 | Layer 1 29 | 31 | 40 | 51 | 55 | 59 | 62 | 66 | 70 | 71 | 74 | 78 | 82 | 83 | 86 | 90 | 94 | 95 | 96 | 97 | NodeFlow 110 | 111 | 112 | -------------------------------------------------------------------------------- /public/images/repository/logo_with_text_dark_mode.svg: -------------------------------------------------------------------------------- 1 | 2 | 35 | 37 | 40 | Layer 1 42 | 44 | 53 | 64 | 68 | 72 | 75 | 79 | 83 | 84 | 87 | 91 | 95 | 96 | 99 | 103 | 107 | 108 | 109 | 110 | NodeFlow 123 | 124 | 125 | -------------------------------------------------------------------------------- /docs/components/LandingPage.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 119 | 120 | 125 | -------------------------------------------------------------------------------- /src/components/Editor/Custom/Palette.vue: -------------------------------------------------------------------------------- 1 | 143 | 144 | 188 | -------------------------------------------------------------------------------- /src/components/Editor/Custom/Node.vue: -------------------------------------------------------------------------------- 1 | 138 | 223 | -------------------------------------------------------------------------------- /public/images/application/docker_not_found.svg: -------------------------------------------------------------------------------- 1 | 2 | 16 | 36 | 41 | 42 |