mirror of
https://github.com/streetwriters/notesnook.git
synced 2025-12-16 19:57:52 +01:00
desktop: show native menus where possible
This commit is contained in:
6988
apps/desktop/package-lock.json
generated
6988
apps/desktop/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -27,6 +27,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@lingui/core": "5.1.2",
|
"@lingui/core": "5.1.2",
|
||||||
"@notesnook/intl": "file:../../packages/intl",
|
"@notesnook/intl": "file:../../packages/intl",
|
||||||
|
"@notesnook/ui": "file:../../packages/ui",
|
||||||
|
"@resvg/resvg-js": "^2.6.2",
|
||||||
"@trpc/client": "10.45.2",
|
"@trpc/client": "10.45.2",
|
||||||
"@trpc/server": "10.45.2",
|
"@trpc/server": "10.45.2",
|
||||||
"better-sqlite3-multiple-ciphers": "11.9.1",
|
"better-sqlite3-multiple-ciphers": "11.9.1",
|
||||||
@@ -63,7 +65,7 @@
|
|||||||
"staging": "node scripts/build.mjs --run",
|
"staging": "node scripts/build.mjs --run",
|
||||||
"release": "node scripts/build.mjs",
|
"release": "node scripts/build.mjs",
|
||||||
"build": "node ../../scripts/build.mjs",
|
"build": "node ../../scripts/build.mjs",
|
||||||
"bundle": "esbuild electron=./src/main.ts ./src/preload.ts --external:electron --external:fsevents --external:better-sqlite3-multiple-ciphers --external:sodium-native --bundle --outdir=./build --platform=node --tsconfig=tsconfig.json --define:MAC_APP_STORE=false --define:RELEASE=true",
|
"bundle": "esbuild electron=./src/main.ts ./src/preload.ts --external:electron --external:fsevents --external:better-sqlite3-multiple-ciphers --external:sodium-native --external:@resvg/resvg-js --bundle --outdir=./build --platform=node --tsconfig=tsconfig.json --define:MAC_APP_STORE=false --define:RELEASE=true",
|
||||||
"bundle:mas": "esbuild electron=./src/main.ts ./src/preload.ts --minify --external:electron --external:fsevents --bundle --outdir=./build --platform=node --tsconfig=tsconfig.json --define:MAC_APP_STORE=true --define:RELEASE=true",
|
"bundle:mas": "esbuild electron=./src/main.ts ./src/preload.ts --minify --external:electron --external:fsevents --bundle --outdir=./build --platform=node --tsconfig=tsconfig.json --define:MAC_APP_STORE=true --define:RELEASE=true",
|
||||||
"postinstall": "patch-package",
|
"postinstall": "patch-package",
|
||||||
"test": "vitest run"
|
"test": "vitest run"
|
||||||
|
|||||||
@@ -19,7 +19,16 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
|
|
||||||
import { initTRPC } from "@trpc/server";
|
import { initTRPC } from "@trpc/server";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { app, dialog, nativeTheme, Notification, shell } from "electron";
|
import {
|
||||||
|
app,
|
||||||
|
dialog,
|
||||||
|
Menu,
|
||||||
|
MenuItem,
|
||||||
|
nativeImage,
|
||||||
|
nativeTheme,
|
||||||
|
Notification,
|
||||||
|
shell
|
||||||
|
} from "electron";
|
||||||
import { AutoLaunch } from "../utils/autolaunch";
|
import { AutoLaunch } from "../utils/autolaunch";
|
||||||
import { config, DesktopIntegration } from "../utils/config";
|
import { config, DesktopIntegration } from "../utils/config";
|
||||||
import { bringToFront } from "../utils/bring-to-front";
|
import { bringToFront } from "../utils/bring-to-front";
|
||||||
@@ -33,6 +42,8 @@ import { isFlatpak, isSnap } from "../utils";
|
|||||||
import { setupDesktopIntegration } from "../utils/desktop-integration";
|
import { setupDesktopIntegration } from "../utils/desktop-integration";
|
||||||
import { rm } from "fs/promises";
|
import { rm } from "fs/promises";
|
||||||
import { disableCustomDns, enableCustomDns } from "../utils/custom-dns";
|
import { disableCustomDns, enableCustomDns } from "../utils/custom-dns";
|
||||||
|
import type { MenuItem as NNMenuItem } from "@notesnook/ui";
|
||||||
|
import { Resvg } from "@resvg/resvg-js";
|
||||||
|
|
||||||
const t = initTRPC.create();
|
const t = initTRPC.create();
|
||||||
|
|
||||||
@@ -225,5 +236,99 @@ export const osIntegrationRouter = t.router({
|
|||||||
nativeTheme.off("updated", updated);
|
nativeTheme.off("updated", updated);
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
)
|
),
|
||||||
|
|
||||||
|
showMenu: t.procedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
menuItems: z.array(z.any()),
|
||||||
|
menuIconColor: z.string()
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.subscription(({ input: { menuItems, menuIconColor } }) =>
|
||||||
|
observable<string[]>((emit) => {
|
||||||
|
const items = menuItems as NNMenuItem[];
|
||||||
|
const menu = new Menu();
|
||||||
|
for (const item of items) {
|
||||||
|
const menuItem = toMenuItem(
|
||||||
|
item,
|
||||||
|
(id) => emit.next(id),
|
||||||
|
menuIconColor
|
||||||
|
);
|
||||||
|
if (menuItem) menu.append(menuItem);
|
||||||
|
}
|
||||||
|
if (menu.items.length > 0) menu.popup();
|
||||||
|
return () => {
|
||||||
|
menu.removeAllListeners();
|
||||||
|
menu.closePopup();
|
||||||
|
};
|
||||||
|
})
|
||||||
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function toMenuItem(
|
||||||
|
item: NNMenuItem,
|
||||||
|
onClick: (id: string[]) => void,
|
||||||
|
menuIconColor: string,
|
||||||
|
parentKey?: string
|
||||||
|
): MenuItem | undefined {
|
||||||
|
switch (item.type) {
|
||||||
|
case "lazy-loader":
|
||||||
|
return undefined;
|
||||||
|
case "separator":
|
||||||
|
return new MenuItem({ type: "separator" });
|
||||||
|
case "button": {
|
||||||
|
const submenu = item.menu ? new Menu() : undefined;
|
||||||
|
if (submenu && item.menu) {
|
||||||
|
for (const subitem of item.menu.items) {
|
||||||
|
const subMenuItem = toMenuItem(
|
||||||
|
subitem,
|
||||||
|
onClick,
|
||||||
|
menuIconColor,
|
||||||
|
item.key
|
||||||
|
);
|
||||||
|
if (subMenuItem) submenu.append(subMenuItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new MenuItem({
|
||||||
|
label: item.title,
|
||||||
|
enabled: !item.isDisabled,
|
||||||
|
visible: !item.isHidden,
|
||||||
|
toolTip: item.tooltip,
|
||||||
|
sublabel: item.tooltip,
|
||||||
|
checked: item.isChecked,
|
||||||
|
type: submenu ? "submenu" : item.isChecked ? "checkbox" : "normal",
|
||||||
|
id: item.key,
|
||||||
|
icon: item.icon
|
||||||
|
? svgPathToPng(
|
||||||
|
item.icon,
|
||||||
|
(item.styles?.icon?.color as string | undefined) || menuIconColor
|
||||||
|
)
|
||||||
|
: undefined,
|
||||||
|
submenu,
|
||||||
|
click: () => onClick(parentKey ? [parentKey, item.key] : [item.key]),
|
||||||
|
accelerator: item.modifier?.replace("Mod", "CommandOrControl")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function svgPathToPng(path: string, color?: string) {
|
||||||
|
const svg = Buffer.from(
|
||||||
|
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" role="presentation" class="icon" style="stroke-width: 0px; stroke: ${
|
||||||
|
color || config.windowControlsIconColor
|
||||||
|
}; width: 14px; height: 14px;"><path d="${path}" style="fill: ${
|
||||||
|
color || config.windowControlsIconColor
|
||||||
|
};"></path></svg>`
|
||||||
|
);
|
||||||
|
const resvg = new Resvg(svg, {
|
||||||
|
fitTo: { mode: "width", value: 14 },
|
||||||
|
logLevel: "error",
|
||||||
|
font: {
|
||||||
|
loadSystemFonts: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const pngData = resvg.render();
|
||||||
|
return nativeImage.createFromBuffer(pngData.asPng());
|
||||||
|
}
|
||||||
|
|||||||
@@ -143,16 +143,44 @@ function setupMenu() {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
if (params.isEditable)
|
if (params.isEditable) {
|
||||||
menu.append(
|
menu.append(
|
||||||
new MenuItem({
|
new MenuItem({
|
||||||
label: strings.paste(),
|
label: strings.paste(),
|
||||||
role: "pasteAndMatchStyle",
|
role: "paste",
|
||||||
enabled: clipboard.readText("clipboard").length > 0,
|
enabled: clipboard.readText("clipboard").length > 0,
|
||||||
accelerator: "CommandOrControl+V"
|
accelerator: "CommandOrControl+V"
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
menu.append(
|
||||||
|
new MenuItem({
|
||||||
|
label:
|
||||||
|
process.platform === "darwin"
|
||||||
|
? strings.pasteAndMatchStyle()
|
||||||
|
: strings.pasteWithoutFormatting(),
|
||||||
|
role: "pasteAndMatchStyle",
|
||||||
|
enabled: clipboard.readText("clipboard").length > 0,
|
||||||
|
accelerator:
|
||||||
|
process.platform === "darwin"
|
||||||
|
? "Option+Shift+Command+V"
|
||||||
|
: "Shift+CommandOrControl+V"
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
menu.append(
|
||||||
|
new MenuItem({
|
||||||
|
type: "separator"
|
||||||
|
})
|
||||||
|
);
|
||||||
|
menu.append(
|
||||||
|
new MenuItem({
|
||||||
|
label: strings.spellCheck(),
|
||||||
|
role: "toggleSpellChecker"
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (menu.items.length > 0) menu.popup();
|
if (menu.items.length > 0) menu.popup();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
import { shallow } from "zustand/shallow";
|
import { shallow } from "zustand/shallow";
|
||||||
import { MenuItem, PositionOptions } from "@notesnook/ui";
|
import { MenuItem, PositionOptions } from "@notesnook/ui";
|
||||||
// import { isUserPremium } from "./use-is-user-premium";
|
import { desktop } from "../common/desktop-bridge";
|
||||||
|
import { useThemeEngineStore } from "@notesnook/theme";
|
||||||
|
|
||||||
type MenuOptions = {
|
type MenuOptions = {
|
||||||
position?: PositionOptions;
|
position?: PositionOptions;
|
||||||
@@ -43,7 +44,28 @@ const useMenuStore = create<MenuStore>((set) => ({
|
|||||||
options: {
|
options: {
|
||||||
blocking: false
|
blocking: false
|
||||||
},
|
},
|
||||||
open: (items, options) => set(() => ({ isOpen: true, items, options })),
|
open: async (items, options) => {
|
||||||
|
if (IS_DESKTOP_APP && canShowNativeMenu(items)) {
|
||||||
|
const serializedItems = await resolveMenuItems(items);
|
||||||
|
const scopes = useThemeEngineStore.getState().theme.scopes;
|
||||||
|
const menuIconColor =
|
||||||
|
scopes.contextMenu?.primary?.icon || scopes.base?.primary?.icon;
|
||||||
|
desktop?.integration.showMenu.subscribe(
|
||||||
|
{
|
||||||
|
menuItems: JSON.parse(JSON.stringify(serializedItems)),
|
||||||
|
menuIconColor
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onData(ids) {
|
||||||
|
findAndCallAction(serializedItems, ids);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
set(() => ({ options }));
|
||||||
|
} else {
|
||||||
|
set(() => ({ isOpen: true, items, options }));
|
||||||
|
}
|
||||||
|
},
|
||||||
close: () =>
|
close: () =>
|
||||||
set(() => ({
|
set(() => ({
|
||||||
isOpen: false,
|
isOpen: false,
|
||||||
@@ -84,3 +106,35 @@ export function useMenu() {
|
|||||||
]);
|
]);
|
||||||
return { items, options };
|
return { items, options };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function resolveMenuItems(items: MenuItem[]): Promise<MenuItem[]> {
|
||||||
|
const serialized = [];
|
||||||
|
for (const item of items) {
|
||||||
|
if (item.type === "lazy-loader")
|
||||||
|
serialized.push(...(await resolveMenuItems(await item.items())));
|
||||||
|
else if (item.type === "button") {
|
||||||
|
if (item.menu) item.menu.items = await resolveMenuItems(item.menu.items);
|
||||||
|
serialized.push(item);
|
||||||
|
} else serialized.push(item);
|
||||||
|
}
|
||||||
|
return serialized;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findAndCallAction(items: MenuItem[], ids: string[]) {
|
||||||
|
let _items: MenuItem[] = items;
|
||||||
|
const actionId = ids.at(-1);
|
||||||
|
for (const id of ids) {
|
||||||
|
const item = _items.find((item) => item.key === id);
|
||||||
|
if (!item || item?.type !== "button") continue;
|
||||||
|
console.log(item);
|
||||||
|
if (id === actionId) {
|
||||||
|
item?.onClick?.();
|
||||||
|
} else {
|
||||||
|
_items = item.menu?.items || [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function canShowNativeMenu(items: MenuItem[]) {
|
||||||
|
return items.every((item) => item.type !== "popup");
|
||||||
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -2231,6 +2231,8 @@ Use this if changes from other devices are not appearing on this device. This wi
|
|||||||
copyLinkText: () => t`Copy link text`,
|
copyLinkText: () => t`Copy link text`,
|
||||||
copyImage: () => t`Copy image`,
|
copyImage: () => t`Copy image`,
|
||||||
paste: () => t`Paste`,
|
paste: () => t`Paste`,
|
||||||
|
pasteAndMatchStyle: () => t`Paste and match style`,
|
||||||
|
pasteWithoutFormatting: () => t`Paste without formatting`,
|
||||||
configure: () => t`Configure`,
|
configure: () => t`Configure`,
|
||||||
usingOfficialInstance: () => t`Using official Notesnook instance`,
|
usingOfficialInstance: () => t`Using official Notesnook instance`,
|
||||||
usingInstance: (instance: string, version: string) =>
|
usingInstance: (instance: string, version: string) =>
|
||||||
|
|||||||
Reference in New Issue
Block a user