mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-12 02:51:46 +00:00
10
plugins/in-app-menu/menu/icons.ts
Normal file
10
plugins/in-app-menu/menu/icons.ts
Normal file
@ -0,0 +1,10 @@
|
||||
const Icons = {
|
||||
submenu: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><polyline points="9 6 15 12 9 18" /></svg>',
|
||||
checkbox: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M5 12l5 5l10 -10" /></svg>',
|
||||
radio: {
|
||||
checked: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" style="padding: 2px"><path fill="currentColor" d="M10,5 C7.2,5 5,7.2 5,10 C5,12.8 7.2,15 10,15 C12.8,15 15,12.8 15,10 C15,7.2 12.8,5 10,5 L10,5 Z M10,0 C4.5,0 0,4.5 0,10 C0,15.5 4.5,20 10,20 C15.5,20 20,15.5 20,10 C20,4.5 15.5,0 10,0 L10,0 Z M10,18 C5.6,18 2,14.4 2,10 C2,5.6 5.6,2 10,2 C14.4,2 18,5.6 18,10 C18,14.4 14.4,18 10,18 L10,18 Z" /></svg>',
|
||||
unchecked: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" style="padding: 2px"><path fill="currentColor" d="M10,0 C4.5,0 0,4.5 0,10 C0,15.5 4.5,20 10,20 C15.5,20 20,15.5 20,10 C20,4.5 15.5,0 10,0 L10,0 Z M10,18 C5.6,18 2,14.4 2,10 C2,5.6 5.6,2 10,2 C14.4,2 18,5.6 18,10 C18,14.4 14.4,18 10,18 L10,18 Z" /></svg>',
|
||||
},
|
||||
};
|
||||
|
||||
export default Icons;
|
||||
125
plugins/in-app-menu/menu/panel.ts
Normal file
125
plugins/in-app-menu/menu/panel.ts
Normal file
@ -0,0 +1,125 @@
|
||||
import { nativeImage, type MenuItem, ipcRenderer, Menu } from 'electron';
|
||||
|
||||
import Icons from './icons';
|
||||
|
||||
import { ElementFromHtml } from '../../utils';
|
||||
|
||||
interface PanelOptions {
|
||||
placement?: 'bottom' | 'right';
|
||||
order?: number;
|
||||
}
|
||||
|
||||
export const createPanel = (
|
||||
parent: HTMLElement,
|
||||
anchor: HTMLElement,
|
||||
items: MenuItem[],
|
||||
options: PanelOptions = { placement: 'bottom', order: 0 },
|
||||
) => {
|
||||
const childPanels: HTMLElement[] = [];
|
||||
const panel = document.createElement('menu-panel');
|
||||
panel.style.zIndex = `${options.order}`;
|
||||
|
||||
const updateIconState = (iconWrapper: HTMLElement, item: MenuItem) => {
|
||||
if (item.type === 'checkbox') {
|
||||
if (item.checked) iconWrapper.innerHTML = Icons.checkbox;
|
||||
else iconWrapper.innerHTML = '';
|
||||
} else if (item.type === 'radio') {
|
||||
if (item.checked) iconWrapper.innerHTML = Icons.radio.checked;
|
||||
else iconWrapper.innerHTML = Icons.radio.unchecked;
|
||||
} else {
|
||||
const nativeImageIcon = typeof item.icon === 'string' ? nativeImage.createFromPath(item.icon) : item.icon;
|
||||
const iconURL = nativeImageIcon?.toDataURL();
|
||||
|
||||
if (iconURL) iconWrapper.style.background = `url(${iconURL})`;
|
||||
}
|
||||
};
|
||||
|
||||
const radioGroups: [MenuItem, HTMLElement][] = [];
|
||||
items.map((item) => {
|
||||
if (item.type === 'separator') return panel.appendChild(document.createElement('menu-separator'));
|
||||
|
||||
const menu = document.createElement('menu-item');
|
||||
const iconWrapper = document.createElement('menu-icon');
|
||||
|
||||
updateIconState(iconWrapper, item);
|
||||
menu.appendChild(iconWrapper);
|
||||
menu.append(item.label);
|
||||
|
||||
menu.addEventListener('click', async () => {
|
||||
await ipcRenderer.invoke('menu-event', item.commandId);
|
||||
const menuItem = await ipcRenderer.invoke('get-menu-by-id', item.commandId) as MenuItem | null;
|
||||
|
||||
if (menuItem) {
|
||||
updateIconState(iconWrapper, menuItem);
|
||||
|
||||
if (menuItem.type === 'radio') {
|
||||
await Promise.all(
|
||||
radioGroups.map(async ([item, iconWrapper]) => {
|
||||
if (item.commandId === menuItem.commandId) return;
|
||||
const newItem = await ipcRenderer.invoke('get-menu-by-id', item.commandId) as MenuItem | null;
|
||||
|
||||
if (newItem) updateIconState(iconWrapper, newItem);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (item.type === 'radio') {
|
||||
radioGroups.push([item, iconWrapper]);
|
||||
}
|
||||
|
||||
if (item.type === 'submenu') {
|
||||
const subMenuIcon = document.createElement('menu-icon');
|
||||
subMenuIcon.appendChild(ElementFromHtml(Icons.submenu));
|
||||
menu.appendChild(subMenuIcon);
|
||||
|
||||
const [child, , children] = createPanel(parent, menu, item.submenu?.items ?? [], {
|
||||
placement: 'right',
|
||||
order: (options?.order ?? 0) + 1,
|
||||
});
|
||||
|
||||
childPanels.push(child);
|
||||
children.push(...children);
|
||||
}
|
||||
|
||||
panel.appendChild(menu);
|
||||
});
|
||||
|
||||
/* methods */
|
||||
const isOpened = () => panel.getAttribute('open') === 'true';
|
||||
const close = () => panel.setAttribute('open', 'false');
|
||||
const open = () => {
|
||||
const rect = anchor.getBoundingClientRect();
|
||||
|
||||
if (options.placement === 'bottom') {
|
||||
panel.style.setProperty('--x', `${rect.x}px`);
|
||||
panel.style.setProperty('--y', `${rect.y + rect.height}px`);
|
||||
} else {
|
||||
panel.style.setProperty('--x', `${rect.x + rect.width}px`);
|
||||
panel.style.setProperty('--y', `${rect.y}px`);
|
||||
}
|
||||
|
||||
panel.setAttribute('open', 'true');
|
||||
};
|
||||
|
||||
anchor.addEventListener('click', () => {
|
||||
if (isOpened()) close();
|
||||
else open();
|
||||
});
|
||||
|
||||
document.body.addEventListener('click', (event) => {
|
||||
const path = event.composedPath();
|
||||
const isInside = path.some((it) => it === panel || it === anchor || childPanels.includes(it as HTMLElement));
|
||||
|
||||
if (!isInside) close();
|
||||
});
|
||||
|
||||
parent.appendChild(panel);
|
||||
|
||||
return [
|
||||
panel,
|
||||
{ isOpened, close, open },
|
||||
childPanels,
|
||||
] as const;
|
||||
};
|
||||
Reference in New Issue
Block a user