feat: move column manager to rust
This commit is contained in:
@@ -1,13 +1,7 @@
|
|||||||
import { useCallback, useEffect, useMemo, useRef } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import {
|
import { getCurrent } from "@tauri-apps/api/window";
|
||||||
LogicalPosition,
|
|
||||||
LogicalSize,
|
|
||||||
getCurrent,
|
|
||||||
} from "@tauri-apps/api/window";
|
|
||||||
import { Webview } from "@tauri-apps/api/webview";
|
|
||||||
import { LumeColumn } from "@lume/types";
|
import { LumeColumn } from "@lume/types";
|
||||||
import { useDebouncedCallback } from "use-debounce";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { type UnlistenFn } from "@tauri-apps/api/event";
|
|
||||||
|
|
||||||
export function Col({
|
export function Col({
|
||||||
column,
|
column,
|
||||||
@@ -18,66 +12,62 @@ export function Col({
|
|||||||
account: string;
|
account: string;
|
||||||
isScroll: boolean;
|
isScroll: boolean;
|
||||||
}) {
|
}) {
|
||||||
const mainWindow = useMemo(() => getCurrent(), []);
|
const window = useMemo(() => getCurrent(), []);
|
||||||
const childWindow = useRef<Webview>(null);
|
|
||||||
const container = useRef<HTMLDivElement>(null);
|
const container = useRef<HTMLDivElement>(null);
|
||||||
const initialRect = useRef<DOMRect>(null);
|
|
||||||
const unlisten = useRef<UnlistenFn>(null);
|
|
||||||
const handleResize = useDebouncedCallback(() => {
|
|
||||||
if (!childWindow.current) return;
|
|
||||||
const newRect = container.current.getBoundingClientRect();
|
|
||||||
if (initialRect.current.height !== newRect.height) {
|
|
||||||
childWindow.current.setSize(
|
|
||||||
new LogicalSize(newRect.width, newRect.height),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}, 500);
|
|
||||||
|
|
||||||
const trackResize = useCallback(async () => {
|
const [webview, setWebview] = useState("");
|
||||||
unlisten.current = await mainWindow.onResized(() => {
|
|
||||||
handleResize();
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!childWindow.current) return;
|
|
||||||
if (isScroll) {
|
|
||||||
const newRect = container.current.getBoundingClientRect();
|
|
||||||
childWindow.current.setPosition(
|
|
||||||
new LogicalPosition(newRect.x, newRect.y),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}, [isScroll]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!mainWindow) return;
|
|
||||||
if (!container.current) return;
|
|
||||||
if (childWindow.current) return;
|
|
||||||
|
|
||||||
|
const createWebview = async () => {
|
||||||
const rect = container.current.getBoundingClientRect();
|
const rect = container.current.getBoundingClientRect();
|
||||||
const name = `column-${column.name.toLowerCase().replace(/\W/g, "")}`;
|
const name = `column-${column.name.toLowerCase().replace(/\W/g, "")}`;
|
||||||
const url = column.content + `?account=${account}&name=${column.name}`;
|
const url = column.content + `?account=${account}&name=${column.name}`;
|
||||||
|
|
||||||
// create new webview
|
// create new webview
|
||||||
initialRect.current = rect;
|
const label: string = await invoke("create_column", {
|
||||||
childWindow.current = new Webview(mainWindow, name, {
|
label: name,
|
||||||
url,
|
|
||||||
x: rect.x,
|
x: rect.x,
|
||||||
y: rect.y,
|
y: rect.y,
|
||||||
width: rect.width,
|
width: rect.width,
|
||||||
height: rect.height,
|
height: rect.height,
|
||||||
transparent: true,
|
url,
|
||||||
userAgent: "Lume/4.0",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// track window resize event
|
setWebview(label);
|
||||||
trackResize();
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (unlisten.current) unlisten.current();
|
|
||||||
if (childWindow.current) childWindow.current.close();
|
|
||||||
};
|
};
|
||||||
}, []);
|
|
||||||
|
const closeWebview = async () => {
|
||||||
|
await invoke("close_column", {
|
||||||
|
label: webview,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const repositionWebview = async () => {
|
||||||
|
const newRect = container.current.getBoundingClientRect();
|
||||||
|
await invoke("reposition_column", {
|
||||||
|
label: webview,
|
||||||
|
x: newRect.x,
|
||||||
|
y: newRect.y,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isScroll) {
|
||||||
|
repositionWebview();
|
||||||
|
}
|
||||||
|
}, [isScroll]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!window) return;
|
||||||
|
if (!container.current) return;
|
||||||
|
|
||||||
|
// create webview for current column
|
||||||
|
createWebview();
|
||||||
|
|
||||||
|
// close webview when unmounted
|
||||||
|
return () => {
|
||||||
|
closeWebview();
|
||||||
|
};
|
||||||
|
}, [window]);
|
||||||
|
|
||||||
return <div ref={container} className="h-full w-[440px] shrink-0 p-2" />;
|
return <div ref={container} className="h-full w-[440px] shrink-0 p-2" />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +1,33 @@
|
|||||||
import { Col } from "@/components/col";
|
import { Col } from "@/components/col";
|
||||||
import { Toolbar } from "@/components/toolbar";
|
import { Toolbar } from "@/components/toolbar";
|
||||||
import { LoaderIcon } from "@lume/icons";
|
import { LoaderIcon } from "@lume/icons";
|
||||||
|
import { EventColumns, LumeColumn } from "@lume/types";
|
||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import { useRef, useState } from "react";
|
import { UnlistenFn } from "@tauri-apps/api/event";
|
||||||
|
import { getCurrent } from "@tauri-apps/api/window";
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { VList, VListHandle } from "virtua";
|
import { VList, VListHandle } from "virtua";
|
||||||
|
|
||||||
export const Route = createFileRoute("/$account/home")({
|
export const Route = createFileRoute("/$account/home")({
|
||||||
component: Screen,
|
component: Screen,
|
||||||
pendingComponent: Pending,
|
pendingComponent: Pending,
|
||||||
loader: async () => {
|
|
||||||
const columns = [
|
|
||||||
{ name: "Newsfeed", content: "/newsfeed" },
|
|
||||||
{ name: "Lume Store", content: "/store/official" },
|
|
||||||
];
|
|
||||||
return columns;
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const COLS: LumeColumn[] = [
|
||||||
|
{ id: 1, name: "Newsfeed", content: "/newsfeed" },
|
||||||
|
{ id: 2, name: "Lume Store", content: "/store/official" },
|
||||||
|
];
|
||||||
|
|
||||||
function Screen() {
|
function Screen() {
|
||||||
const data = Route.useLoaderData();
|
|
||||||
const search = Route.useSearch();
|
const search = Route.useSearch();
|
||||||
const vlistRef = useRef<VListHandle>(null);
|
const vlistRef = useRef<VListHandle>(null);
|
||||||
|
const unlisten = useRef<UnlistenFn>(null);
|
||||||
|
|
||||||
|
const [columns, setColumns] = useState(COLS);
|
||||||
const [selectedIndex, setSelectedIndex] = useState(-1);
|
const [selectedIndex, setSelectedIndex] = useState(-1);
|
||||||
const [isScroll, setIsScroll] = useState(false);
|
const [isScroll, setIsScroll] = useState(false);
|
||||||
|
|
||||||
const moveLeft = () => {
|
const goLeft = () => {
|
||||||
const prevIndex = Math.max(selectedIndex - 1, 0);
|
const prevIndex = Math.max(selectedIndex - 1, 0);
|
||||||
setSelectedIndex(prevIndex);
|
setSelectedIndex(prevIndex);
|
||||||
vlistRef.current.scrollToIndex(prevIndex, {
|
vlistRef.current.scrollToIndex(prevIndex, {
|
||||||
@@ -33,14 +35,44 @@ function Screen() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const moveRight = () => {
|
const goRight = () => {
|
||||||
const nextIndex = Math.min(selectedIndex + 1, data.length - 1);
|
const nextIndex = Math.min(selectedIndex + 1, columns.length - 1);
|
||||||
setSelectedIndex(nextIndex);
|
setSelectedIndex(nextIndex);
|
||||||
vlistRef.current.scrollToIndex(nextIndex, {
|
vlistRef.current.scrollToIndex(nextIndex, {
|
||||||
align: "end",
|
align: "end",
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const add = async (column: LumeColumn) => {
|
||||||
|
setColumns((prev) => [...prev, column]);
|
||||||
|
vlistRef?.current.scrollToIndex(columns.length);
|
||||||
|
};
|
||||||
|
|
||||||
|
const remove = async (id: number) => {
|
||||||
|
setColumns((prev) => prev.filter((t) => t.id !== id));
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function listenUpdateColumn() {
|
||||||
|
const mainWindow = getCurrent();
|
||||||
|
unlisten.current = await mainWindow.listen<EventColumns>(
|
||||||
|
"columns",
|
||||||
|
(data) => {
|
||||||
|
if (data.payload.type === "add") add(data.payload.column);
|
||||||
|
if (data.payload.type === "remove") remove(data.payload.id);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// listen for column changes
|
||||||
|
listenUpdateColumn();
|
||||||
|
|
||||||
|
// clean up
|
||||||
|
return () => {
|
||||||
|
if (unlisten.current) unlisten.current();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full">
|
<div className="h-full w-full">
|
||||||
<VList
|
<VList
|
||||||
@@ -54,13 +86,13 @@ function Screen() {
|
|||||||
case "ArrowUp":
|
case "ArrowUp":
|
||||||
case "ArrowLeft": {
|
case "ArrowLeft": {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
moveLeft();
|
goLeft();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "ArrowDown":
|
case "ArrowDown":
|
||||||
case "ArrowRight": {
|
case "ArrowRight": {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
moveRight();
|
goRight();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -73,9 +105,9 @@ function Screen() {
|
|||||||
}}
|
}}
|
||||||
className="scrollbar-none h-full w-full overflow-x-auto focus:outline-none"
|
className="scrollbar-none h-full w-full overflow-x-auto focus:outline-none"
|
||||||
>
|
>
|
||||||
{data.map((column, index) => (
|
{columns.map((column, index) => (
|
||||||
<Col
|
<Col
|
||||||
key={column.name + index}
|
key={column.id + index}
|
||||||
column={column}
|
column={column}
|
||||||
// @ts-ignore, yolo !!!
|
// @ts-ignore, yolo !!!
|
||||||
account={search.acccount}
|
account={search.acccount}
|
||||||
@@ -83,7 +115,7 @@ function Screen() {
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</VList>
|
</VList>
|
||||||
<Toolbar moveLeft={moveLeft} moveRight={moveRight} />
|
<Toolbar moveLeft={goLeft} moveRight={goRight} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,13 @@ export const Route = createFileRoute("/store/official")({
|
|||||||
});
|
});
|
||||||
|
|
||||||
function Screen() {
|
function Screen() {
|
||||||
|
/*
|
||||||
|
const add = async (column: LumeColumn) => {
|
||||||
|
const mainWindow = getCurrent();
|
||||||
|
await mainWindow.emit("columns", { type: "add", column });
|
||||||
|
};
|
||||||
|
*/
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-3 px-3 pt-3">
|
<div className="flex flex-col gap-3 px-3 pt-3">
|
||||||
<div className="relative h-[200px] w-full overflow-hidden rounded-xl bg-gradient-to-tr from-orange-100 to-blue-200 px-3 pt-3">
|
<div className="relative h-[200px] w-full overflow-hidden rounded-xl bg-gradient-to-tr from-orange-100 to-blue-200 px-3 pt-3">
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { GroupFeedsIcon, LaurelIcon } from "@lume/icons";
|
||||||
import { Column } from "@lume/ui";
|
import { Column } from "@lume/ui";
|
||||||
import { cn } from "@lume/utils";
|
import { cn } from "@lume/utils";
|
||||||
import { Link } from "@tanstack/react-router";
|
import { Link } from "@tanstack/react-router";
|
||||||
@@ -11,17 +12,18 @@ function Screen() {
|
|||||||
return (
|
return (
|
||||||
<Column.Root>
|
<Column.Root>
|
||||||
<Column.Content>
|
<Column.Content>
|
||||||
<div className="flex h-14 shrink-0 items-center gap-3 border-b border-neutral-100 px-3 dark:border-neutral-900">
|
<div className="flex h-14 shrink-0 items-center gap-2 border-b border-neutral-100 px-3 dark:border-neutral-900">
|
||||||
<Link to="/store/official">
|
<Link to="/store/official">
|
||||||
{({ isActive }) => (
|
{({ isActive }) => (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"inline-flex h-8 w-max items-center justify-center rounded-full px-6 text-sm",
|
"inline-flex h-8 w-max items-center justify-center gap-2 rounded-full px-6 text-sm font-medium",
|
||||||
isActive
|
isActive
|
||||||
? "bg-neutral-100 font-medium dark:bg-neutral-900"
|
? "bg-neutral-100 dark:bg-neutral-900"
|
||||||
: "opacity-50",
|
: "opacity-50",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
<LaurelIcon className="size-5" />
|
||||||
Official
|
Official
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -30,12 +32,13 @@ function Screen() {
|
|||||||
{({ isActive }) => (
|
{({ isActive }) => (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"inline-flex h-8 w-max items-center justify-center rounded-full px-6 text-sm",
|
"inline-flex h-8 w-max items-center justify-center gap-2 rounded-full px-6 text-sm font-medium",
|
||||||
isActive
|
isActive
|
||||||
? "bg-neutral-100 font-medium dark:bg-neutral-900"
|
? "bg-neutral-100 dark:bg-neutral-900"
|
||||||
: "opacity-50",
|
: "opacity-50",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
<GroupFeedsIcon className="size-5" />
|
||||||
Community
|
Community
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user