Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
9df1a58
web: horizontal panes
Jan 30, 2026
b98fdd8
web: horizontal panes, search behaviour, new window single note, keyb…
Jan 30, 2026
f21bdaa
web: fix editor buttons
Jan 30, 2026
8dd4a9e
web: revert mobile changes for later
Jan 30, 2026
7cc0a2c
desktop: refined drag drop of tabs
Jan 31, 2026
27838c1
desktop: reverted files just with formatting changes
Jan 31, 2026
2fae991
desktop: drag and drop notes and tabs across windows to create a new …
Jan 31, 2026
4ec82bb
desktop: save the session and layout of multiple windows on quite and…
Jan 31, 2026
9b658ed
desktop: fix colors of draggable object
Jan 31, 2026
7134a37
desktop: fix restoring window and pane layout and lazy loading tabs w…
Jan 31, 2026
735cf4c
desktop: fix moving note to actionbar
Jan 31, 2026
e15144e
desktop: fix desktop moving tabs
Jan 31, 2026
3076d53
desktop: fix odd tiling states
Jan 31, 2026
e7c5e60
web: fix uninitialized tab after after getting visible when moving an…
Feb 1, 2026
8caf3c9
desktop: close secondary single note window when last tab gets closed
Feb 1, 2026
7d2bf99
desktop: proper note cache clearing, security fixes & type safety
Feb 1, 2026
7aa9436
web: fixed strings commit changes
Feb 2, 2026
1cf70da
web: synced strings with master to reduce changed lines
Feb 2, 2026
1efc689
web: restore non-formatted wa-sqlite-async.js from master to reduce c…
Feb 2, 2026
c9372e4
web: restore non-formatted wa-sqlite.js from master to reduce commit …
Feb 2, 2026
46e8d17
desktop: remove multi window, stick more to master
Feb 5, 2026
9b8f210
web: fix refresh cache
Feb 5, 2026
30c518d
web: revert changes to stick to master
Feb 5, 2026
48dfcc1
web: fix sync title with duplicate tabs
Feb 5, 2026
f706199
web: re-arranged preload.ts
Feb 5, 2026
5940365
web: fix type mismatch preload.ts
Feb 5, 2026
7ea282e
web: removed unused function call in attachments
Feb 5, 2026
5b919b5
web: fix hardcoded strings, open new window leftover
Feb 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion apps/desktop/src/api/window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.

import { initTRPC } from "@trpc/server";
import { observable } from "@trpc/server/observable";
import { dragManager } from "../utils/window-manager";
import { z } from "zod";

const t = initTRPC.create();

Expand Down Expand Up @@ -79,5 +81,35 @@ export const windowRouter = t.router({
);
};
});
})
}),
// Added drag-and-drop session management commands.
// These allow the renderer to coordinate cross-window drag operations via the main process.
startDragSession: t.procedure
.input(
z.object({
title: z.string(),
colors: z
.object({ bg: z.string(), fg: z.string(), border: z.string() })
.optional()
})
)
.mutation(({ input }) => {
dragManager.startDragSession(input.title, input.colors);
}),
endDragSession: t.procedure.mutation(() => {
dragManager.endDragSession();
}),
checkInternalDrop: t.procedure
.input(
z.object({
x: z.number(),
y: z.number(),
type: z.enum(["tab", "note"]),
id: z.string()
})
)
.mutation(({ input }) => {
if (!globalThis.window) return { handled: false };
return dragManager.handleExternalDrop(input, globalThis.window.id);
})
});
49 changes: 46 additions & 3 deletions apps/desktop/src/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,39 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

/*
ABOUTME: Switched from direct `globalThis` assignment to `contextBridge.exposeInMainWorld`
to support Electron's Context Isolation (security best practice), which is enabled in `window-manager.ts`.

- Added `electronFS` for secure file writing capability from renderer.
- Added `appEvents` for handling external file drops.
*/

/* eslint-disable no-var */

import { ELECTRON_TRPC_CHANNEL } from "electron-trpc/main";
// import type { NNCrypto } from "@notesnook/crypto";
import { ipcRenderer } from "electron";
import { platform } from "os";
import { createWriteStream, mkdirSync } from "fs";
import { dirname } from "path";
import { Writable } from "stream";

declare global {
var os: () => "mas" | ReturnType<typeof platform>;
var electronTRPC: any;

// file system stream writer for renderer to support secure file writes
var electronFS: {
createWritableStream: (path: string) => Promise<WritableStream<any>>;
};
// var NativeNNCrypto: (new () => NNCrypto) | undefined;

// listener for external file drops to support drag-and-drop features
var appEvents: {
onExternalDrop: (callback: (payload: any) => void) => void;
};
}

process.once("loaded", async () => {
Expand All @@ -36,8 +58,29 @@ process.once("loaded", async () => {
onMessage: (callback: any) =>
ipcRenderer.on(ELECTRON_TRPC_CHANNEL, (_event, args) => callback(args))
};

globalThis.electronTRPC = electronTRPC;
});

// globalThis.NativeNNCrypto = require("@notesnook/crypto").NNCrypto;
globalThis.os = () => (MAC_APP_STORE ? "mas" : platform());
// globalThis.NativeNNCrypto = require("@notesnook/crypto").NNCrypto;
globalThis.appEvents = {
onExternalDrop: (callback: any) => {
const subscription = (_event: any, args: any) => callback(args);
ipcRenderer.on("app:external-drop", subscription);
return () =>
ipcRenderer.removeListener("app:external-drop", subscription);
}
};

globalThis.electronFS = {
createWritableStream: async (path: string) => {
mkdirSync(dirname(path), { recursive: true });
return new WritableStream(
Writable.toWeb(
createWriteStream(path, { encoding: "utf-8" })
).getWriter()
);
}
};

globalThis.os = () => (MAC_APP_STORE ? "mas" : platform());
});
197 changes: 197 additions & 0 deletions apps/desktop/src/utils/window-manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)

