diff --git a/src/@types.d.ts b/src/@types.d.ts index d2367d80..ade0c9b6 100644 --- a/src/@types.d.ts +++ b/src/@types.d.ts @@ -112,6 +112,7 @@ export type System = { // Settings table definition export type Settings = { volume: number; + sidebarWidth: number; audioDeviceId: string; osuSongsDir: string; "window.width": number; diff --git a/src/main/index.ts b/src/main/index.ts index e2e79acf..7d49031f 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -30,9 +30,10 @@ async function createWindow() { show: false, autoHideMenuBar: true, titleBarStyle: "hidden", + minWidth: 500, trafficLightPosition: { x: 20, - y: 20, + y: 28, }, icon: getIcon(), webPreferences: { diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 794c5ec7..932af65f 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -1,5 +1,8 @@ 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"; import ErrorScene from "./scenes/ErrorScene"; import NoScene from "./scenes/NoScene"; @@ -7,7 +10,8 @@ import DirSelectScene from "./scenes/dir-select-scene/DirSelectScene"; import LoadingScene from "./scenes/loading-scene/LoadingScene"; import MainScene from "./scenes/main-scene/MainScene"; import type { JSX } from "solid-js"; -import { createSignal, Match, onCleanup, onMount, Switch } from "solid-js"; +import { createMemo, createSignal, Match, onCleanup, onMount, Show, Switch } from "solid-js"; +import { sidebarWidth } from "./scenes/main-scene/main.utils"; export default function App(): JSX.Element { const [scene, setScene] = createSignal(""); @@ -22,9 +26,12 @@ export default function App(): JSX.Element { setScene(event.detail.scene); }; - onMount(() => { + onMount(async () => { window.api.listen("changeScene", setScene); window.addEventListener("changeScene", eventHandler); + + // Sets current OS + setOs(await fetchOs()); }); onCleanup(() => { @@ -32,21 +39,31 @@ export default function App(): JSX.Element { window.removeEventListener("changeScene", eventHandler); }); + const hasRequiredOptions = createMemo(() => { + return typeof os() !== "undefined" && typeof sidebarWidth() !== "undefined"; + }); + return ( - }> - - - - - - - - - - - - - + + + + }> + + + + + + + + + + + + + + + + ); } diff --git a/src/renderer/src/components/InfiniteScroller.tsx b/src/renderer/src/components/InfiniteScroller.tsx index 94e13bc6..d309b609 100644 --- a/src/renderer/src/components/InfiniteScroller.tsx +++ b/src/renderer/src/components/InfiniteScroller.tsx @@ -223,7 +223,11 @@ const InfiniteScroller: Component = (props) => { }); return ( -
+
No items...
}> {(componentProps) => props.builder(componentProps)} diff --git a/src/renderer/src/components/button/Button.tsx b/src/renderer/src/components/button/Button.tsx index 3947573e..fdcd5f2a 100644 --- a/src/renderer/src/components/button/Button.tsx +++ b/src/renderer/src/components/button/Button.tsx @@ -1,30 +1,27 @@ import { cva, type VariantProps } from "class-variance-authority"; import { Component, JSX, splitProps } from "solid-js"; -import { twMerge } from "tailwind-merge"; -const buttonStyles = cva( - ["rounded-lg", "transition-colors", "duration-200", "ease-in-out", "font-medium"], - { - variants: { - variant: { - primary: "bg-text text-thick-material hover:bg-text/80", - secondary: "bg-surface text-text ring-inset ring-1 ring-stroke hover:bg-surface/40", - outlined: "bg-transparent border-stroke border-solid text-text hover:bg-surface", - ghost: "rounded-full border-none text-xl hover:bg-surface", - link: "bg-transparent text-text hover:underline text-decoration-2 underline-offset-2", - }, - size: { - medium: "px-4 py-2", - large: "px-7 py-2.5", - icon: "grid place-items-center aspect-square size-9 p-1 -m-2", - }, +const buttonStyles = cva(["rounded-lg transition-colors duration-200 ease font-medium"], { + variants: { + variant: { + primary: "bg-text text-thick-material hover:bg-text/80", + secondary: "bg-surface text-text hover:bg-surface/40 border-stroke border border-solid", + outlined: "bg-transparent border-stroke text-text hover:bg-surface border border-solid", + ghost: "border-transparent hover:bg-surface", + link: "bg-transparent text-text hover:underline text-decoration-2 underline-offset-2", }, - defaultVariants: { - variant: "primary", - size: "medium", + size: { + medium: "px-4 py-2", + large: "px-7 py-2.5", + icon: "grid place-items-center aspect-square size-9 p-1 -m-2", + square: "grid place-items-center aspect-square h-10", }, }, -); + defaultVariants: { + variant: "primary", + size: "medium", + }, +}); type ButtonProps = JSX.ButtonHTMLAttributes & VariantProps; @@ -33,7 +30,7 @@ const Button: Component = (props) => { return ( - ); -}; - -export default SongContextMenuItem; 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-detail/SongControls.tsx b/src/renderer/src/components/song/song-detail/SongControls.tsx index 26587ec4..27119bca 100644 --- a/src/renderer/src/components/song/song-detail/SongControls.tsx +++ b/src/renderer/src/components/song/song-detail/SongControls.tsx @@ -26,33 +26,30 @@ import { Volume2Icon, VolumeXIcon, } from "lucide-solid"; -import { Component, createEffect, createSignal, Match, Show, Switch, For } from "solid-js"; +import { Component, createMemo, createSignal, Match, Show, Switch, For } from "solid-js"; import Popover from "../../popover/Popover"; import { ParentComponent } from "solid-js"; import { Portal } from "solid-js/web"; // Add a prop to accept the averageColor type SongControlsProps = { - averageColor?: string; + averageColor: string | undefined; + secondatyColor: string | undefined; }; const SongControls: Component = (props) => { - const [disable, setDisable] = createSignal(isSongUndefined(song())); - const [playHint, setPlayHint] = createSignal(""); + const [isHovering, setIsHovering] = createSignal(false); - createEffect(() => { + const disable = createMemo(() => isSongUndefined(song())); + const playHint = createMemo(() => { const disabled = disable(); - if (disabled) { - setPlayHint(""); - return; + return ""; } - setPlayHint(isPlaying() ? "Pause" : "Play"); + return isPlaying() ? "Pause" : "Play"; }); - createEffect(() => setDisable(isSongUndefined(song()))); - return (
@@ -79,12 +76,14 @@ const SongControls: Component = (props) => {
diff --git a/src/renderer/src/components/song/song-detail/SongDetail.tsx b/src/renderer/src/components/song/song-detail/SongDetail.tsx index 6d576a2f..921d3d24 100644 --- a/src/renderer/src/components/song/song-detail/SongDetail.tsx +++ b/src/renderer/src/components/song/song-detail/SongDetail.tsx @@ -19,13 +19,13 @@ const SongDetail: Component = () => { const colorData = createMemo(() => extractColorFromImage(song())); return ( -
+
@@ -36,7 +36,10 @@ const SongDetail: Component = () => {
- +
); diff --git a/src/renderer/src/components/song/song-item/SongItem.tsx b/src/renderer/src/components/song/song-item/SongItem.tsx index dae96bda..e18e501b 100644 --- a/src/renderer/src/components/song/song-item/SongItem.tsx +++ b/src/renderer/src/components/song/song-item/SongItem.tsx @@ -3,13 +3,11 @@ import draggable from "../../../lib/draggable/draggable"; import SongHint from "../SongHint"; import SongImage from "../SongImage"; import { useColorExtractor } from "../color-extractor"; -import { ignoreClickInContextMenu } from "../context-menu/SongContextMenu"; import { song as selectedSong } from "../song.utils"; 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 { Component, createSignal, JSXElement, onMount, createMemo, Show } from "solid-js"; import { twMerge } from "tailwind-merge"; type SongItemProps = { @@ -19,24 +17,23 @@ type SongItemProps = { onSelect: (songResource: ResourceID) => any; draggable?: true; onDrop?: (before: Element | null) => any; - contextMenu: JSXElement; + contextMenu?: JSXElement; }; const SongItem: Component = (props) => { let item: HTMLDivElement | undefined; - const [, setCoords] = createSignal<[number, number]>([0, 0], { equals: false }); - const { extractColorFromImage } = useColorExtractor(); const { primaryColor, secondaryColor, processImage } = extractColorFromImage(props.song); const [localShow, setLocalShow] = createSignal(false); - const [mousePos, setMousePos] = createSignal<[number, number]>([0, 0]); + const [isHovering, setIsHovering] = createSignal(false); + const [mousePos, setMousePos] = createSignal<[number, number] | undefined>(); onMount(() => { if (!item) return; // Initialize draggable functionality draggable(item, { - onClick: ignoreClickInContextMenu(() => props.onSelect(props.song.path)), + onClick: () => props.onSelect(props.song.path), onDrop: props.onDrop ?? (() => {}), createHint: SongHint, useOnlyAsOnClickBinder: !props.draggable || selectedSong().path === props.song.path, @@ -70,8 +67,11 @@ const SongItem: Component = (props) => { return "rgba(0, 0, 0, 0.72)"; } - const lowerAlpha = transparentize(0.9); - return `linear-gradient(to right, ${color}, ${lowerAlpha(color)})`; + if (isHovering() || localShow() || isSelected()) { + return `linear-gradient(to right, ${transparentize(0.1)(color)} 20%, ${transparentize(0.9)(color)}), rgba(0, 0, 0, 0.1)`; + } + + return `linear-gradient(to right, ${color} 20%, ${transparentize(0.9)(color)}), rgba(0, 0, 0, 0.2)`; }); return ( @@ -80,11 +80,11 @@ const SongItem: Component = (props) => { onValueChange={setLocalShow} placement="right" offset={{ crossAxis: 5, mainAxis: 5 }} - shift={{}} - flip={{}} - mousePos={mousePos} + position={mousePos} + shift + flip > - + { @@ -94,9 +94,16 @@ const SongItem: Component = (props) => { > {props.contextMenu} - + +
{ + setIsHovering(true); + }} + onMouseLeave={() => { + setIsHovering(false); + }} + class="min-h-[72px] rounded-lg py-0.5 pl-1.5 pr-0.5 transition-colors active: group relative isolate overflow-hidden" classList={{ "shadow-glow-blue": isSelected(), }} @@ -110,19 +117,22 @@ const SongItem: Component = (props) => { }} >
setCoords([evt.clientX, evt.clientY])} > +
= (props) => {

{props.song.title}

{props.song.artist}

+
-
- + { + e.stopPropagation(); + setMousePos(undefined); + setLocalShow(true); + }} + class="absolute right-0 top-0 h-full flex items-center text-subtext transition-colors hover:text-text rounded-r-lg animate-song-item-slide-in" + title="Song options" + classList={{ + "text-text": localShow(), + }} + style={{ + background: borderColor(), + }} + > +
- -
-
+
+ + ); diff --git a/src/renderer/src/components/song/song-item/SongItemAnimations.ts b/src/renderer/src/components/song/song-item/SongItemAnimations.ts new file mode 100644 index 00000000..86738fde --- /dev/null +++ b/src/renderer/src/components/song/song-item/SongItemAnimations.ts @@ -0,0 +1,11 @@ +export const songItemAnimations = { + keyframes: { + "song-item-slide-in": { + from: { transform: "translateX(100%)", opacity: 0 }, + to: { transform: "translateX(0)", opacity: 1 }, + }, + }, + animation: { + "song-item-slide-in": "song-item-slide-in 160ms cubic-bezier(0.4, 0, 0.2, 1) forwards", + }, +}; diff --git a/src/renderer/src/components/song/song-list-search/FilterOption.tsx b/src/renderer/src/components/song/song-list-search/FilterOption.tsx new file mode 100644 index 00000000..f2dd4003 --- /dev/null +++ b/src/renderer/src/components/song/song-list-search/FilterOption.tsx @@ -0,0 +1,119 @@ +import Popover, { Props as PopoverProps } from "@renderer/components/popover/Popover"; +import { Props as PopoverContentProps } from "@renderer/components/popover/PopoverContent"; +import { cn } from "@renderer/lib/css.utils"; +import { ChevronDownIcon } from "lucide-solid"; +import { Component, createMemo, JSX, mergeProps, ParentComponent, splitProps } from "solid-js"; + +// ------------ +// Container +// ------------ +type FilterOptionContainerProps = { + popoverProps?: PopoverProps; +} & JSX.IntrinsicElements["div"]; +const FilterOptionContainer: Component = (_props) => { + const [props, rest] = splitProps(_props, ["class", "popoverProps"]); + + const mergedPopoverProps = createMemo(() => { + const mergedProps = mergeProps( + { placement: "bottom-start", offset: 10 }, + props.popoverProps ?? {}, + ); + return mergedProps; + }); + + return ( + +
+ + ); +}; + +// ------------ +// Label +// ------------ +type FilterOptionLabelProps = JSX.IntrinsicElements["p"]; +export const FilterOptionLabel: Component = (_props) => { + const [props, rest] = splitProps(_props, ["class"]); + return

; +}; + +// ------------ +// Trigger +// ------------ +export const FilterOptionList: ParentComponent = (props) => { + return ( +

+ ); +}; + +// ------------ +// Trigger +// ------------ +type FilterOptionTriggerProps = JSX.IntrinsicElements["button"]; +export const FilterOptionTrigger: Component = (_props) => { + const [props, rest] = splitProps(_props, ["class", "children"]); + + return ( + + {props.children} + + + ); +}; + +// ------------ +// Item +// ------------ +type FilterOptionItemProps = JSX.IntrinsicElements["button"]; +export const FilterOptionItem: Component = (_props) => { + const [props, rest] = splitProps(_props, ["class", "children"]); + + return ( + + ); +}; + +// ------------ +// Content +// ------------ +const FilterOptionContent: ParentComponent = (props) => { + return ( + + + + + ); +}; + +const FilterOption = Object.assign(FilterOptionContainer, { + Label: FilterOptionLabel, + List: FilterOptionList, + Trigger: FilterOptionTrigger, + Content: FilterOptionContent, + Item: FilterOptionItem, +}); + +export default FilterOption; diff --git a/src/renderer/src/components/song/song-list-search/SongListSearch.tsx b/src/renderer/src/components/song/song-list-search/SongListSearch.tsx index 548b0212..2e3da0fc 100644 --- a/src/renderer/src/components/song/song-list-search/SongListSearch.tsx +++ b/src/renderer/src/components/song/song-list-search/SongListSearch.tsx @@ -2,8 +2,11 @@ import { Optional, Order, Tag } from "../../../../../@types"; import { SearchQueryError } from "../../../../../main/lib/search-parser/@search-types"; import { setSongsSearch } from "../song-list/song-list.utils"; import SongListSearchOrderBy from "./SongListSearchOrderBy"; -import { SearchIcon } from "lucide-solid"; -import { Accessor, Component, Setter, Signal } from "solid-js"; +import Button from "@renderer/components/button/Button"; +import { Input } from "@renderer/components/input/Input"; +import { FilterIcon, SearchIcon, FilterXIcon } from "lucide-solid"; +import { Accessor, Component, createSignal, Match, Setter, Signal, Switch } from "solid-js"; +import { SongListSearchTags } from "./SongListSearchTags"; export type SearchProps = { tags: Signal; @@ -13,6 +16,8 @@ export type SearchProps = { }; const SongListSearch: Component = (props) => { + const [filterExpanded, setFilterExpanded] = createSignal(false); + // const [editable, setEditable] = createSignal(); // const [doShowError, setDoShowError] = createSignal(false); // const [doShowSuggestion, setDoShowSuggestion] = createSignal(false); @@ -62,27 +67,51 @@ const SongListSearch: Component = (props) => { // }); return ( -
-
- { - setSongsSearch(e.target.value); - }} - /> -