mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-12 02:51:46 +00:00
* feat(music-together): test `peerjs` * feat(music-together): replace `prompt` to `custom-electron-prompt` * fix(music-together): fix * test fix * wow * test * feat(music-together): improve `onStart` * fix: adblocker * fix(adblock): fix crash with `peerjs` * feat(music-together): add host UI * feat(music-together): implement addSong, removeSong, syncQueue * feat(music-together): inject panel * feat(music-together): redesign music together panel * feat(music-together): sync queue, profile * feat(music-together): sync progress, song, state * fix(music-together): fix some bug * fix(music-together): fix sync queue * feat(music-together): support i18n * feat(music-together): improve sync queue * feat(music-together): add profile in music item * refactor(music-together): refactor structure * feat(music-together): add permission * fix(music-together): fix queue sync bug * fix(music-together): fix some bugs * fix(music-together): fix permission not working on guest mode * fix(music-together): fix queue sync relate bugs * fix(music-together): fix automix items not append using music together * fix(music-together): fix * feat(music-together): improve video injection * fix(music-together): fix injection code * fix(music-together): fix broadcast guest * feat(music-together): add more permission * fix(music-together): fix injector * fix(music-together): fix guest add song logic * feat(music-together): add popup close listener * fix(music-together): fix connection issue * fix(music-together): fix connection issue 2 * feat(music-together): reserve playlist * fix(music-together): exclude automix songs * fix(music-together): fix playlist index sync bug * fix(music-together): fix connection failed error and sync index * fix(music-together): fix host set index bug * fix: apply fix from eslint * feat(util): add `ImageElementFromSrc` * chore(util): update jsdoc * feat(music-together): add owner name * chore(music-together): add translation * feat(music-together): add progress sync * chore(music-together): remove `console.log` --------- Co-authored-by: JellyBrick <shlee1503@naver.com>
139 lines
3.9 KiB
TypeScript
139 lines
3.9 KiB
TypeScript
import { ElementFromHtml } from '@/plugins/utils/renderer';
|
|
|
|
import itemHTML from './templates/item.html?raw';
|
|
import popupHTML from './templates/popup.html?raw';
|
|
|
|
type Placement =
|
|
'top'
|
|
| 'bottom'
|
|
| 'right'
|
|
| 'left'
|
|
| 'center'
|
|
| 'middle'
|
|
| 'center-middle'
|
|
| 'top-left'
|
|
| 'top-right'
|
|
| 'bottom-left'
|
|
| 'bottom-right';
|
|
type PopupItem = (ItemRendererProps & { type: 'item'; })
|
|
| { type: 'divider'; }
|
|
| { type: 'custom'; element: HTMLElement; };
|
|
|
|
type PopupProps = {
|
|
data: PopupItem[];
|
|
anchorAt?: Placement;
|
|
popupAt?: Placement;
|
|
}
|
|
export const Popup = (props: PopupProps) => {
|
|
const popup = ElementFromHtml(popupHTML);
|
|
const container = popup.querySelector<HTMLElement>('.music-together-popup-container')!;
|
|
const items = props.data
|
|
.map((props) => {
|
|
if (props.type === 'item') return {
|
|
type: 'item' as const,
|
|
...ItemRenderer(props),
|
|
};
|
|
if (props.type === 'divider') return {
|
|
type: 'divider' as const,
|
|
element: ElementFromHtml('<div class="music-together-divider horizontal"></div>'),
|
|
};
|
|
if (props.type === 'custom') return {
|
|
type: 'custom' as const,
|
|
element: props.element,
|
|
};
|
|
|
|
return null;
|
|
})
|
|
.filter(Boolean);
|
|
|
|
container.append(...items.map(({ element }) => element));
|
|
popup.style.setProperty('opacity', '0');
|
|
popup.style.setProperty('pointer-events', 'none');
|
|
|
|
document.body.append(popup);
|
|
|
|
return {
|
|
element: popup,
|
|
container,
|
|
items,
|
|
|
|
show(x: number, y: number, anchor?: HTMLElement) {
|
|
let left = x;
|
|
let top = y;
|
|
|
|
if (anchor) {
|
|
if (props.anchorAt?.includes('right')) left += anchor.clientWidth;
|
|
if (props.anchorAt?.includes('bottom')) top += anchor.clientHeight;
|
|
if (props.anchorAt?.includes('center')) left += anchor.clientWidth / 2;
|
|
if (props.anchorAt?.includes('middle')) top += anchor.clientHeight / 2;
|
|
}
|
|
|
|
if (props.popupAt?.includes('right')) left -= popup.clientWidth;
|
|
if (props.popupAt?.includes('bottom')) top -= popup.clientHeight;
|
|
if (props.popupAt?.includes('center')) left -= popup.clientWidth / 2;
|
|
if (props.popupAt?.includes('middle')) top -= popup.clientHeight / 2;
|
|
|
|
popup.style.setProperty('left', `${left}px`);
|
|
popup.style.setProperty('top', `${top}px`);
|
|
popup.style.setProperty('opacity', '1');
|
|
popup.style.setProperty('pointer-events', 'unset');
|
|
|
|
setTimeout(() => {
|
|
const onClose = (event: MouseEvent) => {
|
|
const isPopupClick = event.composedPath().some((element) => element === popup);
|
|
if (!isPopupClick) {
|
|
this.dismiss();
|
|
document.removeEventListener('click', onClose);
|
|
}
|
|
};
|
|
document.addEventListener('click', onClose);
|
|
}, 16);
|
|
},
|
|
showAtAnchor(anchor: HTMLElement) {
|
|
const { x, y } = anchor.getBoundingClientRect();
|
|
this.show(x, y, anchor);
|
|
},
|
|
|
|
isShowing() {
|
|
return popup.style.getPropertyValue('opacity') === '1';
|
|
},
|
|
|
|
dismiss() {
|
|
popup.style.setProperty('opacity', '0');
|
|
popup.style.setProperty('pointer-events', 'none');
|
|
}
|
|
};
|
|
};
|
|
|
|
type ItemRendererProps = {
|
|
id?: string;
|
|
icon?: Element;
|
|
text: string;
|
|
onClick?: () => void;
|
|
};
|
|
export const ItemRenderer = (props: ItemRendererProps) => {
|
|
const element = ElementFromHtml(itemHTML);
|
|
const iconContainer = element.querySelector<HTMLElement>('div.icon')!;
|
|
const textContainer = element.querySelector<HTMLElement>('div.text')!;
|
|
if (props.icon) iconContainer.appendChild(props.icon);
|
|
textContainer.append(props.text);
|
|
|
|
if (props.onClick) {
|
|
element.addEventListener('click', () => {
|
|
props.onClick?.();
|
|
});
|
|
}
|
|
if (props.id) element.id = props.id;
|
|
|
|
return {
|
|
element,
|
|
setIcon(icon: Element) {
|
|
iconContainer.replaceChildren(icon);
|
|
},
|
|
setText(text: string) {
|
|
textContainer.replaceChildren(text);
|
|
},
|
|
id: props.id
|
|
};
|
|
};
|