Skip to content

Commit

Permalink
Merge pull request #84 from D0m1nos/context-menu
Browse files Browse the repository at this point in the history
Reimplement context menu
  • Loading branch information
duduBTW authored Oct 24, 2024
2 parents 3f0d272 + 8f40f2d commit 203c7a8
Show file tree
Hide file tree
Showing 10 changed files with 216 additions and 122 deletions.
31 changes: 30 additions & 1 deletion src/renderer/src/components/popover/Popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@ import "./styles.css";
import {
computePosition,
ComputePositionReturn,
flip,
FlipOptions,
offset,
OffsetOptions,
Placement,
shift,
ShiftOptions,
} from "@floating-ui/dom";
import useControllableState from "@renderer/lib/controllable-state";
import { createSignal, createContext, useContext, ParentComponent, Accessor } from "solid-js";
Expand All @@ -16,7 +20,10 @@ export const DEFAULT_POPOVER_OPEN = false;

export type Props = {
offset?: OffsetOptions;
flip?: FlipOptions;
shift?: ShiftOptions;
placement?: Placement;
mousePos?: Accessor<[number, number]>; // [x, y]
defaultProp?: boolean;
isOpen?: Accessor<boolean>;
onValueChange?: (newOpen: boolean) => void;
Expand Down Expand Up @@ -45,6 +52,23 @@ function useProviderValue(props: Props) {
listenResize();
};

let lastMousePos: [number, number];
const useCustomCoords = {
name: "useCustomCoords",
fn() {
if (
props.mousePos !== undefined &&
props.mousePos() !== lastMousePos &&
props.mousePos()[0] !== 0 &&
props.mousePos()[1] !== 0
) {
lastMousePos = props.mousePos();
return { x: lastMousePos[0], y: lastMousePos[1] };
}
return {};
},
};

const listenResize = () => {
const trigger = triggerRef();
const content = contentRef();
Expand All @@ -56,7 +80,12 @@ function useProviderValue(props: Props) {
computePosition(trigger, content, {
placement: props.placement,
strategy: "fixed",
middleware: [offset(props.offset)],
middleware: [
props.mousePos !== undefined && useCustomCoords,
offset(props.offset),
props.shift && shift(props.shift),
props.flip && flip(props.flip),
],
}).then(setPosition);
};

Expand Down
68 changes: 5 additions & 63 deletions src/renderer/src/components/song/context-menu/SongContextMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,70 +1,19 @@
import "../../../assets/css/song/song-context-menu.css";
import { Accessor, Component, createEffect, For, onCleanup, onMount, Show, Signal } from "solid-js";
import { Component, For } from "solid-js";

type SongContextMenuProps = {
show: Signal<boolean>;
coords: Accessor<[number, number]>;
container: any;
children: any;
};

const SongContextMenu: Component<SongContextMenuProps> = (props) => {
const [show, setShow] = props.show;
let menu: HTMLDivElement | undefined;

const windowContextMenu = (evt: MouseEvent) => {
const t = evt.target;

if (!(t instanceof HTMLElement)) {
return;
}

const targetItem = t.closest(".song-item");
const menuParent = menu?.closest(".song-item");

if (targetItem === menuParent) {
evt.stopPropagation();
return;
}

setShow(false);
};

const calculatePosition = () => {
const c = props.coords();
menu?.style.setProperty("--x", `${Math.round(c[0])}px`);
menu?.style.setProperty("--y", `${Math.round(c[1])}px`);
};

onMount(() => {
createEffect(() => {
const s = show();

if (s === false) {
window.removeEventListener("contextmenu", windowContextMenu);
return;
}

calculatePosition();
window.addEventListener("click", () => setShow(false), { once: true });
window.addEventListener("contextmenu", windowContextMenu);
});
});

onCleanup(() => {
window.removeEventListener("click", () => setShow(false));
window.removeEventListener("contextmenu", windowContextMenu);
menu?.removeEventListener("click", (evt) => evt.stopPropagation());
});

return (
<Show when={show()}>
<div class="absolute z-50 overflow-hidden rounded-md bg-surface shadow-lg" ref={menu}>
<div class="bg-gradient-to-b from-black/30 to-transparent">
<For each={props.children}>{(child) => child}</For>
</div>
<div class="z-30 min-w-48 rounded-xl border border-stroke bg-thick-material" ref={menu}>
<div class="flex flex-col gap-1 rounded-xl bg-thick-material p-3">
<For each={props.children}>{(child) => child}</For>
</div>
</Show>
</div>
);
};

Expand All @@ -75,13 +24,6 @@ export function ignoreClickInContextMenu(fn: (evt: MouseEvent) => any): (evt: Mo
const t = evt.target;

if (!(t instanceof HTMLElement)) {
fn(evt);
return;
}

const menu = t.closest(".song-menu");

if (menu !== null) {
return;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import { Component, onCleanup } from "solid-js";
import { Component, JSX, onCleanup } from "solid-js";
import { twMerge } from "tailwind-merge";

type SongContextMenuItemProps = {
onClick: (event: MouseEvent) => any;
children: any;
class?: JSX.ButtonHTMLAttributes<HTMLButtonElement>["class"];
disabled?: JSX.ButtonHTMLAttributes<HTMLButtonElement>["disabled"];
};

const SongContextMenuItem: Component<SongContextMenuItemProps> = (props) => {
let item: HTMLElement | undefined;
const buttonDisabled = props.disabled !== undefined ? props.disabled : false;

Check warning on line 13 in src/renderer/src/components/song/context-menu/SongContextMenuItem.tsx

View workflow job for this annotation

GitHub Actions / lint

The reactive variable 'props.disabled' should be used within JSX, a tracked scope (like createEffect), or inside an event handler function, or else changes will be ignored

const divAccessor = (div: HTMLElement) => {
div.addEventListener("click", props.onClick);
Expand All @@ -20,7 +24,12 @@ const SongContextMenuItem: Component<SongContextMenuItemProps> = (props) => {
return (
<button
ref={divAccessor}
class="w-full px-4 py-2 text-left transition-colors duration-200 hover:bg-accent/20"
class={twMerge(
"flex flex-row items-center justify-between gap-3 rounded-md bg-thick-material p-2 text-left transition-colors duration-200 hover:cursor-pointer hover:bg-accent/20",
props.class,
buttonDisabled && "text-subtext/20 hover:cursor-auto hover:bg-inherit",
)}
disabled={buttonDisabled}
>
{props.children}
</button>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Song } from "../../../../../../@types";
import SongContextMenuItem from "../SongContextMenuItem";
import { PlusIcon } from "lucide-solid";
import { Component } from "solid-js";

type AddToPlaylistProps = {
path: Song["path"] | undefined;
};

const AddToPlaylist: Component<AddToPlaylistProps> = (props) => {
return (
<SongContextMenuItem
onClick={() => {
if (props.path !== undefined && props.path !== "") {
console.log("TODO: add " + props.path + " to playlist");
}
}}
>
<p>Add to Playlist</p>
<PlusIcon />
</SongContextMenuItem>
);
};

export default AddToPlaylist;
32 changes: 15 additions & 17 deletions src/renderer/src/components/song/context-menu/items/PlayNext.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,26 @@
import { Song } from "../../../../../../@types";
import SongContextMenuItem from "../SongContextMenuItem";
import { Component, createSignal, Show } from "solid-js";
import { ListStartIcon } from "lucide-solid";
import { Component } from "solid-js";

type SongPlayNextProps = {
path: Song["path"];
path: Song["path"] | undefined;
disabled: boolean;
};

const PlayNext: Component<SongPlayNextProps> = (props) => {
const [show, setShow] = createSignal(false);

window.api.listen("queue::created", () => {
setShow(true);
});

window.api.listen("queue::destroyed", () => {
setShow(false);
});

return (
<Show when={show()}>
<SongContextMenuItem onClick={() => window.api.request("queue::playNext", props.path)}>
Play Next
</SongContextMenuItem>
</Show>
<SongContextMenuItem
onClick={() => {
if (props.path !== undefined && props.path !== "") {
window.api.request("queue::playNext", props.path);
}
}}
disabled={props.disabled}
>
<p>Play next</p>
<ListStartIcon />
</SongContextMenuItem>
);
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import SongContextMenuItem from "../SongContextMenuItem";
import { DeleteIcon } from "lucide-solid";
import { Component } from "solid-js";
import { Song } from "src/@types";

type RemoveFromQueueProps = {
path: Song["path"] | undefined;
};

const RemoveFromQueue: Component<RemoveFromQueueProps> = (props) => {
return (
<SongContextMenuItem
onClick={() => {
if (props.path !== undefined && props.path !== "") {
window.api.request("queue::removeSong", props.path);
}
}}
class="hover:bg-red/20"
>
<p class="text-red">Remove from queue</p>
<DeleteIcon class="text-red" />
</SongContextMenuItem>
);
};

export default RemoveFromQueue;
88 changes: 66 additions & 22 deletions src/renderer/src/components/song/song-item/SongItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ import SongHint from "../SongHint";
import SongImage from "../SongImage";
import { ignoreClickInContextMenu } from "../context-menu/SongContextMenu";
import { song as selectedSong } from "../song.utils";
import { Component, createMemo, onMount } from "solid-js";
import Popover from "@renderer/components/popover/Popover";
import { EllipsisVerticalIcon } from "lucide-solid";
import { Component, createSignal, JSXElement, onMount, createMemo } from "solid-js";
import { Portal } from "solid-js/web";
import { twMerge } from "tailwind-merge";

type SongItemProps = {
song: Song;
Expand All @@ -13,11 +17,13 @@ type SongItemProps = {
onSelect: (songResource: ResourceID) => any;
draggable?: true;
onDrop?: (before: Element | null) => any;
children?: any;
contextMenu: JSXElement;
};

const SongItem: Component<SongItemProps> = (props) => {
let item: HTMLDivElement | undefined;
const [localShow, setLocalShow] = createSignal(false);
const [mousePos, setMousePos] = createSignal<[number, number]>([0, 0]);

onMount(() => {
if (!item) {
Expand All @@ -41,31 +47,69 @@ const SongItem: Component<SongItemProps> = (props) => {
});

return (
<div
class="group relative isolate select-none rounded-md"
classList={{
"outline outline-2 outline-accent": isActive(),
}}
data-active={isActive()}
ref={item}
data-url={props.song.bg}
<Popover
isOpen={localShow}
onValueChange={setLocalShow}
placement="right"
offset={{ crossAxis: 5, mainAxis: 5 }}
shift={{}}
flip={{}}
mousePos={mousePos}
>
<SongImage
class="absolute inset-0 z-[-1] h-full w-full rounded-md bg-cover bg-center bg-no-repeat opacity-30 group-hover:opacity-90"
<Portal>
<Popover.Overlay />
<Popover.Content
onClick={(e) => {
e.stopImmediatePropagation();
setLocalShow(false);
}}
>
{props.contextMenu}
</Popover.Content>
</Portal>
<div
class="group relative isolate z-20 select-none rounded-md"
classList={{
"opacity-90": isActive(),
"outline outline-2 outline-accent": isActive(),
}}
src={props.song.bg}
group={props.group}
/>
data-active={isActive()}
ref={item}
data-url={props.song.bg}
onContextMenu={(e) => {
setMousePos([e.clientX, e.clientY]);
setLocalShow(true);
}}
>
<SongImage
class={twMerge(
"absolute inset-0 z-[-1] h-full w-full rounded-md bg-cover bg-center bg-no-repeat opacity-30 group-hover:opacity-90",
isActive() && "opacity-90",
)}
src={props.song.bg}
group={props.group}
/>

<div class="flex flex-row items-center justify-between rounded-md bg-black/50">
<div class="z-20 flex min-h-[72px] flex-col justify-center overflow-hidden rounded-md p-3">
<h3 class="text-shadow text-[22px] font-extrabold leading-7 shadow-black/60">
{props.song.title}
</h3>
<p class="text-base text-subtext">{props.song.artist}</p>
</div>

<div class="flex min-h-[72px] flex-col justify-center overflow-hidden rounded-md bg-black/50 p-3">
<h3 class="text-shadow text-[22px] font-extrabold leading-7 shadow-black/60">
{props.song.title}
</h3>
<p class="text-base text-subtext">{props.song.artist}</p>
<div class="mr-2 grid aspect-square size-9 place-items-center rounded border-solid border-stroke bg-transparent p-1 text-text hover:bg-surface">
<Popover.Trigger
class={twMerge(
"opacity-0 transition-opacity group-hover:opacity-100",
localShow() && "opacity-100",
)}
>
<EllipsisVerticalIcon />
</Popover.Trigger>
</div>
</div>
</div>
</div>
</Popover>
);
};

Expand Down
Loading

0 comments on commit 203c7a8

Please sign in to comment.