mirror of
https://github.com/streetwriters/notesnook.git
synced 2025-12-16 11:47:54 +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": {
|
||||
"@lingui/core": "5.1.2",
|
||||
"@notesnook/intl": "file:../../packages/intl",
|
||||
"@notesnook/ui": "file:../../packages/ui",
|
||||
"@resvg/resvg-js": "^2.6.2",
|
||||
"@trpc/client": "10.45.2",
|
||||
"@trpc/server": "10.45.2",
|
||||
"better-sqlite3-multiple-ciphers": "11.9.1",
|
||||
@@ -63,7 +65,7 @@
|
||||
"staging": "node scripts/build.mjs --run",
|
||||
"release": "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",
|
||||
"postinstall": "patch-package",
|
||||
"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 { 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 { config, DesktopIntegration } from "../utils/config";
|
||||
import { bringToFront } from "../utils/bring-to-front";
|
||||
@@ -33,6 +42,8 @@ import { isFlatpak, isSnap } from "../utils";
|
||||
import { setupDesktopIntegration } from "../utils/desktop-integration";
|
||||
import { rm } from "fs/promises";
|
||||
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();
|
||||
|
||||
@@ -225,5 +236,99 @@ export const osIntegrationRouter = t.router({
|
||||
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(
|
||||
new MenuItem({
|
||||
label: strings.paste(),
|
||||
role: "pasteAndMatchStyle",
|
||||
role: "paste",
|
||||
enabled: clipboard.readText("clipboard").length > 0,
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -20,7 +20,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { create } from "zustand";
|
||||
import { shallow } from "zustand/shallow";
|
||||
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 = {
|
||||
position?: PositionOptions;
|
||||
@@ -43,7 +44,28 @@ const useMenuStore = create<MenuStore>((set) => ({
|
||||
options: {
|
||||
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: () =>
|
||||
set(() => ({
|
||||
isOpen: false,
|
||||
@@ -84,3 +106,35 @@ export function useMenu() {
|
||||
]);
|
||||
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`,
|
||||
copyImage: () => t`Copy image`,
|
||||
paste: () => t`Paste`,
|
||||
pasteAndMatchStyle: () => t`Paste and match style`,
|
||||
pasteWithoutFormatting: () => t`Paste without formatting`,
|
||||
configure: () => t`Configure`,
|
||||
usingOfficialInstance: () => t`Using official Notesnook instance`,
|
||||
usingInstance: (instance: string, version: string) =>
|
||||
|
||||
Reference in New Issue
Block a user