mirror of
https://github.com/shadps4-emu/shadPS4-launcher.git
synced 2026-01-31 00:55:20 +01:00
UI: Implement Game Title Scroll on Overflow (#38)
* UI: Implement Game Title Scroll on Overflow * Address Review Comment * Refactor the game title scroll to a component. * Address Review Comments * Address Review Comments x3 * Address Review Comments x4 * Linter Fixes * Remove unused reference
This commit is contained in:
24
src/app.css
24
src/app.css
@@ -139,3 +139,27 @@
|
||||
@apply outline-solid outline-1 outline-current;
|
||||
}
|
||||
}
|
||||
|
||||
.marquee-text-track {
|
||||
display: flex;
|
||||
padding-left: 5rem;
|
||||
gap: 5rem;
|
||||
width: max-content;
|
||||
animation: marquee-move-text 5s linear infinite forwards;
|
||||
}
|
||||
|
||||
@keyframes marquee-move-text {
|
||||
to {
|
||||
transform: translateX(-49.85%);
|
||||
}
|
||||
}
|
||||
|
||||
.fadeout-horizontal {
|
||||
mask-image: linear-gradient(
|
||||
to right,
|
||||
transparent,
|
||||
black 1rem,
|
||||
black calc(100% - 1rem),
|
||||
transparent
|
||||
);
|
||||
}
|
||||
|
||||
71
src/components/animate-ui/effects/marquee-title.tsx
Normal file
71
src/components/animate-ui/effects/marquee-title.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
||||
|
||||
interface MarqueeTitleProps {
|
||||
classNames: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export function MarqueeTitle(props: MarqueeTitleProps) {
|
||||
const [titleAnimate, setTitleAnimate] = useState<boolean>(false);
|
||||
const [boxWidth, setBoxWidth] = useState(150);
|
||||
const titleDivRef = useRef<HTMLDivElement>(null);
|
||||
const titleSpanRef = useRef<HTMLSpanElement>(null);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const handleResize = () => {
|
||||
const width = titleDivRef.current?.getBoundingClientRect().width;
|
||||
setBoxWidth(width ?? 150);
|
||||
};
|
||||
|
||||
if (titleDivRef.current) {
|
||||
const width = titleDivRef.current.getBoundingClientRect().width;
|
||||
setBoxWidth(width);
|
||||
|
||||
window.addEventListener("resize", handleResize);
|
||||
}
|
||||
|
||||
return () => window.removeEventListener("resize", handleResize);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
function textWrapAnimate() {
|
||||
if (!titleSpanRef.current || !titleDivRef.current) {
|
||||
return false;
|
||||
}
|
||||
if (props.title.length < 17) {
|
||||
return false;
|
||||
} else {
|
||||
const boxLength = Math.ceil(boxWidth);
|
||||
if (boxLength > 170 && props.title.length < 19) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
setTitleAnimate(textWrapAnimate());
|
||||
}, [boxWidth, props.title]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
`${titleAnimate ? "fadeout-horizontal" + " " : ""}` +
|
||||
"col-span-full row-start-1 row-end-2 flex-row px-2 py-2 text-center"
|
||||
}
|
||||
ref={titleDivRef}
|
||||
>
|
||||
<span
|
||||
className={
|
||||
`${titleAnimate ? "marquee-text-track" + " " : ""}` +
|
||||
`${props.classNames}`
|
||||
}
|
||||
ref={titleSpanRef}
|
||||
>
|
||||
<p>{props.title}</p>
|
||||
<p aria-hidden="true" hidden={!titleAnimate}>
|
||||
{props.title}
|
||||
</p>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -34,6 +34,7 @@ import { stringifyError } from "@/lib/utils/error";
|
||||
import { cn } from "@/lib/utils/ui";
|
||||
import type { GameEntry } from "@/store/db";
|
||||
import { gamepadActiveAtom } from "@/store/gamepad";
|
||||
import { MarqueeTitle } from "./animate-ui/effects/marquee-title";
|
||||
import { GameBoxCover } from "./game-cover";
|
||||
import GamepadIcon, { ButtonType } from "./gamepad-icon";
|
||||
import { GameDetailsModal } from "./modals/game-details-modal";
|
||||
@@ -222,10 +223,12 @@ export function GameBox({ game }: { game: GameEntry; isFirst?: boolean }) {
|
||||
)}
|
||||
|
||||
<div className="absolute inset-0 grid grid-cols-3 grid-rows-3 bg-black/50 opacity-0 backdrop-blur-[2px] transition-opacity group-focus-within:opacity-100 group-hover:opacity-100 group-data-gamepad-focus:opacity-100">
|
||||
<span className="col-span-full row-start-1 row-end-2 truncate px-3 py-2 text-center font-semibold text-lg">
|
||||
{/* TODO: scroll text on overflow */}
|
||||
{game.title}
|
||||
</span>
|
||||
<MarqueeTitle
|
||||
classNames={
|
||||
"inline-block text-nowrap text-center font-semibold text-lg"
|
||||
}
|
||||
title={game.title}
|
||||
/>
|
||||
|
||||
<div className="col-start-1 col-end-4 row-start-3 row-end-4 m-2 flex h-8 justify-between self-end">
|
||||
<Button
|
||||
|
||||
Reference in New Issue
Block a user