mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-16 12:42:06 +00:00
refactor(in-app-menu): refactor in-app-menu plugin (#1710)
Co-authored-by: JellyBrick <shlee1503@naver.com>
This commit is contained in:
331
src/plugins/in-app-menu/renderer/PanelItem.tsx
Normal file
331
src/plugins/in-app-menu/renderer/PanelItem.tsx
Normal file
@ -0,0 +1,331 @@
|
||||
import { createSignal, Match, Show, Switch } from 'solid-js';
|
||||
import { JSX } from 'solid-js/jsx-runtime';
|
||||
import { css } from 'solid-styled-components';
|
||||
import { Portal } from 'solid-js/web';
|
||||
|
||||
import { Transition } from 'solid-transition-group';
|
||||
import { useFloating } from 'solid-floating-ui';
|
||||
import { autoUpdate, offset, size } from '@floating-ui/dom';
|
||||
|
||||
import { Panel } from './Panel';
|
||||
|
||||
const itemStyle = css`
|
||||
position: relative;
|
||||
|
||||
-webkit-app-region: none;
|
||||
min-height: 32px;
|
||||
height: 32px;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: 32px 1fr auto minmax(32px, auto);
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
|
||||
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
&[data-selected="true"] {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
& * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
`;
|
||||
|
||||
const itemIconStyle = css`
|
||||
height: 32px;
|
||||
padding: 4px;
|
||||
color: white;
|
||||
`;
|
||||
|
||||
const itemLabelStyle = css`
|
||||
font-size: 12px;
|
||||
color: white;
|
||||
`;
|
||||
|
||||
const itemChipStyle = css`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
min-width: 16px;
|
||||
height: 16px;
|
||||
padding: 0 4px;
|
||||
margin-left: 8px;
|
||||
|
||||
border-radius: 4px;
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
color: #f1f1f1;
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
`;
|
||||
|
||||
const toolTipStyle = css`
|
||||
min-width: 32px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
padding: 4px;
|
||||
|
||||
max-width: calc(var(--max-width, 100%) - 8px);
|
||||
max-height: calc(var(--max-height, 100%) - 8px);
|
||||
|
||||
border-radius: 4px;
|
||||
background-color: rgba(25, 25, 25, 0.8);
|
||||
color: #f1f1f1;
|
||||
font-size: 10px;
|
||||
`;
|
||||
|
||||
const popupStyle = css`
|
||||
position: fixed;
|
||||
top: var(--offset-y, 0);
|
||||
left: var(--offset-x, 0);
|
||||
|
||||
max-width: var(--max-width, 100%);
|
||||
max-height: var(--max-height, 100%);
|
||||
|
||||
z-index: 100000000;
|
||||
pointer-events: none;
|
||||
|
||||
`;
|
||||
const animationStyle = {
|
||||
enter: css`
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
`,
|
||||
enterActive: css`
|
||||
transition: opacity 0.225s cubic-bezier(0.33, 1, 0.68, 1), transform 0.225s cubic-bezier(0.33, 1, 0.68, 1);
|
||||
`,
|
||||
exitTo: css`
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
`,
|
||||
exitActive: css`
|
||||
transition: opacity 0.225s cubic-bezier(0.32, 0, 0.67, 0), transform 0.225s cubic-bezier(0.32, 0, 0.67, 0);
|
||||
`,
|
||||
};
|
||||
|
||||
const getParents = (element: Element | null): (HTMLElement | null)[] => {
|
||||
const parents: (HTMLElement | null)[] = [];
|
||||
let now = element;
|
||||
|
||||
while (now) {
|
||||
parents.push(now as HTMLElement | null);
|
||||
now = now.parentElement;
|
||||
}
|
||||
|
||||
return parents;
|
||||
};
|
||||
|
||||
type BasePanelItemProps = {
|
||||
name: string;
|
||||
label?: string;
|
||||
chip?: string;
|
||||
toolTip?: string;
|
||||
commandId?: number;
|
||||
};
|
||||
type NormalPanelItemProps = BasePanelItemProps & {
|
||||
type: 'normal';
|
||||
onClick?: () => void;
|
||||
};
|
||||
type SubmenuItemProps = BasePanelItemProps & {
|
||||
type: 'submenu';
|
||||
level: number[];
|
||||
children: JSX.Element;
|
||||
};
|
||||
type RadioPanelItemProps = BasePanelItemProps & {
|
||||
type: 'radio';
|
||||
checked: boolean;
|
||||
onChange?: (checked: boolean) => void;
|
||||
};
|
||||
type CheckboxPanelItemProps = BasePanelItemProps & {
|
||||
type: 'checkbox';
|
||||
checked: boolean;
|
||||
onChange?: (checked: boolean) => void;
|
||||
};
|
||||
export type PanelItemProps = NormalPanelItemProps | SubmenuItemProps | RadioPanelItemProps | CheckboxPanelItemProps;
|
||||
export const PanelItem = (props: PanelItemProps) => {
|
||||
const [open, setOpen] = createSignal(false);
|
||||
const [toolTipOpen, setToolTipOpen] = createSignal(false);
|
||||
const [toolTip, setToolTip] = createSignal<HTMLElement | null>(null);
|
||||
const [anchor, setAnchor] = createSignal<HTMLElement | null>(null);
|
||||
const [child, setChild] = createSignal<HTMLElement | null>(null);
|
||||
|
||||
const position = useFloating(anchor, toolTip, {
|
||||
whileElementsMounted: autoUpdate,
|
||||
placement: 'bottom-start',
|
||||
strategy: 'fixed',
|
||||
middleware: [
|
||||
offset({ mainAxis: 8 }),
|
||||
size({
|
||||
apply({ rects, elements }) {
|
||||
elements.floating.style.setProperty('--max-width', `${rects.reference.width}px`);
|
||||
}
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const handleHover = (event: MouseEvent) => {
|
||||
setToolTipOpen(true);
|
||||
event.target?.addEventListener('mouseleave', () => {
|
||||
setToolTipOpen(false);
|
||||
}, { once: true });
|
||||
|
||||
if (props.type === 'submenu') {
|
||||
const timer = setTimeout(() => {
|
||||
setOpen(true);
|
||||
|
||||
let mouseX = event.clientX;
|
||||
let mouseY = event.clientY;
|
||||
const onMouseMove = (event: MouseEvent) => {
|
||||
mouseX = event.clientX;
|
||||
mouseY = event.clientY;
|
||||
};
|
||||
document.addEventListener('mousemove', onMouseMove);
|
||||
|
||||
event.target?.addEventListener('mouseleave', () => {
|
||||
setTimeout(() => {
|
||||
document.removeEventListener('mousemove', onMouseMove);
|
||||
const parents = getParents(document.elementFromPoint(mouseX, mouseY));
|
||||
|
||||
if (!parents.includes(child())) {
|
||||
setOpen(false);
|
||||
} else {
|
||||
const onOtherHover = (event: MouseEvent) => {
|
||||
const parents = getParents(event.target as HTMLElement);
|
||||
const closestLevel = parents.find((it) => it?.dataset?.level)?.dataset.level ?? '';
|
||||
const path = event.composedPath();
|
||||
|
||||
const isOtherItem = path.some((it) => it instanceof HTMLElement && it.classList.contains(itemStyle));
|
||||
const isChild = closestLevel.startsWith(props.level.join('/'));
|
||||
|
||||
if (isOtherItem && !isChild) {
|
||||
setOpen(false);
|
||||
document.removeEventListener('mousemove', onOtherHover);
|
||||
}
|
||||
};
|
||||
document.addEventListener('mousemove', onOtherHover);
|
||||
}
|
||||
}, 225);
|
||||
}, { once: true });
|
||||
}, 225);
|
||||
|
||||
event.target?.addEventListener('mouseleave', () => {
|
||||
clearTimeout(timer);
|
||||
}, { once: true });
|
||||
}
|
||||
};
|
||||
|
||||
const handleClick = async () => {
|
||||
await window.ipcRenderer.invoke('ytmd:menu-event', props.commandId);
|
||||
if (props.type === 'radio') {
|
||||
props.onChange?.(!props.checked);
|
||||
} else if (props.type === 'checkbox') {
|
||||
props.onChange?.(!props.checked);
|
||||
} else if (props.type === 'normal') {
|
||||
props.onClick?.();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<li
|
||||
ref={setAnchor}
|
||||
class={itemStyle}
|
||||
onMouseEnter={handleHover}
|
||||
onClick={handleClick}
|
||||
data-selected={open()}
|
||||
>
|
||||
<Switch fallback={<div class={itemIconStyle}/>}>
|
||||
<Match when={props.type === 'checkbox' && props.checked}>
|
||||
<svg class={itemIconStyle} 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>
|
||||
</Match>
|
||||
<Match when={props.type === 'radio' && props.checked}>
|
||||
<svg class={itemIconStyle} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" style={{ padding: '6px' }}>
|
||||
<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>
|
||||
</Match>
|
||||
<Match when={props.type === 'radio' && !props.checked}>
|
||||
<svg class={itemIconStyle} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" style={{ padding: '6px' }}>
|
||||
<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>
|
||||
</Match>
|
||||
</Switch>
|
||||
<span class={itemLabelStyle}>
|
||||
{props.name}
|
||||
</span>
|
||||
<Show when={props.chip} fallback={<div/>}>
|
||||
<span class={itemChipStyle}>
|
||||
{props.chip}
|
||||
</span>
|
||||
</Show>
|
||||
<Show when={props.type === 'submenu'}>
|
||||
<svg class={itemIconStyle} 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>
|
||||
<Panel
|
||||
ref={setChild}
|
||||
open={open()}
|
||||
anchor={anchor()}
|
||||
placement={'right-start'}
|
||||
data-level={props.type === 'submenu' && props.level.join('/')}
|
||||
offset={{ mainAxis: 8 }}
|
||||
>
|
||||
{props.type === 'submenu' && props.children}
|
||||
</Panel>
|
||||
</Show>
|
||||
<Show when={props.toolTip}>
|
||||
<Portal>
|
||||
<div
|
||||
ref={setToolTip}
|
||||
class={popupStyle}
|
||||
style={{
|
||||
'--offset-x': `${position.x}px`,
|
||||
'--offset-y': `${position.y}px`,
|
||||
}}
|
||||
>
|
||||
<Transition
|
||||
appear
|
||||
enterClass={animationStyle.enter}
|
||||
enterActiveClass={animationStyle.enterActive}
|
||||
exitToClass={animationStyle.exitTo}
|
||||
exitActiveClass={animationStyle.exitActive}
|
||||
>
|
||||
<Show when={toolTipOpen()}>
|
||||
<div class={toolTipStyle}>
|
||||
{props.toolTip}
|
||||
</div>
|
||||
</Show>
|
||||
</Transition>
|
||||
</div>
|
||||
</Portal>
|
||||
</Show>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user