mirror of
https://github.com/amitwh/markdown-converter.git
synced 2026-08-03 02:11:07 +05:30
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
69afd5fc54 | ||
|
|
9e315af1b9 | ||
|
|
78914a4d65 | ||
|
|
3c4cc985fb | ||
|
|
09d9f6bdd1 | ||
|
|
f36d918871 | ||
|
|
ab16998922 | ||
|
|
58ca3014d8 | ||
|
|
7c1a79c724 |
@@ -94,8 +94,42 @@ jobs:
|
||||
dist/*.zip
|
||||
retention-days: 5
|
||||
|
||||
build-macos:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: npm
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Download external tools (pandoc)
|
||||
run: node scripts/download-tools.js
|
||||
|
||||
- name: Run tests
|
||||
run: npm test
|
||||
|
||||
- name: Build macOS packages (unsigned)
|
||||
env:
|
||||
CSC_IDENTITY_AUTO_DISCOVERY: 'false'
|
||||
run: npm run build:mac -- --publish=never
|
||||
|
||||
- name: Upload macOS artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: macos-artifacts
|
||||
path: |
|
||||
dist/*.dmg
|
||||
dist/*.zip
|
||||
retention-days: 5
|
||||
|
||||
release:
|
||||
needs: [build-linux, build-windows]
|
||||
needs: [build-linux, build-windows, build-macos]
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -115,6 +149,13 @@ jobs:
|
||||
name: windows-artifacts
|
||||
path: dist
|
||||
|
||||
- name: Download macOS artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
continue-on-error: true
|
||||
with:
|
||||
name: macos-artifacts
|
||||
path: dist
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,270 @@
|
||||
# Packaging — All-Platform Build + GitHub Releases
|
||||
|
||||
**Goal:** Make `npm run dist:all` produce Linux + Windows + macOS artifacts, and have the existing `.github/workflows/release.yml` create a GitHub Release on tag push with those artifacts. No code signing in this iteration.
|
||||
|
||||
**Scope (user decisions 2026-06-06):**
|
||||
- Target platforms: **all three** (Linux .deb/AppImage/snap, Windows NSIS/portable/zip, macOS dmg/zip)
|
||||
- Distribution: **GitHub Releases** (no auto-update; static artifacts only)
|
||||
- Code signing: **skip for now** (unsigned binaries; Windows SmartScreen will warn, macOS Gatekeeper will quarantine)
|
||||
|
||||
**Architecture:** This is mostly about completing the existing scaffold. `electron-builder` 26.0.12, `scripts/download-tools.js`, `scripts/generate-icons.js`, the `package.json` scripts, and `release.yml` are all already in place. We need to (1) close a real bug in the `files` config, (2) add the macOS job to the workflow, and (3) declare macOS targets in the package config.
|
||||
|
||||
**Tech stack:** electron-builder 26.0.12, GitHub Actions `ubuntu-latest`/`windows-latest`/`macos-latest` runners, softprops/action-gh-release@v2 for the release creation.
|
||||
|
||||
---
|
||||
|
||||
## Task 1 — Fix the `files` config in `package.json`
|
||||
|
||||
The current `files: ["src/**/*", ...]` matches `src/renderer/**/*.tsx` and ships dev source into the asar. The shipped renderer should be the built `dist/renderer/**/*` output, not TS source. asarUnpack for ffmpeg-static is correct and stays.
|
||||
|
||||
**Files:** `package.json` (build.files, build.extraResources)
|
||||
|
||||
### Step 1.1 — Replace `files` block
|
||||
|
||||
```json
|
||||
"files": [
|
||||
"src/main/**/*",
|
||||
"src/preload.js",
|
||||
"src/plugins/**/*",
|
||||
"package.json"
|
||||
],
|
||||
"extraResources": [
|
||||
{
|
||||
"from": "dist/renderer",
|
||||
"to": "renderer"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
- `src/main/**/*` — main process source (entry point + all main-process modules)
|
||||
- `src/preload.js` — single preload file at the root of `src/`
|
||||
- `src/plugins/**/*` — built-in plugins (referenced from main process; the `extraFiles: []` line in the current config is empty, so this is the right pattern)
|
||||
- `package.json` — required for the asar `app.asar/package.json` lookup electron does at runtime
|
||||
- `dist/renderer` moves to `extraResources` because it should live alongside the asar (not inside it) — the asar is sealed and we want the renderer to be a separately-updatable resource path. Production main loads it via `loadFile(path.join(__dirname, '../../dist/renderer/index.html'))` which, in the packaged app, resolves to `process.resourcesPath/renderer/index.html`. **This requires changing the loadFile path in `src/main/window/index.js` too** — see Task 4.
|
||||
|
||||
### Step 1.2 — Verify
|
||||
|
||||
- `npm run build:linux` should produce a working .AppImage
|
||||
- Inside the .AppImage (extract with `--appimage-extract`), check `resources/` has `app.asar` and a `renderer/` directory containing `index.html` and `assets/`
|
||||
- Inside `app.asar`, check `src/main/index.js` is present and `src/renderer/` is absent
|
||||
|
||||
---
|
||||
|
||||
## Task 2 — Add macOS targets + icon to `package.json` build config
|
||||
|
||||
**Files:** `package.json`
|
||||
|
||||
### Step 2.1 — Replace the existing `mac` block
|
||||
|
||||
```json
|
||||
"mac": {
|
||||
"category": "public.app-category.productivity",
|
||||
"identity": null,
|
||||
"target": [
|
||||
{ "target": "dmg", "arch": ["x64", "arm64"] },
|
||||
{ "target": "zip", "arch": ["x64", "arm64"] }
|
||||
],
|
||||
"icon": "assets/icon.icns",
|
||||
"darkModeSupport": true,
|
||||
"hardenedRuntime": false,
|
||||
"gatekeeperAssess": false,
|
||||
"entitlements": null
|
||||
}
|
||||
```
|
||||
|
||||
- `identity: null` is intentional for unsigned builds
|
||||
- `darkModeSupport: true` — the app already supports dark mode; this is just metadata
|
||||
- `hardenedRuntime: false` and `gatekeeperAssess: false` — without signing, these are the only way the build succeeds
|
||||
- `entitlements: null` — no entitlements file; some features (jit) won't work but Electron's main process doesn't need them in this app
|
||||
- **`.icns` is missing.** For this iteration we'll use the default Electron icon and add a follow-up to generate one. The build will succeed without it but the produced .app will have the default icon.
|
||||
|
||||
### Step 2.2 — Generate a placeholder `.icns` (optional, can be deferred)
|
||||
|
||||
If we want a proper icon, the path is:
|
||||
1. `npm run generate-icons` to produce `assets/icons/*.png` (already done)
|
||||
2. Use `png2icns` (Linux) or `iconutil` (mac) to bundle them into `assets/icon.icns`
|
||||
3. We don't have either on this Linux container; defer to a follow-up plan
|
||||
|
||||
For this iteration: ship without `.icns` and accept the default icon.
|
||||
|
||||
### Step 2.3 — Add `publish` config
|
||||
|
||||
```json
|
||||
"publish": {
|
||||
"provider": "github",
|
||||
"owner": "amitwh",
|
||||
"repo": "markdown-converter",
|
||||
"releaseType": "release"
|
||||
}
|
||||
```
|
||||
|
||||
Even without auto-update, this tells `electron-builder` to write `latest-mac.yml` / `latest-linux.yml` next to the artifacts. Useful for future auto-update wiring. CI passes `--publish=never` to override.
|
||||
|
||||
---
|
||||
|
||||
## Task 3 — Add macOS job to `.github/workflows/release.yml`
|
||||
|
||||
**Files:** `.github/workflows/release.yml`
|
||||
|
||||
### Step 3.1 — Add a third job between `build-windows` and `release`
|
||||
|
||||
Insert after the `build-windows` job (around line 96):
|
||||
|
||||
```yaml
|
||||
build-macos:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: npm
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Download external tools (pandoc)
|
||||
run: node scripts/download-tools.js
|
||||
|
||||
- name: Run tests
|
||||
run: npm test
|
||||
|
||||
- name: Build macOS packages (unsigned)
|
||||
env:
|
||||
CSC_IDENTITY_AUTO_DISCOVERY: 'false'
|
||||
run: npm run build:mac -- --publish=never
|
||||
|
||||
- name: Upload macOS artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: macos-artifacts
|
||||
path: |
|
||||
dist/*.dmg
|
||||
dist/*.zip
|
||||
retention-days: 5
|
||||
```
|
||||
|
||||
### Step 3.2 — Wire macOS into the `release` job's `needs` and downloads
|
||||
|
||||
Change the `release` job:
|
||||
|
||||
```yaml
|
||||
release:
|
||||
needs: [build-linux, build-windows, build-macos]
|
||||
...
|
||||
```
|
||||
|
||||
And add a third download step (after the Windows download):
|
||||
|
||||
```yaml
|
||||
- name: Download macOS artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
continue-on-error: true
|
||||
with:
|
||||
name: macos-artifacts
|
||||
path: dist
|
||||
```
|
||||
|
||||
`continue-on-error: true` is intentional — a failed macOS build shouldn't block a release of working Linux+Windows artifacts.
|
||||
|
||||
---
|
||||
|
||||
## Task 4 — Update `src/main/window/index.js` for the resource path
|
||||
|
||||
**Files:** `src/main/window/index.js`
|
||||
|
||||
### Step 4.1 — Update the loadFile path
|
||||
|
||||
The current code (line 35):
|
||||
```js
|
||||
const prodPath = path.join(__dirname, '../../dist/renderer/index.html');
|
||||
```
|
||||
|
||||
Inside the packaged app, `__dirname` points into the asar. `process.resourcesPath` points to the directory holding `app.asar` + the `renderer/` extraResources dir. Change to:
|
||||
|
||||
```js
|
||||
const rendererIndex = app.isPackaged
|
||||
? path.join(process.resourcesPath, 'renderer', 'index.html')
|
||||
: path.join(__dirname, '../../dist/renderer/index.html');
|
||||
win.loadFile(rendererIndex);
|
||||
```
|
||||
|
||||
This needs to happen after the dev/prod branch is decided. The existing `if (!app.isPackaged && devServerUrl)` block already handles dev mode; the else branch needs this update.
|
||||
|
||||
### Step 4.2 — Add a guard for missing renderer in prod
|
||||
|
||||
```js
|
||||
if (app.isPackaged) {
|
||||
try {
|
||||
fs.accessSync(path.join(process.resourcesPath, 'renderer', 'index.html'));
|
||||
} catch {
|
||||
console.error('[WINDOW] Renderer not found at', process.resourcesPath, '— did you run `npm run build:renderer` before packaging?');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Failure mode: someone runs `npm run build` without first building the renderer. Without this guard, the window opens blank and the user has no idea why.
|
||||
|
||||
---
|
||||
|
||||
## Task 5 — Test on this Linux box
|
||||
|
||||
### Step 5.1 — Local verification
|
||||
|
||||
```bash
|
||||
npm ci
|
||||
npm run download-tools # idempotent; bin/linux/pandoc already exists
|
||||
npm run build:renderer
|
||||
npm run build:linux # produces .deb, .AppImage in dist/
|
||||
```
|
||||
|
||||
Inspect the .AppImage:
|
||||
```bash
|
||||
chmod +x dist/*.AppImage
|
||||
./dist/MarkdownConverter-*.AppImage --appimage-extract
|
||||
ls squashfs-root/resources/
|
||||
# Expect: app.asar renderer/
|
||||
ls squashfs-root/resources/renderer/
|
||||
# Expect: index.html assets/
|
||||
```
|
||||
|
||||
### Step 5.2 — Run the produced app
|
||||
|
||||
```bash
|
||||
./dist/MarkdownConverter-*.AppImage
|
||||
```
|
||||
|
||||
The app should:
|
||||
- Launch with the AppShell, header, sidebar
|
||||
- Allow opening a folder / file
|
||||
- Export to PDF/DOCX/HTML work (pandoc bundled)
|
||||
|
||||
### Step 5.3 — Tag + push to test the release workflow (optional)
|
||||
|
||||
Only do this if the user wants to verify the full pipeline. A dry-run on the CI side via `act` would be cleaner but `act` doesn't run macos-latest jobs locally. If the user wants to verify the workflow, push a `v5.0.1-rc1` tag and watch the Actions tab.
|
||||
|
||||
---
|
||||
|
||||
## Success criteria
|
||||
|
||||
1. `npm run dist:all` produces all three platform families on this Linux box (or at least Linux + mac, since Windows is built on Windows)
|
||||
2. The .AppImage launches and the app works end-to-end
|
||||
3. `release.yml` runs all three jobs on a tag push and creates a GitHub release with the union of artifacts
|
||||
4. No regressions: 306/306 tests still pass, dev workflow still works
|
||||
5. The `files` bug is fixed: no `.tsx` in the packaged asar
|
||||
|
||||
## Known limitations (intentional, can be follow-ups)
|
||||
|
||||
- **No `.icns`** — the macOS app will have the default Electron icon. Generating a proper `.icns` requires `iconutil` (mac) or `png2icns` (cross-platform); neither is on this Linux box.
|
||||
- **No code signing** — Windows SmartScreen will warn; macOS Gatekeeper will quarantine unsigned `.dmg`. Setting up signing later is a config-only change (no structural rework): add `CSC_LINK`/`CSC_KEY_PASSWORD` for Windows, `CSC_LINK` (Apple Developer ID) for mac.
|
||||
- **No auto-update** — user decisions for this iteration. Wiring `electron-updater` is a follow-up; the `publish` config from Task 2 puts metadata in the right place.
|
||||
- **Cross-build quirks** — building macOS from Linux produces a `.dmg` that is generally usable but may show a "this app is from an unidentified developer" prompt the first time. Right-click → Open to bypass once. Documented in the release notes.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- CHANGELOG.md updates (not part of packaging)
|
||||
- electron-updater wiring
|
||||
- Code signing cert procurement
|
||||
- Snap store / Microsoft Store / Mac App Store submission
|
||||
Generated
+394
-3
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "markdown-converter",
|
||||
"version": "4.4.2",
|
||||
"version": "5.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "markdown-converter",
|
||||
"version": "4.4.2",
|
||||
"version": "5.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.20.1",
|
||||
@@ -86,6 +86,7 @@
|
||||
"autoprefixer": "^10.5.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"concurrently": "^10.0.3",
|
||||
"cross-env": "^10.0.0",
|
||||
"electron": "^41.1.1",
|
||||
"electron-builder": "^26.0.12",
|
||||
@@ -104,7 +105,8 @@
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"typescript": "^6.0.3",
|
||||
"vite": "^8.0.16",
|
||||
"vitest": "^4.1.8"
|
||||
"vitest": "^4.1.8",
|
||||
"wait-on": "^9.0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@adobe/css-tools": {
|
||||
@@ -1902,6 +1904,60 @@
|
||||
"integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@hapi/address": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@hapi/address/-/address-5.1.1.tgz",
|
||||
"integrity": "sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@hapi/hoek": "^11.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@hapi/formula": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@hapi/formula/-/formula-3.0.2.tgz",
|
||||
"integrity": "sha512-hY5YPNXzw1He7s0iqkRQi+uMGh383CGdyyIGYtB+W5N3KHPXoqychklvHhKCC9M3Xtv0OCs/IHw+r4dcHtBYWw==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@hapi/hoek": {
|
||||
"version": "11.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-11.0.7.tgz",
|
||||
"integrity": "sha512-HV5undWkKzcB4RZUusqOpcgxOaq6VOAH7zhhIr2g3G8NF/MlFO75SjOr2NfuSx0Mh40+1FqCkagKLJRykUWoFQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@hapi/pinpoint": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@hapi/pinpoint/-/pinpoint-2.0.1.tgz",
|
||||
"integrity": "sha512-EKQmr16tM8s16vTT3cA5L0kZZcTMU5DUOZTuvpnY738m+jyP3JIUj+Mm1xc1rsLkGBQ/gVnfKYPwOmPg1tUR4Q==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@hapi/tlds": {
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@hapi/tlds/-/tlds-1.1.6.tgz",
|
||||
"integrity": "sha512-xdi7A/4NZokvV0ewovme3aUO5kQhW9pQ2YD1hRqZGhhSi5rBv4usHYidVocXSi9eihYsznZxLtAiEYYUL6VBGw==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@hapi/topo": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-6.0.2.tgz",
|
||||
"integrity": "sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@hapi/hoek": "^11.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@hookform/resolvers": {
|
||||
"version": "5.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.4.0.tgz",
|
||||
@@ -6982,6 +7038,46 @@
|
||||
"postcss": "^8.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.17.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.17.0.tgz",
|
||||
"integrity": "sha512-J8SwNxprqqpbfenehxWYXE7CW+wM1BB4w3+N+g+/Wx40xM4rsLrfPmHHxSWIxJLYDgSY/HqlFPIYb2/S3rxafw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.16.0",
|
||||
"form-data": "^4.0.5",
|
||||
"https-proxy-agent": "^5.0.1",
|
||||
"proxy-from-env": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/axios/node_modules/agent-base": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
||||
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/axios/node_modules/https-proxy-agent": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
|
||||
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"agent-base": "6",
|
||||
"debug": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-jest": {
|
||||
"version": "30.3.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.3.0.tgz",
|
||||
@@ -7889,6 +7985,185 @@
|
||||
"typedarray": "^0.0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/concurrently": {
|
||||
"version": "10.0.3",
|
||||
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-10.0.3.tgz",
|
||||
"integrity": "sha512-hc3LH4UaKWd/bbyDK/IGVa4RB6PtQ3CUYwtrkzqHn+wIG3Hr5fhpRlk0L/gCa8ZE1L/Ufj50Zho69cI5w8SQBA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chalk": "5.6.2",
|
||||
"rxjs": "7.8.2",
|
||||
"shell-quote": "1.8.4",
|
||||
"supports-color": "10.2.2",
|
||||
"tree-kill": "1.2.2",
|
||||
"yargs": "18.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"conc": "dist/bin/index.js",
|
||||
"concurrently": "dist/bin/index.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/concurrently/node_modules/ansi-regex": {
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
|
||||
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/concurrently/node_modules/ansi-styles": {
|
||||
"version": "6.2.3",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
|
||||
"integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/concurrently/node_modules/chalk": {
|
||||
"version": "5.6.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
|
||||
"integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.17.0 || ^14.13 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/concurrently/node_modules/cliui": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz",
|
||||
"integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^7.2.0",
|
||||
"strip-ansi": "^7.1.0",
|
||||
"wrap-ansi": "^9.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/concurrently/node_modules/emoji-regex": {
|
||||
"version": "10.6.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
|
||||
"integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/concurrently/node_modules/string-width": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
|
||||
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^10.3.0",
|
||||
"get-east-asian-width": "^1.0.0",
|
||||
"strip-ansi": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/concurrently/node_modules/strip-ansi": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
|
||||
"integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^6.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/concurrently/node_modules/supports-color": {
|
||||
"version": "10.2.2",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz",
|
||||
"integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/concurrently/node_modules/wrap-ansi": {
|
||||
"version": "9.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz",
|
||||
"integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^6.2.1",
|
||||
"string-width": "^7.0.0",
|
||||
"strip-ansi": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/concurrently/node_modules/yargs": {
|
||||
"version": "18.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz",
|
||||
"integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cliui": "^9.0.1",
|
||||
"escalade": "^3.1.1",
|
||||
"get-caller-file": "^2.0.5",
|
||||
"string-width": "^7.2.0",
|
||||
"y18n": "^5.0.5",
|
||||
"yargs-parser": "^22.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.12.0 || >=23"
|
||||
}
|
||||
},
|
||||
"node_modules/concurrently/node_modules/yargs-parser": {
|
||||
"version": "22.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz",
|
||||
"integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.12.0 || >=23"
|
||||
}
|
||||
},
|
||||
"node_modules/conf": {
|
||||
"version": "14.0.0",
|
||||
"resolved": "https://registry.npmjs.org/conf/-/conf-14.0.0.tgz",
|
||||
@@ -10378,6 +10653,27 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.16.0",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
|
||||
"integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/fontkit": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/fontkit/-/fontkit-2.0.4.tgz",
|
||||
@@ -10563,6 +10859,19 @@
|
||||
"node": "6.* || 8.* || >= 10.*"
|
||||
}
|
||||
},
|
||||
"node_modules/get-east-asian-width": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz",
|
||||
"integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
@@ -12696,6 +13005,25 @@
|
||||
"jiti": "lib/jiti-cli.mjs"
|
||||
}
|
||||
},
|
||||
"node_modules/joi": {
|
||||
"version": "18.2.1",
|
||||
"resolved": "https://registry.npmjs.org/joi/-/joi-18.2.1.tgz",
|
||||
"integrity": "sha512-2/OKlogiESf2Nh3TFCrRjrr9z1DRHeW0I+KReF67+4J0Ns+8hBtHRmoWAZ2OFU6I5+TWLEe6sVlSdXPjHm5UbQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@hapi/address": "^5.1.1",
|
||||
"@hapi/formula": "^3.0.2",
|
||||
"@hapi/hoek": "^11.0.7",
|
||||
"@hapi/pinpoint": "^2.0.1",
|
||||
"@hapi/tlds": "^1.1.1",
|
||||
"@hapi/topo": "^6.0.2",
|
||||
"@standard-schema/spec": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
}
|
||||
},
|
||||
"node_modules/jpeg-exif": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/jpeg-exif/-/jpeg-exif-1.1.4.tgz",
|
||||
@@ -15183,6 +15511,16 @@
|
||||
"signal-exit": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
|
||||
"integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/pump": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",
|
||||
@@ -15758,6 +16096,16 @@
|
||||
"integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/rxjs": {
|
||||
"version": "7.8.2",
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
|
||||
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
@@ -15958,6 +16306,19 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/shell-quote": {
|
||||
"version": "1.8.4",
|
||||
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.4.tgz",
|
||||
"integrity": "sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/siginfo": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
|
||||
@@ -16878,6 +17239,16 @@
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/tree-kill": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
|
||||
"integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"tree-kill": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/truncate-utf8-bytes": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz",
|
||||
@@ -17441,6 +17812,26 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/wait-on": {
|
||||
"version": "9.0.10",
|
||||
"resolved": "https://registry.npmjs.org/wait-on/-/wait-on-9.0.10.tgz",
|
||||
"integrity": "sha512-rCoJEhvMr0X6alHmwc9abbrA5ZrLZFKpFQVKPNFwl2h7DapXOGdmimIHDtLOWhT4PjhZhxFEtZoQgEXbkDWdZw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"axios": "^1.16.0",
|
||||
"joi": "^18.2.1",
|
||||
"lodash": "^4.18.1",
|
||||
"minimist": "^1.2.8",
|
||||
"rxjs": "^7.8.2"
|
||||
},
|
||||
"bin": {
|
||||
"wait-on": "bin/wait-on"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/walker": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz",
|
||||
|
||||
+36
-8
@@ -1,10 +1,15 @@
|
||||
{
|
||||
"name": "markdown-converter",
|
||||
"version": "5.0.0",
|
||||
"version": "5.0.1",
|
||||
"description": "Professional Markdown editor and universal file converter with PDF editing, batch processing, and syntax highlighting",
|
||||
"main": "src/main/index.js",
|
||||
"scripts": {
|
||||
"start": "electron .",
|
||||
"dev:renderer": "vite --config vite.renderer.config.ts",
|
||||
"dev:electron": "wait-on tcp:5173 && cross-env VITE_DEV_SERVER_URL=http://localhost:5173 electron . -- --disable-gpu --disable-software-rasterizer --disable-dev-shm-usage",
|
||||
"dev": "concurrently -k -n vite,electron -c blue,green \"npm:dev:renderer\" \"npm:dev:electron\"",
|
||||
"build:renderer": "vite build --config vite.renderer.config.ts",
|
||||
"preview": "npm run build:renderer && electron .",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:coverage": "jest --coverage",
|
||||
@@ -27,7 +32,8 @@
|
||||
"dist": "electron-builder --publish=never",
|
||||
"dist:all": "electron-builder -mwl",
|
||||
"download-tools": "node scripts/download-tools.js",
|
||||
"generate-icons": "node scripts/generate-icons.js"
|
||||
"generate-icons": "node scripts/generate-icons.js",
|
||||
"build:icon-icns": "node scripts/build-icon-icns.js"
|
||||
},
|
||||
"keywords": [
|
||||
"markdown",
|
||||
@@ -60,6 +66,7 @@
|
||||
"autoprefixer": "^10.5.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"concurrently": "^10.0.3",
|
||||
"cross-env": "^10.0.0",
|
||||
"electron": "^41.1.1",
|
||||
"electron-builder": "^26.0.12",
|
||||
@@ -78,7 +85,8 @@
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"typescript": "^6.0.3",
|
||||
"vite": "^8.0.16",
|
||||
"vitest": "^4.1.8"
|
||||
"vitest": "^4.1.8",
|
||||
"wait-on": "^9.0.10"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.20.1",
|
||||
@@ -159,15 +167,20 @@
|
||||
},
|
||||
"icon": "assets/icon",
|
||||
"files": [
|
||||
"src/**/*",
|
||||
"assets/**/*",
|
||||
"scripts/**/*",
|
||||
"node_modules/**/*",
|
||||
"src/main/**/*",
|
||||
"src/preload.js",
|
||||
"src/plugins/**/*",
|
||||
"package.json"
|
||||
],
|
||||
"asarUnpack": [
|
||||
"node_modules/ffmpeg-static/**"
|
||||
],
|
||||
"extraResources": [
|
||||
{
|
||||
"from": "dist/renderer",
|
||||
"to": "renderer"
|
||||
}
|
||||
],
|
||||
"extraFiles": [],
|
||||
"fileAssociations": [
|
||||
{
|
||||
@@ -194,7 +207,16 @@
|
||||
],
|
||||
"mac": {
|
||||
"category": "public.app-category.productivity",
|
||||
"identity": null
|
||||
"identity": null,
|
||||
"target": [
|
||||
{ "target": "dmg", "arch": ["x64", "arm64"] },
|
||||
{ "target": "zip", "arch": ["x64", "arm64"] }
|
||||
],
|
||||
"icon": "assets/icon.icns",
|
||||
"darkModeSupport": true,
|
||||
"hardenedRuntime": false,
|
||||
"gatekeeperAssess": false,
|
||||
"entitlements": null
|
||||
},
|
||||
"win": {
|
||||
"target": [
|
||||
@@ -267,6 +289,12 @@
|
||||
],
|
||||
"description": "Professional Markdown editor and universal file converter",
|
||||
"maintainer": "ConcreteInfo <amit.wh@gmail.com>"
|
||||
},
|
||||
"publish": {
|
||||
"provider": "github",
|
||||
"owner": "amitwh",
|
||||
"repo": "markdown-converter",
|
||||
"releaseType": "release"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* Build assets/icon.icns from the PNGs in assets/icons/.
|
||||
*
|
||||
* ICNS file format (Apple Icon Image):
|
||||
* Header: 'icns' (4 bytes) + total file length (uint32 BE, includes header)
|
||||
* For each size:
|
||||
* Type tag (4 bytes) + length (uint32 BE, includes the 8-byte entry header) + PNG data
|
||||
*
|
||||
* Standard PNG type tags we'll include:
|
||||
* icp4 = 16x16, icp5 = 32x32, icp6 = 64x64
|
||||
* ic07 = 128x128, ic08 = 256x256, ic09 = 512x512, ic10 = 1024x1024
|
||||
* icsb = 48x48 (small)
|
||||
* ic11 = 16x16@2x (32x32 retina), ic12 = 32x32@2x (64x64 retina)
|
||||
* ic13 = 128x128@2x (256x256 retina), ic14 = 256x256@2x (512x512 retina)
|
||||
*/
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const iconsDir = path.join(__dirname, '..', 'assets', 'icons');
|
||||
const outFile = path.join(__dirname, '..', 'assets', 'icon.icns');
|
||||
|
||||
// (size-in-pixels, icns type tag) — we skip sizes we don't have on disk
|
||||
const sizes = [
|
||||
{ size: 16, type: 'icp4' },
|
||||
{ size: 32, type: 'icp5' },
|
||||
{ size: 64, type: 'icp6' },
|
||||
{ size: 128, type: 'ic07' },
|
||||
{ size: 256, type: 'ic08' },
|
||||
{ size: 512, type: 'ic09' },
|
||||
{ size: 1024, type: 'ic10' },
|
||||
{ size: 48, type: 'icsb' },
|
||||
{ size: 32, type: 'ic11' }, // 16@2x
|
||||
{ size: 64, type: 'ic12' }, // 32@2x
|
||||
{ size: 256, type: 'ic13' }, // 128@2x
|
||||
{ size: 512, type: 'ic14' }, // 256@2x
|
||||
];
|
||||
|
||||
const entries = [];
|
||||
for (const { size, type } of sizes) {
|
||||
const pngPath = path.join(iconsDir, `${size}x${size}.png`);
|
||||
if (!fs.existsSync(pngPath)) continue;
|
||||
const png = fs.readFileSync(pngPath);
|
||||
// ICNS requires 8-byte alignment per entry. paddedLen is the TOTAL entry size
|
||||
// (8-byte type+length header + PNG + padding), rounded up to a multiple of 8.
|
||||
const paddedLen = 8 + Math.ceil(png.length / 8) * 8;
|
||||
entries.push({ type, png, paddedLen });
|
||||
console.log(` ${type} (${size}x${size}) — ${png.length} bytes`);
|
||||
}
|
||||
|
||||
if (entries.length === 0) {
|
||||
console.error('No source PNGs found in', iconsDir);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const totalLen = 8 + entries.reduce((s, e) => s + e.paddedLen, 0);
|
||||
const buf = Buffer.alloc(totalLen);
|
||||
let off = 0;
|
||||
|
||||
// Header
|
||||
buf.write('icns', off, 4, 'ascii'); off += 4;
|
||||
buf.writeUInt32BE(totalLen, off); off += 4;
|
||||
|
||||
// Entries
|
||||
for (const { type, png, paddedLen } of entries) {
|
||||
buf.write(type, off, 4, 'ascii'); off += 4;
|
||||
buf.writeUInt32BE(paddedLen, off); off += 4;
|
||||
png.copy(buf, off);
|
||||
off += png.length;
|
||||
// Pad to 8-byte boundary within the entry. paddedLen already includes the 8-byte header.
|
||||
const pad = paddedLen - 8 - png.length;
|
||||
if (pad > 0) off += pad;
|
||||
}
|
||||
|
||||
fs.writeFileSync(outFile, buf);
|
||||
console.log(`\nWrote ${outFile} (${buf.length} bytes, ${entries.length} sizes)`);
|
||||
@@ -14,6 +14,30 @@ const { execSync } = require('child_process');
|
||||
|
||||
const PANDOC_VERSION = '3.9.0.2';
|
||||
|
||||
/**
|
||||
* Pandoc's archive inner layout has shifted across releases and platforms:
|
||||
* linux tarball: pandoc-3.9.0.2/bin/pandoc
|
||||
* win32 zip: pandoc-3.9.0.2/pandoc.exe
|
||||
* darwin zip: pandoc-3.9.0.2-x86_64/bin/pandoc (arch-suffixed inner dir)
|
||||
* pandoc-3.9.0.2-arm64/bin/pandoc (on Apple Silicon builds)
|
||||
* Rather than hard-coding the intermediate path — which has already broken
|
||||
* once when pandoc 3.9 added the arch suffix to the macOS archive — we walk
|
||||
* the extracted tree and find the binary by name. This is robust to future
|
||||
* layout shifts (e.g., universal binaries renaming the inner dir).
|
||||
*/
|
||||
function findFile(rootDir, targetName) {
|
||||
const entries = fs.readdirSync(rootDir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
const full = path.join(rootDir, entry.name);
|
||||
if (entry.isFile() && entry.name === targetName) return full;
|
||||
if (entry.isDirectory()) {
|
||||
const found = findFile(full, targetName);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const PANDOC_CONFIG = {
|
||||
linux: {
|
||||
url: `https://github.com/jgm/pandoc/releases/download/${PANDOC_VERSION}/pandoc-${PANDOC_VERSION}-linux-amd64.tar.gz`,
|
||||
@@ -22,8 +46,9 @@ const PANDOC_CONFIG = {
|
||||
extract(archivePath, destDir) {
|
||||
const tmpDir = path.join(os.tmpdir(), `pandoc-${Date.now()}`);
|
||||
fs.mkdirSync(tmpDir, { recursive: true });
|
||||
execSync(`tar -xzf "${archivePath}" -C "${tmpDir}" pandoc-${PANDOC_VERSION}/bin/pandoc`);
|
||||
const src = path.join(tmpDir, `pandoc-${PANDOC_VERSION}`, 'bin', 'pandoc');
|
||||
execSync(`tar -xzf "${archivePath}" -C "${tmpDir}"`);
|
||||
const src = findFile(tmpDir, 'pandoc');
|
||||
if (!src) throw new Error(`pandoc binary not found under ${tmpDir}`);
|
||||
fs.copyFileSync(src, path.join(destDir, 'pandoc'));
|
||||
fs.chmodSync(path.join(destDir, 'pandoc'), 0o755);
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
@@ -39,7 +64,8 @@ const PANDOC_CONFIG = {
|
||||
execSync(
|
||||
`powershell -Command "Expand-Archive -Force '${archivePath}' '${tmpDir}'"`,
|
||||
);
|
||||
const src = path.join(tmpDir, `pandoc-${PANDOC_VERSION}`, 'pandoc.exe');
|
||||
const src = findFile(tmpDir, 'pandoc.exe');
|
||||
if (!src) throw new Error(`pandoc.exe not found under ${tmpDir}`);
|
||||
fs.copyFileSync(src, path.join(destDir, 'pandoc.exe'));
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
},
|
||||
@@ -52,7 +78,8 @@ const PANDOC_CONFIG = {
|
||||
const tmpDir = path.join(os.tmpdir(), `pandoc-${Date.now()}`);
|
||||
fs.mkdirSync(tmpDir, { recursive: true });
|
||||
execSync(`unzip -o "${archivePath}" -d "${tmpDir}"`);
|
||||
const src = path.join(tmpDir, `pandoc-${PANDOC_VERSION}`, 'bin', 'pandoc');
|
||||
const src = findFile(tmpDir, 'pandoc');
|
||||
if (!src) throw new Error(`pandoc binary not found under ${tmpDir}`);
|
||||
fs.copyFileSync(src, path.join(destDir, 'pandoc'));
|
||||
fs.chmodSync(path.join(destDir, 'pandoc'), 0o755);
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
|
||||
+65
-65
@@ -16,7 +16,7 @@ function buildRecentFilesMenu(mainWindow) {
|
||||
const items = existing.map(file => ({
|
||||
label: path.basename(file),
|
||||
click: () => {
|
||||
const { openFileFromPath } = require('../main');
|
||||
const { openFileFromPath } = require('../index');
|
||||
openFileFromPath(file);
|
||||
}
|
||||
}));
|
||||
@@ -48,7 +48,7 @@ function fileItems(mainWindow) {
|
||||
label: 'Open',
|
||||
accelerator: 'CmdOrCtrl+O',
|
||||
click: () => {
|
||||
const { openFile } = require('../main');
|
||||
const { openFile } = require('../index');
|
||||
openFile();
|
||||
}
|
||||
},
|
||||
@@ -56,7 +56,7 @@ function fileItems(mainWindow) {
|
||||
label: 'Open PDF',
|
||||
accelerator: 'CmdOrCtrl+Shift+O',
|
||||
click: () => {
|
||||
const { openPdfFile } = require('../main');
|
||||
const { openPdfFile } = require('../index');
|
||||
openPdfFile();
|
||||
}
|
||||
},
|
||||
@@ -69,7 +69,7 @@ function fileItems(mainWindow) {
|
||||
label: 'Save As',
|
||||
accelerator: 'CmdOrCtrl+Shift+S',
|
||||
click: () => {
|
||||
const { saveAsFile } = require('../main');
|
||||
const { saveAsFile } = require('../index');
|
||||
saveAsFile();
|
||||
}
|
||||
},
|
||||
@@ -101,7 +101,7 @@ function fileItems(mainWindow) {
|
||||
label: 'Import Document...',
|
||||
accelerator: 'CmdOrCtrl+I',
|
||||
click: () => {
|
||||
const { importDocument } = require('../main');
|
||||
const { importDocument } = require('../index');
|
||||
importDocument();
|
||||
}
|
||||
},
|
||||
@@ -110,64 +110,64 @@ function fileItems(mainWindow) {
|
||||
submenu: [
|
||||
{
|
||||
label: 'HTML', click: () => {
|
||||
const { exportFile } = require('../main');
|
||||
const { exportFile } = require('../index');
|
||||
exportFile('html');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'PDF', click: () => {
|
||||
const { exportFile } = require('../main');
|
||||
const { exportFile } = require('../index');
|
||||
exportFile('pdf');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'PDF (Enhanced)', click: () => {
|
||||
const { exportPDFViaWordTemplate } = require('../main');
|
||||
const { exportPDFViaWordTemplate } = require('../index');
|
||||
exportPDFViaWordTemplate();
|
||||
}, accelerator: 'Ctrl+Shift+P'
|
||||
},
|
||||
{
|
||||
label: 'DOCX', click: () => {
|
||||
const { exportFile } = require('../main');
|
||||
const { exportFile } = require('../index');
|
||||
exportFile('docx');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'DOCX (Enhanced)', click: () => {
|
||||
const { exportWordWithTemplate } = require('../main');
|
||||
const { exportWordWithTemplate } = require('../index');
|
||||
exportWordWithTemplate();
|
||||
}, accelerator: 'Ctrl+Shift+W'
|
||||
},
|
||||
{ label: 'LaTeX', click: () => { const { exportFile } = require('../main'); exportFile('latex'); } },
|
||||
{ label: 'RTF', click: () => { const { exportFile } = require('../main'); exportFile('rtf'); } },
|
||||
{ label: 'ODT', click: () => { const { exportFile } = require('../main'); exportFile('odt'); } },
|
||||
{ label: 'EPUB', click: () => { const { exportFile } = require('../main'); exportFile('epub'); } },
|
||||
{ label: 'LaTeX', click: () => { const { exportFile } = require('../index'); exportFile('latex'); } },
|
||||
{ label: 'RTF', click: () => { const { exportFile } = require('../index'); exportFile('rtf'); } },
|
||||
{ label: 'ODT', click: () => { const { exportFile } = require('../index'); exportFile('odt'); } },
|
||||
{ label: 'EPUB', click: () => { const { exportFile } = require('../index'); exportFile('epub'); } },
|
||||
{ type: 'separator' },
|
||||
{ label: 'PowerPoint (PPTX)', click: () => { const { exportFile } = require('../main'); exportFile('pptx'); } },
|
||||
{ label: 'OpenDocument Presentation (ODP)', click: () => { const { exportFile } = require('../main'); exportFile('odp'); } },
|
||||
{ label: 'PowerPoint (PPTX)', click: () => { const { exportFile } = require('../index'); exportFile('pptx'); } },
|
||||
{ label: 'OpenDocument Presentation (ODP)', click: () => { const { exportFile } = require('../index'); exportFile('odp'); } },
|
||||
{ type: 'separator' },
|
||||
{ label: 'CSV (Tables)', click: () => { const { exportSpreadsheet } = require('../main'); exportSpreadsheet('csv'); } },
|
||||
{ label: 'CSV (Tables)', click: () => { const { exportSpreadsheet } = require('../index'); exportSpreadsheet('csv'); } },
|
||||
{ type: 'separator' },
|
||||
{ label: 'JSON (.json)', click: () => { const { exportFile } = require('../main'); exportFile('json'); } },
|
||||
{ label: 'YAML (.yaml)', click: () => { const { exportFile } = require('../main'); exportFile('yaml'); } },
|
||||
{ label: 'XML (.xml)', click: () => { const { exportFile } = require('../main'); exportFile('xml'); } },
|
||||
{ label: 'JSON (.json)', click: () => { const { exportFile } = require('../index'); exportFile('json'); } },
|
||||
{ label: 'YAML (.yaml)', click: () => { const { exportFile } = require('../index'); exportFile('yaml'); } },
|
||||
{ label: 'XML (.xml)', click: () => { const { exportFile } = require('../index'); exportFile('xml'); } },
|
||||
{ type: 'separator' },
|
||||
{ label: 'Confluence Wiki (.txt)', click: () => { const { exportFile } = require('../main'); exportFile('confluence'); } },
|
||||
{ label: 'MOBI E-book (.mobi)', click: () => { const { exportFile } = require('../main'); exportFile('mobi'); } }
|
||||
{ label: 'Confluence Wiki (.txt)', click: () => { const { exportFile } = require('../index'); exportFile('confluence'); } },
|
||||
{ label: 'MOBI E-book (.mobi)', click: () => { const { exportFile } = require('../index'); exportFile('mobi'); } }
|
||||
]
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Select Word Template...',
|
||||
click: () => {
|
||||
const { selectWordTemplate } = require('../main');
|
||||
const { selectWordTemplate } = require('../index');
|
||||
selectWordTemplate();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Template Settings...',
|
||||
click: () => {
|
||||
const { showTemplateSettings } = require('../main');
|
||||
const { showTemplateSettings } = require('../index');
|
||||
showTemplateSettings();
|
||||
}
|
||||
},
|
||||
@@ -240,32 +240,32 @@ function viewItems(mainWindow) {
|
||||
{
|
||||
label: 'Theme',
|
||||
submenu: [
|
||||
{ label: 'Atom One Light (Default)', click: () => { const { setTheme } = require('../main'); setTheme('atomonelight'); } },
|
||||
{ label: 'GitHub Light', click: () => { const { setTheme } = require('../main'); setTheme('github'); } },
|
||||
{ label: 'Light', click: () => { const { setTheme } = require('../main'); setTheme('light'); } },
|
||||
{ label: 'Solarized Light', click: () => { const { setTheme } = require('../main'); setTheme('solarized'); } },
|
||||
{ label: 'Gruvbox Light', click: () => { const { setTheme } = require('../main'); setTheme('gruvbox-light'); } },
|
||||
{ label: 'Ayu Light', click: () => { const { setTheme } = require('../main'); setTheme('ayu-light'); } },
|
||||
{ label: 'Sepia', click: () => { const { setTheme } = require('../main'); setTheme('sepia'); } },
|
||||
{ label: 'Paper', click: () => { const { setTheme } = require('../main'); setTheme('paper'); } },
|
||||
{ label: 'Rose Pine Dawn', click: () => { const { setTheme } = require('../main'); setTheme('rosepine-dawn'); } },
|
||||
{ label: 'Concrete Light', click: () => { const { setTheme } = require('../main'); setTheme('concrete-light'); } },
|
||||
{ label: 'Atom One Light (Default)', click: () => { const { setTheme } = require('../index'); setTheme('atomonelight'); } },
|
||||
{ label: 'GitHub Light', click: () => { const { setTheme } = require('../index'); setTheme('github'); } },
|
||||
{ label: 'Light', click: () => { const { setTheme } = require('../index'); setTheme('light'); } },
|
||||
{ label: 'Solarized Light', click: () => { const { setTheme } = require('../index'); setTheme('solarized'); } },
|
||||
{ label: 'Gruvbox Light', click: () => { const { setTheme } = require('../index'); setTheme('gruvbox-light'); } },
|
||||
{ label: 'Ayu Light', click: () => { const { setTheme } = require('../index'); setTheme('ayu-light'); } },
|
||||
{ label: 'Sepia', click: () => { const { setTheme } = require('../index'); setTheme('sepia'); } },
|
||||
{ label: 'Paper', click: () => { const { setTheme } = require('../index'); setTheme('paper'); } },
|
||||
{ label: 'Rose Pine Dawn', click: () => { const { setTheme } = require('../index'); setTheme('rosepine-dawn'); } },
|
||||
{ label: 'Concrete Light', click: () => { const { setTheme } = require('../index'); setTheme('concrete-light'); } },
|
||||
{ type: 'separator' },
|
||||
{ label: 'Dark', click: () => { const { setTheme } = require('../main'); setTheme('dark'); } },
|
||||
{ label: 'One Dark', click: () => { const { setTheme } = require('../main'); setTheme('onedark'); } },
|
||||
{ label: 'Dracula', click: () => { const { setTheme } = require('../main'); setTheme('dracula'); } },
|
||||
{ label: 'Nord', click: () => { const { setTheme } = require('../main'); setTheme('nord'); } },
|
||||
{ label: 'Monokai', click: () => { const { setTheme } = require('../main'); setTheme('monokai'); } },
|
||||
{ label: 'Material', click: () => { const { setTheme } = require('../main'); setTheme('material'); } },
|
||||
{ label: 'Gruvbox Dark', click: () => { const { setTheme } = require('../main'); setTheme('gruvbox-dark'); } },
|
||||
{ label: 'Tokyo Night', click: () => { const { setTheme } = require('../main'); setTheme('tokyonight'); } },
|
||||
{ label: 'Palenight', click: () => { const { setTheme } = require('../main'); setTheme('palenight'); } },
|
||||
{ label: 'Ayu Dark', click: () => { const { setTheme } = require('../main'); setTheme('ayu-dark'); } },
|
||||
{ label: 'Ayu Mirage', click: () => { const { setTheme } = require('../main'); setTheme('ayu-mirage'); } },
|
||||
{ label: 'Oceanic Next', click: () => { const { setTheme } = require('../main'); setTheme('oceanic-next'); } },
|
||||
{ label: 'Cobalt2', click: () => { const { setTheme } = require('../main'); setTheme('cobalt2'); } },
|
||||
{ label: 'Concrete Dark', click: () => { const { setTheme } = require('../main'); setTheme('concrete-dark'); } },
|
||||
{ label: 'Concrete Warm', click: () => { const { setTheme } = require('../main'); setTheme('concrete-warm'); } }
|
||||
{ label: 'Dark', click: () => { const { setTheme } = require('../index'); setTheme('dark'); } },
|
||||
{ label: 'One Dark', click: () => { const { setTheme } = require('../index'); setTheme('onedark'); } },
|
||||
{ label: 'Dracula', click: () => { const { setTheme } = require('../index'); setTheme('dracula'); } },
|
||||
{ label: 'Nord', click: () => { const { setTheme } = require('../index'); setTheme('nord'); } },
|
||||
{ label: 'Monokai', click: () => { const { setTheme } = require('../index'); setTheme('monokai'); } },
|
||||
{ label: 'Material', click: () => { const { setTheme } = require('../index'); setTheme('material'); } },
|
||||
{ label: 'Gruvbox Dark', click: () => { const { setTheme } = require('../index'); setTheme('gruvbox-dark'); } },
|
||||
{ label: 'Tokyo Night', click: () => { const { setTheme } = require('../index'); setTheme('tokyonight'); } },
|
||||
{ label: 'Palenight', click: () => { const { setTheme } = require('../index'); setTheme('palenight'); } },
|
||||
{ label: 'Ayu Dark', click: () => { const { setTheme } = require('../index'); setTheme('ayu-dark'); } },
|
||||
{ label: 'Ayu Mirage', click: () => { const { setTheme } = require('../index'); setTheme('ayu-mirage'); } },
|
||||
{ label: 'Oceanic Next', click: () => { const { setTheme } = require('../index'); setTheme('oceanic-next'); } },
|
||||
{ label: 'Cobalt2', click: () => { const { setTheme } = require('../index'); setTheme('cobalt2'); } },
|
||||
{ label: 'Concrete Dark', click: () => { const { setTheme } = require('../index'); setTheme('concrete-dark'); } },
|
||||
{ label: 'Concrete Warm', click: () => { const { setTheme } = require('../index'); setTheme('concrete-warm'); } }
|
||||
]
|
||||
},
|
||||
{ type: 'separator' },
|
||||
@@ -327,7 +327,7 @@ function batchItems(mainWindow) {
|
||||
{
|
||||
label: 'Convert Markdown Folder...',
|
||||
click: () => {
|
||||
const { showBatchConversionDialog } = require('../main');
|
||||
const { showBatchConversionDialog } = require('../index');
|
||||
showBatchConversionDialog();
|
||||
}
|
||||
},
|
||||
@@ -357,7 +357,7 @@ function convertItems(mainWindow) {
|
||||
label: 'Universal File Converter...',
|
||||
accelerator: 'CmdOrCtrl+Shift+C',
|
||||
click: () => {
|
||||
const { showUniversalConverterDialog } = require('../main');
|
||||
const { showUniversalConverterDialog } = require('../index');
|
||||
showUniversalConverterDialog();
|
||||
}
|
||||
}
|
||||
@@ -370,7 +370,7 @@ function pdfEditorItems(mainWindow) {
|
||||
label: 'Open PDF File...',
|
||||
accelerator: 'CmdOrCtrl+Shift+O',
|
||||
click: () => {
|
||||
const { openPdfFile } = require('../main');
|
||||
const { openPdfFile } = require('../index');
|
||||
openPdfFile();
|
||||
}
|
||||
},
|
||||
@@ -378,21 +378,21 @@ function pdfEditorItems(mainWindow) {
|
||||
{
|
||||
label: 'Merge PDFs...',
|
||||
click: () => {
|
||||
const { showPDFEditorDialog } = require('../main');
|
||||
const { showPDFEditorDialog } = require('../index');
|
||||
showPDFEditorDialog('merge');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Split PDF...',
|
||||
click: () => {
|
||||
const { showPDFEditorDialog } = require('../main');
|
||||
const { showPDFEditorDialog } = require('../index');
|
||||
showPDFEditorDialog('split');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Compress PDF...',
|
||||
click: () => {
|
||||
const { showPDFEditorDialog } = require('../main');
|
||||
const { showPDFEditorDialog } = require('../index');
|
||||
showPDFEditorDialog('compress');
|
||||
}
|
||||
},
|
||||
@@ -400,21 +400,21 @@ function pdfEditorItems(mainWindow) {
|
||||
{
|
||||
label: 'Rotate Pages...',
|
||||
click: () => {
|
||||
const { showPDFEditorDialog } = require('../main');
|
||||
const { showPDFEditorDialog } = require('../index');
|
||||
showPDFEditorDialog('rotate');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Delete Pages...',
|
||||
click: () => {
|
||||
const { showPDFEditorDialog } = require('../main');
|
||||
const { showPDFEditorDialog } = require('../index');
|
||||
showPDFEditorDialog('delete');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Reorder Pages...',
|
||||
click: () => {
|
||||
const { showPDFEditorDialog } = require('../main');
|
||||
const { showPDFEditorDialog } = require('../index');
|
||||
showPDFEditorDialog('reorder');
|
||||
}
|
||||
},
|
||||
@@ -422,7 +422,7 @@ function pdfEditorItems(mainWindow) {
|
||||
{
|
||||
label: 'Add Watermark...',
|
||||
click: () => {
|
||||
const { showPDFEditorDialog } = require('../main');
|
||||
const { showPDFEditorDialog } = require('../index');
|
||||
showPDFEditorDialog('watermark');
|
||||
}
|
||||
},
|
||||
@@ -433,21 +433,21 @@ function pdfEditorItems(mainWindow) {
|
||||
{
|
||||
label: 'Add Password Protection...',
|
||||
click: () => {
|
||||
const { showPDFEditorDialog } = require('../main');
|
||||
const { showPDFEditorDialog } = require('../index');
|
||||
showPDFEditorDialog('encrypt');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Remove Password...',
|
||||
click: () => {
|
||||
const { showPDFEditorDialog } = require('../main');
|
||||
const { showPDFEditorDialog } = require('../index');
|
||||
showPDFEditorDialog('decrypt');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Set Permissions...',
|
||||
click: () => {
|
||||
const { showPDFEditorDialog } = require('../main');
|
||||
const { showPDFEditorDialog } = require('../index');
|
||||
showPDFEditorDialog('permissions');
|
||||
}
|
||||
}
|
||||
@@ -457,7 +457,7 @@ function pdfEditorItems(mainWindow) {
|
||||
{
|
||||
label: 'About PDF Editor',
|
||||
click: () => {
|
||||
const { showAboutDialog } = require('../main');
|
||||
const { showAboutDialog } = require('../index');
|
||||
showAboutDialog();
|
||||
}
|
||||
}
|
||||
@@ -482,7 +482,7 @@ function helpItems(mainWindow) {
|
||||
{
|
||||
label: 'About MarkdownConverter',
|
||||
click: () => {
|
||||
const { showAboutDialog } = require('../main');
|
||||
const { showAboutDialog } = require('../index');
|
||||
showAboutDialog();
|
||||
}
|
||||
},
|
||||
@@ -490,7 +490,7 @@ function helpItems(mainWindow) {
|
||||
{
|
||||
label: 'Dependencies & Requirements',
|
||||
click: () => {
|
||||
const { showDependenciesDialog } = require('../main');
|
||||
const { showDependenciesDialog } = require('../index');
|
||||
showDependenciesDialog();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
// src/main/window/index.js
|
||||
// Main window creation
|
||||
|
||||
const { BrowserWindow } = require('electron');
|
||||
const { app, BrowserWindow } = require('electron');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const state = require('./state');
|
||||
const menu = require('../menu');
|
||||
|
||||
@@ -13,16 +14,47 @@ function createMainWindow() {
|
||||
height: bounds.height,
|
||||
x: bounds.x,
|
||||
y: bounds.y,
|
||||
show: false,
|
||||
show: true,
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
contextIsolation: false,
|
||||
spellcheck: true
|
||||
},
|
||||
icon: path.join(__dirname, '../../assets/icon.png')
|
||||
icon: path.join(__dirname, '../../../assets/icon.png')
|
||||
});
|
||||
|
||||
win.loadFile(path.join(__dirname, '../../renderer/index.html'));
|
||||
// Dev (Vite): load the running dev server so .tsx is transformed on the fly.
|
||||
// Production (running from dist/ directly): load the built renderer at the
|
||||
// relative path from this file.
|
||||
// Production (packaged installer build): the renderer ships under
|
||||
// process.resourcesPath/renderer (configured via build.extraResources in
|
||||
// package.json) — not inside the asar.
|
||||
// VITE_DEV_SERVER_URL is set by `npm run dev` via cross-env.
|
||||
// app.isPackaged is set by electron-builder for installer builds.
|
||||
const devServerUrl = process.env.VITE_DEV_SERVER_URL;
|
||||
if (!app.isPackaged && devServerUrl) {
|
||||
console.log('[WINDOW] Dev mode — loading', devServerUrl);
|
||||
win.loadURL(devServerUrl);
|
||||
} else {
|
||||
const rendererIndex = app.isPackaged
|
||||
? path.join(process.resourcesPath, 'renderer', 'index.html')
|
||||
: path.join(__dirname, '../../../dist/renderer/index.html');
|
||||
|
||||
if (app.isPackaged) {
|
||||
try {
|
||||
fs.accessSync(rendererIndex);
|
||||
} catch {
|
||||
console.error(
|
||||
'[WINDOW] Renderer not found at',
|
||||
rendererIndex,
|
||||
'— did you run `npm run build:renderer` before packaging?',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[WINDOW] Production mode — loading', rendererIndex);
|
||||
win.loadFile(rendererIndex);
|
||||
}
|
||||
|
||||
// Show window only after content is ready — avoids blank flash
|
||||
win.once('ready-to-show', () => {
|
||||
@@ -31,8 +63,10 @@ function createMainWindow() {
|
||||
|
||||
menu.register(win);
|
||||
|
||||
win.on('closed', () => {
|
||||
state.save(win);
|
||||
// Use 'close' (fires before destruction) — 'closed' fires after the
|
||||
// BrowserWindow object is destroyed, so getBounds() would throw.
|
||||
win.on('close', () => {
|
||||
if (!win.isDestroyed()) state.save(win);
|
||||
});
|
||||
|
||||
// Spell check context menu
|
||||
|
||||
@@ -27,7 +27,7 @@ export function AppShell() {
|
||||
<main className="h-screen w-screen overflow-hidden bg-background">
|
||||
<ResizablePanelGroup
|
||||
direction="horizontal"
|
||||
onLayout={(sizes) => setPaneSizes({ sidebar: 0, editor: sizes[0], preview: sizes[1] })}
|
||||
onLayoutChange={(sizes) => setPaneSizes({ sidebar: 0, editor: sizes[0], preview: sizes[1] })}
|
||||
>
|
||||
<ResizablePanel defaultSize={previewVisible ? 50 : 100} minSize={20}>
|
||||
<section className="h-full bg-background">
|
||||
@@ -58,7 +58,7 @@ export function AppShell() {
|
||||
<main className="flex-1 overflow-hidden">
|
||||
<ResizablePanelGroup
|
||||
direction="horizontal"
|
||||
onLayout={(sizes) => setPaneSizes({ sidebar: sizes[0], editor: sizes[1], preview: sizes[2] })}
|
||||
onLayoutChange={(sizes) => setPaneSizes({ sidebar: sizes[0], editor: sizes[1], preview: sizes[2] })}
|
||||
>
|
||||
{sidebarVisible && (
|
||||
<>
|
||||
|
||||
@@ -17,10 +17,10 @@ export function AboutDialog() {
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={(o) => !o && closeModal()}>
|
||||
<DialogContent aria-describedby="about-desc">
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>About MarkdownConverter</DialogTitle>
|
||||
<DialogDescription id="about-desc">
|
||||
<DialogDescription>
|
||||
Professional Markdown editor and universal file converter.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -33,10 +33,10 @@ export function AsciiGeneratorDialog() {
|
||||
|
||||
return (
|
||||
<Dialog open onOpenChange={(o) => !o && closeModal()}>
|
||||
<DialogContent aria-describedby="ascii-desc" className="max-w-2xl">
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>ASCII generator</DialogTitle>
|
||||
<DialogDescription id="ascii-desc">Type text, pick a font, see ASCII art</DialogDescription>
|
||||
<DialogDescription>Type text, pick a font, see ASCII art</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
|
||||
@@ -17,10 +17,10 @@ export function ConfirmDialog(props: ConfirmProps) {
|
||||
|
||||
return (
|
||||
<Dialog open onOpenChange={(o) => !o && handleCancel()}>
|
||||
<DialogContent aria-describedby="confirm-body">
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<DialogDescription id="confirm-body">{body}</DialogDescription>
|
||||
<DialogDescription>{body}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button variant="ghost" onClick={handleCancel}>
|
||||
|
||||
@@ -37,10 +37,10 @@ export function ExportBatchDialog({ sourcePaths }: { sourcePaths: string[] }) {
|
||||
|
||||
return (
|
||||
<Dialog open onOpenChange={(o) => !o && closeModal()}>
|
||||
<DialogContent aria-describedby="batch-desc">
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Batch export</DialogTitle>
|
||||
<DialogDescription id="batch-desc">{sourcePaths.length} files</DialogDescription>
|
||||
<DialogDescription>{sourcePaths.length} files</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-3 text-sm">
|
||||
<div>
|
||||
|
||||
@@ -41,10 +41,10 @@ export function ExportDocxDialog({ sourcePath }: { sourcePath: string }) {
|
||||
|
||||
return (
|
||||
<Dialog open onOpenChange={(o) => !o && closeModal()}>
|
||||
<DialogContent aria-describedby="docx-desc">
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Export to DOCX</DialogTitle>
|
||||
<DialogDescription id="docx-desc">{sourcePath}</DialogDescription>
|
||||
<DialogDescription>{sourcePath}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-3 text-sm">
|
||||
<div>
|
||||
|
||||
@@ -43,10 +43,10 @@ export function ExportHtmlDialog({ sourcePath }: { sourcePath: string }) {
|
||||
|
||||
return (
|
||||
<Dialog open onOpenChange={(o) => !o && closeModal()}>
|
||||
<DialogContent aria-describedby="html-desc">
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Export to HTML</DialogTitle>
|
||||
<DialogDescription id="html-desc">{sourcePath}</DialogDescription>
|
||||
<DialogDescription>{sourcePath}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-3 text-sm">
|
||||
<label className="flex items-center gap-2">
|
||||
|
||||
@@ -56,10 +56,10 @@ export function ExportPdfDialog({ sourcePath }: { sourcePath: string }) {
|
||||
|
||||
return (
|
||||
<Dialog open onOpenChange={(o) => !o && closeModal()}>
|
||||
<DialogContent aria-describedby="pdf-desc">
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Export to PDF</DialogTitle>
|
||||
<DialogDescription id="pdf-desc">{sourcePath}</DialogDescription>
|
||||
<DialogDescription>{sourcePath}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-3 text-sm">
|
||||
<div>
|
||||
|
||||
@@ -47,10 +47,10 @@ export function FindInFilesDialog() {
|
||||
|
||||
return (
|
||||
<Dialog open onOpenChange={(o) => !o && closeModal()}>
|
||||
<DialogContent aria-describedby="find-desc" className="max-w-2xl">
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Find in files</DialogTitle>
|
||||
<DialogDescription id="find-desc">
|
||||
<DialogDescription>
|
||||
{rootPath ? `Search in ${rootPath}` : 'No folder open'}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -15,10 +15,10 @@ export function SettingsSheet() {
|
||||
|
||||
return (
|
||||
<Sheet open onOpenChange={(o) => !o && closeModal()}>
|
||||
<SheetContent aria-describedby="settings-desc" side="right" className="w-full sm:max-w-[480px]">
|
||||
<SheetContent side="right" className="w-full sm:max-w-[480px]">
|
||||
<SheetHeader>
|
||||
<SheetTitle>Settings</SheetTitle>
|
||||
<SheetDescription id="settings-desc">Editor, theme, and export preferences</SheetDescription>
|
||||
<SheetDescription>Editor, theme, and export preferences</SheetDescription>
|
||||
</SheetHeader>
|
||||
<Tabs defaultValue="editor" className="mt-4">
|
||||
<TabsList className="grid w-full grid-cols-5">
|
||||
|
||||
@@ -60,10 +60,10 @@ export function TableGeneratorDialog() {
|
||||
|
||||
return (
|
||||
<Dialog open onOpenChange={(o) => !o && closeModal()}>
|
||||
<DialogContent aria-describedby="table-desc" className="max-w-2xl">
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Table generator</DialogTitle>
|
||||
<DialogDescription id="table-desc">Specify rows × columns to generate a markdown table</DialogDescription>
|
||||
<DialogDescription>Specify rows × columns to generate a markdown table</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-3">
|
||||
<div className="flex gap-3">
|
||||
|
||||
@@ -17,10 +17,10 @@ export function WelcomeDialog() {
|
||||
|
||||
return (
|
||||
<Dialog open onOpenChange={(o) => !o && handleClose()}>
|
||||
<DialogContent aria-describedby="welcome-desc" className="max-w-xl">
|
||||
<DialogContent className="max-w-xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Welcome to MarkdownConverter</DialogTitle>
|
||||
<DialogDescription id="welcome-desc">
|
||||
<DialogDescription>
|
||||
A polished editor for Markdown, with PDF, DOCX, and HTML export.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -65,10 +65,10 @@ export function WordExportDialog({ sourcePath }: { sourcePath: string }) {
|
||||
|
||||
return (
|
||||
<Dialog open onOpenChange={(o) => !o && closeModal()}>
|
||||
<DialogContent aria-describedby="word-desc">
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Export to Word (.docx)</DialogTitle>
|
||||
<DialogDescription id="word-desc">{sourcePath}</DialogDescription>
|
||||
<DialogDescription>{sourcePath}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-3 text-sm">
|
||||
<div>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self';">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: blob:; font-src 'self' data: https://fonts.gstatic.com; connect-src 'self' https://fonts.googleapis.com https://fonts.gstatic.com;">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>MarkdownConverter</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
|
||||
@@ -6,7 +6,7 @@ import './styles/globals.css';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<ThemeProvider defaultTheme="dark" attribute="class" enableSystem>
|
||||
<ThemeProvider defaultTheme="light" attribute="class" enableSystem>
|
||||
<App />
|
||||
</ThemeProvider>
|
||||
</React.StrictMode>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useAppStore } from '@/stores/app-store';
|
||||
|
||||
// Mock react-resizable-panels for jsdom environment
|
||||
vi.mock('@/components/ui/resizable', () => ({
|
||||
ResizablePanelGroup: ({ children, direction, onLayout }: any) => (
|
||||
ResizablePanelGroup: ({ children, direction, onLayoutChange }: any) => (
|
||||
<div data-testid="resizable-panel-group" data-direction={direction}>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -31,4 +31,27 @@ describe('WelcomeDialog', () => {
|
||||
await userEvent.click(screen.getByRole('button', { name: /get started/i }));
|
||||
expect(useSettingsStore.getState().welcomeDismissed).toBe(true);
|
||||
});
|
||||
|
||||
// Regression guard: Radix Dialog emits a console.warn when the Content's
|
||||
// aria-describedby points at an id that isn't in the DOM. The default
|
||||
// shadcn/ui pattern (custom id="X-desc" on DialogDescription) breaks that
|
||||
// link because Radix's internal descriptionId is auto-generated via useId.
|
||||
// The fix is to drop the override and let Radix manage the id itself.
|
||||
it('does not emit the Radix "Missing Description" warning', async () => {
|
||||
const warnings: string[] = [];
|
||||
const spy = vi.spyOn(console, 'warn').mockImplementation((...args) => {
|
||||
warnings.push(args.map(String).join(' '));
|
||||
});
|
||||
try {
|
||||
render(<WelcomeDialog />);
|
||||
// Effects that fire the warning run in a microtask; one tick is enough.
|
||||
await Promise.resolve();
|
||||
const offending = warnings.filter((w) =>
|
||||
/Missing\s+`?Description`?|aria-describedby=\{undefined\}/.test(w),
|
||||
);
|
||||
expect(offending).toEqual([]);
|
||||
} finally {
|
||||
spy.mockRestore();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user