feat: improve column management
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useEffect, useMemo, useRef } from "react";
|
||||
import { getCurrent } from "@tauri-apps/api/window";
|
||||
import { LumeColumn } from "@lume/types";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
@@ -13,39 +13,39 @@ export function Col({
|
||||
isScroll: boolean;
|
||||
}) {
|
||||
const window = useMemo(() => getCurrent(), []);
|
||||
const webview = useRef<string>(null);
|
||||
const container = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [webview, setWebview] = useState<string>(null);
|
||||
|
||||
const createWebview = async () => {
|
||||
const rect = container.current.getBoundingClientRect();
|
||||
const name = `column-${column.name.toLowerCase().replace(/\W/g, "")}`;
|
||||
const url = column.content + `?account=${account}&name=${column.name}`;
|
||||
const label = `column-${column.id}`;
|
||||
const url =
|
||||
column.content +
|
||||
`?account=${account}&id=${column.id}&name=${column.name}`;
|
||||
|
||||
// create new webview
|
||||
const label: string = await invoke("create_column", {
|
||||
label: name,
|
||||
webview.current = await invoke("create_column", {
|
||||
label,
|
||||
x: rect.x,
|
||||
y: rect.y,
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
url,
|
||||
});
|
||||
|
||||
setWebview(label);
|
||||
};
|
||||
|
||||
const closeWebview = async () => {
|
||||
if (!webview) return;
|
||||
await invoke("close_column", {
|
||||
label: webview,
|
||||
const close = await invoke("close_column", {
|
||||
label: webview.current,
|
||||
});
|
||||
if (close) webview.current = null;
|
||||
};
|
||||
|
||||
const repositionWebview = async () => {
|
||||
if (!webview.current) return;
|
||||
const newRect = container.current.getBoundingClientRect();
|
||||
await invoke("reposition_column", {
|
||||
label: webview,
|
||||
label: webview.current,
|
||||
x: newRect.x,
|
||||
y: newRect.y,
|
||||
});
|
||||
@@ -60,13 +60,14 @@ export function Col({
|
||||
useEffect(() => {
|
||||
if (!window) return;
|
||||
if (!container.current) return;
|
||||
if (webview.current) return;
|
||||
|
||||
// create webview for current column
|
||||
createWebview();
|
||||
|
||||
// close webview when unmounted
|
||||
return () => {
|
||||
closeWebview();
|
||||
if (webview.current) closeWebview();
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Col } from "@/components/col";
|
||||
import { Toolbar } from "@/components/toolbar";
|
||||
import { LoaderIcon, PlusIcon } from "@lume/icons";
|
||||
import { LoaderIcon } from "@lume/icons";
|
||||
import { EventColumns, LumeColumn } from "@lume/types";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { UnlistenFn } from "@tauri-apps/api/event";
|
||||
@@ -14,13 +14,14 @@ export const Route = createFileRoute("/$account/home")({
|
||||
});
|
||||
|
||||
const DEFAULT_COLUMNS: LumeColumn[] = [
|
||||
{ id: 1, name: "Newsfeed", content: "/newsfeed" },
|
||||
{ id: 10001, name: "Newsfeed", content: "/newsfeed" },
|
||||
{ id: 10002, name: "For You", content: "/foryou" },
|
||||
{ id: 10000, name: "Open Lume Store", content: "/open" },
|
||||
];
|
||||
|
||||
function Screen() {
|
||||
const search = Route.useSearch();
|
||||
const vlistRef = useRef<VListHandle>(null);
|
||||
const unlisten = useRef<UnlistenFn>(null);
|
||||
|
||||
const [columns, setColumns] = useState(DEFAULT_COLUMNS);
|
||||
const [selectedIndex, setSelectedIndex] = useState(-1);
|
||||
@@ -43,27 +44,51 @@ function Screen() {
|
||||
};
|
||||
|
||||
const add = (column: LumeColumn) => {
|
||||
const col = columns.find((item) => item.id === column.id);
|
||||
if (!col) {
|
||||
setColumns((prev) => [...prev, column]);
|
||||
const existed = columns.find((item) => item.id === column.id);
|
||||
if (!existed) {
|
||||
let lastColIndex: number;
|
||||
const openColIndex = columns.findIndex((item) => item.id === 10000);
|
||||
const storeColIndex = columns.findIndex((item) => item.id === 9999);
|
||||
|
||||
if (storeColIndex) {
|
||||
lastColIndex = storeColIndex;
|
||||
} else {
|
||||
lastColIndex = openColIndex;
|
||||
}
|
||||
|
||||
const newColumns = [
|
||||
...columns.slice(0, lastColIndex),
|
||||
column,
|
||||
...columns.slice(lastColIndex),
|
||||
];
|
||||
|
||||
// update state & scroll to new column
|
||||
setColumns(newColumns);
|
||||
setSelectedIndex(newColumns.length - 1);
|
||||
vlistRef.current.scrollToIndex(newColumns.length - 1, {
|
||||
align: "center",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const remove = (id: number) => {
|
||||
setColumns((prev) => prev.filter((t) => t.id !== id));
|
||||
setSelectedIndex(columns.length);
|
||||
vlistRef.current.scrollToIndex(columns.length, {
|
||||
align: "center",
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let unlisten: UnlistenFn = undefined;
|
||||
|
||||
const listenColumnEvent = async () => {
|
||||
const mainWindow = getCurrent();
|
||||
if (!unlisten) {
|
||||
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);
|
||||
},
|
||||
);
|
||||
unlisten = await mainWindow.listen<EventColumns>("columns", (data) => {
|
||||
if (data.payload.type === "add") add(data.payload.column);
|
||||
if (data.payload.type === "remove") remove(data.payload.id);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -71,7 +96,12 @@ function Screen() {
|
||||
listenColumnEvent();
|
||||
|
||||
// clean up
|
||||
return () => unlisten.current?.();
|
||||
return () => {
|
||||
if (unlisten) {
|
||||
unlisten();
|
||||
unlisten = null;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
@@ -106,9 +136,9 @@ function Screen() {
|
||||
}}
|
||||
className="scrollbar-none h-full w-full overflow-x-auto focus:outline-none"
|
||||
>
|
||||
{columns.map((column, index) => (
|
||||
{columns.map((column) => (
|
||||
<Col
|
||||
key={column.id + index}
|
||||
key={column.id}
|
||||
column={column}
|
||||
// @ts-ignore, yolo !!!
|
||||
account={search.acccount}
|
||||
|
||||
@@ -15,7 +15,7 @@ export const Route = createLazyFileRoute("/antenas")({
|
||||
|
||||
export function Screen() {
|
||||
// @ts-ignore, just work!!!
|
||||
const { name, account } = Route.useSearch();
|
||||
const { id, name, account } = Route.useSearch();
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
data,
|
||||
@@ -38,7 +38,7 @@ export function Screen() {
|
||||
|
||||
return (
|
||||
<Column.Root>
|
||||
<Column.Header name={name} />
|
||||
<Column.Header id={id} name={name} />
|
||||
<Column.Content>
|
||||
{isLoading || isRefetching ? (
|
||||
<div className="flex h-20 w-full flex-col items-center justify-center gap-1">
|
||||
|
||||
@@ -15,7 +15,7 @@ export const Route = createLazyFileRoute("/foryou")({
|
||||
|
||||
export function Screen() {
|
||||
// @ts-ignore, just work!!!
|
||||
const { name, account } = Route.useSearch();
|
||||
const { id, name, account } = Route.useSearch();
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
data,
|
||||
@@ -38,7 +38,7 @@ export function Screen() {
|
||||
|
||||
return (
|
||||
<Column.Root>
|
||||
<Column.Header name={name} />
|
||||
<Column.Header id={id} name={name} />
|
||||
<Column.Content>
|
||||
{isLoading || isRefetching ? (
|
||||
<div className="flex h-20 w-full flex-col items-center justify-center gap-1">
|
||||
|
||||
@@ -15,7 +15,7 @@ export const Route = createLazyFileRoute("/global")({
|
||||
|
||||
export function Screen() {
|
||||
// @ts-ignore, just work!!!
|
||||
const { name, account } = Route.useSearch();
|
||||
const { id, name, account } = Route.useSearch();
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
data,
|
||||
@@ -38,7 +38,7 @@ export function Screen() {
|
||||
|
||||
return (
|
||||
<Column.Root>
|
||||
<Column.Header name={name} />
|
||||
<Column.Header id={id} name={name} />
|
||||
<Column.Content>
|
||||
{isLoading || isRefetching ? (
|
||||
<div className="flex h-20 w-full flex-col items-center justify-center gap-1">
|
||||
|
||||
@@ -15,7 +15,7 @@ export const Route = createLazyFileRoute("/group")({
|
||||
|
||||
export function Screen() {
|
||||
// @ts-ignore, just work!!!
|
||||
const { name, account } = Route.useSearch();
|
||||
const { id, name, account } = Route.useSearch();
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
data,
|
||||
@@ -38,7 +38,7 @@ export function Screen() {
|
||||
|
||||
return (
|
||||
<Column.Root>
|
||||
<Column.Header name={name} />
|
||||
<Column.Header id={id} name={name} />
|
||||
<Column.Content>
|
||||
{isLoading || isRefetching ? (
|
||||
<div className="flex h-20 w-full flex-col items-center justify-center gap-1">
|
||||
|
||||
@@ -15,7 +15,7 @@ export const Route = createLazyFileRoute("/newsfeed")({
|
||||
|
||||
export function Screen() {
|
||||
// @ts-ignore, just work!!!
|
||||
const { name, account } = Route.useSearch();
|
||||
const { id, name, account } = Route.useSearch();
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
data,
|
||||
@@ -38,7 +38,7 @@ export function Screen() {
|
||||
|
||||
return (
|
||||
<Column.Root>
|
||||
<Column.Header name={name} />
|
||||
<Column.Header id={id} name={name} />
|
||||
<Column.Content>
|
||||
{isLoading || isRefetching ? (
|
||||
<div className="flex h-20 w-full flex-col items-center justify-center gap-1">
|
||||
@@ -61,11 +61,13 @@ export function Screen() {
|
||||
</Virtualizer>
|
||||
)}
|
||||
<div className="flex h-20 items-center justify-center">
|
||||
{hasNextPage ? (
|
||||
{!isLoading && hasNextPage ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => fetchNextPage()}
|
||||
disabled={!hasNextPage || isFetchingNextPage}
|
||||
disabled={
|
||||
!hasNextPage || isFetchingNextPage || isFetchingNextPage
|
||||
}
|
||||
className="inline-flex h-12 w-36 items-center justify-center gap-2 rounded-full bg-neutral-100 px-3 font-medium hover:bg-neutral-200 focus:outline-none dark:bg-neutral-900 dark:hover:bg-neutral-800"
|
||||
>
|
||||
{isFetchingNextPage ? (
|
||||
|
||||
36
apps/desktop2/src/routes/open.lazy.tsx
Normal file
36
apps/desktop2/src/routes/open.lazy.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { PlusIcon } from "@lume/icons";
|
||||
import { LumeColumn } from "@lume/types";
|
||||
import { Column } from "@lume/ui";
|
||||
import { createLazyRoute } from "@tanstack/react-router";
|
||||
import { getCurrent } from "@tauri-apps/api/window";
|
||||
|
||||
export const Route = createLazyRoute("/open")({
|
||||
component: Screen,
|
||||
});
|
||||
|
||||
function Screen() {
|
||||
const install = async (column: LumeColumn) => {
|
||||
const mainWindow = getCurrent();
|
||||
await mainWindow.emit("columns", { type: "add", column });
|
||||
};
|
||||
|
||||
return (
|
||||
<Column.Root shadow={false} background={false}>
|
||||
<Column.Content className="flex h-full w-full items-center justify-center">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
install({
|
||||
id: 9999,
|
||||
name: "Lume Store",
|
||||
content: "/store/official",
|
||||
})
|
||||
}
|
||||
className="inline-flex size-14 items-center justify-center rounded-full bg-black/10 backdrop-blur-lg hover:bg-black/20 dark:bg-white/10 dark:hover:bg-white/20"
|
||||
>
|
||||
<PlusIcon className="size-8" />
|
||||
</button>
|
||||
</Column.Content>
|
||||
</Column.Root>
|
||||
);
|
||||
}
|
||||
@@ -7,7 +7,7 @@ export const Route = createFileRoute("/store/official")({
|
||||
loader: () => {
|
||||
const columns: LumeColumn[] = [
|
||||
{
|
||||
id: 10000,
|
||||
id: 10002,
|
||||
name: "For you",
|
||||
content: "/foryou",
|
||||
logo: "",
|
||||
@@ -17,7 +17,7 @@ export const Route = createFileRoute("/store/official")({
|
||||
description: "Keep up to date with content based on your interests.",
|
||||
},
|
||||
{
|
||||
id: 10001,
|
||||
id: 10003,
|
||||
name: "Group Feeds",
|
||||
content: "/group",
|
||||
logo: "",
|
||||
@@ -27,7 +27,7 @@ export const Route = createFileRoute("/store/official")({
|
||||
description: "Collective of people you're interested in.",
|
||||
},
|
||||
{
|
||||
id: 10002,
|
||||
id: 10004,
|
||||
name: "Antenas",
|
||||
content: "/antenas",
|
||||
logo: "",
|
||||
@@ -37,7 +37,7 @@ export const Route = createFileRoute("/store/official")({
|
||||
description: "Keep track to specific content.",
|
||||
},
|
||||
{
|
||||
id: 10003,
|
||||
id: 10005,
|
||||
name: "Trending",
|
||||
content: "/trending",
|
||||
logo: "",
|
||||
@@ -47,7 +47,7 @@ export const Route = createFileRoute("/store/official")({
|
||||
description: "What is trending on Nostr?.",
|
||||
},
|
||||
{
|
||||
id: 10004,
|
||||
id: 10006,
|
||||
name: "Global",
|
||||
content: "/global",
|
||||
logo: "",
|
||||
@@ -56,16 +56,6 @@ export const Route = createFileRoute("/store/official")({
|
||||
author: "Lume",
|
||||
description: "All events from connected relays.",
|
||||
},
|
||||
{
|
||||
id: 10005,
|
||||
name: "Waifu",
|
||||
content: "/waifu",
|
||||
logo: "",
|
||||
cover: "/waifu.png",
|
||||
coverRetina: "/waifu@2x.png",
|
||||
author: "Lume",
|
||||
description: "Show a random waifu image to boost your morale.",
|
||||
},
|
||||
];
|
||||
return columns;
|
||||
},
|
||||
|
||||
@@ -1,48 +1,62 @@
|
||||
import { GlobalIcon, LaurelIcon } from "@lume/icons";
|
||||
import { CancelIcon, GlobalIcon, LaurelIcon } from "@lume/icons";
|
||||
import { Column } from "@lume/ui";
|
||||
import { cn } from "@lume/utils";
|
||||
import { Link } from "@tanstack/react-router";
|
||||
import { Outlet, createFileRoute } from "@tanstack/react-router";
|
||||
import { getCurrent } from "@tauri-apps/api/window";
|
||||
|
||||
export const Route = createFileRoute("/store")({
|
||||
component: Screen,
|
||||
});
|
||||
|
||||
function Screen() {
|
||||
// @ts-ignore, just work!!!
|
||||
const { id } = Route.useSearch();
|
||||
|
||||
const close = async () => {
|
||||
const mainWindow = getCurrent();
|
||||
await mainWindow.emit("columns", { type: "remove", id });
|
||||
};
|
||||
|
||||
return (
|
||||
<Column.Root>
|
||||
<Column.Content>
|
||||
<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">
|
||||
{({ isActive }) => (
|
||||
<div
|
||||
className={cn(
|
||||
"inline-flex h-8 w-max items-center justify-center gap-2 rounded-full px-6 text-sm font-medium",
|
||||
isActive
|
||||
? "bg-neutral-100 dark:bg-neutral-900"
|
||||
: "opacity-50",
|
||||
)}
|
||||
>
|
||||
<LaurelIcon className="size-5" />
|
||||
Official
|
||||
</div>
|
||||
)}
|
||||
</Link>
|
||||
<Link to="/store/community">
|
||||
{({ isActive }) => (
|
||||
<div
|
||||
className={cn(
|
||||
"inline-flex h-8 w-max items-center justify-center gap-2 rounded-full px-6 text-sm font-medium",
|
||||
isActive
|
||||
? "bg-neutral-100 dark:bg-neutral-900"
|
||||
: "opacity-50",
|
||||
)}
|
||||
>
|
||||
<GlobalIcon className="size-5" />
|
||||
Community
|
||||
</div>
|
||||
)}
|
||||
</Link>
|
||||
<div className="flex h-14 shrink-0 items-center justify-between border-b border-neutral-100 px-3 dark:border-neutral-900">
|
||||
<div className="inline-flex h-full w-full items-center gap-2">
|
||||
<Link to="/store/official">
|
||||
{({ isActive }) => (
|
||||
<div
|
||||
className={cn(
|
||||
"inline-flex h-8 w-max items-center justify-center gap-2 rounded-full px-6 text-sm font-medium",
|
||||
isActive
|
||||
? "bg-neutral-100 dark:bg-neutral-900"
|
||||
: "opacity-50",
|
||||
)}
|
||||
>
|
||||
<LaurelIcon className="size-5" />
|
||||
Official
|
||||
</div>
|
||||
)}
|
||||
</Link>
|
||||
<Link to="/store/community">
|
||||
{({ isActive }) => (
|
||||
<div
|
||||
className={cn(
|
||||
"inline-flex h-8 w-max items-center justify-center gap-2 rounded-full px-6 text-sm font-medium",
|
||||
isActive
|
||||
? "bg-neutral-100 dark:bg-neutral-900"
|
||||
: "opacity-50",
|
||||
)}
|
||||
>
|
||||
<GlobalIcon className="size-5" />
|
||||
Community
|
||||
</div>
|
||||
)}
|
||||
</Link>
|
||||
</div>
|
||||
<button type="button" onClick={close}>
|
||||
<CancelIcon className="size-4 text-neutral-700 dark:text-neutral-300" />
|
||||
</button>
|
||||
</div>
|
||||
<Outlet />
|
||||
</Column.Content>
|
||||
|
||||
@@ -15,7 +15,7 @@ export const Route = createLazyFileRoute("/trending")({
|
||||
|
||||
export function Screen() {
|
||||
// @ts-ignore, just work!!!
|
||||
const { name, account } = Route.useSearch();
|
||||
const { id, name, account } = Route.useSearch();
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
data,
|
||||
@@ -38,7 +38,7 @@ export function Screen() {
|
||||
|
||||
return (
|
||||
<Column.Root>
|
||||
<Column.Header name={name} />
|
||||
<Column.Header id={id} name={name} />
|
||||
<Column.Content>
|
||||
{isLoading || isRefetching ? (
|
||||
<div className="flex h-20 w-full flex-col items-center justify-center gap-1">
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import { Column } from "@lume/ui";
|
||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createLazyFileRoute("/waifu")({
|
||||
component: Screen,
|
||||
});
|
||||
|
||||
function Screen() {
|
||||
// @ts-ignore, just work!!!
|
||||
const { name } = Route.useSearch();
|
||||
|
||||
return (
|
||||
<Column.Root>
|
||||
<Column.Header name={name} />
|
||||
<Column.Content>waifu</Column.Content>
|
||||
</Column.Root>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user