├── .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 |
--------------------------------------------------------------------------------