diff --git a/bun.lock b/bun.lock index 36e752df..579a047c 100644 --- a/bun.lock +++ b/bun.lock @@ -49,8 +49,8 @@ "typescript-6": "npm:typescript@^6.0.3", "unplugin-vue-components": "^32.1.0", "uplot": "1.6.32", - "vite": "^8.3.5", - "vue": "^3.5.44", + "vite": "^8.3.6", + "vue": "^3.5.45", "vue-tsc": "^3.3.14", }, }, @@ -178,7 +178,7 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], - "@lucide/vue": ["@lucide/vue@1.68.0", "", { "peerDependencies": { "vue": ">=3.0.1" } }, "sha512-ywFT0JLQC89yjvOD2XXwwT0s+T9ydharRZBgFEIAbk6YyvrNv5MWjkRfzW+hlcxWJJujyzXqofeLjyJC4au4tQ=="], + "@lucide/vue": ["@lucide/vue@1.69.0", "", { "peerDependencies": { "vue": ">=3.0.1" } }, "sha512-uxR2FtureNe37HOQk/e92CZjlTjXVuNiYlD60Mgns0rmbs0wqDnHDgZPUooP6nvmFn/v7Ie6dKRKPzgR6HW5Tw=="], "@malept/cross-spawn-promise": ["@malept/cross-spawn-promise@2.0.0", "", { "dependencies": { "cross-spawn": "^7.0.1" } }, "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg=="], @@ -394,25 +394,25 @@ "@volar/typescript": ["@volar/typescript@2.4.30", "", { "dependencies": { "@volar/language-core": "2.4.30", "@volar/language-service": "2.4.30", "path-browserify": "^1.0.1", "vscode-uri": "^3.0.8" }, "peerDependencies": { "typescript": "<7" }, "optionalPeers": ["typescript"] }, "sha512-/DgA5tgYFPfrLFuSjIMXMGHk1LQv2+P8eny6+BhGNzFJELWIW7UZlc56ZLIRFhLsPpMfIFxM8/HvOPBggrjTZg=="], - "@vue/compiler-core": ["@vue/compiler-core@3.5.44", "", { "dependencies": { "@babel/parser": "^7.29.9", "@vue/shared": "3.5.44", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.2" } }, "sha512-k34nCytbK8PQ1gR2mGROYOw6Pnv/mumqIdrZ3x2tbyXZDWm7ViKGLTfdB+DRrWqEQsLgGJvn0WN9jBif5zCTLQ=="], + "@vue/compiler-core": ["@vue/compiler-core@3.5.45", "", { "dependencies": { "@babel/parser": "^7.29.12", "@vue/shared": "3.5.45", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.2" } }, "sha512-XGHJrM6ufZARAc/w+tyOswStw9tdKiqCKBScI4gGaNxD+aaGu/HQWgKJ+JItjRLPqenQ7iE7G8AJC7T5ew1p+g=="], - "@vue/compiler-dom": ["@vue/compiler-dom@3.5.44", "", { "dependencies": { "@vue/compiler-core": "3.5.44", "@vue/shared": "3.5.44" } }, "sha512-W5AoBFWdw1lQAKiHowkfgFKFab82oSRT6sGsPfw/q1GRjsy/ZJFLHGhJyBRIFN9APLTUduDH9U2JAiPOIyKuYg=="], + "@vue/compiler-dom": ["@vue/compiler-dom@3.5.45", "", { "dependencies": { "@vue/compiler-core": "3.5.45", "@vue/shared": "3.5.45" } }, "sha512-LbP2d19GSOYGPmvMBBCVJVlv7TKtjM1Cfeyu3cYia720pknZP+1cLX7H5MVatvruT6MuJhiPuNbQBW/MDn/EwA=="], - "@vue/compiler-sfc": ["@vue/compiler-sfc@3.5.44", "", { "dependencies": { "@babel/parser": "^7.29.9", "@vue/compiler-core": "3.5.44", "@vue/compiler-dom": "3.5.44", "@vue/compiler-ssr": "3.5.44", "@vue/shared": "3.5.44", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.29", "source-map-js": "^1.2.2" } }, "sha512-dzHmQ4jXTpdr5FjIQUeykVakN/uzqFbUo9/C5ikBFg3a3PeM6uRxeczx1ZbXBML0/zJstEh3ygYipzR5Z7b2Pw=="], + "@vue/compiler-sfc": ["@vue/compiler-sfc@3.5.45", "", { "dependencies": { "@babel/parser": "^7.29.12", "@vue/compiler-core": "3.5.45", "@vue/compiler-dom": "3.5.45", "@vue/compiler-ssr": "3.5.45", "@vue/shared": "3.5.45", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.32", "source-map-js": "^1.2.2" } }, "sha512-MKFjb2UX0M/l/jhfzqMLrI8VM4pfvFKG3pqFZkSZFHRLtNld/O4RtUPZFWSwhro/14qP88UQEkWzHxAZAQ0tsw=="], - "@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.44", "", { "dependencies": { "@vue/compiler-dom": "3.5.44", "@vue/shared": "3.5.44" } }, "sha512-xr5gb1JVHlfXJwSlRz1FeP6uxoazdzHuMTVLE7up+4Vrqxk219CvxLPxD9NXBt47yvZW7+p4DXfyp4O1fkByIA=="], + "@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.45", "", { "dependencies": { "@vue/compiler-dom": "3.5.45", "@vue/shared": "3.5.45" } }, "sha512-RF5sbZOfYz5AK00tOJUn2PwFXR2fS1u2HVzI8D9VF9rGAFxuGBqg2bB1kJ3ZHMprqSltXnT9MZE1IsqjSM8oew=="], "@vue/language-core": ["@vue/language-core@3.3.14", "", { "dependencies": { "@volar/language-core": "2.4.30", "@vue/compiler-dom": "^3.5.0", "@vue/shared": "^3.5.0", "alien-signals": "^3.2.1", "muggle-string": "^0.4.1", "path-browserify": "^1.0.1", "picomatch": "^4.0.4" } }, "sha512-kRB+3hHg0c+BdD1XLAvwjVMQd5pC05zijSlPDRI3ERvC/s04JlRs5upy2KjeMg/9m9oD5zuMVh9yVvkKfZqoJg=="], - "@vue/reactivity": ["@vue/reactivity@3.5.44", "", { "dependencies": { "@vue/shared": "3.5.44" } }, "sha512-u+QJdKeBBEADkPdevsL+6E12QlYVfBD8Zcw5qjDQKtamI6l14KyqazJMobhCKelhWN5zpyKUiEDoTyHE93cqWw=="], + "@vue/reactivity": ["@vue/reactivity@3.5.45", "", { "dependencies": { "@vue/shared": "3.5.45" } }, "sha512-WqkyubNWpI4OYBOipSrjIssMdJO8K1i9Zts/Wm8Er10/0msOEwCyWWWvZnAI7WW3kQsTong6DJp8Cbb+vd4cPA=="], - "@vue/runtime-core": ["@vue/runtime-core@3.5.44", "", { "dependencies": { "@vue/reactivity": "3.5.44", "@vue/shared": "3.5.44" } }, "sha512-uEa/ndwjhrrhnOfb2p+sZy6zeoamJOISqaLSJGk/8R6nABT1ZldpzcJoPzKj+zHVomVBPcmQYyC2r4LtvDV6CQ=="], + "@vue/runtime-core": ["@vue/runtime-core@3.5.45", "", { "dependencies": { "@vue/reactivity": "3.5.45", "@vue/shared": "3.5.45" } }, "sha512-bzIO3+FABjpj85uIKSc+Qnj36/sxDNI3tcH9i6w6xKA1k6e0ejv0G0jMCfeJBvK3yGL5XbJ3DMZj+ClqyCHFYw=="], - "@vue/runtime-dom": ["@vue/runtime-dom@3.5.44", "", { "dependencies": { "@vue/reactivity": "3.5.44", "@vue/runtime-core": "3.5.44", "@vue/shared": "3.5.44", "csstype": "^3.2.3" } }, "sha512-3fc4ARcXVhVBaJ3ujWiNZJARD6pkyRcwG+ZYhMzjKtKUp7MAY0HLwr7r3Smep3Li6ZaWT6ewdmx8XqdaD5oNZg=="], + "@vue/runtime-dom": ["@vue/runtime-dom@3.5.45", "", { "dependencies": { "@vue/reactivity": "3.5.45", "@vue/runtime-core": "3.5.45", "@vue/shared": "3.5.45", "csstype": "^3.2.3" } }, "sha512-ZvRTFMDgfqTDYWvJfoVpQTJx1RN5M6l7Pyd6wfdY2NaXzbPQVh9ezeQYFbNPP6bIcRVn2SqSSR25N/jSW69BFw=="], - "@vue/server-renderer": ["@vue/server-renderer@3.5.44", "", { "dependencies": { "@vue/compiler-ssr": "3.5.44", "@vue/runtime-dom": "3.5.44", "@vue/shared": "3.5.44" } }, "sha512-R4S1UoH4h5Frrauv8Z5XE5TxV9oBiUSgWIC4TyVK1FK+RocRnOLwFA5Xcmo2011RlgJp+AbZCnw6afrhlgRuUQ=="], + "@vue/server-renderer": ["@vue/server-renderer@3.5.45", "", { "dependencies": { "@vue/compiler-ssr": "3.5.45", "@vue/runtime-dom": "3.5.45", "@vue/shared": "3.5.45" } }, "sha512-1fNXUHhhsWf4EVL4XixbGBU5mKdiIXJAB82feHRTyPPo6ZUEYWIJuEeUl3mVV6QDBp50HJOX4obLhxYXlrjrzA=="], - "@vue/shared": ["@vue/shared@3.5.44", "", {}, "sha512-H82XLH0DjmSOdbEwO15x3QTo1f5PWEtv237eLpYKc0mqLu+B8hnq16lFWmuBmNurkeiG3wr+oM1FhOll/YVFRg=="], + "@vue/shared": ["@vue/shared@3.5.45", "", {}, "sha512-ue9fgHmVNPlP7iW3+cxoBX/Cnlw53dHlVSw0JKGY6z1lObg8pHoj928M2nzG4/yVsMZ3NaRZ2s2z61uC16NDwQ=="], "@vue/tsconfig": ["@vue/tsconfig@0.9.1", "", { "peerDependencies": { "typescript": ">= 5.8", "vue": "^3.4.0" }, "optionalPeers": ["typescript", "vue"] }, "sha512-buvjm+9NzLCJL29KY1j1991YYJ5e6275OiK+G4jtmfIb+z4POywbdm0wXusT9adVWqe0xqg70TbI7+mRx4uU9w=="], @@ -1090,7 +1090,7 @@ "ufo": ["ufo@1.6.4", "", {}, "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA=="], - "undici": ["undici@7.30.1", "", {}, "sha512-03KrMbjIFDWQR5RNPrSw3bd1r12JpyFQl9/yxjUbJtqkjmllqC5lFjZh6ujeuJCKr8EOQvRW872Oy/az+nLYxg=="], + "undici": ["undici@7.30.2", "", {}, "sha512-mLyPQnHwUxaM/rideoK/+KL8DaAJLRl4aBCfmX444aCSLnIaomqPLtNRe2jdCYcsk5wZzXrDIQC8ZeYNRAYyIA=="], "undici-types": ["undici-types@8.9.0", "", {}, "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg=="], @@ -1114,7 +1114,7 @@ "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], - "vite": ["vite@8.3.5", "", { "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.7", "postcss": "^8.5.30", "rolldown": "~1.2.14", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.7.1 || ^0.8.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-z1YpjieJ9g0ebTmQpLBv/oq9E3F9EIlOe+7LahW8SrE3LTiG3udBRUXQOlYXP9b+IC2q2phHeVxj9xGlw34OEQ=="], + "vite": ["vite@8.3.6", "", { "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.7", "postcss": "^8.5.32", "rolldown": "~1.2.16", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.7.1 || ^0.8.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-UI7dK29NnS54Fk+mmo7o+jI6U8Fp7hDl+X5Q1k45WH+d4dSlbLVUETGQ+hfkNgvLQGyoElPj837nZyv4MBc0VA=="], "vscode-jsonrpc": ["vscode-jsonrpc@10.0.0", "", {}, "sha512-USSfxg0vDIpTQqlQBGxF3mBARyoS8cU7LHLpbYNh6dOJt32dlskxPWB0Irrh5TllTmyf7oNLAvqDqzklbwwnpA=="], @@ -1126,7 +1126,7 @@ "vscode-uri": ["vscode-uri@3.2.0", "", {}, "sha512-m2gXo3bn0G1kT9InzMf07fTbqMbGtyckj3bH5ktLO+1Ssv+yiATZ4dhwaQv9UZWxJh6E9IFGnQyjgWVDWVBDrg=="], - "vue": ["vue@3.5.44", "", { "dependencies": { "@vue/compiler-dom": "3.5.44", "@vue/compiler-sfc": "3.5.44", "@vue/runtime-dom": "3.5.44", "@vue/server-renderer": "3.5.44", "@vue/shared": "3.5.44" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-dvvmaOCG7lAMkJTPqPwWeGMsPS6DIsFsfZDUgBp9V6mHf9b3+/wyVwYR4Es+WV6uJC0FKRxp81Obcq0ZwUcGFQ=="], + "vue": ["vue@3.5.45", "", { "dependencies": { "@vue/compiler-dom": "3.5.45", "@vue/compiler-sfc": "3.5.45", "@vue/runtime-dom": "3.5.45", "@vue/server-renderer": "3.5.45", "@vue/shared": "3.5.45" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-3cvsib8m9c0vB24+MDKsgcW4+Da4xaBl9Z/5Tbm2pmSIsRPxcil4cdXjXlVsmJ2g64XJo5h+IRd7rsdGT9pZaw=="], "vue-tippy": ["vue-tippy@6.8.0", "", { "dependencies": { "tippy.js": "^6.3.7" }, "peerDependencies": { "vue": "^3.2.0" } }, "sha512-NiWpwncli2vXpestHyvuFPDgQ51qLbBKpZQj7FeSOm1mFbPhzMeaRxWBQQEhl/FqGRXasfGvETgAUW4md6SAbA=="], diff --git a/package.json b/package.json index 667ec400..fbcd63dd 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "url": "git+https://github.com/opencor/webapp.git" }, "type": "module", - "version": "1.20261105.1", + "version": "1.20261106.0", "engines": { "bun": ">=1.2.0" }, @@ -93,8 +93,8 @@ "typescript-6": "npm:typescript@^6.0.3", "unplugin-vue-components": "^32.1.0", "uplot": "1.6.32", - "vite": "^8.3.5", - "vue": "^3.5.44", + "vite": "^8.3.6", + "vue": "^3.5.45", "vue-tsc": "^3.3.14" } } diff --git a/src/main/MainWindow.ts b/src/main/MainWindow.ts index a9fbc846..22e2742a 100644 --- a/src/main/MainWindow.ts +++ b/src/main/MainWindow.ts @@ -254,15 +254,33 @@ export const clearRecentFiles = (): void => { recentFilePaths = []; updateReopenMenu(recentFilePaths); + + // Let our renderer know that our recent files have been cleared, so that it can release the contents that it kept for + // the files that were opened using a data URL and then closed (see OpenCOR.addRecentFile()). + + MainWindow.instance?.send('clear-recent-files'); }; -export const fileClosed = (filePath: string): void => { - // Make sure that the file was not opened using a data URL (see isDataUrlFileName()). +// Note: a file opened using a data URL only exists in memory, in our renderer (see isDataUrlFileName()), so it can only +// be reopened during the current session and only as long as our renderer is not gone. Hence, we never save such +// a file as a recent file (see the constructor of MainWindow) and we forget about it when our renderer is gone +// (see onRenderProcessGone()). - if (isDataUrlFileName(filePath)) { - return; +const persistentRecentFilePaths = (): string[] => { + return recentFilePaths.filter((filePath) => !isDataUrlFileName(filePath)); +}; + +const forgetDataUrlRecentFilePaths = (): void => { + const filePaths = persistentRecentFilePaths(); + + if (filePaths.length !== recentFilePaths.length) { + recentFilePaths = filePaths; + + updateReopenMenu(recentFilePaths); } +}; +export const fileClosed = (filePath: string): void => { removeRecentFilePath(filePath); recentFilePaths.unshift(filePath); @@ -825,7 +843,7 @@ export class MainWindow extends ApplicationWindow { // Recent files. - electronConf.set('app.files.recent', recentFilePaths); + electronConf.set('app.files.recent', persistentRecentFilePaths()); // Opened files and selected file. // Note: if our renderer is not ready, then either it crashed and we are being closed before it could be @@ -1082,6 +1100,11 @@ export class MainWindow extends ApplicationWindow { disallowSavingAnyFile(); + // Our renderer is gone, so are the contents of the files that were opened using a data URL and then closed, i.e. + // those files cannot be reopened anymore. + + forgetDataUrlRecentFilePaths(); + // Ignore our renderer being gone if we are being closed (e.g., because OpenCOR is being quit), whatever the reason. // Indeed, our renderer will normally exit cleanly in that case, but it may also be killed (e.g., when logging off or // shutting down the system) or crash while being torn down. Either way, there is nothing to recover since we are diff --git a/src/preload/index.ts b/src/preload/index.ts index 59a4d8f1..4f1558ec 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -144,6 +144,7 @@ electron.contextBridge.exposeInMainWorld('electronApi', { onIpc('canonicalise-recent-file-paths', callback), onChangelog: (callback: () => void) => onIpc('changelog', callback), onCheckForUpdates: (callback: () => void) => onIpc('check-for-updates', callback), + onClearRecentFiles: (callback: () => void) => onIpc('clear-recent-files', callback), onEnableDisableUi: (callback: (enable: boolean) => void) => onIpc('enable-disable-ui', callback), onOpen: (callback: (filePath: string) => void) => onIpc('open', callback), onOpenRemote: (callback: () => void) => onIpc('open-remote', callback), diff --git a/src/renderer/bun.lock b/src/renderer/bun.lock index 94d3339b..4bfc117a 100644 --- a/src/renderer/bun.lock +++ b/src/renderer/bun.lock @@ -35,13 +35,13 @@ "typescript-6": "npm:typescript@^6.0.3", "unplugin-vue-components": "^32.1.0", "uplot": "1.6.32", - "vite": "^8.3.5", - "vue": "^3.5.44", + "vite": "^8.3.6", + "vue": "^3.5.45", "vue-tsc": "^3.3.14", "xxhash-wasm": "^1.1.0", }, "peerDependencies": { - "vue": "^3.5.44", + "vue": "^3.5.45", }, }, }, @@ -118,7 +118,7 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], - "@lucide/vue": ["@lucide/vue@1.68.0", "", { "peerDependencies": { "vue": ">=3.0.1" } }, "sha512-ywFT0JLQC89yjvOD2XXwwT0s+T9ydharRZBgFEIAbk6YyvrNv5MWjkRfzW+hlcxWJJujyzXqofeLjyJC4au4tQ=="], + "@lucide/vue": ["@lucide/vue@1.69.0", "", { "peerDependencies": { "vue": ">=3.0.1" } }, "sha512-uxR2FtureNe37HOQk/e92CZjlTjXVuNiYlD60Mgns0rmbs0wqDnHDgZPUooP6nvmFn/v7Ie6dKRKPzgR6HW5Tw=="], "@mathjax/mathjax-newcm-font": ["@mathjax/mathjax-newcm-font@4.1.3", "", {}, "sha512-gzAB3dFHilHX1l5x2xUqRL+1jDQt3Fyza1DkEMVXWC4E8SvsGdlgEza47HYi2WhVcgfkvf4zgUGzuhbq3Pjlew=="], @@ -276,25 +276,25 @@ "@volar/typescript": ["@volar/typescript@2.4.30", "", { "dependencies": { "@volar/language-core": "2.4.30", "@volar/language-service": "2.4.30", "path-browserify": "^1.0.1", "vscode-uri": "^3.0.8" }, "peerDependencies": { "typescript": "<7" }, "optionalPeers": ["typescript"] }, "sha512-/DgA5tgYFPfrLFuSjIMXMGHk1LQv2+P8eny6+BhGNzFJELWIW7UZlc56ZLIRFhLsPpMfIFxM8/HvOPBggrjTZg=="], - "@vue/compiler-core": ["@vue/compiler-core@3.5.44", "", { "dependencies": { "@babel/parser": "^7.29.9", "@vue/shared": "3.5.44", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.2" } }, "sha512-k34nCytbK8PQ1gR2mGROYOw6Pnv/mumqIdrZ3x2tbyXZDWm7ViKGLTfdB+DRrWqEQsLgGJvn0WN9jBif5zCTLQ=="], + "@vue/compiler-core": ["@vue/compiler-core@3.5.45", "", { "dependencies": { "@babel/parser": "^7.29.12", "@vue/shared": "3.5.45", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.2" } }, "sha512-XGHJrM6ufZARAc/w+tyOswStw9tdKiqCKBScI4gGaNxD+aaGu/HQWgKJ+JItjRLPqenQ7iE7G8AJC7T5ew1p+g=="], - "@vue/compiler-dom": ["@vue/compiler-dom@3.5.44", "", { "dependencies": { "@vue/compiler-core": "3.5.44", "@vue/shared": "3.5.44" } }, "sha512-W5AoBFWdw1lQAKiHowkfgFKFab82oSRT6sGsPfw/q1GRjsy/ZJFLHGhJyBRIFN9APLTUduDH9U2JAiPOIyKuYg=="], + "@vue/compiler-dom": ["@vue/compiler-dom@3.5.45", "", { "dependencies": { "@vue/compiler-core": "3.5.45", "@vue/shared": "3.5.45" } }, "sha512-LbP2d19GSOYGPmvMBBCVJVlv7TKtjM1Cfeyu3cYia720pknZP+1cLX7H5MVatvruT6MuJhiPuNbQBW/MDn/EwA=="], - "@vue/compiler-sfc": ["@vue/compiler-sfc@3.5.44", "", { "dependencies": { "@babel/parser": "^7.29.9", "@vue/compiler-core": "3.5.44", "@vue/compiler-dom": "3.5.44", "@vue/compiler-ssr": "3.5.44", "@vue/shared": "3.5.44", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.29", "source-map-js": "^1.2.2" } }, "sha512-dzHmQ4jXTpdr5FjIQUeykVakN/uzqFbUo9/C5ikBFg3a3PeM6uRxeczx1ZbXBML0/zJstEh3ygYipzR5Z7b2Pw=="], + "@vue/compiler-sfc": ["@vue/compiler-sfc@3.5.45", "", { "dependencies": { "@babel/parser": "^7.29.12", "@vue/compiler-core": "3.5.45", "@vue/compiler-dom": "3.5.45", "@vue/compiler-ssr": "3.5.45", "@vue/shared": "3.5.45", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.32", "source-map-js": "^1.2.2" } }, "sha512-MKFjb2UX0M/l/jhfzqMLrI8VM4pfvFKG3pqFZkSZFHRLtNld/O4RtUPZFWSwhro/14qP88UQEkWzHxAZAQ0tsw=="], - "@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.44", "", { "dependencies": { "@vue/compiler-dom": "3.5.44", "@vue/shared": "3.5.44" } }, "sha512-xr5gb1JVHlfXJwSlRz1FeP6uxoazdzHuMTVLE7up+4Vrqxk219CvxLPxD9NXBt47yvZW7+p4DXfyp4O1fkByIA=="], + "@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.45", "", { "dependencies": { "@vue/compiler-dom": "3.5.45", "@vue/shared": "3.5.45" } }, "sha512-RF5sbZOfYz5AK00tOJUn2PwFXR2fS1u2HVzI8D9VF9rGAFxuGBqg2bB1kJ3ZHMprqSltXnT9MZE1IsqjSM8oew=="], "@vue/language-core": ["@vue/language-core@3.3.14", "", { "dependencies": { "@volar/language-core": "2.4.30", "@vue/compiler-dom": "^3.5.0", "@vue/shared": "^3.5.0", "alien-signals": "^3.2.1", "muggle-string": "^0.4.1", "path-browserify": "^1.0.1", "picomatch": "^4.0.4" } }, "sha512-kRB+3hHg0c+BdD1XLAvwjVMQd5pC05zijSlPDRI3ERvC/s04JlRs5upy2KjeMg/9m9oD5zuMVh9yVvkKfZqoJg=="], - "@vue/reactivity": ["@vue/reactivity@3.5.44", "", { "dependencies": { "@vue/shared": "3.5.44" } }, "sha512-u+QJdKeBBEADkPdevsL+6E12QlYVfBD8Zcw5qjDQKtamI6l14KyqazJMobhCKelhWN5zpyKUiEDoTyHE93cqWw=="], + "@vue/reactivity": ["@vue/reactivity@3.5.45", "", { "dependencies": { "@vue/shared": "3.5.45" } }, "sha512-WqkyubNWpI4OYBOipSrjIssMdJO8K1i9Zts/Wm8Er10/0msOEwCyWWWvZnAI7WW3kQsTong6DJp8Cbb+vd4cPA=="], - "@vue/runtime-core": ["@vue/runtime-core@3.5.44", "", { "dependencies": { "@vue/reactivity": "3.5.44", "@vue/shared": "3.5.44" } }, "sha512-uEa/ndwjhrrhnOfb2p+sZy6zeoamJOISqaLSJGk/8R6nABT1ZldpzcJoPzKj+zHVomVBPcmQYyC2r4LtvDV6CQ=="], + "@vue/runtime-core": ["@vue/runtime-core@3.5.45", "", { "dependencies": { "@vue/reactivity": "3.5.45", "@vue/shared": "3.5.45" } }, "sha512-bzIO3+FABjpj85uIKSc+Qnj36/sxDNI3tcH9i6w6xKA1k6e0ejv0G0jMCfeJBvK3yGL5XbJ3DMZj+ClqyCHFYw=="], - "@vue/runtime-dom": ["@vue/runtime-dom@3.5.44", "", { "dependencies": { "@vue/reactivity": "3.5.44", "@vue/runtime-core": "3.5.44", "@vue/shared": "3.5.44", "csstype": "^3.2.3" } }, "sha512-3fc4ARcXVhVBaJ3ujWiNZJARD6pkyRcwG+ZYhMzjKtKUp7MAY0HLwr7r3Smep3Li6ZaWT6ewdmx8XqdaD5oNZg=="], + "@vue/runtime-dom": ["@vue/runtime-dom@3.5.45", "", { "dependencies": { "@vue/reactivity": "3.5.45", "@vue/runtime-core": "3.5.45", "@vue/shared": "3.5.45", "csstype": "^3.2.3" } }, "sha512-ZvRTFMDgfqTDYWvJfoVpQTJx1RN5M6l7Pyd6wfdY2NaXzbPQVh9ezeQYFbNPP6bIcRVn2SqSSR25N/jSW69BFw=="], - "@vue/server-renderer": ["@vue/server-renderer@3.5.44", "", { "dependencies": { "@vue/compiler-ssr": "3.5.44", "@vue/runtime-dom": "3.5.44", "@vue/shared": "3.5.44" } }, "sha512-R4S1UoH4h5Frrauv8Z5XE5TxV9oBiUSgWIC4TyVK1FK+RocRnOLwFA5Xcmo2011RlgJp+AbZCnw6afrhlgRuUQ=="], + "@vue/server-renderer": ["@vue/server-renderer@3.5.45", "", { "dependencies": { "@vue/compiler-ssr": "3.5.45", "@vue/runtime-dom": "3.5.45", "@vue/shared": "3.5.45" } }, "sha512-1fNXUHhhsWf4EVL4XixbGBU5mKdiIXJAB82feHRTyPPo6ZUEYWIJuEeUl3mVV6QDBp50HJOX4obLhxYXlrjrzA=="], - "@vue/shared": ["@vue/shared@3.5.44", "", {}, "sha512-H82XLH0DjmSOdbEwO15x3QTo1f5PWEtv237eLpYKc0mqLu+B8hnq16lFWmuBmNurkeiG3wr+oM1FhOll/YVFRg=="], + "@vue/shared": ["@vue/shared@3.5.45", "", {}, "sha512-ue9fgHmVNPlP7iW3+cxoBX/Cnlw53dHlVSw0JKGY6z1lObg8pHoj928M2nzG4/yVsMZ3NaRZ2s2z61uC16NDwQ=="], "@vue/tsconfig": ["@vue/tsconfig@0.9.1", "", { "peerDependencies": { "typescript": ">= 5.8", "vue": "^3.4.0" }, "optionalPeers": ["typescript", "vue"] }, "sha512-buvjm+9NzLCJL29KY1j1991YYJ5e6275OiK+G4jtmfIb+z4POywbdm0wXusT9adVWqe0xqg70TbI7+mRx4uU9w=="], @@ -624,7 +624,7 @@ "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], - "vite": ["vite@8.3.5", "", { "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.7", "postcss": "^8.5.30", "rolldown": "~1.2.14", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.7.1 || ^0.8.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-z1YpjieJ9g0ebTmQpLBv/oq9E3F9EIlOe+7LahW8SrE3LTiG3udBRUXQOlYXP9b+IC2q2phHeVxj9xGlw34OEQ=="], + "vite": ["vite@8.3.6", "", { "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.7", "postcss": "^8.5.32", "rolldown": "~1.2.16", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.7.1 || ^0.8.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-UI7dK29NnS54Fk+mmo7o+jI6U8Fp7hDl+X5Q1k45WH+d4dSlbLVUETGQ+hfkNgvLQGyoElPj837nZyv4MBc0VA=="], "vscode-jsonrpc": ["vscode-jsonrpc@10.0.0", "", {}, "sha512-USSfxg0vDIpTQqlQBGxF3mBARyoS8cU7LHLpbYNh6dOJt32dlskxPWB0Irrh5TllTmyf7oNLAvqDqzklbwwnpA=="], @@ -636,7 +636,7 @@ "vscode-uri": ["vscode-uri@3.2.0", "", {}, "sha512-m2gXo3bn0G1kT9InzMf07fTbqMbGtyckj3bH5ktLO+1Ssv+yiATZ4dhwaQv9UZWxJh6E9IFGnQyjgWVDWVBDrg=="], - "vue": ["vue@3.5.44", "", { "dependencies": { "@vue/compiler-dom": "3.5.44", "@vue/compiler-sfc": "3.5.44", "@vue/runtime-dom": "3.5.44", "@vue/server-renderer": "3.5.44", "@vue/shared": "3.5.44" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-dvvmaOCG7lAMkJTPqPwWeGMsPS6DIsFsfZDUgBp9V6mHf9b3+/wyVwYR4Es+WV6uJC0FKRxp81Obcq0ZwUcGFQ=="], + "vue": ["vue@3.5.45", "", { "dependencies": { "@vue/compiler-dom": "3.5.45", "@vue/compiler-sfc": "3.5.45", "@vue/runtime-dom": "3.5.45", "@vue/server-renderer": "3.5.45", "@vue/shared": "3.5.45" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-3cvsib8m9c0vB24+MDKsgcW4+Da4xaBl9Z/5Tbm2pmSIsRPxcil4cdXjXlVsmJ2g64XJo5h+IRd7rsdGT9pZaw=="], "vue-tippy": ["vue-tippy@6.8.0", "", { "dependencies": { "tippy.js": "^6.3.7" }, "peerDependencies": { "vue": "^3.2.0" } }, "sha512-NiWpwncli2vXpestHyvuFPDgQ51qLbBKpZQj7FeSOm1mFbPhzMeaRxWBQQEhl/FqGRXasfGvETgAUW4md6SAbA=="], diff --git a/src/renderer/changelog.json b/src/renderer/changelog.json index eace1538..d194d355 100644 --- a/src/renderer/changelog.json +++ b/src/renderer/changelog.json @@ -1,4 +1,13 @@ [ + { + "version": "1.20261106.0", + "changes": [ + { + "type": "improved", + "description": "A file opened using a data URL can now be reopened, using File|Reopen, once closed (but only during the current session)." + } + ] + }, { "version": "1.20261105.1", "changes": [ diff --git a/src/renderer/package.json b/src/renderer/package.json index 19abfbce..497d65ae 100644 --- a/src/renderer/package.json +++ b/src/renderer/package.json @@ -42,7 +42,7 @@ }, "./style.css": "./dist/opencor.css" }, - "version": "1.20261105.1", + "version": "1.20261106.0", "libopencorVersion": "1.20261029.0", "scripts": { "build": "vite build && bun scripts/generate.version.ts", @@ -59,7 +59,7 @@ "version:new": "bun scripts/version.new.ts" }, "peerDependencies": { - "vue": "^3.5.44" + "vue": "^3.5.45" }, "devDependencies": { "@biomejs/biome": "^2.6.5", @@ -92,8 +92,8 @@ "typescript-6": "npm:typescript@^6.0.3", "unplugin-vue-components": "^32.1.0", "uplot": "1.6.32", - "vite": "^8.3.5", - "vue": "^3.5.44", + "vite": "^8.3.6", + "vue": "^3.5.45", "vue-tsc": "^3.3.14", "xxhash-wasm": "^1.1.0" } diff --git a/src/renderer/src/common/common.ts b/src/renderer/src/common/common.ts index 85a4fa25..c8084649 100644 --- a/src/renderer/src/common/common.ts +++ b/src/renderer/src/common/common.ts @@ -80,8 +80,8 @@ export const enableDisableMainMenu = (enable: boolean): void => { // Some prefixes for the (made-up) name of a file opened using a data URL, and a method to determine whether a file name // is such a name. -// Note #1: a file opened using a data URL only exists in memory, so it cannot be reopened (e.g., the next time OpenCOR -// is started) and must therefore not be added to our recent files either. +// Note #1: a file opened using a data URL only exists in memory, so it can only be reopened during the current session +// (see OpenCOR.addRecentFile()), i.e. not, say, the next time OpenCOR is started. // Note #2: a COMBINE archive is named "OMEX #" while a CellML file (from a ZIP-CellML data URL) is named // "CellML #/" where is the name of the CellML file, so that it is still shown under // its own name (see common.fileName()). diff --git a/src/renderer/src/common/electronApi.ts b/src/renderer/src/common/electronApi.ts index 95a86550..8793cdf1 100644 --- a/src/renderer/src/common/electronApi.ts +++ b/src/renderer/src/common/electronApi.ts @@ -48,6 +48,7 @@ interface IElectronApi { onCanonicaliseRecentFilePaths: (callback: (filePaths: string[]) => void) => () => void; onChangelog: (callback: () => void) => () => void; onCheckForUpdates: (callback: () => void) => () => void; + onClearRecentFiles: (callback: () => void) => () => void; onEnableDisableUi: (callback: (enable: boolean) => void) => () => void; onOpen: (callback: (filePath: string) => void) => () => void; onOpenRemote: (callback: () => void) => () => void; diff --git a/src/renderer/src/components/OpenCOR.vue b/src/renderer/src/components/OpenCOR.vue index 316f64c2..537fe756 100644 --- a/src/renderer/src/components/OpenCOR.vue +++ b/src/renderer/src/components/OpenCOR.vue @@ -1128,6 +1128,10 @@ interface IFileInfo { file: locApi.File | null; filePath: string; canonicalFilePath: string; + dataUrlContents?: Uint8Array; + // Note: the contents of the file if it was opened using a data URL (undefined otherwise). We keep them because such a + // file only exists in memory, so they are what we need to reopen it once it has been closed (see + // addRecentFile()). } // A helper function to describe a file for which we don't (yet) have a file path. @@ -1170,10 +1174,14 @@ const reportFileIssue = (filePath: string, issueMessage: string, canonicalFilePa emit('file', { type: 'issue', filePath, issues: [issueMessage] }); }; -const processFile = async (fileFilePathOrFileContents: string | Uint8Array | File): Promise => { +const processFile = async ( + fileFilePathOrFileContents: string | Uint8Array | File, + dataUrlFileName: string = '' +): Promise => { // Check whether we were passed a ZIP-CellML data URL. + // Note: we may also have been passed the contents of a file that was opened using a data URL and then closed, along + // with its name, so that it can be reopened under that name (see onReopenMenu() and electronApi.onOpen()). - let cellmlDataUrlFileName: string = ''; let omexDataUrlCounter: number = 0; const zipCellmlDataUriInfo = await locCommon.zipCellmlDataUrl(fileFilePathOrFileContents); @@ -1192,7 +1200,7 @@ const processFile = async (fileFilePathOrFileContents: string | Uint8Array | Fil const zipCellmlFileName = zipCellmlDataUriInfo.fileName as string; - cellmlDataUrlFileName = `${common.CELLML_PREFIX}${common.nextCellmlDataUrlNumber()}/${common.fileName(zipCellmlFileName) || zipCellmlFileName}`; + dataUrlFileName = `${common.CELLML_PREFIX}${common.nextCellmlDataUrlNumber()}/${common.fileName(zipCellmlFileName) || zipCellmlFileName}`; fileFilePathOrFileContents = zipCellmlDataUriInfo.data as Uint8Array; } else { // Check whether we were passed a COMBINE archive data URL. @@ -1228,7 +1236,7 @@ const processFile = async (fileFilePathOrFileContents: string | Uint8Array | Fil let fileIsOpen = false; try { - filePath = locCommon.filePath(fileFilePathOrFileContents, cellmlDataUrlFileName, omexDataUrlCounter); + filePath = locCommon.filePath(fileFilePathOrFileContents, dataUrlFileName, omexDataUrlCounter); canonicalFilePath = locApi.fileManager.canonicalPath(filePath); fileIsOpen = contentsRef.value?.hasFile(filePath) ?? false; } catch (error: unknown) { @@ -1268,7 +1276,7 @@ const processFile = async (fileFilePathOrFileContents: string | Uint8Array | Fil let fileToBeOpened = false; try { - file = await locCommon.file(fileFilePathOrFileContents, cellmlDataUrlFileName, omexDataUrlCounter); + file = await locCommon.file(fileFilePathOrFileContents, dataUrlFileName, omexDataUrlCounter); // Note: to retrieve the type of the file may fail (e.g., if libOpenCOR throws an exception), in which case we must // release the file since nothing else would (see the catch block below). @@ -1297,7 +1305,11 @@ const processFile = async (fileFilePathOrFileContents: string | Uint8Array | Fil alreadyOpen: false, file, filePath, - canonicalFilePath + canonicalFilePath, + dataUrlContents: + (dataUrlFileName || omexDataUrlCounter) && fileFilePathOrFileContents instanceof Uint8Array + ? fileFilePathOrFileContents + : undefined }; } catch (error: unknown) { try { @@ -1422,8 +1434,12 @@ const openProcessedFile = async ( fileFilePathOrFileContents instanceof File ? fileHandles.get(fileFilePathOrFileContents) : undefined ); - if (fileFilePathOrFileContents instanceof File && contentsRef.value?.hasFile(fileInfo.filePath)) { - openedFiles.set(fileInfo.filePath, fileFilePathOrFileContents); + const source = + fileInfo.dataUrlContents ?? + (fileFilePathOrFileContents instanceof File ? fileFilePathOrFileContents : undefined); + + if (source && contentsRef.value?.hasFile(fileInfo.filePath)) { + openedFiles.set(fileInfo.filePath, source); } } } catch (error: unknown) { @@ -1437,8 +1453,8 @@ const openProcessedFile = async ( } }; -const openFile = (fileFilePathOrFileContents: string | Uint8Array | File): void => { - void openProcessedFile(fileFilePathOrFileContents, processFile(fileFilePathOrFileContents), false); +const openFile = (fileFilePathOrFileContents: string | Uint8Array | File, dataUrlFileName: string = ''): void => { + void openProcessedFile(fileFilePathOrFileContents, processFile(fileFilePathOrFileContents, dataUrlFileName), false); }; const openFiles = (filesFilePathsOrFileContents: (string | Uint8Array | File)[]): void => { @@ -1574,7 +1590,18 @@ const onDragLeave = (): void => { trackCleanup( electronApi?.onOpen((filePath: string) => { handleExternalRequest(() => { - openFile(filePath); + // Note: our main process may ask us to reopen a file that was opened using a data URL (through its File|Reopen + // menu), in which case we reopen it using the contents that we kept for it (see addRecentFile()). + + const closedDataUrlFile = closedDataUrlFiles.get(filePath); + + if (closedDataUrlFile) { + closedDataUrlFiles.delete(filePath); + + openFile(closedDataUrlFile.contents, closedDataUrlFile.filePath); + } else { + openFile(filePath); + } }); }) ); @@ -1656,14 +1683,25 @@ const onOpenSampleLorenzMenu = (): void => { // to do it for OpenCOR's Web app, and only for the current session. // Note #2: in OpenCOR's Web app, a local file is only known by its name, so we keep track of what is needed to reopen // it, i.e. its file handle, if any (see droppedFileHandle() and ContentsComponent.doSaveFileAs()), or its File -// object. A file opened using a data URL or some contents cannot be reopened (see common.isDataUrlFileName()). +// object. Similarly, a file opened using a data URL only exists in memory (see common.isDataUrlFileName()), so +// we keep track of its contents, so that it can be reopened under the same name, but only for the current +// session, including in OpenCOR where we keep them for the main process to ask us to reopen the file (see +// closedDataUrlFiles). A file opened using some contents cannot be reopened. interface IRecentFile { filePath: string; - source: string | File | FileSystemFileHandle; + source: string | File | FileSystemFileHandle | Uint8Array; +} + +interface IClosedDataUrlFile { + filePath: string; + contents: Uint8Array; } -const openedFiles = new Map(); +const openedFiles = new Map(); +const closedDataUrlFiles = new Map(); +// Note: keyed by the canonical path of the file since that is what our main process refers to it by. It only lists up +// to MAX_RECENT_FILES files, so we never need to keep more than that. const recentFiles = vue.shallowRef([]); const recentFilePaths = vue.computed(() => { return recentFiles.value.map((recentFile) => recentFile.filePath); @@ -1680,7 +1718,36 @@ const addRecentFile = (filePath: string, fileHandle?: FileSystemFileHandle): voi openedFiles.delete(filePath); - if (!isFullWebApp.value || common.isDataUrlFileName(filePath)) { + if (electronApi) { + if (!(file instanceof Uint8Array)) { + return; + } + + let canonicalFilePath: string; + + try { + canonicalFilePath = locApi.fileManager.canonicalPath(filePath); + } catch (error: unknown) { + console.warn(`OpenCOR: failed to canonicalise a file path (${filePath}):`, common.formatError(error)); + + return; + } + + closedDataUrlFiles.delete(canonicalFilePath); + closedDataUrlFiles.set(canonicalFilePath, { filePath, contents: file }); + + for (const oldCanonicalFilePath of closedDataUrlFiles.keys()) { + if (closedDataUrlFiles.size <= MAX_RECENT_FILES) { + break; + } + + closedDataUrlFiles.delete(oldCanonicalFilePath); + } + + return; + } + + if (!isFullWebApp.value) { return; } @@ -1705,6 +1772,12 @@ const onReopenMenu = async (filePath: string): Promise => { removeRecentFile(filePath); + if (recentFile.source instanceof Uint8Array) { + openFile(recentFile.source, filePath); + + return; + } + if (typeof recentFile.source === 'string' || recentFile.source instanceof File) { openFile(recentFile.source); @@ -1729,6 +1802,15 @@ const onReopenMenu = async (filePath: string): Promise => { openFile(file); }; +// Note: in OpenCOR, our main process clears its recent files itself, but it lets us know about it so that we can +// release the contents of the files that were opened using a data URL and then closed (see addRecentFile()). + +trackCleanup( + electronApi?.onClearRecentFiles(() => { + closedDataUrlFiles.clear(); + }) +); + const onClearRecentFilesMenu = (): void => { recentFiles.value = []; };