Skip to content

Commit

Permalink
- Add resizable sidebar
Browse files Browse the repository at this point in the history
  • Loading branch information
duduBTW committed Nov 17, 2024
1 parent 7c7d766 commit 9380b5e
Show file tree
Hide file tree
Showing 13 changed files with 384 additions and 115 deletions.
1 change: 1 addition & 0 deletions src/@types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ export type System = {
// Settings table definition
export type Settings = {
volume: number;
sidebarWidth: number;
audioDeviceId: string;
osuSongsDir: string;
"window.width": number;
Expand Down
9 changes: 7 additions & 2 deletions src/renderer/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,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, Show, 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<Scenes>("");
Expand Down Expand Up @@ -38,8 +39,12 @@ export default function App(): JSX.Element {
window.removeEventListener("changeScene", eventHandler);
});

const hasRequiredOptions = createMemo(() => {
return typeof os() !== "undefined" && typeof sidebarWidth() !== "undefined";
});

return (
<Show when={typeof os() !== "undefined"}>
<Show when={hasRequiredOptions()}>
<NoticeContainer />

<Switch fallback={<NoScene />}>
Expand Down
6 changes: 5 additions & 1 deletion src/renderer/src/components/InfiniteScroller.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,11 @@ const InfiniteScroller: Component<InfinityScrollerProps> = (props) => {
});

return (
<div class="flex flex-col gap-4 py-4" ref={container} {...rest}>
<div
class="grid grid-cols-[repeat(auto-fit,_minmax(340px,_1fr))] gap-4 py-4 items-stretch"
ref={container}
{...rest}
>
<Show when={show() === true} fallback={props.fallback ?? <div>No items...</div>}>
<For each={elements()}>{(componentProps) => props.builder(componentProps)}</For>
</Show>
Expand Down
43 changes: 43 additions & 0 deletions src/renderer/src/components/resizable-panel/DragHandler.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Component } from "solid-js";
import { useResizablePanel } from "./ResizablePanel";
import { cn } from "@renderer/lib/css.utils";

type DragHandlerProps = {
class?: string;
};
export const DragHandler: Component<DragHandlerProps> = (props) => {
const state = useResizablePanel();

return (
<div
onPointerDown={(event) => {
const target = event.target as HTMLElement;
target.setPointerCapture(event.pointerId);
event.preventDefault();

state.handlePointerStart(event);
}}
onPointerMove={(event) => {
const target = event.target as HTMLElement;
if (!target.hasPointerCapture(event.pointerId)) {
return;
}

state.handlePointerMove(event);
}}
onPointerUp={(event) => {
const target = event.target as HTMLElement;
if (target.hasPointerCapture(event.pointerId)) {
target.releasePointerCapture(event.pointerId);
state.handlePointerEnd();
}
}}
class={cn(
"opacity-0 hover:opacity-100 h-full w-4 translate-x-[-50%] cursor-w-resize flex flex-col items-center justify-center",
props.class,
)}
>
<div class="bg-white/60 flex-1 w-0.5" />
</div>
);
};
97 changes: 97 additions & 0 deletions src/renderer/src/components/resizable-panel/ResizablePanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { Accessor, createContext, ParentComponent, useContext } from "solid-js";
import { DragHandler } from "./DragHandler";
import useControllableState from "@renderer/lib/controllable-state";
import { DOMElement } from "solid-js/jsx-runtime";
import { clamp } from "@renderer/lib/tungsten/math";

const DEFAULT_RESIZABLE_PANEL_VALUE = 100;
const DEFAULT_MIN = 0;
const DEFAULT_MAX = Infinity;

type Event = PointerEvent & {
currentTarget: HTMLDivElement;
target: DOMElement;
};

export type Props = {
min?: number;
max?: number;

offsetFromPanel?: number;

defaultValue?: number;
value?: Accessor<number>;
onValueChange?: (newValue: number) => void;
onValueCommit?: (value: number) => void;
onValueStart?: () => void;
};

export type Context = ReturnType<typeof useProviderValue>;
function useProviderValue(props: Props) {
let startDragValue: number | undefined;
const [value, setValue] = useControllableState({
defaultProp: props.defaultValue ?? DEFAULT_RESIZABLE_PANEL_VALUE,
prop: props.value,
onChange: props.onValueChange,
});

const getValueFromPointer = (pointerPosition: number) => {
if (!startDragValue) {
return;
}

const min = props.min ?? DEFAULT_MIN;
const max = props.max ?? DEFAULT_MAX;

const value = pointerPosition - (props.offsetFromPanel ?? 0);
return clamp(min, max, value);
};

const handlePointerStart = (event: Event) => {
startDragValue = value();
const newValue = getValueFromPointer(event.clientX);
if (typeof newValue === "undefined") {
return;
}

setValue(newValue);
props.onValueStart?.();
};
const handlePointerMove = (event: Event) => {
const newValue = getValueFromPointer(event.clientX);
if (typeof newValue === "undefined") {
return;
}

setValue(newValue);
};
const handlePointerEnd = () => {
props.onValueCommit?.(value());
};

return { value, handlePointerStart, handlePointerMove, handlePointerEnd };
}

export const ResizablePanelContext = createContext<Context>();
const ResizablePanelRoot: ParentComponent<Props> = (props) => {
const value = useProviderValue(props);
return (
<ResizablePanelContext.Provider value={value}>{props.children}</ResizablePanelContext.Provider>
);
};

export function useResizablePanel(): Context {
const state = useContext(ResizablePanelContext);
if (!state) {
throw new Error(
"useResizablePanel needs to be used inisde of the `ResizablePanelContext` component.",
);
}
return state;
}

const ResizablePanel = Object.assign(ResizablePanelRoot, {
DragHandler,
});

export default ResizablePanel;
64 changes: 33 additions & 31 deletions src/renderer/src/components/song/song-item/SongItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ 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 { Component, createSignal, JSXElement, onMount, createMemo, Show } from "solid-js";
import { twMerge } from "tailwind-merge";

type SongItemProps = {
Expand Down Expand Up @@ -67,11 +67,11 @@ const SongItem: Component<SongItemProps> = (props) => {
return "rgba(0, 0, 0, 0.72)";
}

if (isHovering() || localShow()) {
return `linear-gradient(to right, ${color} 20%, ${transparentize(0.9)(color)}), rgba(255, 255, 255, 0.2)`;
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} 16%, ${transparentize(0.92)(color)})`;
return `linear-gradient(to right, ${color} 20%, ${transparentize(0.9)(color)}), rgba(0, 0, 0, 0.2)`;
});

return (
Expand Down Expand Up @@ -103,14 +103,12 @@ const SongItem: Component<SongItemProps> = (props) => {
onMouseLeave={() => {
setIsHovering(false);
}}
class="min-h-[72px] rounded-lg py-0.5 pl-1.5 pr-0.5 transition-colors active: group relative"
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(),
"pr-6": isHovering() || localShow(),
}}
style={{
background: borderColor(),
"transition-property": "padding, background",
}}
onContextMenu={(e) => {
e.preventDefault();
Expand All @@ -119,53 +117,57 @@ const SongItem: Component<SongItemProps> = (props) => {
}}
>
<div
class="relative isolate select-none rounded-lg"
classList={{}}
class="relative isolate select-none rounded-lg h-full"
ref={item}
data-url={props.song.bg}
>
<SongImage
class={`absolute inset-0 z-[-1] h-full w-full rounded-l-[9px] rounded-r-md bg-cover bg-center bg-no-repeat bg-scroll`}
class={`absolute z-[-1] inset-0 h-full w-full rounded-l-[9px] rounded-r-md bg-cover bg-center bg-no-repeat bg-scroll opacity-70 rounded-lg`}
classList={{
"opacity-100": isSelected(),
}}
src={props.song.bg}
group={props.group}
onImageLoaded={processImage}
/>

<div
class="flex flex-col justify-center overflow-hidden rounded-md p-3 transition-transform pr-10 group-hover:pr-6"
class="flex flex-col justify-center overflow-hidden rounded-md p-3 transition-transform pr-10 h-full"
style={{
background: backgrund(),
"transition-property": "padding, background",
}}
>
<h3 class="drop-shadow-md text-[22px] font-[740] leading-7">{props.song.title}</h3>
<p class="text-base text-subtext drop-shadow-sm">{props.song.artist}</p>
</div>
</div>

<Popover.Anchor
onClick={(e) => {
e.stopPropagation();
setMousePos(undefined);
setLocalShow(true);
}}
class="absolute right-0 top-0 h-full flex items-center text-subtext transition-colors hover:text-text"
title="Song options"
classList={{
"text-text": localShow(),
}}
>
<div
class={twMerge("opacity-0 transition-opacity z-10")}
<Show when={isHovering() || localShow()}>
<Popover.Anchor
onClick={(e) => {
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={{
"opacity-100": isHovering() || localShow(),
"text-text": localShow(),
}}
style={{
color: isSelected() ? secondaryColor() : undefined,
background: borderColor(),
}}
>
<EllipsisVerticalIcon />
</div>
</Popover.Anchor>
<div
class={twMerge("transition-opacity z-10")}
style={{
color: isSelected() ? secondaryColor() : undefined,
}}
>
<EllipsisVerticalIcon />
</div>
</Popover.Anchor>
</Show>
</div>
</Popover>
);
Expand Down
11 changes: 11 additions & 0 deletions src/renderer/src/components/song/song-item/SongItemAnimations.ts
Original file line number Diff line number Diff line change
@@ -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",
},
};
14 changes: 6 additions & 8 deletions src/renderer/src/components/song/song-list/SongList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,14 +88,12 @@ const SongList: Component<SongViewProps> = (props) => {
reset={resetListing}
fallback={<div class="py-8 text-center text-lg uppercase text-subtext">No songs</div>}
builder={(s) => (
<div>
<SongItem
song={s}
group={group}
onSelect={createQueue}
contextMenu={<SongListContextMenuContent song={s} />}
/>
</div>
<SongItem
song={s}
group={group}
onSelect={createQueue}
contextMenu={<SongListContextMenuContent song={s} />}
/>
)}
/>
</div>
Expand Down
Loading

0 comments on commit 9380b5e

Please sign in to comment.