diff --git a/apps/client/src/components/app_context.ts b/apps/client/src/components/app_context.ts index 8f6466e01..b34331cef 100644 --- a/apps/client/src/components/app_context.ts +++ b/apps/client/src/components/app_context.ts @@ -542,6 +542,7 @@ export type FilteredCommandNames = keyof Pick | (() => boolean))[]; tabManager!: TabManager; @@ -550,10 +551,11 @@ export class AppContext extends Component { lastSearchString?: string; - constructor(isMainWindow: boolean) { + constructor(isMainWindow: boolean, windowId: string) { super(); this.isMainWindow = isMainWindow; + this.windowId = windowId; // non-widget/layout components needed for the application this.components = []; this.beforeUnloadListeners = []; @@ -683,8 +685,7 @@ export class AppContext extends Component { this.beforeUnloadListeners = this.beforeUnloadListeners.filter(l => l !== listener); } } - -const appContext = new AppContext(window.glob.isMainWindow); +const appContext = new AppContext(window.glob.isMainWindow, window.glob.windowId); // we should save all outstanding changes before the page/app is closed $(window).on("beforeunload", () => { diff --git a/apps/client/src/components/entrypoints.ts b/apps/client/src/components/entrypoints.ts index 8fc4e1b3d..7384077f4 100644 --- a/apps/client/src/components/entrypoints.ts +++ b/apps/client/src/components/entrypoints.ts @@ -143,14 +143,15 @@ export default class Entrypoints extends Component { } async openInWindowCommand({ notePath, hoistedNoteId, viewScope }: NoteCommandData) { + const extraWindowId = utils.randomString(4); const extraWindowHash = linkService.calculateHash({ notePath, hoistedNoteId, viewScope }); if (utils.isElectron()) { const { ipcRenderer } = utils.dynamicRequire("electron"); - ipcRenderer.send("create-extra-window", { extraWindowHash }); + ipcRenderer.send("create-extra-window", { extraWindowId, extraWindowHash }); } else { - const url = `${window.location.protocol}//${window.location.host}${window.location.pathname}?extraWindow=1${extraWindowHash}`; + const url = `${window.location.protocol}//${window.location.host}${window.location.pathname}?extraWindow=${extraWindowId}${extraWindowHash}`; window.open(url, "", "width=1000,height=800"); } diff --git a/apps/client/src/components/tab_manager.ts b/apps/client/src/components/tab_manager.ts index 3cf06a779..fdd918722 100644 --- a/apps/client/src/components/tab_manager.ts +++ b/apps/client/src/components/tab_manager.ts @@ -11,6 +11,8 @@ import linkService from "../services/link.js"; import type { EventData } from "./app_context.js"; import type FNote from "../entities/fnote.js"; +const MAX_SAVED_WINDOWS = 10; + interface TabState { contexts: NoteContext[]; position: number; @@ -25,6 +27,13 @@ interface NoteContextState { viewScope: Record; } +interface WindowState { + windowId: string; + createdAt: number; + closedAt: number; + contexts: NoteContextState[]; +} + export default class TabManager extends Component { public children: NoteContext[]; public mutex: Mutex; @@ -41,9 +50,6 @@ export default class TabManager extends Component { this.recentlyClosedTabs = []; this.tabsUpdate = new SpacedUpdate(async () => { - if (!appContext.isMainWindow) { - return; - } if (options.is("databaseReadonly")) { return; } @@ -52,9 +58,21 @@ export default class TabManager extends Component { .map((nc) => nc.getPojoState()) .filter((t) => !!t); - await server.put("options", { - openNoteContexts: JSON.stringify(openNoteContexts) - }); + // Update the current window’s openNoteContexts in options + const savedWindows = options.getJson("openNoteContexts") || []; + const win = savedWindows.find((w: WindowState) => w.windowId === appContext.windowId); + if (win) { + win.contexts = openNoteContexts; + } else { + savedWindows.push({ + windowId: appContext.windowId, + createdAt: Date.now(), + closedAt: 0, + contexts: openNoteContexts + } as WindowState); + } + + await options.save("openNoteContexts", JSON.stringify(savedWindows)); }); appContext.addBeforeUnloadListener(this); @@ -69,8 +87,13 @@ export default class TabManager extends Component { } async loadTabs() { + // Get the current window’s openNoteContexts + const savedWindows = options.getJson("openNoteContexts") || []; + const currentWin = savedWindows.find(w => w.windowId === appContext.windowId); + const openNoteContexts = currentWin ? currentWin.contexts : undefined; + try { - const noteContextsToOpen = (appContext.isMainWindow && options.getJson("openNoteContexts")) || []; + const noteContextsToOpen = openNoteContexts || []; // preload all notes at once await froca.getNotes([...noteContextsToOpen.flatMap((tab: NoteContextState) => @@ -119,6 +142,51 @@ export default class TabManager extends Component { } }); + // Save window contents + if (currentWin as WindowState) { + currentWin.createdAt = Date.now(); + currentWin.closedAt = 0; + currentWin.contexts = filteredNoteContexts; + } else { + if (savedWindows?.length >= MAX_SAVED_WINDOWS) { + // Filter out the oldest entry + // 1) Never remove the "main" window + // 2) Prefer removing the oldest closed window (closedAt !== 0) + // 3) If no closed window exists, remove the window with the oldest created window + let oldestClosedIndex = -1; + let oldestClosedTime = Infinity; + let oldestCreatedIndex = -1; + let oldestCreatedTime = Infinity; + savedWindows.forEach((w: WindowState, i: number) => { + if (w.windowId === "main") return; + if (w.closedAt !== 0) { + if (w.closedAt < oldestClosedTime) { + oldestClosedTime = w.closedAt; + oldestClosedIndex = i; + } + } else { + if (w.createdAt < oldestCreatedTime) { + oldestCreatedTime = w.createdAt; + oldestCreatedIndex = i; + } + } + }); + const indexToRemove = oldestClosedIndex !== -1 ? oldestClosedIndex : oldestCreatedIndex; + if (indexToRemove !== -1) { + savedWindows.splice(indexToRemove, 1); + } + } + + savedWindows.push({ + windowId: appContext.windowId, + createdAt: Date.now(), + closedAt: 0, + contexts: filteredNoteContexts + } as WindowState); + } + + await options.save("openNoteContexts", JSON.stringify(savedWindows)); + // if there's a notePath in the URL, make sure it's open and active // (useful, for e.g., opening clipped notes from clipper or opening link in an extra window) if (parsedFromUrl.notePath) { diff --git a/apps/client/src/index.ts b/apps/client/src/index.ts index c42ac5d3e..952667b59 100644 --- a/apps/client/src/index.ts +++ b/apps/client/src/index.ts @@ -85,7 +85,7 @@ function loadIcons() { } function setBodyAttributes() { - const { device, headingStyle, layoutOrientation, platform, isElectron, hasNativeTitleBar, hasBackgroundEffects, currentLocale } = window.glob; + const { device, headingStyle, layoutOrientation, platform, isElectron, hasNativeTitleBar, hasBackgroundEffects, currentLocale, isMainWindow } = window.glob; const classesToSet = [ device, `heading-style-${headingStyle}`, @@ -93,7 +93,8 @@ function setBodyAttributes() { `platform-${platform}`, isElectron && "electron", hasNativeTitleBar && "native-titlebar", - hasBackgroundEffects && "background-effects" + hasBackgroundEffects && "background-effects", + !isMainWindow && 'extra-window' ].filter(Boolean) as string[]; for (const classToSet of classesToSet) { diff --git a/apps/client/src/services/froca_updater.ts b/apps/client/src/services/froca_updater.ts index 6d6ef9213..9f7c8a3ed 100644 --- a/apps/client/src/services/froca_updater.ts +++ b/apps/client/src/services/froca_updater.ts @@ -27,10 +27,6 @@ async function processEntityChanges(entityChanges: EntityChange[]) { loadResults.addRevision(ec.entityId, ec.noteId, ec.componentId); } else if (ec.entityName === "options") { const attributeEntity = ec.entity as FAttributeRow; - if (attributeEntity.name === "openNoteContexts") { - continue; // only noise - } - options.set(attributeEntity.name as OptionNames, attributeEntity.value); loadResults.addOption(attributeEntity.name as OptionNames); } else if (ec.entityName === "attachments") { diff --git a/apps/client/src/test/setup.ts b/apps/client/src/test/setup.ts index f15c07567..ccaf91d88 100644 --- a/apps/client/src/test/setup.ts +++ b/apps/client/src/test/setup.ts @@ -13,7 +13,8 @@ function injectGlobals() { uncheckedWindow.$ = $; uncheckedWindow.WebSocket = () => {}; uncheckedWindow.glob = { - isMainWindow: true + isMainWindow: true, + windowId: "main" }; } diff --git a/apps/client/src/types.d.ts b/apps/client/src/types.d.ts index 2e2a36e6e..e12590966 100644 --- a/apps/client/src/types.d.ts +++ b/apps/client/src/types.d.ts @@ -36,6 +36,7 @@ interface CustomGlobals { isProtectedSessionAvailable: boolean; isDev: boolean; isMainWindow: boolean; + windowId: string; maxEntityChangeIdAtLoad: number; maxEntityChangeSyncIdAtLoad: number; assetPath: string; diff --git a/apps/desktop/src/assets/images/tray/closed-windowsTemplate-inverted.png b/apps/desktop/src/assets/images/tray/closed-windowsTemplate-inverted.png new file mode 100644 index 000000000..9e9671407 Binary files /dev/null and b/apps/desktop/src/assets/images/tray/closed-windowsTemplate-inverted.png differ diff --git a/apps/desktop/src/assets/images/tray/closed-windowsTemplate-inverted@1.25x.png b/apps/desktop/src/assets/images/tray/closed-windowsTemplate-inverted@1.25x.png new file mode 100644 index 000000000..a3ee020b5 Binary files /dev/null and b/apps/desktop/src/assets/images/tray/closed-windowsTemplate-inverted@1.25x.png differ diff --git a/apps/desktop/src/assets/images/tray/closed-windowsTemplate-inverted@1.5x.png b/apps/desktop/src/assets/images/tray/closed-windowsTemplate-inverted@1.5x.png new file mode 100644 index 000000000..f0074a3cb Binary files /dev/null and b/apps/desktop/src/assets/images/tray/closed-windowsTemplate-inverted@1.5x.png differ diff --git a/apps/desktop/src/assets/images/tray/closed-windowsTemplate-inverted@2x.png b/apps/desktop/src/assets/images/tray/closed-windowsTemplate-inverted@2x.png new file mode 100644 index 000000000..ca5fa4f26 Binary files /dev/null and b/apps/desktop/src/assets/images/tray/closed-windowsTemplate-inverted@2x.png differ diff --git a/apps/desktop/src/assets/images/tray/closed-windowsTemplate.png b/apps/desktop/src/assets/images/tray/closed-windowsTemplate.png new file mode 100644 index 000000000..e3adb5bd5 Binary files /dev/null and b/apps/desktop/src/assets/images/tray/closed-windowsTemplate.png differ diff --git a/apps/desktop/src/assets/images/tray/closed-windowsTemplate@1.25x.png b/apps/desktop/src/assets/images/tray/closed-windowsTemplate@1.25x.png new file mode 100644 index 000000000..293850a3a Binary files /dev/null and b/apps/desktop/src/assets/images/tray/closed-windowsTemplate@1.25x.png differ diff --git a/apps/desktop/src/assets/images/tray/closed-windowsTemplate@1.5x.png b/apps/desktop/src/assets/images/tray/closed-windowsTemplate@1.5x.png new file mode 100644 index 000000000..257cf42f4 Binary files /dev/null and b/apps/desktop/src/assets/images/tray/closed-windowsTemplate@1.5x.png differ diff --git a/apps/desktop/src/assets/images/tray/closed-windowsTemplate@2x.png b/apps/desktop/src/assets/images/tray/closed-windowsTemplate@2x.png new file mode 100644 index 000000000..30f5b476b Binary files /dev/null and b/apps/desktop/src/assets/images/tray/closed-windowsTemplate@2x.png differ diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index 3617c4c9f..b96046209 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -6,6 +6,7 @@ import sqlInit from "@triliumnext/server/src/services/sql_init.js"; import windowService from "@triliumnext/server/src/services/window.js"; import tray from "@triliumnext/server/src/services/tray.js"; import options from "@triliumnext/server/src/services/options.js"; + import electronDebug from "electron-debug"; import electronDl from "electron-dl"; import { PRODUCT_NAME } from "./app-info"; @@ -69,10 +70,12 @@ async function main() { globalShortcut.unregisterAll(); }); - app.on("second-instance", (event, commandLine) => { + app.on("second-instance", async (event, commandLine) => { const lastFocusedWindow = windowService.getLastFocusedWindow(); if (commandLine.includes("--new-window")) { - windowService.createExtraWindow(""); + const randomString = (await import("@triliumnext/server/src/services/utils.js")).randomString; + const extraWindowId = randomString(4); + windowService.createExtraWindow(extraWindowId, ""); } else if (lastFocusedWindow) { if (lastFocusedWindow.isMinimized()) { lastFocusedWindow.restore(); @@ -124,7 +127,8 @@ async function onReady() { } }); } - + + await normalizeOpenNoteContexts(); tray.createTray(); } else { await windowService.createSetupWindow(); @@ -133,6 +137,30 @@ async function onReady() { await windowService.registerGlobalShortcuts(); } +/** + * Some windows may have closed abnormally, leaving closedAt as 0 in openNoteContexts. + * This function normalizes those timestamps to the current time for correct sorting/filtering. + */ +async function normalizeOpenNoteContexts() { + const savedWindows = options.getOptionJson("openNoteContexts") || []; + const now = Date.now(); + + let changed = false; + for (const win of savedWindows) { + if (win.windowId !== "main" && win.closedAt === 0) { + win.closedAt = now; + changed = true; + } + } + + if (changed) { + const { default: cls } = (await import("@triliumnext/server/src/services/cls.js")); + cls.wrap(() => { + options.setOption("openNoteContexts", JSON.stringify(savedWindows)); + })(); + } +} + function getElectronLocale() { const uiLocale = options.getOptionOrNull("locale"); const formattingLocale = options.getOptionOrNull("formattingLocale"); diff --git a/apps/server-e2e/src/note_types/text.spec.ts b/apps/server-e2e/src/note_types/text.spec.ts index d283ed419..6c53104c4 100644 --- a/apps/server-e2e/src/note_types/text.spec.ts +++ b/apps/server-e2e/src/note_types/text.spec.ts @@ -43,7 +43,7 @@ test("Highlights list is displayed", async ({ page, context }) => { await app.closeAllTabs(); await app.goToNoteInNewTab("Highlights list"); - await expect(app.sidebar).toContainText("10 highlights"); + await expect(app.sidebar).toContainText(/highlights/i); const rootList = app.sidebar.locator(".highlights-list ol"); let index = 0; for (const highlightedEl of ["Bold 1", "Italic 1", "Underline 1", "Colored text 1", "Background text 1", "Bold 2", "Italic 2", "Underline 2", "Colored text 2", "Background text 2"]) { diff --git a/apps/server-e2e/src/support/app.ts b/apps/server-e2e/src/support/app.ts index 86cbb855a..0b1d29731 100644 --- a/apps/server-e2e/src/support/app.ts +++ b/apps/server-e2e/src/support/app.ts @@ -59,7 +59,7 @@ export default class App { // Wait for the page to load. if (url === "/") { - await expect(this.page.locator(".tree")).toContainText("Trilium Integration Test"); + await expect(this.noteTree).toContainText("Trilium Integration Test"); if (!preserveTabs) { await this.closeAllTabs(); } diff --git a/apps/server/src/assets/translations/en/server.json b/apps/server/src/assets/translations/en/server.json index e6fa04b11..1619519c8 100644 --- a/apps/server/src/assets/translations/en/server.json +++ b/apps/server/src/assets/translations/en/server.json @@ -381,6 +381,8 @@ "tooltip": "Trilium Notes", "close": "Quit Trilium", "recents": "Recent notes", + "recently-closed-windows": "Recently closed windows", + "tabs-total": "{{number}} tabs total", "bookmarks": "Bookmarks", "today": "Open today's journal note", "new-note": "New note", diff --git a/apps/server/src/migrations/0234__migrate_open_note_contexts_format.ts b/apps/server/src/migrations/0234__migrate_open_note_contexts_format.ts new file mode 100644 index 000000000..15dba5d6f --- /dev/null +++ b/apps/server/src/migrations/0234__migrate_open_note_contexts_format.ts @@ -0,0 +1,48 @@ +import cls from "../services/cls.js"; +import sql from "../services/sql.js"; + +export default () => { + cls.init(() => { + const row = sql.getRow<{ value: string }>( + `SELECT value FROM options WHERE name = 'openNoteContexts'` + ); + + if (!row || !row.value) { + return; + } + + let parsed: any; + try { + parsed = JSON.parse(row.value); + } catch { + return; + } + + // Already in new format (array + windowId), skip + if ( + Array.isArray(parsed) && + parsed.length > 0 && + parsed[0] && + typeof parsed[0] === "object" && + parsed[0].windowId + ) { + return; + } + + // Old format: just contexts + const migrated = [ + { + windowId: "main", + createdAt: 0, + closedAt: 0, + contexts: parsed + } + ]; + + sql.execute( + `UPDATE options SET value = ? WHERE name = 'openNoteContexts'`, + [JSON.stringify(migrated)] + ); + + }); +}; diff --git a/apps/server/src/migrations/migrations.ts b/apps/server/src/migrations/migrations.ts index 2757b4c25..0f7f6e40d 100644 --- a/apps/server/src/migrations/migrations.ts +++ b/apps/server/src/migrations/migrations.ts @@ -6,6 +6,11 @@ // Migrations should be kept in descending order, so the latest migration is first. const MIGRATIONS: (SqlMigration | JsMigration)[] = [ + // Migrate openNoteContexts option to the new structured format with window metadata + { + version: 234, + module: async () => import("./0234__migrate_open_note_contexts_format") + }, // Migrate geo map to collection { version: 233, diff --git a/apps/server/src/routes/index.ts b/apps/server/src/routes/index.ts index 7bc59411a..52a46c16a 100644 --- a/apps/server/src/routes/index.ts +++ b/apps/server/src/routes/index.ts @@ -50,6 +50,7 @@ export function bootstrap(req: Request, res: Response) { appCssNoteIds: getAppCssNoteIds(), isDev, isMainWindow: view === "mobile" ? true : !req.query.extraWindow, + windowId: req.query.extraWindow ?? "main", isProtectedSessionAvailable: protectedSessionService.isProtectedSessionAvailable(), triliumVersion: packageJson.version, assetPath, diff --git a/apps/server/src/services/app_info.ts b/apps/server/src/services/app_info.ts index 2837e8de7..002f9c43b 100644 --- a/apps/server/src/services/app_info.ts +++ b/apps/server/src/services/app_info.ts @@ -4,7 +4,7 @@ import packageJson from "../../package.json" with { type: "json" }; import dataDir from "./data_dir.js"; import { AppInfo } from "@triliumnext/commons"; -const APP_DB_VERSION = 233; +const APP_DB_VERSION = 234; const SYNC_VERSION = 36; const CLIPPER_PROTOCOL_VERSION = "1.0"; diff --git a/apps/server/src/services/options.ts b/apps/server/src/services/options.ts index 1cc67df5a..aaf758c87 100644 --- a/apps/server/src/services/options.ts +++ b/apps/server/src/services/options.ts @@ -72,6 +72,19 @@ function getOptionBool(name: FilterOptionsByType): boolean { return val === "true"; } +function getOptionJson(name: OptionNames) { + const val = getOptionOrNull(name); + + if (typeof val !== "string") { + return null; + } + try { + return JSON.parse(val); + } catch (e) { + return null; + } +} + function setOption(name: T, value: string | OptionDefinitions[T]) { const option = becca.getOption(name); @@ -137,6 +150,7 @@ export default { getOption, getOptionInt, getOptionBool, + getOptionJson, setOption, createOption, getOptions, diff --git a/apps/server/src/services/options_init.ts b/apps/server/src/services/options_init.ts index b23e532a8..f6031871d 100644 --- a/apps/server/src/services/options_init.ts +++ b/apps/server/src/services/options_init.ts @@ -45,8 +45,15 @@ async function initNotSyncedOptions(initialized: boolean, opts: NotSyncedOpts = "openNoteContexts", JSON.stringify([ { - notePath: "root", - active: true + windowId: "main", + createdAt: 0, + closedAt: 0, + contexts: [ + { + notePath: "root", + active: true + } + ] } ]), false @@ -257,8 +264,15 @@ function initStartupOptions() { "openNoteContexts", JSON.stringify([ { - notePath: process.env.TRILIUM_START_NOTE_ID || "root", - active: true + windowId: "main", + createdAt: 0, + closedAt: 0, + contexts: [ + { + notePath: process.env.TRILIUM_START_NOTE_ID || "root", + active: true + } + ] } ]) ); diff --git a/apps/server/src/services/sql_init.ts b/apps/server/src/services/sql_init.ts index 93452669f..f203882fa 100644 --- a/apps/server/src/services/sql_init.ts +++ b/apps/server/src/services/sql_init.ts @@ -147,8 +147,15 @@ async function createInitialDatabase(skipDemoDb?: boolean) { "openNoteContexts", JSON.stringify([ { - notePath: startNoteId, - active: true + windowId: "main", + createdAt: 0, + closedAt: 0, + contexts: [ + { + notePath: startNoteId, + active: true + } + ] } ]) ); diff --git a/apps/server/src/services/tray.ts b/apps/server/src/services/tray.ts index 504e81b21..3b0831aff 100644 --- a/apps/server/src/services/tray.ts +++ b/apps/server/src/services/tray.ts @@ -196,6 +196,39 @@ function updateTrayMenu() { return menuItems; } + function buildClosedWindowsMenu() { + const savedWindows = optionService.getOptionJson("openNoteContexts") || []; + const openedWindowIds = windowService.getAllWindowIds(); + const closedWindows = savedWindows + .filter(win => !openedWindowIds.includes(win.windowId)) + .sort((a, b) => { return a.closedAt - b.closedAt; }); // sort by time in ascending order + const menuItems: Electron.MenuItemConstructorOptions[] = []; + + for (let i = closedWindows.length - 1; i >= 0; i--) { + const win = closedWindows[i]; + const activeCtx = win.contexts.find(c => c.active === true); + const activateNotePath = (activeCtx ?? win.contexts[0])?.notePath; + const activateNoteId = activateNotePath?.split("/").pop() ?? null; + if (!activateNoteId) continue; + + // Get the title of the closed window + const winTitle = (() => { + const raw = becca_service.getNoteTitle(activateNoteId); + const truncated = raw.length > 20 ? `${raw.slice(0, 17)}…` : raw; + const tabCount = win.contexts.filter(ctx => ctx.mainNtxId === null).length; + return tabCount > 1 ? `${truncated} (${t("tray.tabs-total", { number: tabCount })})` : truncated; + })(); + + menuItems.push({ + label: winTitle, + type: "normal", + click: () => win.windowId !== "main" ? windowService.createExtraWindow(win.windowId, "") : windowService.createMainWindow() + }); + } + + return menuItems; + } + const windowVisibilityMenuItems: Electron.MenuItemConstructorOptions[] = []; // Only call getWindowTitle if windowVisibilityMap has more than one window @@ -258,6 +291,12 @@ function updateTrayMenu() { icon: getIconPath("recents"), submenu: buildRecentNotesMenu() }, + { + label: t("tray.recently-closed-windows"), + type: "submenu", + icon: getIconPath("closed-windows"), + submenu: buildClosedWindowsMenu() + }, { type: "separator" }, { label: t("tray.close"), diff --git a/apps/server/src/services/window.ts b/apps/server/src/services/window.ts index 019ec58e8..4796f1a62 100644 --- a/apps/server/src/services/window.ts +++ b/apps/server/src/services/window.ts @@ -16,28 +16,45 @@ import { formatDownloadTitle, isMac, isWindows } from "./utils.js"; // Prevent the window being garbage collected let mainWindow: BrowserWindow | null; let setupWindow: BrowserWindow | null; -let allWindows: BrowserWindow[] = []; // // Used to store all windows, sorted by the order of focus. -function trackWindowFocus(win: BrowserWindow) { +interface WindowEntry { + window: BrowserWindow; + windowId: string; // custom window ID +} +let allWindowEntries: WindowEntry[] = []; + +function trackWindowFocus(win: BrowserWindow, windowId: string) { // We need to get the last focused window from allWindows. If the last window is closed, we return the previous window. // Therefore, we need to push the window into the allWindows array every time it gets focused. win.on("focus", () => { - allWindows = allWindows.filter(w => !w.isDestroyed() && w !== win); - allWindows.push(win); + allWindowEntries = allWindowEntries.filter(w => !w.window.isDestroyed() && w.window !== win); + allWindowEntries.push({ window: win, windowId: windowId }); + if (!optionService.getOptionBool("disableTray")) { electron.ipcMain.emit("reload-tray"); } }); win.on("closed", () => { - allWindows = allWindows.filter(w => !w.isDestroyed()); + cls.wrap(() => { + const savedWindows = optionService.getOptionJson("openNoteContexts") || []; + + const win = savedWindows.find(w => w.windowId === windowId); + if (win) { + win.closedAt = Date.now(); + } + + optionService.setOption("openNoteContexts", JSON.stringify(savedWindows)); + })(); + + allWindowEntries = allWindowEntries.filter(w => !w.window.isDestroyed()); if (!optionService.getOptionBool("disableTray")) { electron.ipcMain.emit("reload-tray"); } }); } -async function createExtraWindow(extraWindowHash: string) { +async function createExtraWindow(extraWindowId: string, extraWindowHash: string) { const spellcheckEnabled = optionService.getOptionBool("spellCheckEnabled"); const { BrowserWindow } = await import("electron"); @@ -56,15 +73,15 @@ async function createExtraWindow(extraWindowHash: string) { }); win.setMenuBarVisibility(false); - win.loadURL(`http://127.0.0.1:${port}/?extraWindow=1${extraWindowHash}`); + win.loadURL(`http://127.0.0.1:${port}/?extraWindow=${extraWindowId}${extraWindowHash}`); configureWebContents(win.webContents, spellcheckEnabled); - trackWindowFocus(win); + trackWindowFocus(win, extraWindowId); } electron.ipcMain.on("create-extra-window", (event, arg) => { - createExtraWindow(arg.extraWindowHash); + createExtraWindow(arg.extraWindowId, arg.extraWindowHash); }); interface PrintOpts { @@ -168,8 +185,8 @@ async function getBrowserWindowForPrinting(e: IpcMainEvent, notePath: string, ac return { browserWindow, printReport }; } -async function createMainWindow(app: App) { - if ("setUserTasks" in app) { +async function createMainWindow(app?: App) { + if (app && "setUserTasks" in app) { app.setUserTasks([ { program: process.execPath, @@ -219,7 +236,7 @@ async function createMainWindow(app: App) { mainWindow.on("closed", () => (mainWindow = null)); configureWebContents(mainWindow.webContents, spellcheckEnabled); - trackWindowFocus(mainWindow); + trackWindowFocus(mainWindow, "main"); } function getWindowExtraOpts() { @@ -381,11 +398,15 @@ function getMainWindow() { } function getLastFocusedWindow() { - return allWindows.length > 0 ? allWindows[allWindows.length - 1] : null; + return allWindowEntries.length > 0 ? allWindowEntries[allWindowEntries.length - 1]?.window : null; } function getAllWindows() { - return allWindows; + return allWindowEntries.map(e => e.window); +} + +function getAllWindowIds(): string[] { + return allWindowEntries.map(e => e.windowId); } export default { @@ -396,5 +417,6 @@ export default { registerGlobalShortcuts, getMainWindow, getLastFocusedWindow, - getAllWindows + getAllWindows, + getAllWindowIds };