diff --git a/public/images/cafeberry_title.png b/public/images/cafeberry_title.png index 2d93760..fbf875c 100644 Binary files a/public/images/cafeberry_title.png and b/public/images/cafeberry_title.png differ diff --git a/public/images/gear.png b/public/images/gear.png new file mode 100644 index 0000000..d114271 Binary files /dev/null and b/public/images/gear.png differ diff --git a/public/images/neoLegacy.png b/public/images/neoLegacy.png index add9fd9..96a1539 100644 Binary files a/public/images/neoLegacy.png and b/public/images/neoLegacy.png differ diff --git a/src/components/views/VersionsView.tsx b/src/components/views/VersionsView.tsx index 94f8ead..0c2194d 100644 --- a/src/components/views/VersionsView.tsx +++ b/src/components/views/VersionsView.tsx @@ -63,15 +63,6 @@ const DeleteConfirmButton = memo(function DeleteConfirmButton({ const ROW_ESTIMATE = 52; const ROW_GAP = 4; - -function formatPlaytime(seconds: number): string { - const h = Math.floor(seconds / 3600); - const m = Math.floor((seconds % 3600) / 60); - if (h > 0) return `${h}h ${m}m`; - if (m > 0) return `${m}m`; - return seconds > 0 ? `${seconds}s` : ""; -} - const VersionsView = memo(function VersionsView() { const { t } = useTranslation(); const { setActiveView } = useUI(); @@ -123,11 +114,11 @@ const VersionsView = memo(function VersionsView() { } | null>(null); const [isCustomizeModalOpen, setIsCustomizeModalOpen] = useState(false); const [customizeTarget, setCustomizeTarget] = useState(null); - const [playtimeMap, setPlaytimeMap] = useState< + const [_playtimeMap, setPlaytimeMap] = useState< Record >({}); const [initialPath, setInitialPath] = useState(""); - const [hoveredBtn, setHoveredBtn] = useState<{ + const [_hoveredBtn, setHoveredBtn] = useState<{ row: number; btn: string; } | null>(null); @@ -144,8 +135,13 @@ const VersionsView = memo(function VersionsView() { name: string; } | null>(null); const [argsSchemas, setArgsSchemas] = useState>({}); + const [thumb, setThumb] = useState({ height: 0, top: 0 }); const containerRef = useRef(null); const listRef = useRef(null); + const trackRef = useRef(null); + const thumbRef = useRef(null); + const dragOffsetRef = useRef(0); + const draggingRef = useRef(false); const menuRef = useRef(null); const [menuDir, setMenuDir] = useState<"down" | "up">("down"); const ITEM_COUNT = visibleEditions.length + 3; @@ -162,6 +158,63 @@ const VersionsView = memo(function VersionsView() { visibleEditionsRef.current = visibleEditions; const focusIndexRef = useRef(focusIndex); const openMenuIdRef = useRef(openMenuId); + const updateThumb = useCallback(() => { + const container = listRef.current; + const track = trackRef.current; + if (!container || !track) return; + const scrollHeight = container.scrollHeight; + const clientHeight = container.clientHeight; + const scrollable = scrollHeight - clientHeight; + if (scrollable <= 0) { + setThumb((prev) => + prev.height === 0 && prev.top === 0 ? prev : { height: 0, top: 0 }, + ); + return; + } + const trackHeight = track.clientHeight; + const HANDLE = 40; + const STEP = HANDLE + 4; + const maxTop = trackHeight - HANDLE; + const maxIndex = Math.max(1, Math.floor(maxTop / STEP)); + const index = Math.min( + maxIndex, + Math.round((container.scrollTop / scrollable) * maxIndex), + ); + const top = index * STEP; + setThumb((prev) => + prev.top === top && prev.height === HANDLE + ? prev + : { height: HANDLE, top }, + ); + }, []); + + const handleThumbPointerDown = (e: React.PointerEvent) => { + e.preventDefault(); + draggingRef.current = true; + dragOffsetRef.current = + e.clientY - e.currentTarget.getBoundingClientRect().top; + e.currentTarget.setPointerCapture(e.pointerId); + }; + + const handleThumbPointerMove = (e: React.PointerEvent) => { + if (!draggingRef.current) return; + const container = listRef.current; + const track = trackRef.current; + if (!container || !track) return; + const trackRect = track.getBoundingClientRect(); + const trackTop = trackRect.top + track.clientTop; + const y = e.clientY - trackTop - dragOffsetRef.current; + const trackHeight = track.clientHeight; + const maxTop = Math.max(0, trackHeight - thumb.height); + const scrollable = container.scrollHeight - container.clientHeight; + if (scrollable <= 0) return; + const clampedY = Math.min(Math.max(y, 0), maxTop); + container.scrollTop = (clampedY / maxTop) * scrollable; + }; + + const handleThumbPointerUp = () => { + draggingRef.current = false; + }; const updateVisibleRange = useCallback(() => { const container = listRef.current; @@ -176,11 +229,8 @@ const VersionsView = memo(function VersionsView() { let end = -1; for (let i = 0; i < n; i++) { const itemTop = acc; - const itemBottom = acc + (heights[i] ?? ROW_ESTIMATE); - if ( - itemTop >= scrollTop && - itemBottom <= scrollTop + viewportHeight - ) { + const itemNeo = acc + (heights[i] ?? ROW_ESTIMATE); //neo: me fr fr :3 + if (itemTop >= scrollTop && itemNeo <= scrollTop + viewportHeight) { if (start === n) start = i; end = i; } @@ -204,7 +254,8 @@ const VersionsView = memo(function VersionsView() { setVisibleRange((prev) => prev.start === start && prev.end === end ? prev : { start, end }, ); - }, []); + updateThumb(); + }, [updateThumb]); const setRowRef = (index: number) => (el: HTMLDivElement | null) => { if (el) { @@ -428,11 +479,8 @@ const VersionsView = memo(function VersionsView() { }, [installedVersions]); const handleEditionClick = (edition: Edition, index: number) => { - const isInstalled = installedVersions.includes(edition.instanceId); - if (isInstalled) { - playPressSound(); - setSelectedProfile(edition.instanceId); - } + playPressSound(); + setSelectedProfile(edition.instanceId); setFocusIndex(index); }; @@ -465,12 +513,12 @@ const VersionsView = memo(function VersionsView() { transition={{ duration: animationsEnabled ? 0.25 : 0 }} className="flex flex-col items-center w-full max-w-5xl outline-none" > -
-
+
+
{visibleEditions.map((edition: Edition, i: number) => { if (i < visibleRange.start || i > visibleRange.end) { @@ -499,6 +547,10 @@ const VersionsView = memo(function VersionsView() { ref={setRowRef(i)} className={`w-[calc(100%-20px)] snap-start flex items-center gap-3 ${!isFocused ? "mc-button-ninesliced" : "mc-button-ninesliced-selected"} ${isComingSoon ? "opacity-50 cursor-not-allowed" : ""} relative ${openMenuId === edition.id ? "z-50" : "z-0"}`} onMouseEnter={() => !isComingSoon && setFocusIndex(i)} + onMouseLeave={() => setFocusIndex(-1)} //neo: you see this, smartcmd? + onClick={() => + !isComingSoon && handleEditionClick(edition, i) + } >
{edition.logo ? ( @@ -524,55 +576,15 @@ const VersionsView = memo(function VersionsView() {
- !isComingSoon && handleEditionClick(edition, i) - } className={`flex-1 text-left min-w-0 outline-none rounded cursor-pointer ${isComingSoon ? "cursor-not-allowed" : ""}`} >
{edition.name} - {isInstalled && ( - - )} {edition.category && edition.category.map((cat: string) => ( setHoveredBtn(null)} - className={`w-9 h-9 flex items-center justify-center ${ - isDownloading ? "opacity-50" : "" - }`} + className={ + "w-10 h-10 flex items-center justify-center bg-[url(/images/empty.png)] bg-cover bg-no-repeat bg-center" + } style={{ - backgroundImage: - (hoveredBtn?.row === i && - hoveredBtn?.btn === "main") || - (focusIndex === i && focusBtn === 0) - ? "url('/images/Button_Square_Highlighted.png')" - : "url('/images/Button_Square.png')", - backgroundSize: "100% 100%", imageRendering: "pixelated", }} > @@ -626,7 +631,7 @@ const VersionsView = memo(function VersionsView() { : "/images/Download_Icon.png" } alt="" - className="w-5 h-5 object-contain" + className="w-8 h-8 object-contain" style={{ imageRendering: "pixelated", filter: isDownloading @@ -648,31 +653,20 @@ const VersionsView = memo(function VersionsView() { setHoveredBtn({ row: i, btn: "menu" }) } onMouseLeave={() => setHoveredBtn(null)} - className="w-9 h-9 flex flex-col items-center justify-center gap-1 transition-colors relative" + className="w-10 h-10 flex flex-col items-center justify-center gap-1 transition-colors relative bg-[url(/images/empty.png)] bg-cover bg-no-repeat bg-center" style={{ - backgroundImage: - (hoveredBtn?.row === i && - hoveredBtn?.btn === "menu") || - (focusIndex === i && - (focusBtn === 0 || focusBtn === 1)) - ? "url('/images/Button_Square_Highlighted.png')" - : "url('/images/Button_Square.png')", - backgroundSize: "100% 100%", imageRendering: "pixelated", - filter: updatesAvailable?.[edition.instanceId] - ? "drop-shadow(0 0 4px rgba(255,255,0,0.8))" - : "none", }} > -
-
-
+
+ +
{openMenuId === edition.id && ( @@ -1104,73 +1098,87 @@ const VersionsView = memo(function VersionsView() {
); })} - -
- - - {!isAndroid && ( - - )} -
+ +
+ {thumb.height > 0 && ( +
+ { + //neo: oh my goodness this took me a lot to figure out, anyway its just a transparent
so the div isnt empty so that the border-image renders + } +
+
+ )} +
+ + {!isAndroid && ( +
+ + + +
+ )} +
+ +
+ {visibleEditions[focusIndex]?.desc ?? + visibleEditions.find((e) => e.id == useConfig().profile)?.desc ?? + ""}
{!isAndroid && ( @@ -1182,7 +1190,7 @@ const VersionsView = memo(function VersionsView() { playBackSound(); setActiveView("main"); }} - className="w-48 h-10 flex items-center justify-center text-xl mc-text-shadow outline-none border-none text-white" + className="w-48 h-10 flex items-center justify-center text-xl mc-text-shadow text-white" style={{ backgroundImage: focusIndex === visibleEditions.length + 2 diff --git a/src/css/index.css b/src/css/index.css index faef469..16e5172 100644 --- a/src/css/index.css +++ b/src/css/index.css @@ -82,6 +82,13 @@ image-rendering: pixelated; } +.mc-optionbutton { + appearance: none; + background: url("/images/options_background.png") center / 100% 100% + no-repeat; + image-rendering: pixelated; +} + .mc-button-ninesliced { /*neo: tysm https://leanrada.com/9-slicer/ */ border-image: url("/images/Button_Background.png") 13 13 13 13 fill / 13px @@ -104,7 +111,8 @@ .mc-versionrecess { /*neo: tysm https://leanrada.com/9-slicer/ */ - border-image: url("/images/versionrecess.png") 12 30 30 12 fill / 12px 30px 30px 12px; + border-image: url("/images/versionrecess.png") 12 30 30 12 fill / 12px 30px + 30px 12px; border-width: 12px; border-style: solid; border-color: transparent; @@ -113,7 +121,7 @@ .mc-help { /*neo: tysm https://leanrada.com/9-slicer/ */ - border-image: url("/images/helpbg.png") 5 15 17 6 fill / 5px 15px 17px 6px; + border-image: url("/images/helpbg.png") 7 16 16 7 fill / 7px 16px 16px 7px; border-width: 12px; border-style: solid; border-color: transparent; diff --git a/src/pages/App.tsx b/src/pages/App.tsx index a0d195e..a6cf3db 100644 --- a/src/pages/App.tsx +++ b/src/pages/App.tsx @@ -309,6 +309,7 @@ export default function App() { "model-editor", "swf-editor", "goldmapper", + "versions", //neo: didnt expect that but oh well ]); useEffect(() => { const handleContextMenu = (e: MouseEvent) => e.preventDefault(); @@ -775,12 +776,14 @@ export default function App() { className="shrink-0 p-4 flex justify-between items-end text-[10px] text-[#A0A0A0] mc-text-shadow bg-gradient-to-t from-black/80 to-transparent uppercase tracking-widest opacity-60 font-[var(--font-base)]" style={{ fontWeight: "normal" }} > -
- {t("app.version", { version: `${pkg.version} (${__BUILD_DATE__})` })} -
-
- {connected && t("app.controllerConnected")} -
+
+ {t("app.version", { + version: `${pkg.version} (${__BUILD_DATE__})`, + })} +
+
+ {connected && t("app.controllerConnected")} +