fix: apply fix from eslint

This commit is contained in:
JellyBrick
2024-10-13 22:45:11 +09:00
parent f42f20f770
commit cb1381bbb3
85 changed files with 1858 additions and 1042 deletions

View File

@ -4,24 +4,12 @@ export interface InAppMenuConfig {
}
export const defaultInAppMenuConfig: InAppMenuConfig = {
enabled:
(
(
typeof window !== 'undefined' &&
!window.navigator?.userAgent?.toLowerCase().includes('mac')
) ||
(
typeof global !== 'undefined' &&
global.process?.platform !== 'darwin'
)
) && (
(
typeof window !== 'undefined' &&
!window.navigator?.userAgent?.toLowerCase().includes('linux')
) ||
(
typeof global !== 'undefined' &&
global.process?.platform !== 'linux'
)
),
((typeof window !== 'undefined' &&
!window.navigator?.userAgent?.toLowerCase().includes('mac')) ||
(typeof global !== 'undefined' &&
global.process?.platform !== 'darwin')) &&
((typeof window !== 'undefined' &&
!window.navigator?.userAgent?.toLowerCase().includes('linux')) ||
(typeof global !== 'undefined' && global.process?.platform !== 'linux')),
hideDOMWindowControls: false,
};

View File

@ -1,6 +1,13 @@
import { register } from 'electron-localshortcut';
import { BrowserWindow, Menu, MenuItem, ipcMain, nativeImage } from 'electron';
import {
BrowserWindow,
Menu,
MenuItem,
ipcMain,
nativeImage,
WebContents,
} from 'electron';
import type { BackendContext } from '@/types/contexts';
import type { InAppMenuConfig } from './constants';
@ -50,11 +57,13 @@ export const onMainLoad = ({
ipcMain.handle('ytmd:menu-event', (event, commandId: number) => {
const target = getMenuItemById(commandId);
if (target)
target.click(
undefined,
BrowserWindow.fromWebContents(event.sender),
event.sender,
);
(
target.click as (
args0: unknown,
args1: BrowserWindow | null,
args3: WebContents,
) => void
)(undefined, BrowserWindow.fromWebContents(event.sender), event.sender);
});
handle('get-menu-by-id', (commandId: number) => {

View File

@ -16,8 +16,9 @@ const isMacOS = navigator.userAgent.includes('Macintosh');
const isNotWindowsOrMacOS =
!navigator.userAgent.includes('Windows') && !isMacOS;
const [config, setConfig] = createSignal<InAppMenuConfig>(defaultInAppMenuConfig);
const [config, setConfig] = createSignal<InAppMenuConfig>(
defaultInAppMenuConfig,
);
export const onRendererLoad = async ({
getConfig,
ipc,
@ -29,14 +30,19 @@ export const onRendererLoad = async ({
stylesheet.replaceSync(scrollStyle);
document.adoptedStyleSheets = [...document.adoptedStyleSheets, stylesheet];
render(() => (
<TitleBar
ipc={ipc}
isMacOS={isMacOS}
enableController={isNotWindowsOrMacOS && !config().hideDOMWindowControls}
initialCollapsed={window.mainConfig.get('options.hideMenu')}
/>
), document.body);
render(
() => (
<TitleBar
ipc={ipc}
isMacOS={isMacOS}
enableController={
isNotWindowsOrMacOS && !config().hideDOMWindowControls
}
initialCollapsed={window.mainConfig.get('options.hideMenu')}
/>
),
document.body,
);
};
export const onPlayerApiReady = () => {

View File

@ -3,36 +3,38 @@ import { css } from 'solid-styled-components';
import { cacheNoArgs } from '@/providers/decorators';
const iconButton = cacheNoArgs(() => css`
-webkit-app-region: none;
const iconButton = cacheNoArgs(
() => css`
-webkit-app-region: none;
background: transparent;
background: transparent;
width: 24px;
height: 24px;
width: 24px;
height: 24px;
padding: 2px;
border-radius: 2px;
padding: 2px;
border-radius: 2px;
display: flex;
justify-content: center;
align-items: center;
display: flex;
justify-content: center;
align-items: center;
color: white;
color: white;
outline: none;
border: none;
outline: none;
border: none;
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
&:hover {
background: rgba(255, 255, 255, 0.1);
}
&:hover {
background: rgba(255, 255, 255, 0.1);
}
&:active {
scale: 0.9;
}
`);
&:active {
scale: 0.9;
}
`,
);
type CollapseIconButtonProps = JSX.HTMLAttributes<HTMLButtonElement>;
export const IconButton = (props: CollapseIconButtonProps) => {

View File

@ -3,31 +3,33 @@ import { css } from 'solid-styled-components';
import { cacheNoArgs } from '@/providers/decorators';
const menuStyle = cacheNoArgs(() => css`
-webkit-app-region: none;
const menuStyle = cacheNoArgs(
() => css`
-webkit-app-region: none;
display: flex;
justify-content: center;
align-items: center;
align-self: stretch;
display: flex;
justify-content: center;
align-items: center;
align-self: stretch;
padding: 2px 8px;
border-radius: 4px;
padding: 2px 8px;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
cursor: pointer;
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
&:hover {
background-color: rgba(255, 255, 255, 0.1);
}
&:active {
scale: 0.9;
}
&:hover {
background-color: rgba(255, 255, 255, 0.1);
}
&:active {
scale: 0.9;
}
&[data-selected="true"] {
background-color: rgba(255, 255, 255, 0.2);
}
`);
&[data-selected='true'] {
background-color: rgba(255, 255, 255, 0.2);
}
`,
);
export type MenuButtonProps = JSX.HTMLAttributes<HTMLLIElement> & {
text?: string;

View File

@ -2,39 +2,48 @@ import { createSignal, JSX, Show, splitProps } from 'solid-js';
import { mergeProps, Portal } from 'solid-js/web';
import { css } from 'solid-styled-components';
import { Transition } from 'solid-transition-group';
import { autoUpdate, flip, offset, OffsetOptions, size } from '@floating-ui/dom';
import {
autoUpdate,
flip,
offset,
OffsetOptions,
size,
} from '@floating-ui/dom';
import { useFloating } from 'solid-floating-ui';
import { cacheNoArgs } from '@/providers/decorators';
const panelStyle = cacheNoArgs(() => css`
position: fixed;
top: var(--offset-y, 0);
left: var(--offset-x, 0);
const panelStyle = cacheNoArgs(
() => 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%);
max-width: var(--max-width, 100%);
max-height: var(--max-height, 100%);
z-index: 10000;
width: fit-content;
height: fit-content;
z-index: 10000;
width: fit-content;
height: fit-content;
padding: 4px;
box-sizing: border-box;
border-radius: 8px;
overflow: auto;
padding: 4px;
box-sizing: border-box;
border-radius: 8px;
overflow: auto;
background-color: color-mix(
in srgb,
var(--titlebar-background-color, #030303) 50%,
rgba(0, 0, 0, 0.1)
);
backdrop-filter: blur(8px);
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05),
0 2px 8px rgba(0, 0, 0, 0.2);
background-color: color-mix(
in srgb,
var(--titlebar-background-color, #030303) 50%,
rgba(0, 0, 0, 0.1)
);
backdrop-filter: blur(8px);
box-shadow:
0 0 0 1px rgba(0, 0, 0, 0.05),
0 2px 8px rgba(0, 0, 0, 0.2);
transform-origin: var(--origin-x, 50%) var(--origin-y, 50%);
`);
transform-origin: var(--origin-x, 50%) var(--origin-y, 50%);
`,
);
const animationStyle = cacheNoArgs(() => ({
enter: css`
@ -42,19 +51,23 @@ const animationStyle = cacheNoArgs(() => ({
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);
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);
transition:
opacity 0.225s cubic-bezier(0.32, 0, 0.67, 0),
transform 0.225s cubic-bezier(0.32, 0, 0.67, 0);
`,
}));
export type Placement =
'top'
| 'top'
| 'bottom'
| 'left'
| 'right'
@ -92,9 +105,15 @@ export const Panel = (props: PanelProps) => {
size({
padding: 8,
apply({ elements, availableWidth, availableHeight }) {
elements.floating.style.setProperty('--max-width', `${Math.max(200, availableWidth)}px`);
elements.floating.style.setProperty('--max-height', `${Math.max(200, availableHeight)}px`);
}
elements.floating.style.setProperty(
'--max-width',
`${Math.max(200, availableWidth)}px`,
);
elements.floating.style.setProperty(
'--max-height',
`${Math.max(200, availableHeight)}px`,
);
},
}),
flip({ fallbackStrategy: 'initialPlacement' }),
],
@ -103,7 +122,10 @@ export const Panel = (props: PanelProps) => {
const originX = () => {
if (position.placement.includes('left')) return '100%';
if (position.placement.includes('right')) return '0';
if (position.placement.includes('top') || position.placement.includes('bottom')) {
if (
position.placement.includes('top') ||
position.placement.includes('bottom')
) {
if (position.placement.includes('start')) return '0';
if (position.placement.includes('end')) return '100%';
}
@ -113,7 +135,10 @@ export const Panel = (props: PanelProps) => {
const originY = () => {
if (position.placement.includes('top')) return '100%';
if (position.placement.includes('bottom')) return '0';
if (position.placement.includes('left') || position.placement.includes('right')) {
if (
position.placement.includes('left') ||
position.placement.includes('right')
) {
if (position.placement.includes('start')) return '0';
if (position.placement.includes('end')) return '100%';
}

View File

@ -10,100 +10,111 @@ import { autoUpdate, offset, size } from '@floating-ui/dom';
import { Panel } from './Panel';
import { cacheNoArgs } from '@/providers/decorators';
const itemStyle = cacheNoArgs(() => css`
position: relative;
const itemStyle = cacheNoArgs(
() => css`
position: relative;
-webkit-app-region: none;
min-height: 32px;
height: 32px;
-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;
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);
}
& * {
border-radius: 4px;
cursor: pointer;
box-sizing: border-box;
}
`);
user-select: none;
-webkit-user-drag: none;
const itemIconStyle = cacheNoArgs(() => css`
height: 32px;
padding: 4px;
color: white;
`);
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
const itemLabelStyle = cacheNoArgs(() => css`
font-size: 12px;
color: white;
`);
&:hover {
background-color: rgba(255, 255, 255, 0.1);
}
const itemChipStyle = cacheNoArgs(() => css`
display: flex;
justify-content: center;
align-items: center;
&:active {
background-color: rgba(255, 255, 255, 0.2);
}
min-width: 16px;
height: 16px;
padding: 0 4px;
margin-left: 8px;
&[data-selected='true'] {
background-color: rgba(255, 255, 255, 0.2);
}
border-radius: 4px;
background-color: rgba(255, 255, 255, 0.2);
color: #f1f1f1;
font-size: 10px;
font-weight: 500;
line-height: 1;
`);
& * {
box-sizing: border-box;
}
`,
);
const toolTipStyle = cacheNoArgs(() => css`
min-width: 32px;
width: 100%;
height: 100%;
const itemIconStyle = cacheNoArgs(
() => css`
height: 32px;
padding: 4px;
color: white;
`,
);
padding: 4px;
const itemLabelStyle = cacheNoArgs(
() => css`
font-size: 12px;
color: white;
`,
);
max-width: calc(var(--max-width, 100%) - 8px);
max-height: calc(var(--max-height, 100%) - 8px);
const itemChipStyle = cacheNoArgs(
() => css`
display: flex;
justify-content: center;
align-items: center;
border-radius: 4px;
background-color: rgba(25, 25, 25, 0.8);
color: #f1f1f1;
font-size: 10px;
`);
min-width: 16px;
height: 16px;
padding: 0 4px;
margin-left: 8px;
const popupStyle = cacheNoArgs(() => css`
position: fixed;
top: var(--offset-y, 0);
left: var(--offset-x, 0);
border-radius: 4px;
background-color: rgba(255, 255, 255, 0.2);
color: #f1f1f1;
font-size: 10px;
font-weight: 500;
line-height: 1;
`,
);
max-width: var(--max-width, 100%);
max-height: var(--max-height, 100%);
const toolTipStyle = cacheNoArgs(
() => css`
min-width: 32px;
width: 100%;
height: 100%;
z-index: 100000000;
pointer-events: none;
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 = cacheNoArgs(
() => 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 = cacheNoArgs(() => ({
enter: css`
@ -111,14 +122,18 @@ const animationStyle = cacheNoArgs(() => ({
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);
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);
transition:
opacity 0.225s cubic-bezier(0.32, 0, 0.67, 0),
transform 0.225s cubic-bezier(0.32, 0, 0.67, 0);
`,
}));
@ -160,7 +175,11 @@ type CheckboxPanelItemProps = BasePanelItemProps & {
checked: boolean;
onChange?: (checked: boolean) => void;
};
export type PanelItemProps = NormalPanelItemProps | SubmenuItemProps | RadioPanelItemProps | CheckboxPanelItemProps;
export type PanelItemProps =
| NormalPanelItemProps
| SubmenuItemProps
| RadioPanelItemProps
| CheckboxPanelItemProps;
export const PanelItem = (props: PanelItemProps) => {
const [open, setOpen] = createSignal(false);
const [toolTipOpen, setToolTipOpen] = createSignal(false);
@ -176,17 +195,24 @@ export const PanelItem = (props: PanelItemProps) => {
offset({ mainAxis: 8 }),
size({
apply({ rects, elements }) {
elements.floating.style.setProperty('--max-width', `${rects.reference.width}px`);
}
elements.floating.style.setProperty(
'--max-width',
`${rects.reference.width}px`,
);
},
}),
],
});
const handleHover = (event: MouseEvent) => {
setToolTipOpen(true);
event.target?.addEventListener('mouseleave', () => {
setToolTipOpen(false);
}, { once: true });
event.target?.addEventListener(
'mouseleave',
() => {
setToolTipOpen(false);
},
{ once: true },
);
if (props.type === 'submenu') {
const timer = setTimeout(() => {
@ -200,36 +226,54 @@ export const PanelItem = (props: PanelItemProps) => {
};
document.addEventListener('mousemove', onMouseMove);
event.target?.addEventListener('mouseleave', () => {
setTimeout(() => {
document.removeEventListener('mousemove', onMouseMove);
const parents = getParents(document.elementFromPoint(mouseX, mouseY));
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();
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('/'));
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 });
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 });
event.target?.addEventListener(
'mouseleave',
() => {
clearTimeout(timer);
},
{ once: true },
);
}
};
@ -244,7 +288,6 @@ export const PanelItem = (props: PanelItemProps) => {
}
};
return (
<li
ref={setAnchor}
@ -253,45 +296,66 @@ export const PanelItem = (props: PanelItemProps) => {
onClick={handleClick}
data-selected={open()}
>
<Switch fallback={<div class={itemIconStyle()}/>}>
<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
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
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
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>
<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
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}
@ -322,9 +386,7 @@ export const PanelItem = (props: PanelItemProps) => {
exitActiveClass={animationStyle().exitActive}
>
<Show when={toolTipOpen()}>
<div class={toolTipStyle()}>
{props.toolTip}
</div>
<div class={toolTipStyle()}>{props.toolTip}</div>
</Show>
</Transition>
</div>

View File

@ -1,5 +1,15 @@
import { Menu, MenuItem } from 'electron';
import { createEffect, createResource, createSignal, Index, Match, onCleanup, onMount, Show, Switch } from 'solid-js';
import {
createEffect,
createResource,
createSignal,
Index,
Match,
onCleanup,
onMount,
Show,
Switch,
} from 'solid-js';
import { css } from 'solid-styled-components';
import { TransitionGroup } from 'solid-transition-group';
@ -14,49 +24,55 @@ import { cacheNoArgs } from '@/providers/decorators';
import type { RendererContext } from '@/types/contexts';
import type { InAppMenuConfig } from '../constants';
const titleStyle = cacheNoArgs(() => css`
-webkit-app-region: drag;
box-sizing: border-box;
const titleStyle = cacheNoArgs(
() => css`
-webkit-app-region: drag;
box-sizing: border-box;
position: fixed;
top: 0;
z-index: 10000000;
position: fixed;
top: 0;
z-index: 10000000;
width: 100%;
height: var(--menu-bar-height, 32px);
width: 100%;
height: var(--menu-bar-height, 32px);
display: flex;
flex-flow: row;
justify-content: flex-start;
align-items: center;
gap: 4px;
display: flex;
flex-flow: row;
justify-content: flex-start;
align-items: center;
gap: 4px;
color: #f1f1f1;
font-size: 12px;
padding: 4px 4px 4px var(--offset-left, 4px);
background-color: var(--titlebar-background-color, #030303);
user-select: none;
color: #f1f1f1;
font-size: 12px;
padding: 4px 4px 4px var(--offset-left, 4px);
background-color: var(--titlebar-background-color, #030303);
user-select: none;
transition: opacity 200ms ease 0s,
transform 300ms cubic-bezier(0.2, 0, 0.6, 1) 0s,
background-color 300ms cubic-bezier(0.2, 0, 0.6, 1) 0s;
transition:
opacity 200ms ease 0s,
transform 300ms cubic-bezier(0.2, 0, 0.6, 1) 0s,
background-color 300ms cubic-bezier(0.2, 0, 0.6, 1) 0s;
&[data-macos="true"] {
padding: 4px 4px 4px 74px;
}
&[data-macos='true'] {
padding: 4px 4px 4px 74px;
}
ytmusic-app:has(ytmusic-player[player-ui-state=FULLSCREEN]) ~ &:not([data-show="true"]) {
transform: translateY(calc(-1 * var(--menu-bar-height, 32px)));
}
`);
ytmusic-app:has(ytmusic-player[player-ui-state='FULLSCREEN'])
~ &:not([data-show='true']) {
transform: translateY(calc(-1 * var(--menu-bar-height, 32px)));
}
`,
);
const separatorStyle = cacheNoArgs(() => css`
min-height: 1px;
height: 1px;
margin: 4px 0;
const separatorStyle = cacheNoArgs(
() => css`
min-height: 1px;
height: 1px;
margin: 4px 0;
background-color: rgba(255, 255, 255, 0.2);
`);
background-color: rgba(255, 255, 255, 0.2);
`,
);
const animationStyle = cacheNoArgs(() => ({
enter: css`
@ -64,14 +80,18 @@ const animationStyle = cacheNoArgs(() => ({
transform: translateX(-50%) scale(0.8);
`,
enterActive: css`
transition: opacity 0.1s cubic-bezier(0.33, 1, 0.68, 1), transform 0.1s cubic-bezier(0.33, 1, 0.68, 1);
transition:
opacity 0.1s cubic-bezier(0.33, 1, 0.68, 1),
transform 0.1s cubic-bezier(0.33, 1, 0.68, 1);
`,
exitTo: css`
opacity: 0;
transform: translateX(-50%) scale(0.8);
`,
exitActive: css`
transition: opacity 0.1s cubic-bezier(0.32, 0, 0.67, 0), transform 0.1s cubic-bezier(0.32, 0, 0.67, 0);
transition:
opacity 0.1s cubic-bezier(0.32, 0, 0.67, 0),
transform 0.1s cubic-bezier(0.32, 0, 0.67, 0);
`,
move: css`
transition: all 0.1s cubic-bezier(0.65, 0, 0.35, 1);
@ -89,7 +109,7 @@ export type PanelRendererProps = {
items: Electron.Menu['items'];
level?: number[];
onClick?: (commandId: number, radioGroup?: MenuItem[]) => void;
}
};
const PanelRenderer = (props: PanelRendererProps) => {
const radioGroup = () => props.items.filter((it) => it.type === 'radio');
@ -114,12 +134,12 @@ const PanelRenderer = (props: PanelRendererProps) => {
name={subItem().label}
chip={subItem().sublabel}
toolTip={subItem().toolTip}
level={[...props.level ?? [], subItem().commandId]}
level={[...(props.level ?? []), subItem().commandId]}
commandId={subItem().commandId}
>
<PanelRenderer
items={subItem().submenu?.items ?? []}
level={[...props.level ?? [], subItem().commandId]}
level={[...(props.level ?? []), subItem().commandId]}
onClick={props.onClick}
/>
</PanelItem>
@ -143,11 +163,13 @@ const PanelRenderer = (props: PanelRendererProps) => {
chip={subItem().sublabel}
toolTip={subItem().toolTip}
commandId={subItem().commandId}
onChange={() => props.onClick?.(subItem().commandId, radioGroup())}
onChange={() =>
props.onClick?.(subItem().commandId, radioGroup())
}
/>
</Match>
<Match when={subItem().type === 'separator'}>
<hr class={separatorStyle()}/>
<hr class={separatorStyle()} />
</Match>
</Switch>
</Show>
@ -169,8 +191,13 @@ export const TitleBar = (props: TitleBarProps) => {
const [menu, setMenu] = createSignal<Menu | null>(null);
const [mouseY, setMouseY] = createSignal(0);
const [data, { refetch }] = createResource(async () => await props.ipc.invoke('get-menu') as Promise<Menu | null>);
const [isMaximized, { refetch: refetchMaximize }] = createResource(async () => await props.ipc.invoke('window-is-maximized') as Promise<boolean>);
const [data, { refetch }] = createResource(
async () => (await props.ipc.invoke('get-menu')) as Promise<Menu | null>,
);
const [isMaximized, { refetch: refetchMaximize }] = createResource(
async () =>
(await props.ipc.invoke('window-is-maximized')) as Promise<boolean>,
);
const handleToggleMaximize = async () => {
if (isMaximized()) {
@ -194,10 +221,12 @@ export const TitleBar = (props: TitleBarProps) => {
)) as MenuItem | null;
const newMenu = structuredClone(originalMenu);
const stack = [...newMenu?.items ?? []];
const stack = [...(newMenu?.items ?? [])];
let now: MenuItem | undefined = stack.pop();
while (now) {
const index = now?.submenu?.items?.findIndex((it) => it.commandId === commandId) ?? -1;
const index =
now?.submenu?.items?.findIndex((it) => it.commandId === commandId) ??
-1;
if (index >= 0) {
if (menuItem) now?.submenu?.items?.splice(index, 1, menuItem);
@ -213,13 +242,16 @@ export const TitleBar = (props: TitleBarProps) => {
return newMenu;
};
const handleItemClick = async (commandId: number, radioGroup?: MenuItem[]) => {
const handleItemClick = async (
commandId: number,
radioGroup?: MenuItem[],
) => {
const menuData = menu();
if (!menuData) return;
if (Array.isArray(radioGroup)) {
let newMenu = menuData;
for await (const item of radioGroup) {
for (const item of radioGroup) {
newMenu = await refreshMenuItem(newMenu, item.commandId);
}
@ -272,18 +304,15 @@ export const TitleBar = (props: TitleBarProps) => {
window.addEventListener('mousemove', listener);
const ytmusicAppLayout = document.querySelector<HTMLElement>('#layout');
ytmusicAppLayout?.addEventListener('scroll', () => {
const scrollValue = ytmusicAppLayout.scrollTop;
if (scrollValue > 20){
ytmusicAppLayout.classList.add('content-scrolled');
}
else{
ytmusicAppLayout.classList.remove('content-scrolled');
}
const scrollValue = ytmusicAppLayout.scrollTop;
if (scrollValue > 20) {
ytmusicAppLayout.classList.add('content-scrolled');
} else {
ytmusicAppLayout.classList.remove('content-scrolled');
}
});
});
createEffect(() => {
if (!menu() && data()) {
setMenu(data() ?? null);
@ -295,7 +324,12 @@ export const TitleBar = (props: TitleBarProps) => {
});
return (
<nav data-ytmd-main-panel={true} class={titleStyle()} data-macos={props.isMacOS} data-show={mouseY() < 32}>
<nav
data-ytmd-main-panel={true}
class={titleStyle()}
data-macos={props.isMacOS}
data-show={mouseY() < 32}
>
<IconButton
onClick={() => setCollapsed(!collapsed())}
style={{
@ -310,15 +344,34 @@ export const TitleBar = (props: TitleBarProps) => {
</svg>
</IconButton>
<TransitionGroup
enterClass={ignoreTransition() ? animationStyle().fakeTarget : animationStyle().enter}
enterActiveClass={ignoreTransition() ? animationStyle().fake : animationStyle().enterActive}
exitToClass={ignoreTransition() ? animationStyle().fakeTarget : animationStyle().exitTo}
exitActiveClass={ignoreTransition() ? animationStyle().fake : animationStyle().exitActive}
enterClass={
ignoreTransition()
? animationStyle().fakeTarget
: animationStyle().enter
}
enterActiveClass={
ignoreTransition()
? animationStyle().fake
: animationStyle().enterActive
}
exitToClass={
ignoreTransition()
? animationStyle().fakeTarget
: animationStyle().exitTo
}
exitActiveClass={
ignoreTransition()
? animationStyle().fake
: animationStyle().exitActive
}
onBeforeEnter={(element) => {
if (ignoreTransition()) return;
const index = Number(element.getAttribute('data-index') ?? 0);
(element as HTMLElement).style.setProperty('transition-delay', `${(index * 0.025)}s`);
(element as HTMLElement).style.setProperty(
'transition-delay',
`${index * 0.025}s`,
);
}}
onAfterEnter={(element) => {
(element as HTMLElement).style.removeProperty('transition-delay');
@ -328,13 +381,18 @@ export const TitleBar = (props: TitleBarProps) => {
const index = Number(element.getAttribute('data-index') ?? 0);
const length = Number(element.getAttribute('data-length') ?? 1);
(element as HTMLElement).style.setProperty('transition-delay', `${(length * 0.025) - (index * 0.025)}s`);
(element as HTMLElement).style.setProperty(
'transition-delay',
`${length * 0.025 - index * 0.025}s`,
);
}}
>
<Show when={!collapsed()}>
<Index each={menu()?.items}>
{(item, index) => {
const [anchor, setAnchor] = createSignal<HTMLElement | null>(null);
const [anchor, setAnchor] = createSignal<HTMLElement | null>(
null,
);
const handleClick = () => {
if (openTarget() === anchor()) {
@ -372,7 +430,7 @@ export const TitleBar = (props: TitleBarProps) => {
</Show>
</TransitionGroup>
<Show when={props.enableController}>
<div style={{ flex: 1 }}/>
<div style={{ flex: 1 }} />
<WindowController
isMaximize={isMaximized()}
onToggleMaximize={handleToggleMaximize}
@ -383,4 +441,3 @@ export const TitleBar = (props: TitleBarProps) => {
</nav>
);
};

View File

@ -4,19 +4,21 @@ import { Show } from 'solid-js';
import { IconButton } from './IconButton';
import { cacheNoArgs } from '@/providers/decorators';
const containerStyle = cacheNoArgs(() => css`
display: flex;
justify-content: flex-end;
align-items: center;
const containerStyle = cacheNoArgs(
() => css`
display: flex;
justify-content: flex-end;
align-items: center;
& > *:last-of-type {
border-top-right-radius: 4px;
& > *:last-of-type {
border-top-right-radius: 4px;
&:hover {
background: rgba(255, 0, 0, 0.5);
&:hover {
background: rgba(255, 0, 0, 0.5);
}
}
}
`);
`,
);
export type WindowControllerProps = {
isMaximize?: boolean;
@ -24,20 +26,35 @@ export type WindowControllerProps = {
onToggleMaximize?: () => void;
onMinimize?: () => void;
onClose?: () => void;
}
};
export const WindowController = (props: WindowControllerProps) => {
return (
<div class={containerStyle()}>
<IconButton onClick={props.onMinimize}>
<svg width={16} height={16} fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor" d="M3.755 12.5h16.492a.75.75 0 0 0 0-1.5H3.755a.75.75 0 0 0 0 1.5Z"/>
<svg
width={16}
height={16}
fill="none"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill="currentColor"
d="M3.755 12.5h16.492a.75.75 0 0 0 0-1.5H3.755a.75.75 0 0 0 0 1.5Z"
/>
</svg>
</IconButton>
<IconButton onClick={props.onToggleMaximize}>
<Show
when={props.isMaximize}
fallback={
<svg width={16} height={16} fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<svg
width={16}
height={16}
fill="none"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill="currentColor"
d="M6 3h12a3 3 0 0 1 3 3v12a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3Zm0 2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1H6Z"
@ -45,7 +62,13 @@ export const WindowController = (props: WindowControllerProps) => {
</svg>
}
>
<svg width={16} height={16} fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<svg
width={16}
height={16}
fill="none"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill="currentColor"
d="M7.518 5H6.009a3.25 3.25 0 0 1 3.24-3h8.001A4.75 4.75 0 0 1 22 6.75v8a3.25 3.25 0 0 1-3 3.24v-1.508a1.75 1.75 0 0 0 1.5-1.732v-8a3.25 3.25 0 0 0-3.25-3.25h-8A1.75 1.75 0 0 0 7.518 5ZM5.25 6A3.25 3.25 0 0 0 2 9.25v9.5A3.25 3.25 0 0 0 5.25 22h9.5A3.25 3.25 0 0 0 18 18.75v-9.5A3.25 3.25 0 0 0 14.75 6h-9.5ZM3.5 9.25c0-.966.784-1.75 1.75-1.75h9.5c.967 0 1.75.784 1.75 1.75v9.5a1.75 1.75 0 0 1-1.75 1.75h-9.5a1.75 1.75 0 0 1-1.75-1.75v-9.5Z"
@ -54,7 +77,13 @@ export const WindowController = (props: WindowControllerProps) => {
</Show>
</IconButton>
<IconButton onClick={props.onClose}>
<svg width={16} height={16} fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<svg
width={16}
height={16}
fill="none"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill="currentColor"
d="m4.21 4.387.083-.094a1 1 0 0 1 1.32-.083l.094.083L12 10.585l6.293-6.292a1 1 0 1 1 1.414 1.414L13.415 12l6.292 6.293a1 1 0 0 1 .083 1.32l-.083.094a1 1 0 0 1-1.32.083l-.094-.083L12 13.415l-6.293 6.292a1 1 0 0 1-1.414-1.414L10.585 12 4.293 5.707a1 1 0 0 1-.083-1.32l.083-.094-.083.094Z"