working window manager
This commit is contained in:
parent
3ff1248887
commit
9471a13388
@ -6,10 +6,12 @@ import HomeText from "@/components/home-text.mdx"
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { FakeWindow, HomeWindows } from "@/components/windows";
|
import { FakeWindow, HomeWindows } from "@/components/windows";
|
||||||
import Taskbar from "@/components/taskbar";
|
import Taskbar from "@/components/taskbar";
|
||||||
|
import { WindowManagerProvider } from "@/hooks/window-manager";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (<>
|
return (<>
|
||||||
<main className="flex items-center pt-16 md:pt-24 pb-12 px-8 md:px-0 overflow-x-hidden">
|
<main className="flex items-center pt-16 md:pt-24 pb-12 px-8 md:px-0 overflow-x-hidden">
|
||||||
|
<WindowManagerProvider>
|
||||||
<FakeWindow windowText="Homepage">
|
<FakeWindow windowText="Homepage">
|
||||||
<HomeWindows />
|
<HomeWindows />
|
||||||
<header className="text-center mb-8">
|
<header className="text-center mb-8">
|
||||||
@ -35,6 +37,7 @@ export default function Home() {
|
|||||||
</p>
|
</p>
|
||||||
</footer>
|
</footer>
|
||||||
</FakeWindow>
|
</FakeWindow>
|
||||||
|
</WindowManagerProvider>
|
||||||
</main>
|
</main>
|
||||||
</>);
|
</>);
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,23 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
|
import { FakeWindow, useWindowManager } from "@/hooks/window-manager";
|
||||||
import { Icon } from "@iconify/react";
|
import { Icon } from "@iconify/react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { useState, useRef, useEffect } from "react";
|
import React, { useState, useRef, useEffect, useReducer } from "react";
|
||||||
|
|
||||||
|
export const RestoreWindowsButton = ({ onClick, className, ...props}: React.ComponentProps<"button">) => {
|
||||||
|
const windowManager = useWindowManager();
|
||||||
|
|
||||||
|
const isAnyWindowsClosed = !windowManager.windows.find(w => w.closed == true);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={clsx("bg-primary border border-[#FFA826]", className, isAnyWindowsClosed && "hidden")}
|
||||||
|
onClick={windowManager.openAll}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export const FakeRelativeWindow = ({
|
export const FakeRelativeWindow = ({
|
||||||
windowText,
|
windowText,
|
||||||
@ -17,12 +32,15 @@ export const FakeRelativeWindow = ({
|
|||||||
withAnim?: boolean,
|
withAnim?: boolean,
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
}) => {
|
}) => {
|
||||||
const [isMinimized, setMinimize] = useState(false);
|
const windowManager = useWindowManager();
|
||||||
const [offset, setOffset] = useState({ x: 0, y: 0 });
|
const windowName = windowText.toLocaleLowerCase();
|
||||||
|
const currentWindow = windowManager.get(windowName);
|
||||||
|
|
||||||
const [animation, setAnimation] = useState({
|
const [animation, setAnimation] = useState({
|
||||||
window: "animate-window-popup",
|
window: "animate-window-popup",
|
||||||
content: "animate-[fade-in-half_1s]"
|
content: "animate-[fade-in-half_1s]"
|
||||||
});
|
});
|
||||||
|
|
||||||
const popupRef = useRef<HTMLDivElement>(null);
|
const popupRef = useRef<HTMLDivElement>(null);
|
||||||
const pos = useRef({
|
const pos = useRef({
|
||||||
dragging: false,
|
dragging: false,
|
||||||
@ -32,6 +50,32 @@ export const FakeRelativeWindow = ({
|
|||||||
y: 0
|
y: 0
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const populateWindow = () => {
|
||||||
|
windowManager.add({
|
||||||
|
name: windowName,
|
||||||
|
closed: false,
|
||||||
|
minimized: false,
|
||||||
|
offset: { x: 0, y: 0 }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!windowManager.isLocalDataExists) {
|
||||||
|
if (!currentWindow) populateWindow();
|
||||||
|
return () => {
|
||||||
|
windowManager.remove(windowName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log(windowManager.windows)
|
||||||
|
}, [windowManager.windows]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (popupRef.current) popupRef.current.style.transform = `translate(${currentWindow?.offset.x || 0}px, ${currentWindow?.offset.y || 0}px)`;
|
||||||
|
}, [currentWindow?.offset])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!withAnim) return;
|
if (!withAnim) return;
|
||||||
const node = popupRef.current;
|
const node = popupRef.current;
|
||||||
@ -56,16 +100,17 @@ export const FakeRelativeWindow = ({
|
|||||||
return () => observer.disconnect();
|
return () => observer.disconnect();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const toggleMinimize = () => setMinimize(!isMinimized);
|
const toggleMinimize = () => windowManager.toggleMinimize(windowName);
|
||||||
|
const handleClose = () => windowManager.close(windowName);
|
||||||
|
|
||||||
const onMouseDown = (e: React.MouseEvent) => {
|
const onMouseDown = (e: React.MouseEvent) => {
|
||||||
if (!draggable) return;
|
if (!draggable || !currentWindow) return;
|
||||||
if (popupRef.current) {
|
if (popupRef.current) {
|
||||||
pos.current.dragging = true;
|
pos.current.dragging = true;
|
||||||
pos.current.mouseX = e.clientX;
|
pos.current.mouseX = e.clientX;
|
||||||
pos.current.mouseY = e.clientY;
|
pos.current.mouseY = e.clientY;
|
||||||
pos.current.x = offset.x;
|
pos.current.x = currentWindow.offset.x;
|
||||||
pos.current.y = offset.y;
|
pos.current.y = currentWindow.offset.y;
|
||||||
document.body.style.userSelect = "none";
|
document.body.style.userSelect = "none";
|
||||||
}
|
}
|
||||||
window.addEventListener("mousemove", onMouseMove);
|
window.addEventListener("mousemove", onMouseMove);
|
||||||
@ -80,16 +125,21 @@ export const FakeRelativeWindow = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onMouseMove = (e: MouseEvent) => {
|
const onMouseMove = (e: MouseEvent) => {
|
||||||
|
if (!currentWindow) return;
|
||||||
if (pos.current.dragging && popupRef.current) {
|
if (pos.current.dragging && popupRef.current) {
|
||||||
const dx = e.clientX - pos.current.mouseX;
|
const dx = e.clientX - pos.current.mouseX;
|
||||||
const dy = e.clientY - pos.current.mouseY;
|
const dy = e.clientY - pos.current.mouseY;
|
||||||
const newX = pos.current.x + dx;
|
const newX = pos.current.x + dx;
|
||||||
const newY = pos.current.y + dy;
|
const newY = pos.current.y + dy;
|
||||||
popupRef.current.style.transform = `translate(${newX}px, ${newY}px)`;
|
windowManager.move(windowName, {
|
||||||
setOffset({ x: newX, y: newY });
|
x: newX,
|
||||||
|
y: newY
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!currentWindow || currentWindow?.closed) return
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={clsx("absolute hidden lg:block", className)}>
|
<div className={clsx("absolute hidden lg:block", className)}>
|
||||||
<div className={clsx("mx-auto md:border bg-background border-primary", withAnim && animation.window)} ref={popupRef}>
|
<div className={clsx("mx-auto md:border bg-background border-primary", withAnim && animation.window)} ref={popupRef}>
|
||||||
@ -98,14 +148,24 @@ export const FakeRelativeWindow = ({
|
|||||||
onMouseDown={onMouseDown}
|
onMouseDown={onMouseDown}
|
||||||
>
|
>
|
||||||
<div className="ms-1 pointer-events-none">
|
<div className="ms-1 pointer-events-none">
|
||||||
{windowText}
|
{currentWindow ? currentWindow.name : "Error!"}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button className="bg-primary border border-[#FFA826] border-outset p-1" onClick={toggleMinimize}><Icon icon="lucide:minus"/></button>
|
<button
|
||||||
<button className="bg-primary border border-[#FFA826] border-outset p-1"><Icon icon="lucide:x"/></button>
|
className="bg-primary border border-[#FFA826] border-outset p-1"
|
||||||
|
onClick={toggleMinimize}
|
||||||
|
>
|
||||||
|
<Icon icon="lucide:minus"/>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="bg-primary border border-[#FFA826] border-outset p-1"
|
||||||
|
onClick={handleClose}
|
||||||
|
>
|
||||||
|
<Icon icon="lucide:x"/>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={clsx("m-1 border border-primary", isMinimized ? "h-0 overflow-y-clip" : "h-fit")}>
|
<div className={clsx("m-1 border border-primary", currentWindow?.minimized ? "h-0 overflow-y-clip" : "h-fit")}>
|
||||||
<div className={clsx("md:p-4", withAnim && animation.content)}>
|
<div className={clsx("md:p-4", withAnim && animation.content)}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
@ -8,10 +8,8 @@ export const FloatingLabel = ({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
|
||||||
<PopoverText text={placeholder}>
|
<PopoverText text={placeholder}>
|
||||||
{children}
|
{children}
|
||||||
</PopoverText>
|
</PopoverText>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
@ -1,6 +1,6 @@
|
|||||||
import { Icon } from "@iconify/react";
|
import { Icon } from "@iconify/react";
|
||||||
import { FloatingLabel } from "@/components/floating-label";
|
import { FloatingLabel } from "@/components/floating-label";
|
||||||
import { FakeRelativeWindow } from "./client-windows";
|
import { FakeRelativeWindow, RestoreWindowsButton } from "./client-windows";
|
||||||
import Image from "next/image"
|
import Image from "next/image"
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
@ -16,6 +16,10 @@ export const FakeWindow = ({
|
|||||||
<div className="ms-1 pointer-events-none">
|
<div className="ms-1 pointer-events-none">
|
||||||
{windowText}
|
{windowText}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="inline-flex">
|
||||||
|
<RestoreWindowsButton className="border-outset px-2 text-xs me-2">
|
||||||
|
Restore Windows
|
||||||
|
</RestoreWindowsButton>
|
||||||
<FloatingLabel placeholder="Useless button btw">
|
<FloatingLabel placeholder="Useless button btw">
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button className="bg-primary border border-[#FFA826] border-outset p-1"><Icon icon="lucide:minus"/></button>
|
<button className="bg-primary border border-[#FFA826] border-outset p-1"><Icon icon="lucide:minus"/></button>
|
||||||
@ -24,6 +28,7 @@ export const FakeWindow = ({
|
|||||||
</FloatingLabel>
|
</FloatingLabel>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div className="m-1 md:border border-primary">
|
<div className="m-1 md:border border-primary">
|
||||||
<div className="md:p-8">
|
<div className="md:p-8">
|
||||||
{children}
|
{children}
|
||||||
@ -34,23 +39,6 @@ export const FakeWindow = ({
|
|||||||
|
|
||||||
export const HomeWindows = () => (
|
export const HomeWindows = () => (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
{/* <FakeRelativeWindow
|
|
||||||
windowText="featured.exe"
|
|
||||||
className="left-[540px] top-0 z-20"
|
|
||||||
draggable
|
|
||||||
>
|
|
||||||
<div className="space-y-2" style={{ width: 300 }}>
|
|
||||||
<Image
|
|
||||||
className=""
|
|
||||||
alt="Left 4 Dead"
|
|
||||||
src="https://shared.fastly.steamstatic.com/store_item_assets/steam/apps/550/header.jpg?t=1745368562"
|
|
||||||
width={300}
|
|
||||||
height={200}
|
|
||||||
unoptimized
|
|
||||||
/>
|
|
||||||
<p>Join my Left 4 Dead 2 server!</p>
|
|
||||||
</div>
|
|
||||||
</FakeRelativeWindow> */}
|
|
||||||
<FakeRelativeWindow
|
<FakeRelativeWindow
|
||||||
windowText="nola.png"
|
windowText="nola.png"
|
||||||
className="-right-[73%] top-[1300px] z-20"
|
className="-right-[73%] top-[1300px] z-20"
|
||||||
|
@ -1,9 +1,86 @@
|
|||||||
interface iFakeWindow {
|
'use client'
|
||||||
|
|
||||||
|
import React, { createContext, useContext, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
export interface FakeWindow {
|
||||||
name: string;
|
name: string;
|
||||||
minimized: boolean;
|
minimized: boolean;
|
||||||
closed: boolean;
|
closed: boolean;
|
||||||
offset: {
|
offset: {
|
||||||
x: number
|
x: number;
|
||||||
y: number
|
y: number;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface WindowManagerContextType {
|
||||||
|
windows: FakeWindow[];
|
||||||
|
isLocalDataExists: boolean;
|
||||||
|
get: (name: string) => FakeWindow | undefined;
|
||||||
|
add: (window: FakeWindow) => void;
|
||||||
|
updateWindow: (name: string, changes: Partial<FakeWindow>) => void;
|
||||||
|
toggleMinimize: (name: string) => void;
|
||||||
|
close: (name: string) => void;
|
||||||
|
remove: (name: string) => void;
|
||||||
|
move: (name: string, offset: { x: number; y: number }) => void;
|
||||||
|
openAll: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const WindowManagerContext = createContext<WindowManagerContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
export const WindowManagerProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||||
|
const [windows, setWindows] = useState<FakeWindow[]>([]);
|
||||||
|
const [isInit, setInit] = useState(false);
|
||||||
|
const [isLocalDataExists, setExists] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const data = localStorage.getItem("fake-windows-data");
|
||||||
|
if (data) {
|
||||||
|
setWindows(JSON.parse(data));
|
||||||
|
setExists(true);
|
||||||
|
}
|
||||||
|
setInit(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isInit && typeof window !== 'undefined' && windows.length > 0) {
|
||||||
|
window.localStorage.setItem('fake-windows-data', JSON.stringify(windows));
|
||||||
|
}
|
||||||
|
}, [windows]);
|
||||||
|
|
||||||
|
const get = (name: string) => windows.find(window => window.name == name);
|
||||||
|
const add = (window: FakeWindow) => setWindows(w => [...w, window]);
|
||||||
|
const updateWindow = (name: string, changes: Partial<FakeWindow>) =>
|
||||||
|
setWindows(w =>
|
||||||
|
w.map(win => win.name === name ? { ...win, ...changes } : win)
|
||||||
|
);
|
||||||
|
const toggleMinimize = (name: string) =>
|
||||||
|
setWindows(w =>
|
||||||
|
w.map(win => win.name === name ? { ...win, minimized: !win.minimized } : win)
|
||||||
|
);
|
||||||
|
const open = (name: string) => updateWindow(name, { closed: false });
|
||||||
|
const close = (name: string) => updateWindow(name, { closed: true });
|
||||||
|
const move = (name: string, offset: { x: number; y: number }) =>
|
||||||
|
updateWindow(name, { offset });
|
||||||
|
|
||||||
|
const remove = (name: string) => setWindows(w => w.filter(win => win.name !== name));
|
||||||
|
|
||||||
|
const openAll = () => setWindows(w =>
|
||||||
|
w.map(win => ({ ...win, closed: false }))
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<WindowManagerContext.Provider value={{
|
||||||
|
windows,
|
||||||
|
isLocalDataExists,
|
||||||
|
get, add, updateWindow, toggleMinimize, close, move, remove, openAll
|
||||||
|
}}>
|
||||||
|
{children}
|
||||||
|
</WindowManagerContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useWindowManager() {
|
||||||
|
const ctx = useContext(WindowManagerContext);
|
||||||
|
if (!ctx) throw new Error("useWindowManager must be used within WindowManagerProvider");
|
||||||
|
return ctx;
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user