Copyright (C) 2023 Streetwriters (Private) Limited

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

/*
ABOUTME: This file implements the DragManager class (formerly WindowManager).
It handles custom drag-and-drop sessions with visual feedback (ghost window).
*/

import { BrowserWindow, screen } from "electron";
import { getTheme } from "./theme";

/**
* DragManager handles custom drag-and-drop sessions with visual feedback.
* It creates a lightweight transparent window that follows the cursor during drag operations.
*/
export class DragManager {
private dragWindow: BrowserWindow | null = null;
private dragInterval: NodeJS.Timeout | null = null;

constructor() {}

/**
* Starts a custom drag session by creating a small, transparent window that follows the cursor.
* Used to provide visual feedback during drag operations (e.g. dragging a note).
*
* @param title - The text to display in the drag preview.
* @param colors - Color theme for the drag preview (background, foreground, border).
*/
startDragSession(
title: string,
colors: { bg: string; fg: string; border: string } = {
bg: getTheme() === "dark" ? "#333" : "#fff",
fg: getTheme() === "dark" ? "#fff" : "#000",
border: getTheme() === "dark" ? "#555" : "#ccc"
}
) {
if (this.dragWindow) {
this.endDragSession();
}

const cursor = screen.getCursorScreenPoint();
const width = 200;
const height = 45;

this.dragWindow = new BrowserWindow({
x: Math.round(cursor.x - width / 2),
y: Math.round(cursor.y - height / 2),
width,
height,
frame: false,
transparent: true,
alwaysOnTop: true,
skipTaskbar: true,
hasShadow: false,
focusable: false,
resizable: false,
webPreferences: {
nodeIntegration: false,
contextIsolation: true
}
});

this.dragWindow.on("ready-to-show", () => {
this.dragWindow?.showInactive();
});

this.dragWindow.setAlwaysOnTop(true, "screen-saver");

const safeTitle = title.replace(/</g, "&lt;").replace(/>/g, "&gt;");

const html = `
<html>
<body style="background: transparent; margin: 0; padding: 4px; height: 100vh; display: flex; align-items: center; box-sizing: border-box; overflow: hidden; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;">
<div style="
background: ${colors.bg};
color: ${colors.fg};
border: 1px solid ${colors.border || "transparent"};
border-radius: 8px;
padding: 0 12px;
font-size: 13px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: flex;
align-items: center;
height: 36px;
width: 100%;
box-shadow: 0 4px 16px rgba(0,0,0,0.5);
">
${safeTitle}
</div>
</body>
</html>
`;

// We use a simple data URL for the drag image
this.dragWindow.loadURL(
`data:text/html;charset=utf-8,${encodeURIComponent(html)}`
);

this.dragWindow.setIgnoreMouseEvents(true);

this.dragInterval = setInterval(() => {
if (!this.dragWindow || this.dragWindow.isDestroyed()) {
this.endDragSession();
return;
}

const point = screen.getCursorScreenPoint();
try {
this.dragWindow.setPosition(
Math.round(point.x - width / 2),
Math.round(point.y - height / 2)
);
} catch (e) {
// console.error("[DragManager] Error setting position:", e);
}

// Check if we are over any of our app windows
// For single window mode, checking global window existence is probably enough
// But we just want to keep showing it until endDragSession is called.
if (!this.dragWindow.isVisible()) {
this.dragWindow.showInactive();
}
}, 16); // ~60fps
}

/**
* Ends the current drag session, destroying the drag preview window and clearing the interval.
*/
endDragSession() {
if (this.dragInterval) {
clearInterval(this.dragInterval);
this.dragInterval = null;
}

if (this.dragWindow) {
if (!this.dragWindow.isDestroyed()) {
this.dragWindow.destroy();
}
this.dragWindow = null;
}
}

/**
* Handles drop events that occur outside the source window (external drops).
* Determines which window triggers the drop and forwards the event to it.
*
* @param payload - The drop event data including coordinates and item type.
* @param excludeWindowId - The ID of the window where the drag originated (to avoid self-drops if needed).
* @returns Object indicating if the drop was handled.
*/
handleExternalDrop(
payload: {
x: number;
y: number;
type: "tab" | "note";
id: string;
},
excludeWindowId: number
) {
const { x, y } = payload;
const window = globalThis.window; // Main window reference
if (window && !window.isDestroyed() && window.id !== excludeWindowId) {
const bounds = window.getBounds();
if (
x >= bounds.x &&
x <= bounds.x + bounds.width &&
y >= bounds.y &&
y <= bounds.y + bounds.height
) {
// Let's send the event to that window
window.webContents.send("app:external-drop", payload);
return { handled: true };
}
}
return { handled: false };
}
}

export const dragManager = new DragManager();
Loading