Merge 91237918d894ea94f44241363b86036a9caa8ba0 into d639de03c3a20b5263a40f1f96c3ee4d4478fb8e
@ -542,6 +542,7 @@ export type FilteredCommandNames<T extends CommandData> = keyof Pick<CommandMapp
|
|||||||
|
|
||||||
export class AppContext extends Component {
|
export class AppContext extends Component {
|
||||||
isMainWindow: boolean;
|
isMainWindow: boolean;
|
||||||
|
windowId: string;
|
||||||
components: Component[];
|
components: Component[];
|
||||||
beforeUnloadListeners: (WeakRef<BeforeUploadListener> | (() => boolean))[];
|
beforeUnloadListeners: (WeakRef<BeforeUploadListener> | (() => boolean))[];
|
||||||
tabManager!: TabManager;
|
tabManager!: TabManager;
|
||||||
@ -550,10 +551,11 @@ export class AppContext extends Component {
|
|||||||
|
|
||||||
lastSearchString?: string;
|
lastSearchString?: string;
|
||||||
|
|
||||||
constructor(isMainWindow: boolean) {
|
constructor(isMainWindow: boolean, windowId: string) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.isMainWindow = isMainWindow;
|
this.isMainWindow = isMainWindow;
|
||||||
|
this.windowId = windowId;
|
||||||
// non-widget/layout components needed for the application
|
// non-widget/layout components needed for the application
|
||||||
this.components = [];
|
this.components = [];
|
||||||
this.beforeUnloadListeners = [];
|
this.beforeUnloadListeners = [];
|
||||||
@ -683,8 +685,7 @@ export class AppContext extends Component {
|
|||||||
this.beforeUnloadListeners = this.beforeUnloadListeners.filter(l => l !== listener);
|
this.beforeUnloadListeners = this.beforeUnloadListeners.filter(l => l !== listener);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const appContext = new AppContext(window.glob.isMainWindow, window.glob.windowId);
|
||||||
const appContext = new AppContext(window.glob.isMainWindow);
|
|
||||||
|
|
||||||
// we should save all outstanding changes before the page/app is closed
|
// we should save all outstanding changes before the page/app is closed
|
||||||
$(window).on("beforeunload", () => {
|
$(window).on("beforeunload", () => {
|
||||||
|
|||||||
@ -143,14 +143,15 @@ export default class Entrypoints extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async openInWindowCommand({ notePath, hoistedNoteId, viewScope }: NoteCommandData) {
|
async openInWindowCommand({ notePath, hoistedNoteId, viewScope }: NoteCommandData) {
|
||||||
|
const extraWindowId = utils.randomString(4);
|
||||||
const extraWindowHash = linkService.calculateHash({ notePath, hoistedNoteId, viewScope });
|
const extraWindowHash = linkService.calculateHash({ notePath, hoistedNoteId, viewScope });
|
||||||
|
|
||||||
if (utils.isElectron()) {
|
if (utils.isElectron()) {
|
||||||
const { ipcRenderer } = utils.dynamicRequire("electron");
|
const { ipcRenderer } = utils.dynamicRequire("electron");
|
||||||
|
|
||||||
ipcRenderer.send("create-extra-window", { extraWindowHash });
|
ipcRenderer.send("create-extra-window", { extraWindowId, extraWindowHash });
|
||||||
} else {
|
} 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");
|
window.open(url, "", "width=1000,height=800");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,6 +11,8 @@ import linkService from "../services/link.js";
|
|||||||
import type { EventData } from "./app_context.js";
|
import type { EventData } from "./app_context.js";
|
||||||
import type FNote from "../entities/fnote.js";
|
import type FNote from "../entities/fnote.js";
|
||||||
|
|
||||||
|
const MAX_SAVED_WINDOWS = 10;
|
||||||
|
|
||||||
interface TabState {
|
interface TabState {
|
||||||
contexts: NoteContext[];
|
contexts: NoteContext[];
|
||||||
position: number;
|
position: number;
|
||||||
@ -25,6 +27,13 @@ interface NoteContextState {
|
|||||||
viewScope: Record<string, any>;
|
viewScope: Record<string, any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface WindowState {
|
||||||
|
windowId: string;
|
||||||
|
createdAt: number;
|
||||||
|
closedAt: number;
|
||||||
|
contexts: NoteContextState[];
|
||||||
|
}
|
||||||
|
|
||||||
export default class TabManager extends Component {
|
export default class TabManager extends Component {
|
||||||
public children: NoteContext[];
|
public children: NoteContext[];
|
||||||
public mutex: Mutex;
|
public mutex: Mutex;
|
||||||
@ -41,9 +50,6 @@ export default class TabManager extends Component {
|
|||||||
this.recentlyClosedTabs = [];
|
this.recentlyClosedTabs = [];
|
||||||
|
|
||||||
this.tabsUpdate = new SpacedUpdate(async () => {
|
this.tabsUpdate = new SpacedUpdate(async () => {
|
||||||
if (!appContext.isMainWindow) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (options.is("databaseReadonly")) {
|
if (options.is("databaseReadonly")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -52,9 +58,21 @@ export default class TabManager extends Component {
|
|||||||
.map((nc) => nc.getPojoState())
|
.map((nc) => nc.getPojoState())
|
||||||
.filter((t) => !!t);
|
.filter((t) => !!t);
|
||||||
|
|
||||||
await server.put("options", {
|
// Update the current window’s openNoteContexts in options
|
||||||
openNoteContexts: JSON.stringify(openNoteContexts)
|
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);
|
appContext.addBeforeUnloadListener(this);
|
||||||
@ -69,8 +87,13 @@ export default class TabManager extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async loadTabs() {
|
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 {
|
try {
|
||||||
const noteContextsToOpen = (appContext.isMainWindow && options.getJson("openNoteContexts")) || [];
|
const noteContextsToOpen = openNoteContexts || [];
|
||||||
|
|
||||||
// preload all notes at once
|
// preload all notes at once
|
||||||
await froca.getNotes([...noteContextsToOpen.flatMap((tab: NoteContextState) =>
|
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
|
// 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)
|
// (useful, for e.g., opening clipped notes from clipper or opening link in an extra window)
|
||||||
if (parsedFromUrl.notePath) {
|
if (parsedFromUrl.notePath) {
|
||||||
|
|||||||
@ -85,7 +85,7 @@ function loadIcons() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setBodyAttributes() {
|
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 = [
|
const classesToSet = [
|
||||||
device,
|
device,
|
||||||
`heading-style-${headingStyle}`,
|
`heading-style-${headingStyle}`,
|
||||||
@ -93,7 +93,8 @@ function setBodyAttributes() {
|
|||||||
`platform-${platform}`,
|
`platform-${platform}`,
|
||||||
isElectron && "electron",
|
isElectron && "electron",
|
||||||
hasNativeTitleBar && "native-titlebar",
|
hasNativeTitleBar && "native-titlebar",
|
||||||
hasBackgroundEffects && "background-effects"
|
hasBackgroundEffects && "background-effects",
|
||||||
|
!isMainWindow && 'extra-window'
|
||||||
].filter(Boolean) as string[];
|
].filter(Boolean) as string[];
|
||||||
|
|
||||||
for (const classToSet of classesToSet) {
|
for (const classToSet of classesToSet) {
|
||||||
|
|||||||
@ -27,10 +27,6 @@ async function processEntityChanges(entityChanges: EntityChange[]) {
|
|||||||
loadResults.addRevision(ec.entityId, ec.noteId, ec.componentId);
|
loadResults.addRevision(ec.entityId, ec.noteId, ec.componentId);
|
||||||
} else if (ec.entityName === "options") {
|
} else if (ec.entityName === "options") {
|
||||||
const attributeEntity = ec.entity as FAttributeRow;
|
const attributeEntity = ec.entity as FAttributeRow;
|
||||||
if (attributeEntity.name === "openNoteContexts") {
|
|
||||||
continue; // only noise
|
|
||||||
}
|
|
||||||
|
|
||||||
options.set(attributeEntity.name as OptionNames, attributeEntity.value);
|
options.set(attributeEntity.name as OptionNames, attributeEntity.value);
|
||||||
loadResults.addOption(attributeEntity.name as OptionNames);
|
loadResults.addOption(attributeEntity.name as OptionNames);
|
||||||
} else if (ec.entityName === "attachments") {
|
} else if (ec.entityName === "attachments") {
|
||||||
|
|||||||
@ -13,7 +13,8 @@ function injectGlobals() {
|
|||||||
uncheckedWindow.$ = $;
|
uncheckedWindow.$ = $;
|
||||||
uncheckedWindow.WebSocket = () => {};
|
uncheckedWindow.WebSocket = () => {};
|
||||||
uncheckedWindow.glob = {
|
uncheckedWindow.glob = {
|
||||||
isMainWindow: true
|
isMainWindow: true,
|
||||||
|
windowId: "main"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
1
apps/client/src/types.d.ts
vendored
@ -36,6 +36,7 @@ interface CustomGlobals {
|
|||||||
isProtectedSessionAvailable: boolean;
|
isProtectedSessionAvailable: boolean;
|
||||||
isDev: boolean;
|
isDev: boolean;
|
||||||
isMainWindow: boolean;
|
isMainWindow: boolean;
|
||||||
|
windowId: string;
|
||||||
maxEntityChangeIdAtLoad: number;
|
maxEntityChangeIdAtLoad: number;
|
||||||
maxEntityChangeSyncIdAtLoad: number;
|
maxEntityChangeSyncIdAtLoad: number;
|
||||||
assetPath: string;
|
assetPath: string;
|
||||||
|
|||||||
|
After Width: | Height: | Size: 545 B |
|
After Width: | Height: | Size: 727 B |
|
After Width: | Height: | Size: 828 B |
|
After Width: | Height: | Size: 931 B |
BIN
apps/desktop/src/assets/images/tray/closed-windowsTemplate.png
Normal file
|
After Width: | Height: | Size: 292 B |
|
After Width: | Height: | Size: 355 B |
|
After Width: | Height: | Size: 434 B |
|
After Width: | Height: | Size: 492 B |
@ -6,6 +6,7 @@ import sqlInit from "@triliumnext/server/src/services/sql_init.js";
|
|||||||
import windowService from "@triliumnext/server/src/services/window.js";
|
import windowService from "@triliumnext/server/src/services/window.js";
|
||||||
import tray from "@triliumnext/server/src/services/tray.js";
|
import tray from "@triliumnext/server/src/services/tray.js";
|
||||||
import options from "@triliumnext/server/src/services/options.js";
|
import options from "@triliumnext/server/src/services/options.js";
|
||||||
|
|
||||||
import electronDebug from "electron-debug";
|
import electronDebug from "electron-debug";
|
||||||
import electronDl from "electron-dl";
|
import electronDl from "electron-dl";
|
||||||
import { PRODUCT_NAME } from "./app-info";
|
import { PRODUCT_NAME } from "./app-info";
|
||||||
@ -69,10 +70,12 @@ async function main() {
|
|||||||
globalShortcut.unregisterAll();
|
globalShortcut.unregisterAll();
|
||||||
});
|
});
|
||||||
|
|
||||||
app.on("second-instance", (event, commandLine) => {
|
app.on("second-instance", async (event, commandLine) => {
|
||||||
const lastFocusedWindow = windowService.getLastFocusedWindow();
|
const lastFocusedWindow = windowService.getLastFocusedWindow();
|
||||||
if (commandLine.includes("--new-window")) {
|
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) {
|
} else if (lastFocusedWindow) {
|
||||||
if (lastFocusedWindow.isMinimized()) {
|
if (lastFocusedWindow.isMinimized()) {
|
||||||
lastFocusedWindow.restore();
|
lastFocusedWindow.restore();
|
||||||
@ -124,7 +127,8 @@ async function onReady() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await normalizeOpenNoteContexts();
|
||||||
tray.createTray();
|
tray.createTray();
|
||||||
} else {
|
} else {
|
||||||
await windowService.createSetupWindow();
|
await windowService.createSetupWindow();
|
||||||
@ -133,6 +137,30 @@ async function onReady() {
|
|||||||
await windowService.registerGlobalShortcuts();
|
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() {
|
function getElectronLocale() {
|
||||||
const uiLocale = options.getOptionOrNull("locale");
|
const uiLocale = options.getOptionOrNull("locale");
|
||||||
const formattingLocale = options.getOptionOrNull("formattingLocale");
|
const formattingLocale = options.getOptionOrNull("formattingLocale");
|
||||||
|
|||||||
@ -43,7 +43,7 @@ test("Highlights list is displayed", async ({ page, context }) => {
|
|||||||
await app.closeAllTabs();
|
await app.closeAllTabs();
|
||||||
await app.goToNoteInNewTab("Highlights list");
|
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");
|
const rootList = app.sidebar.locator(".highlights-list ol");
|
||||||
let index = 0;
|
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"]) {
|
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"]) {
|
||||||
|
|||||||
@ -59,7 +59,7 @@ export default class App {
|
|||||||
|
|
||||||
// Wait for the page to load.
|
// Wait for the page to load.
|
||||||
if (url === "/") {
|
if (url === "/") {
|
||||||
await expect(this.page.locator(".tree")).toContainText("Trilium Integration Test");
|
await expect(this.noteTree).toContainText("Trilium Integration Test");
|
||||||
if (!preserveTabs) {
|
if (!preserveTabs) {
|
||||||
await this.closeAllTabs();
|
await this.closeAllTabs();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -381,6 +381,8 @@
|
|||||||
"tooltip": "Trilium Notes",
|
"tooltip": "Trilium Notes",
|
||||||
"close": "Quit Trilium",
|
"close": "Quit Trilium",
|
||||||
"recents": "Recent notes",
|
"recents": "Recent notes",
|
||||||
|
"recently-closed-windows": "Recently closed windows",
|
||||||
|
"tabs-total": "{{number}} tabs total",
|
||||||
"bookmarks": "Bookmarks",
|
"bookmarks": "Bookmarks",
|
||||||
"today": "Open today's journal note",
|
"today": "Open today's journal note",
|
||||||
"new-note": "New note",
|
"new-note": "New note",
|
||||||
|
|||||||
@ -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)]
|
||||||
|
);
|
||||||
|
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -6,6 +6,11 @@
|
|||||||
|
|
||||||
// Migrations should be kept in descending order, so the latest migration is first.
|
// Migrations should be kept in descending order, so the latest migration is first.
|
||||||
const MIGRATIONS: (SqlMigration | JsMigration)[] = [
|
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
|
// Migrate geo map to collection
|
||||||
{
|
{
|
||||||
version: 233,
|
version: 233,
|
||||||
|
|||||||
@ -50,6 +50,7 @@ export function bootstrap(req: Request, res: Response) {
|
|||||||
appCssNoteIds: getAppCssNoteIds(),
|
appCssNoteIds: getAppCssNoteIds(),
|
||||||
isDev,
|
isDev,
|
||||||
isMainWindow: view === "mobile" ? true : !req.query.extraWindow,
|
isMainWindow: view === "mobile" ? true : !req.query.extraWindow,
|
||||||
|
windowId: req.query.extraWindow ?? "main",
|
||||||
isProtectedSessionAvailable: protectedSessionService.isProtectedSessionAvailable(),
|
isProtectedSessionAvailable: protectedSessionService.isProtectedSessionAvailable(),
|
||||||
triliumVersion: packageJson.version,
|
triliumVersion: packageJson.version,
|
||||||
assetPath,
|
assetPath,
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import packageJson from "../../package.json" with { type: "json" };
|
|||||||
import dataDir from "./data_dir.js";
|
import dataDir from "./data_dir.js";
|
||||||
import { AppInfo } from "@triliumnext/commons";
|
import { AppInfo } from "@triliumnext/commons";
|
||||||
|
|
||||||
const APP_DB_VERSION = 233;
|
const APP_DB_VERSION = 234;
|
||||||
const SYNC_VERSION = 36;
|
const SYNC_VERSION = 36;
|
||||||
const CLIPPER_PROTOCOL_VERSION = "1.0";
|
const CLIPPER_PROTOCOL_VERSION = "1.0";
|
||||||
|
|
||||||
|
|||||||
@ -72,6 +72,19 @@ function getOptionBool(name: FilterOptionsByType<boolean>): boolean {
|
|||||||
return val === "true";
|
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<T extends OptionNames>(name: T, value: string | OptionDefinitions[T]) {
|
function setOption<T extends OptionNames>(name: T, value: string | OptionDefinitions[T]) {
|
||||||
const option = becca.getOption(name);
|
const option = becca.getOption(name);
|
||||||
|
|
||||||
@ -137,6 +150,7 @@ export default {
|
|||||||
getOption,
|
getOption,
|
||||||
getOptionInt,
|
getOptionInt,
|
||||||
getOptionBool,
|
getOptionBool,
|
||||||
|
getOptionJson,
|
||||||
setOption,
|
setOption,
|
||||||
createOption,
|
createOption,
|
||||||
getOptions,
|
getOptions,
|
||||||
|
|||||||
@ -45,8 +45,15 @@ async function initNotSyncedOptions(initialized: boolean, opts: NotSyncedOpts =
|
|||||||
"openNoteContexts",
|
"openNoteContexts",
|
||||||
JSON.stringify([
|
JSON.stringify([
|
||||||
{
|
{
|
||||||
notePath: "root",
|
windowId: "main",
|
||||||
active: true
|
createdAt: 0,
|
||||||
|
closedAt: 0,
|
||||||
|
contexts: [
|
||||||
|
{
|
||||||
|
notePath: "root",
|
||||||
|
active: true
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]),
|
]),
|
||||||
false
|
false
|
||||||
@ -257,8 +264,15 @@ function initStartupOptions() {
|
|||||||
"openNoteContexts",
|
"openNoteContexts",
|
||||||
JSON.stringify([
|
JSON.stringify([
|
||||||
{
|
{
|
||||||
notePath: process.env.TRILIUM_START_NOTE_ID || "root",
|
windowId: "main",
|
||||||
active: true
|
createdAt: 0,
|
||||||
|
closedAt: 0,
|
||||||
|
contexts: [
|
||||||
|
{
|
||||||
|
notePath: process.env.TRILIUM_START_NOTE_ID || "root",
|
||||||
|
active: true
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
|
|||||||
@ -147,8 +147,15 @@ async function createInitialDatabase(skipDemoDb?: boolean) {
|
|||||||
"openNoteContexts",
|
"openNoteContexts",
|
||||||
JSON.stringify([
|
JSON.stringify([
|
||||||
{
|
{
|
||||||
notePath: startNoteId,
|
windowId: "main",
|
||||||
active: true
|
createdAt: 0,
|
||||||
|
closedAt: 0,
|
||||||
|
contexts: [
|
||||||
|
{
|
||||||
|
notePath: startNoteId,
|
||||||
|
active: true
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
|
|||||||
@ -196,6 +196,39 @@ function updateTrayMenu() {
|
|||||||
return menuItems;
|
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[] = [];
|
const windowVisibilityMenuItems: Electron.MenuItemConstructorOptions[] = [];
|
||||||
|
|
||||||
// Only call getWindowTitle if windowVisibilityMap has more than one window
|
// Only call getWindowTitle if windowVisibilityMap has more than one window
|
||||||
@ -258,6 +291,12 @@ function updateTrayMenu() {
|
|||||||
icon: getIconPath("recents"),
|
icon: getIconPath("recents"),
|
||||||
submenu: buildRecentNotesMenu()
|
submenu: buildRecentNotesMenu()
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: t("tray.recently-closed-windows"),
|
||||||
|
type: "submenu",
|
||||||
|
icon: getIconPath("closed-windows"),
|
||||||
|
submenu: buildClosedWindowsMenu()
|
||||||
|
},
|
||||||
{ type: "separator" },
|
{ type: "separator" },
|
||||||
{
|
{
|
||||||
label: t("tray.close"),
|
label: t("tray.close"),
|
||||||
|
|||||||
@ -16,28 +16,45 @@ import { formatDownloadTitle, isMac, isWindows } from "./utils.js";
|
|||||||
// Prevent the window being garbage collected
|
// Prevent the window being garbage collected
|
||||||
let mainWindow: BrowserWindow | null;
|
let mainWindow: BrowserWindow | null;
|
||||||
let setupWindow: 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.
|
// 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.
|
// Therefore, we need to push the window into the allWindows array every time it gets focused.
|
||||||
win.on("focus", () => {
|
win.on("focus", () => {
|
||||||
allWindows = allWindows.filter(w => !w.isDestroyed() && w !== win);
|
allWindowEntries = allWindowEntries.filter(w => !w.window.isDestroyed() && w.window !== win);
|
||||||
allWindows.push(win);
|
allWindowEntries.push({ window: win, windowId: windowId });
|
||||||
|
|
||||||
if (!optionService.getOptionBool("disableTray")) {
|
if (!optionService.getOptionBool("disableTray")) {
|
||||||
electron.ipcMain.emit("reload-tray");
|
electron.ipcMain.emit("reload-tray");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
win.on("closed", () => {
|
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")) {
|
if (!optionService.getOptionBool("disableTray")) {
|
||||||
electron.ipcMain.emit("reload-tray");
|
electron.ipcMain.emit("reload-tray");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createExtraWindow(extraWindowHash: string) {
|
async function createExtraWindow(extraWindowId: string, extraWindowHash: string) {
|
||||||
const spellcheckEnabled = optionService.getOptionBool("spellCheckEnabled");
|
const spellcheckEnabled = optionService.getOptionBool("spellCheckEnabled");
|
||||||
|
|
||||||
const { BrowserWindow } = await import("electron");
|
const { BrowserWindow } = await import("electron");
|
||||||
@ -56,15 +73,15 @@ async function createExtraWindow(extraWindowHash: string) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
win.setMenuBarVisibility(false);
|
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);
|
configureWebContents(win.webContents, spellcheckEnabled);
|
||||||
|
|
||||||
trackWindowFocus(win);
|
trackWindowFocus(win, extraWindowId);
|
||||||
}
|
}
|
||||||
|
|
||||||
electron.ipcMain.on("create-extra-window", (event, arg) => {
|
electron.ipcMain.on("create-extra-window", (event, arg) => {
|
||||||
createExtraWindow(arg.extraWindowHash);
|
createExtraWindow(arg.extraWindowId, arg.extraWindowHash);
|
||||||
});
|
});
|
||||||
|
|
||||||
interface PrintOpts {
|
interface PrintOpts {
|
||||||
@ -168,8 +185,8 @@ async function getBrowserWindowForPrinting(e: IpcMainEvent, notePath: string, ac
|
|||||||
return { browserWindow, printReport };
|
return { browserWindow, printReport };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createMainWindow(app: App) {
|
async function createMainWindow(app?: App) {
|
||||||
if ("setUserTasks" in app) {
|
if (app && "setUserTasks" in app) {
|
||||||
app.setUserTasks([
|
app.setUserTasks([
|
||||||
{
|
{
|
||||||
program: process.execPath,
|
program: process.execPath,
|
||||||
@ -219,7 +236,7 @@ async function createMainWindow(app: App) {
|
|||||||
mainWindow.on("closed", () => (mainWindow = null));
|
mainWindow.on("closed", () => (mainWindow = null));
|
||||||
|
|
||||||
configureWebContents(mainWindow.webContents, spellcheckEnabled);
|
configureWebContents(mainWindow.webContents, spellcheckEnabled);
|
||||||
trackWindowFocus(mainWindow);
|
trackWindowFocus(mainWindow, "main");
|
||||||
}
|
}
|
||||||
|
|
||||||
function getWindowExtraOpts() {
|
function getWindowExtraOpts() {
|
||||||
@ -381,11 +398,15 @@ function getMainWindow() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getLastFocusedWindow() {
|
function getLastFocusedWindow() {
|
||||||
return allWindows.length > 0 ? allWindows[allWindows.length - 1] : null;
|
return allWindowEntries.length > 0 ? allWindowEntries[allWindowEntries.length - 1]?.window : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAllWindows() {
|
function getAllWindows() {
|
||||||
return allWindows;
|
return allWindowEntries.map(e => e.window);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAllWindowIds(): string[] {
|
||||||
|
return allWindowEntries.map(e => e.windowId);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -396,5 +417,6 @@ export default {
|
|||||||
registerGlobalShortcuts,
|
registerGlobalShortcuts,
|
||||||
getMainWindow,
|
getMainWindow,
|
||||||
getLastFocusedWindow,
|
getLastFocusedWindow,
|
||||||
getAllWindows
|
getAllWindows,
|
||||||
|
getAllWindowIds
|
||||||
};
|
};
|
||||||
|
|||||||