├── .eslintrc.json ├── .github └── workflows │ └── build.yml ├── .gitignore ├── .husky └── pre-commit ├── .nvmrc ├── .prettierignore ├── LICENSE ├── README.md ├── entitlements.mac.inherit.plist ├── package.json ├── prettier.config.cjs ├── src ├── img │ ├── active.png │ ├── app.icns │ ├── appIcon.png │ ├── cross.svg │ ├── folder_blue.png │ ├── inactive.png │ ├── memex-logo.png │ ├── tray_icon.png │ └── tray_icon_dev.png ├── index.css ├── index.html ├── index.ts ├── loading.html ├── preload.cjs └── renderer.ts ├── tsconfig.json └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/eslint-recommended", 10 | "plugin:@typescript-eslint/recommended", 11 | "plugin:import/recommended", 12 | "plugin:import/electron", 13 | "plugin:import/typescript", 14 | "prettier" 15 | ], 16 | "parser": "@typescript-eslint/parser" 17 | } 18 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build and release Memex-Desktop builds 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v[0-9]+.[0-9]+.[0-9]+' 7 | jobs: 8 | release: 9 | runs-on: ${{ matrix.os }} 10 | 11 | strategy: 12 | matrix: 13 | os: [macos-latest, ubuntu-latest, windows-latest] 14 | 15 | steps: 16 | - name: Check out Git repository 17 | uses: actions/checkout@v1 18 | 19 | - name: Install Node.js, NPM and Yarn 20 | uses: actions/setup-node@v4 21 | with: 22 | node-version: 18 23 | 24 | - name: Copy necessary files to build directory 25 | run: | 26 | mkdir -p build 27 | cp src/preload.cjs build 28 | cp src/*.html build 29 | cp src/*.css build 30 | 31 | - name: Prepare for app notarization 32 | if: startsWith(matrix.os, 'macos') 33 | # Import Apple API key for app notarization on macOS 34 | run: | 35 | mkdir -p ~/private_keys/ 36 | echo '${{ secrets.api_key }}' > ~/private_keys/AuthKey_${{ secrets.api_key_id }}.p8 37 | 38 | - name: Build/release Electron app 39 | uses: samuelmeuli/action-electron-builder@v1 40 | with: 41 | # GitHub token, automatically provided to the action 42 | # (No need to define this secret in the repo settings) 43 | github_token: ${{ secrets.github_token }} 44 | 45 | # If the commit is tagged with a version (e.g. "v1.0.0"), 46 | # release the app after building 47 | release: ${{ startsWith(github.ref, 'refs/tags/v') }} 48 | 49 | # macOS signing certs 50 | mac_certs: ${{ secrets.mac_certs }} 51 | mac_certs_password: ${{ secrets.mac_certs_password }} 52 | env: 53 | # macOS notarization API key 54 | API_KEY_ID: ${{ secrets.api_key_id }} 55 | API_KEY_ISSUER_ID: ${{ secrets.api_key_issuer_id }} 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | dist 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | .DS_Store 19 | 20 | # Directory for instrumented libs generated by jscoverage/JSCover 21 | lib-cov 22 | 23 | # Coverage directory used by tools like istanbul 24 | coverage 25 | *.lcov 26 | 27 | # nyc test coverage 28 | .nyc_output 29 | 30 | # node-waf configuration 31 | .lock-wscript 32 | 33 | # Compiled binary addons (https://nodejs.org/api/addons.html) 34 | build/Release 35 | 36 | # Dependency directories 37 | node_modules/ 38 | jspm_packages/ 39 | 40 | # TypeScript v1 declaration files 41 | typings/ 42 | 43 | # TypeScript cache 44 | *.tsbuildinfo 45 | 46 | # Optional npm cache directory 47 | .npm 48 | 49 | # Optional eslint cache 50 | .eslintcache 51 | 52 | # Optional REPL history 53 | .node_repl_history 54 | 55 | # Output of 'npm pack' 56 | *.tgz 57 | 58 | # Yarn Integrity file 59 | .yarn-integrity 60 | 61 | # dotenv environment variables file 62 | .env 63 | .env.test 64 | 65 | # parcel-bundler cache (https://parceljs.org/) 66 | .cache 67 | 68 | # next.js build output 69 | .next 70 | 71 | # nuxt.js build output 72 | .nuxt 73 | 74 | # vuepress build output 75 | .vuepress/dist 76 | 77 | # Serverless directories 78 | .serverless/ 79 | 80 | # FuseBox cache 81 | .fusebox/ 82 | 83 | # DynamoDB Local files 84 | .dynamodb/ 85 | 86 | # Webpack 87 | .webpack/ 88 | 89 | # Vite 90 | .vite/ 91 | 92 | # Electron-Forge 93 | out/ 94 | 95 | backup_location.txt 96 | 97 | config.json 98 | 99 | env.yml 100 | 101 | electron-builder.yml 102 | 103 | .env 104 | 105 | data 106 | 107 | forge.config.js 108 | 109 | afterSignHook.js 110 | 111 | buildAPIkeys.js 112 | AuthKey_7LNRLB4RZ6.p8 113 | build 114 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v18.18.2 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | data/ 3 | node_modules/ 4 | .git/ 5 | package.json 6 | yarn.lock 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 WorldBrain.io - Collective Web Intelligence 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # memex-local-sync 2 | 3 | ## Instructions 4 | 5 | 1. Set node version `nvm use` 6 | 7 | 1. Install deps: `yarn` 8 | 9 | 1. Build and run app: `yarn start` 10 | -------------------------------------------------------------------------------- /entitlements.mac.inherit.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.cs.allow-jit 6 | 7 | com.apple.security.cs.allow-unsigned-executable-memory 8 | 9 | com.apple.security.cs.allow-dyld-environment-variables 10 | 11 | 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "memex-desktop", 3 | "productName": "Memex Desktop", 4 | "version": "0.0.51", 5 | "description": "Backup and sync to your favorite PKM tools", 6 | "main": "build/index.js", 7 | "type": "module", 8 | "resolutions": { 9 | "node-abi": "^3.47.0" 10 | }, 11 | "scripts": { 12 | "start": "tsc && cp src/preload.cjs build/ && electron-forge start", 13 | "package": "electron-forge package", 14 | "package:mac:arm64": "yarn package --platform mac --arch arm64", 15 | "package:mac:x64": "yarn package --platform mac --arch x64", 16 | "package:mac": "yarn package --platform mac --arch x64", 17 | "package:win:x64": "yarn package --platform win nsis --arch x64", 18 | "package:linux:x64": "yarn package --platform linux --arch x64", 19 | "package:all": "yarn package:mac:arm64 && yarn package:mac:x64 && yarn package:win:x64 && yarn package:linux:x64", 20 | "make": "electron-forge make", 21 | "publish": "electron-forge publish", 22 | "lint": "eslint --ext .ts,.tsx .", 23 | "deploy": "tsc && electron-builder build --mac --linux --win --publish never", 24 | "build": "tsc", 25 | "build:manual": "tsc && cp src/preload.cjs build/ && cp src/*.html build && cp src/*.css build/ && electron-builder build -mwl --publish never", 26 | "build:mac:x64": "tsc && cp src/preload.cjs build/ && cp src/*.html build && cp src/*.css build/ && electron-builder build --mac dmg zip --x64", 27 | "build:mac:arm64:dmg": "tsc && cp src/preload.cjs build/ && cp src/*.html build && cp src/*.css build/ && electron-builder build --mac dmg --arm64", 28 | "build:mac:arm64:dmg:withoutSign": "tsc && cp src/preload.cjs build/ && cp src/*.html build && cp src/*.css build/ && electron-builder build --mac dmg --arm64 --config.afterSign", 29 | "build:mac:arm64": "tsc && cp src/preload.cjs build/ && cp src/*.html build && cp src/*.css build/ && electron-builder build --mac dmg zip --arm64", 30 | "build:mac": "tsc && cp src/preload.cjs build/ && cp src/*.html build && cp src/*.css build/ && electron-builder build --mac dmg zip --x64 --arm64", 31 | "build:win": "tsc && cp src/preload.cjs build/ && cp src/*.html build && cp src/*.css build/ && electron-builder build --win nsis --x64", 32 | "build:linux": "tsc && cp src/preload.cjs build/ && cp src/*.html build && cp src/*.css build/ && electron-builder build --linux AppImage --x64", 33 | "build:all": "tsc && cp src/preload.cjs build/ && cp src/*.html build && cp src/*.css build/ && yarn build:mac && yarn build:win && yarn build:linux", 34 | "build:allbutMac": "yarn build:win:x64 && yarn build:win:ia32 && yarn build:linux:x64", 35 | "prepare": "husky install" 36 | }, 37 | "build": { 38 | "appId": "memex-desktop", 39 | "productName": "Memex-Desktop", 40 | "copyright": "Copyright (c) 2023 Memex.Garden", 41 | "afterSign": "electron-builder-notarize", 42 | "artifactName": "${productName}-v${version}-${os}-${arch}.${ext}", 43 | "asar": false, 44 | "publish": [ 45 | { 46 | "provider": "github", 47 | "owner": "worldbrain", 48 | "repo": "memex-desktop" 49 | } 50 | ], 51 | "mac": { 52 | "icon": "src/img/memex-logo.png", 53 | "target": [ 54 | { 55 | "target": "dmg", 56 | "arch": [ 57 | "arm64", 58 | "x64" 59 | ] 60 | }, 61 | { 62 | "target": "zip", 63 | "arch": [ 64 | "arm64", 65 | "x64" 66 | ] 67 | } 68 | ], 69 | "hardenedRuntime": true, 70 | "entitlements": "entitlements.mac.inherit.plist" 71 | }, 72 | "win": { 73 | "icon": "src/img/memex-logo.png", 74 | "target": [ 75 | { 76 | "target": "nsis", 77 | "arch": [ 78 | "x64" 79 | ] 80 | } 81 | ] 82 | }, 83 | "linux": { 84 | "icon": "src/img/memex-logo.png", 85 | "target": [ 86 | { 87 | "target": "AppImage", 88 | "arch": [ 89 | "x64" 90 | ] 91 | } 92 | ] 93 | }, 94 | "extraResources": [ 95 | "src/img/tray_icon.png", 96 | "src/models/*", 97 | "!data/*", 98 | "node_modules/@xenova/*", 99 | "src/*.css", 100 | "src/*.html" 101 | ], 102 | "files": [ 103 | "!dist/**/*", 104 | "!data/**/*", 105 | "build/**/*", 106 | "src/**/*", 107 | "src/*", 108 | "./index.html", 109 | "./loading.html", 110 | "index.css", 111 | "src/*.css", 112 | "src/*.html" 113 | ] 114 | }, 115 | "repository": { 116 | "type": "git", 117 | "url": "https://github.com/WorldBrain/memex-desktop" 118 | }, 119 | "keywords": [], 120 | "author": { 121 | "name": "Oliver Sauter", 122 | "email": "oli@worldbrain.io" 123 | }, 124 | "license": "MIT", 125 | "lint-staged": { 126 | "**/*": "prettier --write --ignore-unknown" 127 | }, 128 | "devDependencies": { 129 | "@electron-forge/cli": "^7.2.0", 130 | "@electron-forge/maker-deb": "^7.2.0", 131 | "@electron-forge/maker-rpm": "^7.2.0", 132 | "@electron-forge/maker-squirrel": "^7.2.0", 133 | "@electron-forge/maker-zip": "^7.2.0", 134 | "@electron-forge/plugin-auto-unpack-natives": "^7.2.0", 135 | "@types/jsdom": "^21.1.6", 136 | "@types/mkdirp": "^2.0.0", 137 | "@types/turndown": "^5.0.4", 138 | "@typescript-eslint/eslint-plugin": "^5.0.0", 139 | "@typescript-eslint/parser": "^5.0.0", 140 | "@vercel/webpack-asset-relocator-loader": "1.7.3", 141 | "copy-webpack-plugin": "^11.0.0", 142 | "css-loader": "^6.0.0", 143 | "dotenv-webpack": "^8.0.1", 144 | "electron": "32.1.2", 145 | "electron-builder": "^25.0.5", 146 | "eslint": "^8.0.1", 147 | "eslint-config-prettier": "^9.1.0", 148 | "eslint-plugin-import": "^2.25.0", 149 | "fork-ts-checker-webpack-plugin": "^7.2.13", 150 | "husky": "^8.0.3", 151 | "node-loader": "^2.0.0", 152 | "prettier": "^3.1.1", 153 | "style-loader": "^3.0.0", 154 | "ts-loader": "^9.2.2", 155 | "ts-node": "^10.0.0", 156 | "typescript": "^4.8.3" 157 | }, 158 | "dependencies": { 159 | "@types/cors": "^2.8.17", 160 | "@types/express": "^4.17.21", 161 | "cors": "^2.8.5", 162 | "dotenv": "^16.3.1", 163 | "electron-builder-notarize": "^1.5.2", 164 | "electron-log": "^4.4.8", 165 | "electron-settings": "^4.0.2", 166 | "electron-squirrel-startup": "^1.0.0", 167 | "electron-store": "^8.1.0", 168 | "electron-updater": "^6.1.4", 169 | "express": "^4.18.2", 170 | "mkdirp": "^3.0.1", 171 | "moment": "^2.29.4", 172 | "node-fetch": "^2.6.1", 173 | "notarytool": "^0.0.4" 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /prettier.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: false, 3 | singleQuote: true, 4 | trailingComma: 'all', 5 | tabWidth: 4, 6 | } 7 | -------------------------------------------------------------------------------- /src/img/active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WorldBrain/memex-desktop/69e81a4c34817312c893807b2cd3beced17530e7/src/img/active.png -------------------------------------------------------------------------------- /src/img/app.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WorldBrain/memex-desktop/69e81a4c34817312c893807b2cd3beced17530e7/src/img/app.icns -------------------------------------------------------------------------------- /src/img/appIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WorldBrain/memex-desktop/69e81a4c34817312c893807b2cd3beced17530e7/src/img/appIcon.png -------------------------------------------------------------------------------- /src/img/cross.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | noun_Cross_1049918 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/img/folder_blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WorldBrain/memex-desktop/69e81a4c34817312c893807b2cd3beced17530e7/src/img/folder_blue.png -------------------------------------------------------------------------------- /src/img/inactive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WorldBrain/memex-desktop/69e81a4c34817312c893807b2cd3beced17530e7/src/img/inactive.png -------------------------------------------------------------------------------- /src/img/memex-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WorldBrain/memex-desktop/69e81a4c34817312c893807b2cd3beced17530e7/src/img/memex-logo.png -------------------------------------------------------------------------------- /src/img/tray_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WorldBrain/memex-desktop/69e81a4c34817312c893807b2cd3beced17530e7/src/img/tray_icon.png -------------------------------------------------------------------------------- /src/img/tray_icon_dev.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WorldBrain/memex-desktop/69e81a4c34817312c893807b2cd3beced17530e7/src/img/tray_icon_dev.png -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 3 | Helvetica, Arial, sans-serif; 4 | margin: auto; 5 | max-width: 38rem; 6 | padding: 2rem; 7 | background-color: #12131b; 8 | display: flex; 9 | align-items: center; 10 | justify-content: center; 11 | flex-direction: column; 12 | grid-gap: 20px; 13 | height: 500px; 14 | 15 | & > svg { 16 | height: 50px; 17 | width: 50px; 18 | } 19 | } 20 | 21 | h1 { 22 | font-family: Arial, sans-serif; 23 | color: #cacad1; 24 | text-align: center; 25 | font-size: 18px; 26 | margin-bottom: -25px; 27 | } 28 | p { 29 | font-family: Arial, sans-serif; 30 | color: #a9a9b1; 31 | font-size: 16px; 32 | text-align: center; 33 | } 34 | 35 | .downloadProgress { 36 | margin-right: 10px; 37 | } 38 | 39 | .warning { 40 | border: 1px solid #ff0000; 41 | font-size: 14px; 42 | text-align: center; 43 | margin-top: 10px; 44 | padding: 10px; 45 | color: #ff0000; 46 | border-radius: 5px; 47 | } 48 | 49 | #dbPath { 50 | color: white; 51 | } 52 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Successfully connected 6 | 7 | 8 | 9 | 16 | 22 | 23 | 24 |

