diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 4364a7e9..a85f0e14 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -1,5 +1,6 @@ import { Scenes } from "../../@types"; import NoticeContainer from "./components/notice/NoticeContainer"; +import Popover from "./components/popover/Popover"; import "./keyboard-registers/initialize"; import { fetchOs, os, setOs } from "./lib/os"; import { TokenNamespace } from "./lib/tungsten/token"; @@ -55,6 +56,8 @@ export default function App(): JSX.Element { + + ); } diff --git a/src/renderer/src/components/popover/Popover.tsx b/src/renderer/src/components/popover/Popover.tsx index 30ff222e..3686a18a 100644 --- a/src/renderer/src/components/popover/Popover.tsx +++ b/src/renderer/src/components/popover/Popover.tsx @@ -1,3 +1,4 @@ +import { Token, TokenNamespace } from "@renderer/lib/tungsten/token"; import PopoverContent from "./PopoverContent"; import PopoverOverlay from "./PopoverOverlay"; import PopoverTrigger, { PopoverAnchor } from "./PopoverTrigger"; @@ -22,7 +23,10 @@ import { ParentComponent, Accessor, createEffect, + onMount, + onCleanup, } from "solid-js"; +import { PopoverPortal, PopoverPortalMountStack } from "./PopoverPortal"; export const DEFAULT_POPOVER_OPEN = false; @@ -37,6 +41,9 @@ export type Props = { onValueChange?: (newOpen: boolean) => void; }; +export const [popoverStack, setPopoverStack] = createSignal([]); +const stackIds = new TokenNamespace(); + export type Context = ReturnType; function useProviderValue(props: Props) { @@ -51,6 +58,13 @@ function useProviderValue(props: Props) { const [position, setPosition] = createSignal(null); const [triggerRef, _setTriggerRef] = createSignal(null); const [contentRef, _setContentRef] = createSignal(null); + const [id, setId] = createSignal(""); + + onMount(() => { + onCleanup(() => { + stackIds.destroy(id()); + }); + }); createEffect(() => { const triggerElement = triggerRef(); @@ -124,10 +138,26 @@ function useProviderValue(props: Props) { }).then(setPosition); }; + createEffect(() => { + if (isOpen()) { + const newId = stackIds.create(); + setPopoverStack((p) => [...p, newId]); + setId(newId); + } else { + setPopoverStack((p) => p.filter((popoverId) => popoverId !== id())); + setId(""); + } + }); + return { + id, isOpen, - open: () => setIsOpen(true), - close: () => setIsOpen(false), + open: () => { + setIsOpen(true); + }, + close: () => { + setIsOpen(false); + }, toggle: () => setIsOpen((o) => !o), position, setPosition, @@ -154,6 +184,8 @@ export function usePopover(): Context { } const Popover = Object.assign(PopoverRoot, { + Portal: PopoverPortal, + PortalMountStack: PopoverPortalMountStack, Content: PopoverContent, Trigger: PopoverTrigger, Anchor: PopoverAnchor, diff --git a/src/renderer/src/components/popover/PopoverContent.tsx b/src/renderer/src/components/popover/PopoverContent.tsx index c79f977f..623c2c2b 100644 --- a/src/renderer/src/components/popover/PopoverContent.tsx +++ b/src/renderer/src/components/popover/PopoverContent.tsx @@ -1,8 +1,8 @@ import { cn, sn } from "@renderer/lib/css.utils"; -import { usePopover } from "./Popover"; +import { popoverStack, usePopover } from "./Popover"; import { ComputePositionReturn } from "@floating-ui/dom"; import createFocusTrap from "solid-focus-trap"; -import { Component, onCleanup, onMount, Show } from "solid-js"; +import { Component, createMemo, onCleanup, onMount, Show } from "solid-js"; import { JSX } from "solid-js/jsx-runtime"; function stylesFromPosition(position: ComputePositionReturn | null): JSX.CSSProperties | undefined { @@ -20,13 +20,21 @@ export type Props = JSX.IntrinsicElements["div"]; const PopoverContent: Component = (props) => { const state = usePopover(); + const isLastPopoupOpen = createMemo(() => { + return state.isOpen() && popoverStack().at(-1) === state.id(); + }); + createFocusTrap({ element: state.contentRef, - enabled: state.isOpen, + enabled: isLastPopoupOpen, }); onMount(() => { const handleKeyUp = (e: KeyboardEvent) => { + if (!isLastPopoupOpen()) { + return; + } + switch (e.key) { case "Escape": state.close(); diff --git a/src/renderer/src/components/popover/PopoverPortal.tsx b/src/renderer/src/components/popover/PopoverPortal.tsx new file mode 100644 index 00000000..50b1e4fd --- /dev/null +++ b/src/renderer/src/components/popover/PopoverPortal.tsx @@ -0,0 +1,28 @@ +import { popoverStack, usePopover } from "./Popover"; +import { Component, For, ParentComponent, Show } from "solid-js"; +import { Portal } from "solid-js/web"; + +export const PopoverPortal: ParentComponent = (props) => { + const state = usePopover(); + return ( + + {props.children} + + ); +}; + +export const PopoverPortalMountStack: Component = () => { + return ( + + {(id, index) => ( +
+ )} + + ); +}; diff --git a/src/renderer/src/components/select/Select.tsx b/src/renderer/src/components/select/Select.tsx index f40d980a..567941b8 100644 --- a/src/renderer/src/components/select/Select.tsx +++ b/src/renderer/src/components/select/Select.tsx @@ -41,7 +41,7 @@ export const SelectContent: Component = (props) => { }); return ( - <> + = (props) => { > - + ); }; export const SelectOption: Component = (props) => { diff --git a/src/renderer/src/components/song/context-menu/items/AddToPlaylist.tsx b/src/renderer/src/components/song/context-menu/items/AddToPlaylist.tsx deleted file mode 100644 index 035b0ffc..00000000 --- a/src/renderer/src/components/song/context-menu/items/AddToPlaylist.tsx +++ /dev/null @@ -1,25 +0,0 @@ -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 = (props) => { - return ( - { - if (props.path !== undefined && props.path !== "") { - console.log("TODO: add " + props.path + " to playlist"); - } - }} - > -

Add to Playlist

- -
- ); -}; - -export default AddToPlaylist; diff --git a/src/renderer/src/components/song/context-menu/items/PlayNext.tsx b/src/renderer/src/components/song/context-menu/items/PlayNext.tsx deleted file mode 100644 index 293f99d0..00000000 --- a/src/renderer/src/components/song/context-menu/items/PlayNext.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { Song } from "../../../../../../@types"; -import SongContextMenuItem from "../SongContextMenuItem"; -import { ListStartIcon } from "lucide-solid"; -import { Component } from "solid-js"; - -type SongPlayNextProps = { - path: Song["path"] | undefined; - disabled: boolean; -}; - -const PlayNext: Component = (props) => { - return ( - { - if (props.path !== undefined && props.path !== "") { - window.api.request("queue::playNext", props.path); - } - }} - disabled={props.disabled} - > -

Play next

- -
- ); -}; - -export default PlayNext; diff --git a/src/renderer/src/components/song/context-menu/items/RemoveFromQueue.tsx b/src/renderer/src/components/song/context-menu/items/RemoveFromQueue.tsx deleted file mode 100644 index bb1dec88..00000000 --- a/src/renderer/src/components/song/context-menu/items/RemoveFromQueue.tsx +++ /dev/null @@ -1,26 +0,0 @@ -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 = (props) => { - return ( - { - if (props.path !== undefined && props.path !== "") { - window.api.request("queue::removeSong", props.path); - } - }} - class="hover:bg-red/20 hover:ring-1 ring-red/30" - > -

Remove from queue

- -
- ); -}; - -export default RemoveFromQueue; diff --git a/src/renderer/src/components/song/song-item/SongItem.tsx b/src/renderer/src/components/song/song-item/SongItem.tsx index cb16767e..7b970e85 100644 --- a/src/renderer/src/components/song/song-item/SongItem.tsx +++ b/src/renderer/src/components/song/song-item/SongItem.tsx @@ -8,7 +8,6 @@ import { transparentize } from "polished"; 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 = { @@ -83,7 +82,7 @@ const SongItem: Component = (props) => { shift flip > - + { @@ -93,7 +92,8 @@ const SongItem: Component = (props) => { > {props.contextMenu} - + +
{ setIsHovering(true); @@ -128,7 +128,7 @@ const SongItem: Component = (props) => { onImageLoaded={processImage} />
= (_props) @@ -102,7 +101,7 @@ export const FilterOptionItem: Component = (_props) => { // ------------ const FilterOptionContent: ParentComponent = (props) => { return ( - + = (props) => { props.class, )} /> - + ); }; diff --git a/src/renderer/src/components/song/song-list/SongList.tsx b/src/renderer/src/components/song/song-list/SongList.tsx index e505664b..134e3eeb 100644 --- a/src/renderer/src/components/song/song-list/SongList.tsx +++ b/src/renderer/src/components/song/song-list/SongList.tsx @@ -1,5 +1,5 @@ import List from "@renderer/components/list/List"; -import { Optional, Order, ResourceID, Song, SongsQueryPayload, Tag } from "../../../../../@types"; +import { Optional, Order, ResourceID, SongsQueryPayload, Tag } from "../../../../../@types"; import { SearchQueryError } from "../../../../../main/lib/search-parser/@search-types"; import { namespace } from "../../../App"; import Impulse from "../../../lib/Impulse"; @@ -105,8 +105,8 @@ const SongList: Component = (props) => { const SongListContextMenuContent: Component = () => { return ( - - + + Add to Playlist diff --git a/src/renderer/src/components/song/song-queue/SongQueue.tsx b/src/renderer/src/components/song/song-queue/SongQueue.tsx index 643888b4..9adccc90 100644 --- a/src/renderer/src/components/song/song-queue/SongQueue.tsx +++ b/src/renderer/src/components/song/song-queue/SongQueue.tsx @@ -4,11 +4,9 @@ import { namespace } from "../../../App"; import Impulse from "../../../lib/Impulse"; import scrollIfNeeded from "../../../lib/tungsten/scroll-if-needed"; import InfiniteScroller from "../../InfiniteScroller"; -import SongContextMenu from "../context-menu/SongContextMenu"; -import AddToPlaylist from "../context-menu/items/AddToPlaylist"; -import RemoveFromQueue from "../context-menu/items/RemoveFromQueue"; import SongItem from "../song-item/SongItem"; import { Component, createSignal, onCleanup, onMount } from "solid-js"; +import { DeleteIcon, ListPlus } from "lucide-solid"; const SongQueue: Component = () => { const [count, setCount] = createSignal(0); @@ -78,8 +76,8 @@ const SongQueue: Component = () => { }); return ( -
-
+
+

Next songs on the queue ({count()}) @@ -99,10 +97,9 @@ const SongQueue: Component = () => { song={s} group={group} selectable={true} - draggable={true} onSelect={() => window.api.request("queue::play", s.path)} onDrop={onDrop(s)} - contextMenu={} + contextMenu={} /> )} /> @@ -111,13 +108,21 @@ const SongQueue: Component = () => { ); }; -const QueueContextMenuContent: Component = () => { +type QueueContextMenuContentProps = { song: Song }; +const QueueContextMenuContent: Component = (props) => { return ( - - Add to Playlist - Remove from queue - {/* - */} + + + Add to Playlist + + + window.api.request("queue::removeSong", props.song.path)} + class="text-red" + > + Remove from queue + + ); }; diff --git a/src/renderer/src/lib/roving-focus-group/rovingFocusGroup.ts b/src/renderer/src/lib/roving-focus-group/rovingFocusGroup.ts index f6367af7..3c631e8c 100644 --- a/src/renderer/src/lib/roving-focus-group/rovingFocusGroup.ts +++ b/src/renderer/src/lib/roving-focus-group/rovingFocusGroup.ts @@ -1,5 +1,5 @@ import useControllableState from "../controllable-state"; -import { Accessor, createMemo, createSignal } from "solid-js"; +import { Accessor, createMemo, createSignal, onMount } from "solid-js"; const ITEM_DATA_ATTR = "data-item"; const DEFAULT_SELECTED_VALUE = ""; @@ -34,10 +34,26 @@ export function useRovingFocusGroup(props: Params) { setHasMounted(true); }; + const findOrderedNodes = () => { + return Array.from(container.querySelectorAll(`[${ITEM_DATA_ATTR}]:not([disabled])`)); + }; + + onMount(() => { + if (currentStopId()) { + return; + } + + const [firstNode] = findOrderedNodes(); + if (!canFocus(firstNode)) { + return; + } + + firstNode.focus(); + setCurrentStopId(firstNode.getAttribute(ITEM_DATA_ATTR)!); + }); + const handleKeyUp = (event: KeyboardEvent) => { - const orderedNodes = Array.from( - container.querySelectorAll(`[${ITEM_DATA_ATTR}]:not([disabled])`), - ); + const orderedNodes = findOrderedNodes(); const currentlySelectedNodeIndex = orderedNodes.findIndex( (node) => node.getAttribute(ITEM_DATA_ATTR) === currentStopId(), ); diff --git a/src/renderer/src/scenes/main-scene/MainScene.tsx b/src/renderer/src/scenes/main-scene/MainScene.tsx index c6fef583..686e7756 100644 --- a/src/renderer/src/scenes/main-scene/MainScene.tsx +++ b/src/renderer/src/scenes/main-scene/MainScene.tsx @@ -6,8 +6,10 @@ import { song } from "@renderer/components/song/song.utils"; import { WindowsControls } from "@renderer/components/windows-control/WindowsControl"; import { os } from "@renderer/lib/os"; import { Layers3Icon } from "lucide-solid"; -import { Component, JSX, Match, Switch } from "solid-js"; +import { Component, createSignal, Match, Switch } from "solid-js"; import { Sidebar } from "./Sidebar"; +import Popover from "@renderer/components/popover/Popover"; +import SongQueue from "@renderer/components/song/song-queue/SongQueue"; const MainScene: Component = () => { return ( @@ -39,7 +41,7 @@ const MainScene: Component = () => { class="absolute inset-0 bg-cover bg-fixed bg-left-top opacity-30 blur-lg filter" />

- +
@@ -56,8 +58,36 @@ const MainScene: Component = () => { ); }; +const Queue: Component = () => { + const [isOpen, setIsOpen] = createSignal(false); + + return ( + + setIsOpen(true)} class="no-drag absolute right-2 top-2"> + + + + + + + + + + + ); +}; + const MacNav: Component = () => { - return