The desktop app is now set up

25 |

26 | It'll run in the background and you can interact with it via the 27 | small M icon in your system tray 28 |

29 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | //////////////////////////////// 2 | /// GENERAL SETUP /// 3 | //////////////////////////////// 4 | 5 | import { fileURLToPath } from 'url' 6 | import { dirname } from 'path' 7 | import { shell } from 'electron' 8 | 9 | const __filename = fileURLToPath(import.meta.url) 10 | const __dirname = dirname(__filename) 11 | 12 | import express from 'express' 13 | import electron, { 14 | app, 15 | BrowserWindow, 16 | Tray, 17 | Menu, 18 | nativeImage, 19 | dialog, 20 | ipcMain, 21 | } from 'electron' 22 | import url from 'url' 23 | import pkg from 'electron-updater' 24 | const { autoUpdater } = pkg 25 | 26 | import Store from 'electron-store' 27 | import fs from 'fs' 28 | import path from 'path' 29 | import log from 'electron-log' 30 | // import * as lancedb from 'vectordb' 31 | import dotEnv from 'dotenv' 32 | import cors from 'cors' 33 | import { Server } from 'http' 34 | dotEnv.config() 35 | 36 | const isPackaged = app.isPackaged 37 | let updateStage: 'pristine' | 'checking' | 'downloading' | string = 'pristine' 38 | let tray: Tray | null = null 39 | let mainWindow: BrowserWindow 40 | // let downloadProgress: number = 0 41 | 42 | let EXPRESS_PORT: number 43 | if (isPackaged) { 44 | EXPRESS_PORT = 11922 // Different from common React port 3000 to avoid conflicts 45 | } else { 46 | EXPRESS_PORT = 11923 // Different from common React port 3000 to avoid conflicts 47 | } 48 | let expressApp: express.Express = express() 49 | expressApp.use(cors({ origin: '*' })) 50 | 51 | ipcMain.handle('get-db-path', () => { 52 | return path.join(app.getPath('userData')) 53 | }) 54 | 55 | //////////////////////////////// 56 | /// DATATBASE SETUP STUFF /// 57 | //////////////////////////////// 58 | 59 | import settings from 'electron-settings' 60 | 61 | if (!isPackaged) { 62 | settings.configure({ 63 | dir: path.join(electron.app.getAppPath(), '..', 'MemexDesktopData'), 64 | }) 65 | } 66 | const store = isPackaged 67 | ? new Store() 68 | : new Store({ 69 | cwd: path.join(electron.app.getAppPath(), '..', 'MemexDesktopData'), 70 | }) 71 | 72 | //////////////////////////////// 73 | /// ELECTRON APP BASIC SETUP /// 74 | //////////////////////////////// 75 | 76 | if (app.dock) { 77 | // Check if the dock API is available (macOS specific) 78 | app.dock.hide() 79 | } 80 | 81 | if (!settings.has('userPref.startOnStartup')) { 82 | settings.set('userPref.startOnStartup', true) 83 | app.setLoginItemSettings({ 84 | openAtLogin: true, 85 | }) 86 | } 87 | 88 | // if (require('electron-squirrel-startup')) { 89 | // app.quit() 90 | // } 91 | 92 | expressApp.use(express.json({ limit: '50mb' })) // adjust the limit as required 93 | expressApp.use(express.urlencoded({ extended: true, limit: '50mb' })) // adjust the limit as required 94 | 95 | process.on('uncaughtException', (err) => { 96 | log.error('There was an uncaught error', err) 97 | }) 98 | 99 | process.on('unhandledRejection', (reason, promise) => { 100 | log.error('Unhandled Rejection at:', promise, 'reason:', reason) 101 | }) 102 | 103 | // Example route 1: Simple Hello World 104 | expressApp.get('/hello', (req, res) => { 105 | res.send("Hello World from Electron's Express server!") 106 | }) 107 | 108 | // Example route 2: Send back query params 109 | expressApp.get('/echo', (req, res) => { 110 | res.json(req.query) 111 | }) 112 | 113 | // Example route 3other functionality you want to add 114 | let server: Server | null = null 115 | 116 | function startExpress() { 117 | if (!server || !server.listening) { 118 | server = expressApp.listen(EXPRESS_PORT, () => { 119 | log.info( 120 | `Express server started on http://localhost:${EXPRESS_PORT}`, 121 | ) 122 | console.log( 123 | `Express server started on http://localhost:${EXPRESS_PORT}`, 124 | ) 125 | }) 126 | server.keepAliveTimeout = 300000000000000000 127 | server.timeout = 0 128 | 129 | server.on('close', () => { 130 | console.log('Express server has shut down') 131 | log.info('Express server has shut down') 132 | }) 133 | } else { 134 | console.log( 135 | `Express server is already running on http://localhost:${EXPRESS_PORT}`, 136 | ) 137 | } 138 | } 139 | 140 | function checkSyncKey(inputKey: string) { 141 | var storedKey = store.get('syncKey') 142 | 143 | if (!storedKey) { 144 | store.set('syncKey', inputKey) 145 | return true 146 | } else if (storedKey === inputKey) { 147 | return true 148 | } else { 149 | return false 150 | } 151 | } 152 | 153 | function stopExpress() { 154 | return new Promise((resolve, reject) => { 155 | if (server) { 156 | server.close((err) => { 157 | if (err) { 158 | log.error('Error stopping Express server:', err) 159 | reject(err) 160 | } else { 161 | console.log('Express server stopped.') 162 | server = null // Nullify the server 163 | resolve() 164 | } 165 | process.exit(0) 166 | }) 167 | } else { 168 | resolve() 169 | } 170 | }) 171 | } 172 | 173 | interface CustomError extends Error { 174 | code?: string 175 | } 176 | 177 | function pickDirectory(type: string) { 178 | console.log('pickDirectory', type) 179 | try { 180 | var directories = dialog.showOpenDialogSync({ 181 | properties: ['openDirectory'], 182 | }) 183 | if (directories && directories.length > 0) { 184 | var path = directories[0] 185 | 186 | store.set(type, path) 187 | 188 | return path // Return the first selected directory 189 | } 190 | } catch (error) { 191 | const err = error as CustomError 192 | if (err.code === 'EACCES') { 193 | dialog.showErrorBox( 194 | 'Permission Denied', 195 | 'You do not have permission to access this directory. Please select a different directory or change your permission settings.', 196 | ) 197 | } else { 198 | dialog.showErrorBox( 199 | 'An error occurred', 200 | 'An error occurred while selecting the directory. Please try again.', 201 | ) 202 | } 203 | log.error(error) 204 | } 205 | return null 206 | } 207 | 208 | async function createWindow() { 209 | return new Promise((resolve, reject) => { 210 | mainWindow = new BrowserWindow({ 211 | height: 600, 212 | width: 800, 213 | webPreferences: { 214 | preload: path.join(__dirname, 'preload.cjs'), 215 | nodeIntegration: true, 216 | }, 217 | }) 218 | 219 | let indexPath 220 | if (isPackaged) { 221 | indexPath = path.join( 222 | electron.app.getAppPath(), 223 | 'build', 224 | 'index.html', 225 | ) 226 | } else { 227 | indexPath = path.join( 228 | electron.app.getAppPath(), 229 | 'src', 230 | 'index.html', 231 | ) 232 | } 233 | // mainWindow.webContents.openDevTools() 234 | mainWindow.on('close', (event) => { 235 | event.preventDefault() 236 | mainWindow.hide() 237 | }) 238 | 239 | mainWindow 240 | .loadURL( 241 | url.format({ 242 | pathname: indexPath, 243 | protocol: 'file:', 244 | slashes: true, 245 | }), 246 | ) 247 | .then(() => { 248 | resolve() // Resolve the promise when the window is loaded 249 | }) 250 | .catch((error) => { 251 | reject(error) // Reject the promise if there's an error 252 | }) 253 | }) 254 | } 255 | 256 | app.on('before-quit', async function () { 257 | tray?.destroy() 258 | if (server) { 259 | log.info('Stopping Express server as parto of quit process') 260 | await stopExpress() 261 | } 262 | log.info('before-quit') 263 | }) 264 | 265 | app.on('ready', async () => { 266 | if ((await settings.get('hasOnboarded')) === undefined) { 267 | await settings.set('onboardedAfterRabbitHoleRemove', true) 268 | await createWindow() 269 | mainWindow.loadURL( 270 | url.format({ 271 | pathname: isPackaged 272 | ? path.join( 273 | electron.app.getAppPath(), 274 | 'build', 275 | 'index.html', 276 | ) 277 | : path.join(electron.app.getAppPath(), 'src', 'index.html'), 278 | protocol: 'file:', 279 | slashes: true, 280 | }), 281 | ) 282 | await settings.set('hasOnboarded', true) 283 | } 284 | const oldUser = await settings.get('onboardedAfterRabbitHoleRemove') 285 | if (oldUser === undefined) { 286 | } 287 | 288 | try { 289 | startExpress() // Start Express server first 290 | 291 | log.catchErrors() 292 | let trayIconPath = '' 293 | if (isPackaged) { 294 | trayIconPath = path.join( 295 | process.resourcesPath, 296 | 'src/img/tray_icon.png', 297 | ) 298 | } else { 299 | trayIconPath = path.join( 300 | electron.app.getAppPath(), 301 | 'src', 302 | 'img', 303 | 'tray_icon_dev.png', 304 | ) 305 | } 306 | var trayIcon = nativeImage.createFromPath(trayIconPath) 307 | 308 | if (!fs.existsSync(trayIconPath)) { 309 | log.error('Tray icon not found:', trayIconPath) 310 | return 311 | } 312 | 313 | tray = new Tray(trayIcon) 314 | tray.setImage(trayIcon) 315 | 316 | let updateLabel = 'Check for Updates' 317 | 318 | if (updateStage === 'checking') { 319 | updateLabel = 'Checking for Updates' 320 | } 321 | if (updateStage === 'downloading') { 322 | updateLabel = 'Update Downloading' 323 | } 324 | 325 | var updateMenuItem = { 326 | label: 'Check for Updates', 327 | click: function () { 328 | autoUpdater.checkForUpdates() 329 | }, 330 | } 331 | 332 | var contextMenu = Menu.buildFromTemplate([ 333 | { 334 | label: `Memex Local Sync - v${app.getVersion()}`, 335 | enabled: false, // This makes the menu item non-clickable 336 | }, 337 | { 338 | label: 'Start on Startup', 339 | type: 'checkbox', 340 | checked: app.getLoginItemSettings().openAtLogin, // Check if the app is set to start on login 341 | click: function (item) { 342 | var startOnStartup = item.checked 343 | app.setLoginItemSettings({ openAtLogin: startOnStartup }) 344 | }, 345 | }, 346 | { 347 | label: 'Refresh Sync Key', 348 | click: function () { 349 | store.delete('syncKey') 350 | }, 351 | }, 352 | updateMenuItem, 353 | { 354 | label: 'Exit', 355 | click: function () { 356 | console.log('exit clicked before') 357 | app.quit() 358 | console.log('exit clicked') 359 | }, 360 | }, 361 | ]) 362 | 363 | // Set the context menu to the Tray 364 | tray.setContextMenu(contextMenu) 365 | 366 | // Optional: Add a tooltip to the Tray 367 | tray.setToolTip('Memex Local Sync Helper') 368 | try { 369 | autoUpdater 370 | .checkForUpdates() 371 | .then(function () { 372 | updateStage = 'checking' 373 | }) 374 | .catch(function (err) { 375 | log.error('err', err) 376 | }) 377 | autoUpdater.on('update-available', async function () { 378 | log.info('update available') 379 | updateStage = 'downloading' 380 | log.info(autoUpdater.downloadUpdate()) 381 | }) 382 | 383 | autoUpdater.on('update-downloaded', function () { 384 | log.info('update downloaded') 385 | autoUpdater.quitAndInstall() 386 | }) 387 | } catch (error) { 388 | console.log('error', error) 389 | } 390 | } catch (error) { 391 | log.error('error', error) 392 | app.quit() 393 | } 394 | }) 395 | 396 | function isPathComponentValid(component: string) { 397 | if ( 398 | typeof component !== 'string' || 399 | !component.match(/^[a-z0-9\-]{2,20}$/) 400 | ) { 401 | return false 402 | } else { 403 | return true 404 | } 405 | } 406 | 407 | /////////////////////////// 408 | /// PKM SYNC ENDPOINTS /// 409 | ///////////////////////// 410 | 411 | expressApp.post('/set-directory', async function (req, res) { 412 | if (!checkSyncKey(req.body.syncKey)) { 413 | return res.status(403).send('Only one app instance allowed') 414 | } 415 | let directoryPath 416 | let pkmSyncType 417 | try { 418 | pkmSyncType = req.body.pkmSyncType 419 | if (typeof pkmSyncType !== 'string') { 420 | res.status(400).json({ error: 'Invalid pkmSyncType' }) 421 | return 422 | } 423 | directoryPath = pickDirectory(pkmSyncType) 424 | if (directoryPath) { 425 | store.set(pkmSyncType, directoryPath) 426 | res.status(200).send(directoryPath) 427 | return path 428 | } else { 429 | res.status(400).json({ error: 'No directory selected' }) 430 | return null 431 | } 432 | } catch (error) { 433 | log.error('Error in /set-directory:', error) 434 | res.status(500).json({ 435 | error: 'Internal server error', 436 | }) 437 | return null 438 | } 439 | }) 440 | 441 | expressApp.put('/update-file', async function (req, res) { 442 | if (!checkSyncKey(req.body.syncKey)) { 443 | return res.status(403).send('Only one app instance allowed') 444 | } 445 | try { 446 | var body = req.body 447 | 448 | var pkmSyncType = body.pkmSyncType 449 | var pageTitle = body.pageTitle 450 | var fileContent = body.fileContent 451 | 452 | if ( 453 | typeof pkmSyncType !== 'string' || 454 | typeof pageTitle !== 'string' || 455 | typeof fileContent !== 'string' 456 | ) { 457 | res.status(400).json({ error: 'Invalid input' }) 458 | return 459 | } 460 | 461 | var directoryPath = store.get(pkmSyncType) 462 | 463 | if (!directoryPath) { 464 | res.status(400).json({ 465 | error: 'No directory found for given pkmSyncType', 466 | }) 467 | return 468 | } 469 | 470 | var filePath = `${directoryPath}/${pageTitle}.md` 471 | fs.writeFileSync(filePath, fileContent) 472 | res.status(200).send(filePath) 473 | } catch (error) { 474 | log.error('Error in /update-file:', error) 475 | res.status(500).json({ error: 'Internal server error' }) 476 | } 477 | }) 478 | 479 | expressApp.post('/get-file-content', async function (req, res) { 480 | if (!checkSyncKey(req.body.syncKey)) { 481 | return res.status(403).send('Only one app instance allowed') 482 | } 483 | try { 484 | var pkmSyncType = req.body.pkmSyncType 485 | var pageTitle = req.body.pageTitle 486 | 487 | if (typeof pkmSyncType !== 'string' || typeof pageTitle !== 'string') { 488 | res.status(400).json({ error: 'Invalid input' }) 489 | return 490 | } 491 | 492 | var directoryPath = store.get(pkmSyncType) 493 | if (!directoryPath) { 494 | res.status(400).json({ 495 | error: 'No directory found for given pkmSyncType', 496 | }) 497 | return 498 | } 499 | 500 | var filePath = directoryPath + '/' + pageTitle + '.md' 501 | if (!fs.existsSync(filePath)) { 502 | res.status(400).json({ error: 'File not found' }) 503 | return 504 | } 505 | 506 | var fileContent = fs.readFileSync(filePath, 'utf-8') 507 | res.status(200).send(fileContent) 508 | } catch (error) { 509 | log.error('Error in /get-file-content:', error) 510 | res.status(500).json({ error: 'Internal server error' }) 511 | } 512 | }) 513 | 514 | /////////////////////////// 515 | /// BACKUP ENDPOINTS /// 516 | ///////////////////////// 517 | 518 | // Exposing Server Endpoints for BACKUPS 519 | 520 | let backupPath = '' 521 | 522 | expressApp.post('/status', (req, res) => { 523 | console.log(' /status called') 524 | if (!checkSyncKey(req.body.syncKey)) { 525 | return res.status(403).send('Only one app instance allowed') 526 | } 527 | 528 | res.status(200).send('running') 529 | }) 530 | expressApp.get('/status', (req, res) => { 531 | console.log(' /status called') 532 | if (!checkSyncKey(req.body.syncKey)) { 533 | return res.status(403).send('Only one app instance allowed') 534 | } 535 | res.status(200).send('running') 536 | }) 537 | 538 | expressApp.post('/pick-directory', (req, res) => { 539 | if (!checkSyncKey(req.body.syncKey)) { 540 | return res.status(403).send('Only one app instance allowed') 541 | } 542 | try { 543 | var directoryPath = pickDirectory('backup') 544 | if (directoryPath) { 545 | res.json({ path: directoryPath }) 546 | res.status(200).send(directoryPath) 547 | } else { 548 | res.status(400).json({ error: 'No directory selected' }) 549 | } 550 | } catch (error) { 551 | log.error('Error in /pick-directory:', error) 552 | res.status(500).json({ error: 'Internal server error' }) 553 | } 554 | }) 555 | 556 | // get the backup folder location 557 | expressApp.get('/backup/location', async (req, res) => { 558 | if (!checkSyncKey(req.body.syncKey)) { 559 | res.status(403) 560 | } else { 561 | let backupPath = store.get('backupPath') 562 | if (!backupPath) { 563 | backupPath = await pickDirectory('backup') 564 | } 565 | store.set('backup', backupPath) 566 | res.status(200).send(backupPath) 567 | } 568 | }) 569 | 570 | expressApp.get('/backup/start-change-location', async (req, res) => { 571 | if (!checkSyncKey(req.body.syncKey)) { 572 | return res.status(403).send('Only one app instance allowed') 573 | } 574 | res.status(200).send(await pickDirectory('backup')) 575 | }) 576 | 577 | // listing files 578 | expressApp.get('/backup/:collection', (req, res) => { 579 | if (!checkSyncKey(req.body.syncKey)) { 580 | return res.status(403).send('Only one app instance allowed') 581 | } 582 | var collection = req.params.collection 583 | if (!isPathComponentValid(collection)) { 584 | return res.status(400).send('Malformed collection parameter') 585 | } 586 | 587 | var dirpath = backupPath + `/backup/${collection}` 588 | try { 589 | let filelist = fs.readdirSync(dirpath, 'utf-8') 590 | filelist = filelist.filter((filename) => { 591 | // check if filename contains digits only to ignore system files like .DS_STORE 592 | return /^\d+$/.test(filename) 593 | }) 594 | res.status(200).send(filelist.toString()) 595 | } catch (err) { 596 | if ((err as CustomError).code === 'ENOENT') { 597 | res.status(404) 598 | res.status(404).json({ error: 'Collection not found.' }) 599 | } else throw err 600 | } 601 | }) 602 | 603 | // getting files 604 | expressApp.get('/backup/:collection/:timestamp', (req, res) => { 605 | if (!checkSyncKey(req.body.syncKey)) { 606 | return res.status(403).send('Only one app instance allowed') 607 | } 608 | var filename = req.params.timestamp 609 | if (!isPathComponentValid(filename)) { 610 | return res.status(400).send('Malformed timestamp parameter') 611 | } 612 | 613 | var collection = req.params.collection 614 | if (!isPathComponentValid(collection)) { 615 | return res.status(400).send('Malformed collection parameter') 616 | } 617 | 618 | var filepath = backupPath + `/backup/${collection}/` + filename + '.json' 619 | try { 620 | res.status(200).send(fs.readFileSync(filepath, 'utf-8')) 621 | } catch (err) { 622 | if ((err as CustomError).code === 'ENOENT') { 623 | res.status(404) 624 | req.body = 'File not found.' 625 | } else throw err 626 | } 627 | }) 628 | 629 | expressApp.put('/backup/:collection/:timestamp', async (req, res) => { 630 | if (!checkSyncKey(req.body.syncKey)) { 631 | return res.status(403).send('Only one app instance allowed') 632 | } 633 | 634 | var filename = req.params.timestamp 635 | if (!isPathComponentValid(filename)) { 636 | return res.status(400).send('Malformed timestamp parameter') 637 | } 638 | 639 | var collection = req.params.collection 640 | if (!isPathComponentValid(collection)) { 641 | return res.status(400).send('Malformed collection parameter') 642 | } 643 | 644 | var dirpath = req.body.backupPath + `/backup/${collection}` 645 | try { 646 | fs.mkdirSync(dirpath, { recursive: true }) 647 | } catch (err) { 648 | log.error(err) 649 | return res.status(500).send('Failed to create directory.') 650 | } 651 | 652 | var filepath = dirpath + `/${filename}` 653 | console.log('filepath', filepath) 654 | 655 | fs.access(dirpath, fs.constants.W_OK, (err) => { 656 | if (err) { 657 | console.log('not writeable', err) 658 | // Adjust permissions in a cross-platform manner 659 | fs.chmod(dirpath, 0o766, (chmodErr) => { 660 | // Sets read, write, and execute permissions for the owner, and read/write for group and others 661 | if (chmodErr) { 662 | console.error(`Failed to adjust permissions:`, chmodErr) 663 | } else { 664 | console.log(`${dirpath} permissions adjusted.`) 665 | } 666 | }) 667 | } else { 668 | console.log(`${filepath} is writable.`) 669 | } 670 | }) 671 | fs.writeFile(filepath, JSON.stringify(req.body), function (err) { 672 | if (err) { 673 | log.error(err) 674 | console.log('err', err) 675 | return res.status(500).send('Failed to write to file.') 676 | } 677 | console.log('File written successfully') 678 | res.status(200).send('Data saved successfully.') 679 | }) 680 | }) 681 | -------------------------------------------------------------------------------- /src/loading.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Preparing Indexing 6 | 7 | 8 | 9 | 16 | 21 | 26 | 31 | 36 | 43 | 49 | 50 | 58 | 59 | 60 | 61 | 69 | 70 | 71 | 72 | 80 | 81 | 82 | 83 | 91 | 92 | 93 | 94 | 102 | 103 | 104 | 105 | 113 | 114 | 115 | 116 | 117 | 118 | 119 |

120 | Preparing the app 122 |

123 |

124 | We have to download a few things to enable the indexing of your 125 | content. Allow notifications to get notified even if you close or 126 | not focus on this screen. 127 |

128 | 137 | 138 | 139 | -------------------------------------------------------------------------------- /src/preload.cjs: -------------------------------------------------------------------------------- 1 | // src/preload.ts 2 | // See the Electron documentation for details on how to use preload scripts: 3 | // https://www.electronjs.org/docs/latest/tutorial/process-model#preload-scripts 4 | 5 | const { contextBridge, ipcRenderer } = require('electron') 6 | 7 | contextBridge.exposeInMainWorld('electron', { 8 | ipcRenderer: { 9 | send: function (channel, data) { 10 | ipcRenderer.send(channel, data) 11 | }, 12 | on: function (channel, func) { 13 | ipcRenderer.on(channel, func) 14 | }, 15 | getDbPath: async function () { 16 | return await ipcRenderer.invoke('get-db-path') 17 | }, 18 | }, 19 | }) 20 | -------------------------------------------------------------------------------- /src/renderer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file will automatically be loaded by webpack and run in the "renderer" context. 3 | * To learn more about the differences between the "main" and the "renderer" context in 4 | * Electron, visit: 5 | * 6 | * https://electronjs.org/docs/latest/tutorial/process-model 7 | * 8 | * By default, Node.js integration in this file is disabled. When enabling Node.js integration 9 | * in a renderer process, please be aware of potential security implications. You can read 10 | * more about security risks here: 11 | * 12 | * https://electronjs.org/docs/tutorial/security 13 | * 14 | * To enable Node.js integration in this file, open up `main.js` and enable the `nodeIntegration` 15 | * flag: 16 | * 17 | * ``` 18 | * // Create the browser window. 19 | * mainWindow = new BrowserWindow({ 20 | * width: 800, 21 | * height: 600, 22 | * webPreferences: { 23 | * nodeIntegration: true 24 | * } 25 | * }); 26 | * ``` 27 | */ 28 | 29 | import './index.css' 30 | 31 | console.log( 32 | '👋 This message is being logged by "renderer.js", included via webpack', 33 | ) 34 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "module": "ES2020", 5 | "skipLibCheck": true, 6 | "esModuleInterop": true, 7 | "noImplicitAny": true, 8 | "sourceMap": true, 9 | "baseUrl": ".", 10 | "outDir": "build", 11 | "strict": true, 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "paths": { 15 | "*": ["node_modules/*"] 16 | } 17 | }, 18 | "include": ["src/**/*"], 19 | "exclude": ["src/preload.cjs"] 20 | } 21 | --------------------------------------------------------------------------------