feat: improve editor

This commit is contained in:
reya
2024-05-07 14:14:21 +07:00
parent afb7c87fa3
commit 437cd71f7e
14 changed files with 1608 additions and 1564 deletions

View File

@@ -13,179 +13,179 @@ import { useDebouncedCallback } from "use-debounce";
import { VList, type VListHandle } from "virtua"; import { VList, type VListHandle } from "virtua";
export const Route = createFileRoute("/$account/home")({ export const Route = createFileRoute("/$account/home")({
beforeLoad: async ({ context }) => { beforeLoad: async ({ context }) => {
try { try {
const ark = context.ark; const ark = context.ark;
const resourcePath = await resolveResource( const resourcePath = await resolveResource(
"resources/system_columns.json", "resources/system_columns.json",
); );
const systemColumns: LumeColumn[] = JSON.parse( const systemColumns: LumeColumn[] = JSON.parse(
await readTextFile(resourcePath), await readTextFile(resourcePath),
); );
const userColumns = await ark.get_columns(); const userColumns = await ark.get_columns();
return { return {
storedColumns: !userColumns.length ? systemColumns : userColumns, storedColumns: !userColumns.length ? systemColumns : userColumns,
}; };
} catch (e) { } catch (e) {
console.error(String(e)); console.error(String(e));
} }
}, },
component: Screen, component: Screen,
}); });
function Screen() { function Screen() {
const vlistRef = useRef<VListHandle>(null); const vlistRef = useRef<VListHandle>(null);
const { account } = Route.useParams(); const { account } = Route.useParams();
const { ark, storedColumns } = Route.useRouteContext(); const { ark, storedColumns } = Route.useRouteContext();
const [selectedIndex, setSelectedIndex] = useState(-1); const [selectedIndex, setSelectedIndex] = useState(-1);
const [columns, setColumns] = useState(storedColumns); const [columns, setColumns] = useState(storedColumns);
const [isScroll, setIsScroll] = useState(false); const [isScroll, setIsScroll] = useState(false);
const [isResize, setIsResize] = useState(false); const [isResize, setIsResize] = useState(false);
const goLeft = () => { 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, {
align: "center", align: "center",
}); });
}; };
const goRight = () => { const goRight = () => {
const nextIndex = Math.min(selectedIndex + 1, columns.length - 1); const nextIndex = Math.min(selectedIndex + 1, columns.length - 1);
setSelectedIndex(nextIndex); setSelectedIndex(nextIndex);
vlistRef.current.scrollToIndex(nextIndex, { vlistRef.current.scrollToIndex(nextIndex, {
align: "center", align: "center",
}); });
}; };
const add = useDebouncedCallback((column: LumeColumn) => { const add = useDebouncedCallback((column: LumeColumn) => {
// update col label // update col label
column.label = `${column.label}-${nanoid()}`; column.label = `${column.label}-${nanoid()}`;
// create new cols // create new cols
const cols = [...columns]; const cols = [...columns];
const openColIndex = cols.findIndex((col) => col.label === "open"); const openColIndex = cols.findIndex((col) => col.label === "open");
const newCols = [ const newCols = [
...cols.slice(0, openColIndex), ...cols.slice(0, openColIndex),
column, column,
...cols.slice(openColIndex), ...cols.slice(openColIndex),
]; ];
setColumns(newCols); setColumns(newCols);
setSelectedIndex(newCols.length); setSelectedIndex(newCols.length);
setIsScroll(true); setIsScroll(true);
// scroll to the newest column // scroll to the newest column
vlistRef.current.scrollToIndex(newCols.length - 1, { vlistRef.current.scrollToIndex(newCols.length - 1, {
align: "end", align: "end",
}); });
}, 150); }, 150);
const remove = useDebouncedCallback((label: string) => { const remove = useDebouncedCallback((label: string) => {
const newCols = columns.filter((t) => t.label !== label); const newCols = columns.filter((t) => t.label !== label);
setColumns(newCols); setColumns(newCols);
setSelectedIndex(newCols.length); setSelectedIndex(newCols.length);
setIsScroll(true); setIsScroll(true);
// scroll to the first column // scroll to the first column
vlistRef.current.scrollToIndex(newCols.length - 1, { vlistRef.current.scrollToIndex(newCols.length - 1, {
align: "start", align: "start",
}); });
}, 150); }, 150);
const updateName = useDebouncedCallback((label: string, title: string) => { const updateName = useDebouncedCallback((label: string, title: string) => {
const currentColIndex = columns.findIndex((col) => col.label === label); const currentColIndex = columns.findIndex((col) => col.label === label);
const updatedCol = Object.assign({}, columns[currentColIndex]); const updatedCol = Object.assign({}, columns[currentColIndex]);
updatedCol.name = title; updatedCol.name = title;
const newCols = columns.slice(); const newCols = columns.slice();
newCols[currentColIndex] = updatedCol; newCols[currentColIndex] = updatedCol;
setColumns(newCols); setColumns(newCols);
}, 150); }, 150);
const startResize = useDebouncedCallback( const startResize = useDebouncedCallback(
() => setIsResize((prev) => !prev), () => setIsResize((prev) => !prev),
150, 150,
); );
useEffect(() => { useEffect(() => {
// save state // save state
ark.set_columns(columns); ark.set_columns(columns);
}, [columns]); }, [columns]);
useEffect(() => { useEffect(() => {
let unlistenColEvent: Awaited<ReturnType<typeof listen>> | undefined = let unlistenColEvent: Awaited<ReturnType<typeof listen>> | undefined =
undefined; undefined;
let unlistenWindowResize: Awaited<ReturnType<typeof listen>> | undefined = let unlistenWindowResize: Awaited<ReturnType<typeof listen>> | undefined =
undefined; undefined;
(async () => { (async () => {
if (unlistenColEvent && unlistenWindowResize) return; if (unlistenColEvent && unlistenWindowResize) return;
unlistenColEvent = await listen<EventColumns>("columns", (data) => { unlistenColEvent = await listen<EventColumns>("columns", (data) => {
if (data.payload.type === "add") add(data.payload.column); if (data.payload.type === "add") add(data.payload.column);
if (data.payload.type === "remove") remove(data.payload.label); if (data.payload.type === "remove") remove(data.payload.label);
if (data.payload.type === "set_title") if (data.payload.type === "set_title")
updateName(data.payload.label, data.payload.title); updateName(data.payload.label, data.payload.title);
}); });
unlistenWindowResize = await getCurrent().listen("tauri://resize", () => { unlistenWindowResize = await getCurrent().listen("tauri://resize", () => {
startResize(); startResize();
}); });
})(); })();
return () => { return () => {
if (unlistenColEvent) unlistenColEvent(); if (unlistenColEvent) unlistenColEvent();
if (unlistenWindowResize) unlistenWindowResize(); if (unlistenWindowResize) unlistenWindowResize();
}; };
}, []); }, []);
return ( return (
<div className="h-full w-full"> <div className="h-full w-full">
<VList <VList
ref={vlistRef} ref={vlistRef}
horizontal horizontal
tabIndex={-1} tabIndex={-1}
itemSize={440} itemSize={440}
overscan={3} overscan={3}
onScroll={() => setIsScroll(true)} onScroll={() => setIsScroll(true)}
onScrollEnd={() => setIsScroll(false)} onScrollEnd={() => setIsScroll(false)}
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"
> >
{columns.map((column) => ( {columns.map((column) => (
<Col <Col
key={column.label} key={column.label}
column={column} column={column}
account={account} account={account}
isScroll={isScroll} isScroll={isScroll}
isResize={isResize} isResize={isResize}
/> />
))} ))}
</VList> </VList>
<Toolbar> <Toolbar>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<button <button
type="button" type="button"
onClick={() => goLeft()} onClick={() => goLeft()}
className="inline-flex size-8 items-center justify-center rounded-full text-neutral-800 hover:bg-black/10 dark:text-neutral-200 dark:hover:bg-white/10" className="inline-flex size-8 items-center justify-center rounded-full text-neutral-800 hover:bg-black/10 dark:text-neutral-200 dark:hover:bg-white/10"
> >
<ArrowLeftIcon className="size-5" /> <ArrowLeftIcon className="size-5" />
</button> </button>
<button <button
type="button" type="button"
onClick={() => goRight()} onClick={() => goRight()}
className="inline-flex size-8 items-center justify-center rounded-full text-neutral-800 hover:bg-black/10 dark:text-neutral-200 dark:hover:bg-white/10" className="inline-flex size-8 items-center justify-center rounded-full text-neutral-800 hover:bg-black/10 dark:text-neutral-200 dark:hover:bg-white/10"
> >
<ArrowRightIcon className="size-5" /> <ArrowRightIcon className="size-5" />
</button> </button>
</div> </div>
</Toolbar> </Toolbar>
</div> </div>
); );
} }

View File

@@ -2,10 +2,10 @@ import { BellIcon, ComposeFilledIcon, PlusIcon, SearchIcon } from "@lume/icons";
import { Event, Kind } from "@lume/types"; import { Event, Kind } from "@lume/types";
import { User } from "@lume/ui"; import { User } from "@lume/ui";
import { import {
cn, cn,
decodeZapInvoice, decodeZapInvoice,
displayNpub, displayNpub,
sendNativeNotification, sendNativeNotification,
} from "@lume/utils"; } from "@lume/utils";
import { Outlet, createFileRoute } from "@tanstack/react-router"; import { Outlet, createFileRoute } from "@tanstack/react-router";
import { UnlistenFn } from "@tauri-apps/api/event"; import { UnlistenFn } from "@tauri-apps/api/event";
@@ -13,171 +13,171 @@ import { getCurrent } from "@tauri-apps/api/window";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
export const Route = createFileRoute("/$account")({ export const Route = createFileRoute("/$account")({
beforeLoad: async ({ context }) => { beforeLoad: async ({ context }) => {
const ark = context.ark; const ark = context.ark;
const accounts = await ark.get_all_accounts(); const accounts = await ark.get_all_accounts();
return { accounts }; return { accounts };
}, },
component: Screen, component: Screen,
}); });
function Screen() { function Screen() {
const { ark, platform } = Route.useRouteContext(); const { ark, platform } = Route.useRouteContext();
const navigate = Route.useNavigate(); const navigate = Route.useNavigate();
return ( return (
<div className="flex h-screen w-screen flex-col"> <div className="flex h-screen w-screen flex-col">
<div <div
data-tauri-drag-region data-tauri-drag-region
className={cn( className={cn(
"flex h-11 shrink-0 items-center justify-between pr-2", "flex h-11 shrink-0 items-center justify-between pr-2",
platform === "macos" ? "ml-2 pl-20" : "pl-4", platform === "macos" ? "ml-2 pl-20" : "pl-4",
)} )}
> >
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Accounts /> <Accounts />
<button <button
type="button" type="button"
onClick={() => navigate({ to: "/landing" })} onClick={() => navigate({ to: "/landing" })}
className="inline-flex size-8 items-center justify-center rounded-full bg-black/10 text-neutral-800 hover:bg-black/20 dark:bg-white/10 dark:text-neutral-200 dark:hover:bg-white/20" className="inline-flex size-8 items-center justify-center rounded-full bg-black/10 text-neutral-800 hover:bg-black/20 dark:bg-white/10 dark:text-neutral-200 dark:hover:bg-white/20"
> >
<PlusIcon className="size-5" /> <PlusIcon className="size-5" />
</button> </button>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button <button
type="button" type="button"
onClick={() => ark.open_editor()} onClick={() => ark.open_editor()}
className="inline-flex h-8 w-max items-center justify-center gap-1 rounded-full bg-blue-500 px-3 text-sm font-medium text-white hover:bg-blue-600" className="inline-flex h-8 w-max items-center justify-center gap-1 rounded-full bg-blue-500 px-3 text-sm font-medium text-white hover:bg-blue-600"
> >
<ComposeFilledIcon className="size-4" /> <ComposeFilledIcon className="size-4" />
New Post New Post
</button> </button>
<Bell /> <Bell />
<button <button
type="button" type="button"
onClick={() => ark.open_search()} onClick={() => ark.open_search()}
className="inline-flex size-8 items-center justify-center rounded-full text-neutral-800 hover:bg-black/10 dark:text-neutral-200 dark:hover:bg-white/10" className="inline-flex size-8 items-center justify-center rounded-full text-neutral-800 hover:bg-black/10 dark:text-neutral-200 dark:hover:bg-white/10"
> >
<SearchIcon className="size-5" /> <SearchIcon className="size-5" />
</button> </button>
<div id="toolbar" /> <div id="toolbar" />
</div> </div>
</div> </div>
<div className="flex-1"> <div className="flex-1">
<Outlet /> <Outlet />
</div> </div>
</div> </div>
); );
} }
function Accounts() { function Accounts() {
const navigate = Route.useNavigate(); const navigate = Route.useNavigate();
const { ark, accounts } = Route.useRouteContext(); const { ark, accounts } = Route.useRouteContext();
const { account } = Route.useParams(); const { account } = Route.useParams();
const changeAccount = async (npub: string) => { const changeAccount = async (npub: string) => {
if (npub === account) return; if (npub === account) return;
const select = await ark.load_selected_account(npub); const select = await ark.load_selected_account(npub);
if (select) { if (select) {
return navigate({ to: "/$account/home", params: { account: npub } }); return navigate({ to: "/$account/home", params: { account: npub } });
} }
}; };
return ( return (
<div data-tauri-drag-region className="flex items-center gap-3"> <div data-tauri-drag-region className="flex items-center gap-3">
{accounts.map((user) => ( {accounts.map((user) => (
<button key={user} type="button" onClick={() => changeAccount(user)}> <button key={user} type="button" onClick={() => changeAccount(user)}>
<User.Provider pubkey={user}> <User.Provider pubkey={user}>
<User.Root <User.Root
className={cn( className={cn(
"rounded-full", "rounded-full",
user === account user === account
? "ring-1 ring-teal-500 ring-offset-2 ring-offset-neutral-200 dark:ring-offset-neutral-950" ? "ring-1 ring-teal-500 ring-offset-2 ring-offset-neutral-200 dark:ring-offset-neutral-950"
: "", : "",
)} )}
> >
<User.Avatar <User.Avatar
className={cn( className={cn(
"aspect-square h-auto rounded-full object-cover", "aspect-square h-auto rounded-full object-cover",
user === account ? "w-7" : "w-8", user === account ? "w-7" : "w-8",
)} )}
/> />
</User.Root> </User.Root>
</User.Provider> </User.Provider>
</button> </button>
))} ))}
</div> </div>
); );
} }
function Bell() { function Bell() {
const { ark } = Route.useRouteContext(); const { ark } = Route.useRouteContext();
const { account } = Route.useParams(); const { account } = Route.useParams();
const [isRing, setIsRing] = useState(false); const [isRing, setIsRing] = useState(false);
useEffect(() => { useEffect(() => {
let unlisten: UnlistenFn = undefined; let unlisten: UnlistenFn = undefined;
async function listenNotify() { async function listenNotify() {
unlisten = await getCurrent().listen<string>( unlisten = await getCurrent().listen<string>(
"activity", "activity",
async (payload) => { async (payload) => {
setIsRing(true); setIsRing(true);
const event: Event = JSON.parse(payload.payload); const event: Event = JSON.parse(payload.payload);
const user = await ark.get_profile(event.pubkey); const user = await ark.get_profile(event.pubkey);
const userName = const userName =
user.display_name || user.name || displayNpub(event.pubkey, 16); user.display_name || user.name || displayNpub(event.pubkey, 16);
switch (event.kind) { switch (event.kind) {
case Kind.Text: { case Kind.Text: {
sendNativeNotification("Mentioned you in a note", userName); sendNativeNotification("Mentioned you in a note", userName);
break; break;
} }
case Kind.Repost: { case Kind.Repost: {
sendNativeNotification("Reposted your note", userName); sendNativeNotification("Reposted your note", userName);
break; break;
} }
case Kind.ZapReceipt: { case Kind.ZapReceipt: {
const amount = decodeZapInvoice(event.tags); const amount = decodeZapInvoice(event.tags);
sendNativeNotification( sendNativeNotification(
`Zapped ₿ ${amount.bitcoinFormatted}`, `Zapped ₿ ${amount.bitcoinFormatted}`,
userName, userName,
); );
break; break;
} }
default: default:
break; break;
} }
}, },
); );
} }
if (!unlisten) listenNotify(); if (!unlisten) listenNotify();
return () => { return () => {
if (unlisten) unlisten(); if (unlisten) unlisten();
}; };
}, []); }, []);
return ( return (
<button <button
type="button" type="button"
onClick={() => { onClick={() => {
setIsRing(false); setIsRing(false);
ark.open_activity(account); ark.open_activity(account);
}} }}
className="relative inline-flex size-8 items-center justify-center rounded-full text-neutral-800 hover:bg-black/10 dark:text-neutral-200 dark:hover:bg-white/10" className="relative inline-flex size-8 items-center justify-center rounded-full text-neutral-800 hover:bg-black/10 dark:text-neutral-200 dark:hover:bg-white/10"
> >
<BellIcon className="size-5" /> <BellIcon className="size-5" />
{isRing ? ( {isRing ? (
<span className="absolute right-0 top-0 block size-2 rounded-full bg-teal-500 ring-1 ring-black/5" /> <span className="absolute right-0 top-0 block size-2 rounded-full bg-teal-500 ring-1 ring-black/5" />
) : null} ) : null}
</button> </button>
); );
} }

View File

@@ -7,38 +7,38 @@ import type { Platform } from "@tauri-apps/plugin-os";
import type { Descendant } from "slate"; import type { Descendant } from "slate";
type EditorElement = { type EditorElement = {
type: string; type: string;
children: Descendant[]; children: Descendant[];
eventId?: string; eventId?: string;
}; };
interface RouterContext { interface RouterContext {
// System // System
ark: Ark; ark: Ark;
queryClient: QueryClient; queryClient: QueryClient;
// App info // App info
platform?: Platform; platform?: Platform;
locale?: string; locale?: string;
// Settings // Settings
settings?: Settings; settings?: Settings;
interests?: Interests; interests?: Interests;
// Profile // Profile
accounts?: string[]; accounts?: string[];
profile?: Metadata; profile?: Metadata;
// Editor // Editor
initialValue?: EditorElement[]; initialValue?: EditorElement[];
} }
export const Route = createRootRouteWithContext<RouterContext>()({ export const Route = createRootRouteWithContext<RouterContext>()({
component: () => <Outlet />, component: () => <Outlet />,
pendingComponent: Pending, pendingComponent: Pending,
wrapInSuspense: true, wrapInSuspense: true,
}); });
function Pending() { function Pending() {
return ( return (
<div className="flex h-screen w-screen flex-col items-center justify-center"> <div className="flex h-screen w-screen flex-col items-center justify-center">
<Spinner className="size-5" /> <Spinner className="size-5" />
</div> </div>
); );
} }

View File

@@ -10,9 +10,8 @@ import { useSlateStatic } from "slate-react";
import { toast } from "sonner"; import { toast } from "sonner";
export function MediaButton({ className }: { className?: string }) { export function MediaButton({ className }: { className?: string }) {
const { ark } = useRouteContext({ strict: false });
const editor = useSlateStatic(); const editor = useSlateStatic();
const { ark } = useRouteContext({ strict: false });
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const uploadToNostrBuild = async () => { const uploadToNostrBuild = async () => {

View File

@@ -0,0 +1,83 @@
import { MentionIcon } from "@lume/icons";
import { cn, insertMention } from "@lume/utils";
import * as Tooltip from "@radix-ui/react-tooltip";
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
import { useEffect, useState } from "react";
import { useRouteContext } from "@tanstack/react-router";
import { User } from "@lume/ui";
import { useSlateStatic } from "slate-react";
import type { Contact } from "@lume/types";
import { toast } from "sonner";
export function MentionButton({ className }: { className?: string }) {
const editor = useSlateStatic();
const { ark } = useRouteContext({ strict: false });
const [contacts, setContacts] = useState<string[]>([]);
const select = async (user: string) => {
try {
const metadata = await ark.get_profile(user);
const contact: Contact = { pubkey: user, profile: metadata };
insertMention(editor, contact);
} catch (e) {
toast.error(String(e));
}
};
useEffect(() => {
async function getContacts() {
const data = await ark.get_contact_list();
setContacts(data);
}
getContacts();
}, []);
return (
<DropdownMenu.Root>
<Tooltip.Provider>
<Tooltip.Root delayDuration={150}>
<DropdownMenu.Trigger asChild>
<Tooltip.Trigger asChild>
<button
type="button"
className={cn(
"inline-flex items-center justify-center",
className,
)}
>
<MentionIcon className="size-4" />
</button>
</Tooltip.Trigger>
</DropdownMenu.Trigger>
<Tooltip.Portal>
<Tooltip.Content className="inline-flex h-7 select-none items-center justify-center rounded-md bg-neutral-950 px-3.5 text-sm text-neutral-50 will-change-[transform,opacity] data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade dark:bg-neutral-50 dark:text-neutral-950">
Mention
<Tooltip.Arrow className="fill-neutral-950 dark:fill-neutral-50" />
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
</Tooltip.Provider>
<DropdownMenu.Portal>
<DropdownMenu.Content className="flex w-[220px] h-[220px] scrollbar-none flex-col overflow-y-auto rounded-xl bg-black py-1 shadow-md shadow-neutral-500/20 focus:outline-none dark:bg-white">
{contacts.map((contact) => (
<DropdownMenu.Item
key={contact}
onClick={() => select(contact)}
className="shrink-0 h-11 flex items-center hover:bg-white/10 px-2"
>
<User.Provider pubkey={contact}>
<User.Root className="flex items-center gap-2">
<User.Avatar className="shrink-0 size-8 rounded-full" />
<User.Name className="text-sm font-medium text-white dark:text-black" />
</User.Root>
</User.Provider>
</DropdownMenu.Item>
))}
<DropdownMenu.Arrow className="fill-neutral-950 dark:fill-neutral-50" />
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
);
}

View File

@@ -0,0 +1,40 @@
import { NsfwIcon } from "@lume/icons";
import { cn } from "@lume/utils";
import * as Tooltip from "@radix-ui/react-tooltip";
import type { Dispatch, SetStateAction } from "react";
export function PowToggle({
pow,
setPow,
className,
}: {
pow: boolean;
setPow: Dispatch<SetStateAction<boolean>>;
className?: string;
}) {
return (
<Tooltip.Provider>
<Tooltip.Root delayDuration={150}>
<Tooltip.Trigger asChild>
<button
type="button"
onClick={() => setPow((prev) => !prev)}
className={cn(
"inline-flex items-center justify-center",
className,
pow ? "bg-blue-500 text-white" : "",
)}
>
<NsfwIcon className="size-4" />
</button>
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content className="inline-flex h-7 select-none items-center justify-center rounded-md bg-neutral-950 px-3.5 text-sm text-neutral-50 will-change-[transform,opacity] data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade dark:bg-neutral-50 dark:text-neutral-950">
Proof of Work
<Tooltip.Arrow className="fill-neutral-950 dark:fill-neutral-50" />
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
</Tooltip.Provider>
);
}

View File

@@ -1,27 +1,18 @@
import { ComposeFilledIcon, TrashIcon } from "@lume/icons"; import { ComposeFilledIcon, TrashIcon } from "@lume/icons";
import { Spinner, User } from "@lume/ui"; import { Spinner } from "@lume/ui";
import { MentionNote } from "@lume/ui/src/note/mentions/note"; import { MentionNote } from "@lume/ui/src/note/mentions/note";
import { import {
Portal,
cn, cn,
insertImage, insertImage,
insertMention,
insertNostrEvent, insertNostrEvent,
isImageUrl, isImageUrl,
sendNativeNotification, sendNativeNotification,
} from "@lume/utils"; } from "@lume/utils";
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute } from "@tanstack/react-router";
import { nip19 } from "nostr-tools"; import { nip19 } from "nostr-tools";
import { useEffect, useRef, useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { import { type Descendant, Node, Transforms, createEditor } from "slate";
type Descendant,
Editor,
Node,
Range,
Transforms,
createEditor,
} from "slate";
import { import {
Editable, Editable,
ReactEditor, ReactEditor,
@@ -33,6 +24,7 @@ import {
} from "slate-react"; } from "slate-react";
import { MediaButton } from "./-components/media"; import { MediaButton } from "./-components/media";
import { NsfwToggle } from "./-components/nsfw"; import { NsfwToggle } from "./-components/nsfw";
import { MentionButton } from "./-components/mention";
type EditorSearch = { type EditorSearch = {
reply_to: string; reply_to: string;
@@ -73,32 +65,20 @@ export const Route = createFileRoute("/editor/")({
}; };
}, },
component: Screen, component: Screen,
pendingComponent: Pending,
}); });
function Screen() { function Screen() {
const ref = useRef<HTMLDivElement | null>();
const { reply_to, quote } = Route.useSearch(); const { reply_to, quote } = Route.useSearch();
const { ark, initialValue, contacts } = Route.useRouteContext(); const { ark, initialValue } = Route.useRouteContext();
const [t] = useTranslation(); const [t] = useTranslation();
const [editorValue, setEditorValue] = useState(initialValue); const [editorValue, setEditorValue] = useState(initialValue);
const [target, setTarget] = useState<Range | undefined>();
const [index, setIndex] = useState(0);
const [search, setSearch] = useState("");
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [nsfw, setNsfw] = useState(false); const [nsfw, setNsfw] = useState(false);
const [editor] = useState(() => const [editor] = useState(() =>
withMentions(withNostrEvent(withImages(withReact(createEditor())))), withMentions(withNostrEvent(withImages(withReact(createEditor())))),
); );
const filters =
contacts
?.filter((c) =>
c?.profile.name?.toLowerCase().startsWith(search.toLowerCase()),
)
?.slice(0, 5) ?? [];
const reset = () => { const reset = () => {
// @ts-expect-error, backlog // @ts-expect-error, backlog
editor.children = [{ type: "paragraph", children: [{ text: "" }] }]; editor.children = [{ type: "paragraph", children: [{ text: "" }] }];
@@ -138,11 +118,15 @@ function Screen() {
const eventId = await ark.publish(content, reply_to, quote); const eventId = await ark.publish(content, reply_to, quote);
if (eventId) { if (eventId) {
await sendNativeNotification("You've publish new post successfully."); await sendNativeNotification(
"Your note has been published successfully.",
"Lume",
);
} }
// stop loading // stop loading
setLoading(false); setLoading(false);
// reset form // reset form
reset(); reset();
} catch (e) { } catch (e) {
@@ -151,58 +135,20 @@ function Screen() {
} }
}; };
useEffect(() => {
if (target && filters.length > 0) {
const el = ref.current;
const domRange = ReactEditor.toDOMRange(editor, target);
const rect = domRange.getBoundingClientRect();
el.style.top = `${rect.top + window.scrollY + 24}px`;
el.style.left = `${rect.left + window.scrollX}px`;
}
}, [filters.length, editor, index, search, target]);
return ( return (
<div className="flex h-screen w-screen flex-col bg-gradient-to-tr from-neutral-200 to-neutral-100 dark:from-neutral-950 dark:to-neutral-900"> <div className="w-full h-full">
<Slate <Slate editor={editor} initialValue={editorValue}>
editor={editor}
initialValue={editorValue}
onChange={() => {
const { selection } = editor;
if (selection && Range.isCollapsed(selection)) {
const [start] = Range.edges(selection);
const wordBefore = Editor.before(editor, start, { unit: "word" });
const before = wordBefore && Editor.before(editor, wordBefore);
const beforeRange = before && Editor.range(editor, before, start);
const beforeText =
beforeRange && Editor.string(editor, beforeRange);
const beforeMatch = beforeText?.match(/^@(\w+)$/);
const after = Editor.after(editor, start);
const afterRange = Editor.range(editor, start, after);
const afterText = Editor.string(editor, afterRange);
const afterMatch = afterText.match(/^(\s|$)/);
if (beforeMatch && afterMatch) {
setTarget(beforeRange);
setSearch(beforeMatch[1]);
setIndex(0);
return;
}
}
setTarget(null);
}}
>
<div <div
data-tauri-drag-region data-tauri-drag-region
className="flex h-14 w-full shrink-0 items-center justify-end gap-2 px-2" className="flex h-14 w-full shrink-0 items-center justify-end gap-2 px-2 border-b border-black/10 dark:border-white/10"
> >
<NsfwToggle <NsfwToggle
nsfw={nsfw} nsfw={nsfw}
setNsfw={setNsfw} setNsfw={setNsfw}
className="size-8 rounded-full bg-neutral-200 hover:bg-neutral-300 dark:bg-neutral-800 dark:hover:bg-neutral-700" className="size-8 rounded-full bg-black/10 hover:bg-black/20 dark:bg-white/10 dark:hover:bg-white/20"
/> />
<MediaButton className="size-8 rounded-full bg-neutral-200 hover:bg-neutral-300 dark:bg-neutral-800 dark:hover:bg-neutral-700" /> <MentionButton className="size-8 rounded-full bg-black/10 hover:bg-black/20 dark:bg-white/10 dark:hover:bg-white/20" />
<MediaButton className="size-8 rounded-full bg-black/10 hover:bg-black/20 dark:bg-white/10 dark:hover:bg-white/20" />
<button <button
type="button" type="button"
onClick={() => publish()} onClick={() => publish()}
@@ -216,53 +162,25 @@ function Screen() {
{t("global.post")} {t("global.post")}
</button> </button>
</div> </div>
<div className="flex h-full min-h-0 w-full"> <div className="flex h-full w-full flex-1 flex-col">
<div className="flex h-full w-full flex-1 flex-col gap-2 px-2 pb-2"> {reply_to && !quote ? (
{reply_to && !quote ? <MentionNote eventId={reply_to} /> : null} <div className="px-4 py-2">
<div className="h-full w-full flex-1 overflow-hidden overflow-y-auto rounded-xl bg-white p-5 shadow-[rgba(50,_50,_105,_0.15)_0px_2px_5px_0px,_rgba(0,_0,_0,_0.05)_0px_1px_1px_0px] dark:bg-black dark:shadow-none dark:ring-1 dark:ring-white/5"> <MentionNote eventId={reply_to} />
<Editable
key={JSON.stringify(editorValue)}
autoFocus={true}
autoCapitalize="none"
autoCorrect="none"
spellCheck={false}
renderElement={(props) => <Element {...props} />}
placeholder={
reply_to ? "Type your reply..." : t("editor.placeholder")
}
className="focus:outline-none"
/>
{target && filters.length > 0 && (
<Portal>
<div
ref={ref}
className="absolute left-[-9999px] top-[-9999px] z-10 w-[250px] rounded-xl border border-neutral-50 bg-white p-2 shadow-lg dark:border-neutral-900 dark:bg-neutral-950"
>
{filters.map((contact) => (
<button
key={contact.pubkey}
type="button"
onClick={() => {
Transforms.select(editor, target);
insertMention(editor, contact);
setTarget(null);
}}
className="flex w-full flex-col rounded-lg p-2 hover:bg-neutral-100 dark:hover:bg-neutral-900"
>
<User.Provider pubkey={contact.pubkey}>
<User.Root className="flex w-full items-center gap-2">
<User.Avatar className="size-7 shrink-0 rounded-full object-cover" />
<div className="flex w-full flex-col items-start">
<User.Name className="max-w-[8rem] truncate text-sm font-medium" />
</div>
</User.Root>
</User.Provider>
</button>
))}
</div>
</Portal>
)}
</div> </div>
) : null}
<div className="overflow-y-auto p-4">
<Editable
key={JSON.stringify(editorValue)}
autoFocus={true}
autoCapitalize="none"
autoCorrect="none"
spellCheck={false}
renderElement={(props) => <Element {...props} />}
placeholder={
reply_to ? "Type your reply..." : t("editor.placeholder")
}
className="focus:outline-none"
/>
</div> </div>
</div> </div>
</Slate> </Slate>
@@ -270,20 +188,6 @@ function Screen() {
); );
} }
function Pending() {
return (
<div
data-tauri-drag-region
className="flex h-full w-full items-center justify-center gap-2.5"
>
<button type="button" disabled>
<Spinner className="size-5" />
</button>
<p>Loading cache...</p>
</div>
);
}
const withNostrEvent = (editor: ReactEditor) => { const withNostrEvent = (editor: ReactEditor) => {
const { insertData, isVoid } = editor; const { insertData, isVoid } = editor;
@@ -429,7 +333,7 @@ const Element = (props) => {
return <Event {...props} />; return <Event {...props} />;
default: default:
return ( return (
<p {...attributes} className="text-lg"> <p {...attributes} className="text-[15px]">
{children} {children}
</p> </p>
); );

View File

@@ -7,112 +7,112 @@ import { useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
export const Route = createFileRoute("/")({ export const Route = createFileRoute("/")({
beforeLoad: async ({ context }) => { beforeLoad: async ({ context }) => {
const ark = context.ark; const ark = context.ark;
const accounts = await ark.get_all_accounts(); const accounts = await ark.get_all_accounts();
if (!accounts.length) { if (!accounts.length) {
throw redirect({ throw redirect({
to: "/landing", to: "/landing",
replace: true, replace: true,
}); });
} }
// Run notification service // Run notification service
await invoke("run_notification", { accounts }); await invoke("run_notification", { accounts });
return { accounts }; return { accounts };
}, },
component: Screen, component: Screen,
}); });
function Screen() { function Screen() {
const navigate = Route.useNavigate(); const navigate = Route.useNavigate();
const { ark, accounts } = Route.useRouteContext(); const { ark, accounts } = Route.useRouteContext();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const select = async (npub: string) => { const select = async (npub: string) => {
try { try {
setLoading(true); setLoading(true);
const loadAccount = await ark.load_selected_account(npub); const loadAccount = await ark.load_selected_account(npub);
if (loadAccount) { if (loadAccount) {
return navigate({ return navigate({
to: "/$account/home", to: "/$account/home",
params: { account: npub }, params: { account: npub },
replace: true, replace: true,
}); });
} }
} catch (e) { } catch (e) {
setLoading(false); setLoading(false);
toast.error(String(e)); toast.error(String(e));
} }
}; };
const currentDate = new Date().toLocaleString("default", { const currentDate = new Date().toLocaleString("default", {
weekday: "long", weekday: "long",
month: "long", month: "long",
day: "numeric", day: "numeric",
}); });
return ( return (
<div className="relative flex h-full w-full items-center justify-center"> <div className="relative flex h-full w-full items-center justify-center">
<div className="relative z-20 flex flex-col items-center gap-16"> <div className="relative z-20 flex flex-col items-center gap-16">
<div className="text-center text-white"> <div className="text-center text-white">
<h2 className="mb-1 text-2xl">{currentDate}</h2> <h2 className="mb-1 text-2xl">{currentDate}</h2>
<h2 className="text-2xl font-semibold">Welcome back!</h2> <h2 className="text-2xl font-semibold">Welcome back!</h2>
</div> </div>
<div className="flex items-center justify-center gap-6"> <div className="flex items-center justify-center gap-6">
{loading ? ( {loading ? (
<div className="inline-flex size-6 items-center justify-center"> <div className="inline-flex size-6 items-center justify-center">
<Spinner className="size-6" /> <Spinner className="size-6 text-white" />
</div> </div>
) : ( ) : (
<> <>
{accounts.map((account) => ( {accounts.map((account) => (
<button <button
type="button" type="button"
key={account} key={account}
onClick={() => select(account)} onClick={() => select(account)}
> >
<User.Provider pubkey={account}> <User.Provider pubkey={account}>
<User.Root className="flex h-36 w-32 flex-col items-center justify-center gap-4 rounded-2xl p-2 hover:bg-white/10 dark:hover:bg-black/10"> <User.Root className="flex h-36 w-32 flex-col items-center justify-center gap-4 rounded-2xl p-2 hover:bg-white/10 dark:hover:bg-black/10">
<User.Avatar className="size-20 rounded-full object-cover" /> <User.Avatar className="size-20 rounded-full object-cover" />
<User.Name className="max-w-[5rem] truncate text-lg font-medium leading-tight text-white" /> <User.Name className="max-w-[5rem] truncate text-lg font-medium leading-tight text-white" />
</User.Root> </User.Root>
</User.Provider> </User.Provider>
</button> </button>
))} ))}
<Link to="/landing"> <Link to="/landing">
<div className="flex h-36 w-32 flex-col items-center justify-center gap-4 rounded-2xl p-2 text-white hover:bg-white/10 dark:hover:bg-black/10"> <div className="flex h-36 w-32 flex-col items-center justify-center gap-4 rounded-2xl p-2 text-white hover:bg-white/10 dark:hover:bg-black/10">
<div className="flex size-20 items-center justify-center rounded-full bg-white/20 dark:bg-black/20"> <div className="flex size-20 items-center justify-center rounded-full bg-white/20 dark:bg-black/20">
<PlusIcon className="size-5" /> <PlusIcon className="size-5" />
</div> </div>
<p className="text-lg font-medium leading-tight">Add</p> <p className="text-lg font-medium leading-tight">Add</p>
</div> </div>
</Link> </Link>
</> </>
)} )}
</div> </div>
</div> </div>
<div className="absolute z-10 h-full w-full bg-white/10 backdrop-blur-lg dark:bg-black/10" /> <div className="absolute z-10 h-full w-full bg-white/10 backdrop-blur-lg dark:bg-black/10" />
<div className="absolute inset-0 h-full w-full"> <div className="absolute inset-0 h-full w-full">
<img <img
src="/lock-screen.jpg" src="/lock-screen.jpg"
srcSet="/lock-screen@2x.jpg 2x" srcSet="/lock-screen@2x.jpg 2x"
alt="Lock Screen Background" alt="Lock Screen Background"
className="h-full w-full object-cover" className="h-full w-full object-cover"
/> />
<a <a
href="https://njump.me/nprofile1qqs9tuz9jpn57djg7nxunhyvuvk69g5zqaxdpvpqt9hwqv7395u9rpg6zq5uw" href="https://njump.me/nprofile1qqs9tuz9jpn57djg7nxunhyvuvk69g5zqaxdpvpqt9hwqv7395u9rpg6zq5uw"
target="_blank" target="_blank"
className="absolute bottom-3 right-3 z-50 rounded-md bg-white/20 px-2 py-1 text-xs font-medium text-white dark:bg-black/20" className="absolute bottom-3 right-3 z-50 rounded-md bg-white/20 px-2 py-1 text-xs font-medium text-white dark:bg-black/20"
rel="noreferrer" rel="noreferrer"
> >
Design by NoGood Design by NoGood
</a> </a>
</div> </div>
</div> </div>
); );
} }

File diff suppressed because it is too large Load Diff

View File

@@ -4,21 +4,13 @@ export function MentionIcon(
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>, props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg width="24" height="24" fill="none" viewBox="0 0 24 24" {...props}>
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="none"
viewBox="0 0 24 24"
{...props}
>
<path <path
stroke="currentColor" stroke="currentColor"
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5" strokeWidth="1.5"
d="M11.85 13.251c-3.719.065-6.427 2.567-7.18 5.915-.13.575.338 1.084.927 1.084h6.901m-.647-6.999l.147-.001c.353 0 .696.022 1.03.064m-1.177-.063a7.889 7.889 0 00-1.852.249m3.028-.186c.334.042.658.104.972.186m-.972-.186a7.475 7.475 0 011.972.524m3.25 1.412v3m0 0v3m0-3h-3m3 0h3m-5.5-11.75a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z" d="M16.868 19.867A9.25 9.25 0 1 1 21.25 12c0 1.98-.984 4.024-3.279 3.816a3.312 3.312 0 0 1-2.978-3.767l.53-3.646m-.585 4.077c-.308 2.188-2.109 3.744-4.023 3.474-1.914-.269-3.217-2.26-2.91-4.448.308-2.187 2.11-3.743 4.023-3.474 1.914.27 3.217 2.26 2.91 4.448Z"
></path> />
</svg> </svg>
); );
} }

View File

@@ -1,167 +1,167 @@
export interface Settings { export interface Settings {
notification: boolean; notification: boolean;
enhancedPrivacy: boolean; enhancedPrivacy: boolean;
autoUpdate: boolean; autoUpdate: boolean;
zap: boolean; zap: boolean;
nsfw: boolean; nsfw: boolean;
[key: string]: string | number | boolean; [key: string]: string | number | boolean;
} }
export interface Keys { export interface Keys {
npub: string; npub: string;
nsec: string; nsec: string;
} }
export enum Kind { export enum Kind {
Metadata = 0, Metadata = 0,
Text = 1, Text = 1,
RecommendRelay = 2, RecommendRelay = 2,
Contacts = 3, Contacts = 3,
Repost = 6, Repost = 6,
Reaction = 7, Reaction = 7,
ZapReceipt = 9735, ZapReceipt = 9735,
// NIP-89: App Metadata // NIP-89: App Metadata
AppRecommendation = 31989, AppRecommendation = 31989,
AppHandler = 31990, AppHandler = 31990,
// #TODO: Add all nostr kinds // #TODO: Add all nostr kinds
} }
export interface Event { export interface Event {
id: string; id: string;
pubkey: string; pubkey: string;
created_at: number; created_at: number;
kind: Kind; kind: Kind;
tags: string[][]; tags: string[][];
content: string; content: string;
sig: string; sig: string;
relay?: string; relay?: string;
} }
export interface EventWithReplies extends Event { export interface EventWithReplies extends Event {
replies: Array<Event>; replies: Array<Event>;
} }
export interface Metadata { export interface Metadata {
name?: string; name?: string;
display_name?: string; display_name?: string;
about?: string; about?: string;
website?: string; website?: string;
picture?: string; picture?: string;
banner?: string; banner?: string;
nip05?: string; nip05?: string;
lud06?: string; lud06?: string;
lud16?: string; lud16?: string;
} }
export interface Contact { export interface Contact {
pubkey: string; pubkey: string;
profile: Metadata; profile: Metadata;
} }
export interface Account { export interface Account {
npub: string; npub: string;
nsec?: string; nsec?: string;
contacts?: string[]; contacts?: string[];
interests?: Interests; interests?: Interests;
} }
export interface Interests { export interface Interests {
hashtags: string[]; hashtags: string[];
users: string[]; users: string[];
words: string[]; words: string[];
} }
export interface RichContent { export interface RichContent {
parsed: string; parsed: string;
images: string[]; images: string[];
videos: string[]; videos: string[];
links: string[]; links: string[];
notes: string[]; notes: string[];
} }
export interface AppRouteSearch { export interface AppRouteSearch {
account: string; account: string;
} }
export interface ColumnRouteSearch { export interface ColumnRouteSearch {
account: string; account: string;
label: string; label: string;
name: string; name: string;
redirect?: string; redirect?: string;
} }
export interface LumeColumn { export interface LumeColumn {
label: string; label: string;
name: string; name: string;
content: URL | string; content: URL | string;
description?: string; description?: string;
author?: string; author?: string;
logo?: string; logo?: string;
cover?: string; cover?: string;
coverRetina?: string; coverRetina?: string;
featured?: boolean; featured?: boolean;
} }
export interface EventColumns { export interface EventColumns {
type: "add" | "remove" | "update" | "left" | "right" | "set_title"; type: "add" | "remove" | "update" | "left" | "right" | "set_title";
label?: string; label?: string;
title?: string; title?: string;
column?: LumeColumn; column?: LumeColumn;
} }
export interface Opengraph { export interface Opengraph {
url: string; url: string;
title?: string; title?: string;
description?: string; description?: string;
image?: string; image?: string;
} }
export interface NostrBuildResponse { export interface NostrBuildResponse {
ok: boolean; ok: boolean;
data?: { data?: {
message: string; message: string;
status: string; status: string;
data: Array<{ data: Array<{
blurhash: string; blurhash: string;
dimensions: { dimensions: {
width: number; width: number;
height: number; height: number;
}; };
mime: string; mime: string;
name: string; name: string;
sha256: string; sha256: string;
size: number; size: number;
url: string; url: string;
}>; }>;
}; };
} }
export interface NIP11 { export interface NIP11 {
name: string; name: string;
description: string; description: string;
pubkey: string; pubkey: string;
contact: string; contact: string;
supported_nips: number[]; supported_nips: number[];
software: string; software: string;
version: string; version: string;
limitation: { limitation: {
[key: string]: string | number | boolean; [key: string]: string | number | boolean;
}; };
relay_countries: string[]; relay_countries: string[];
language_tags: string[]; language_tags: string[];
tags: string[]; tags: string[];
posting_policy: string; posting_policy: string;
payments_url: string; payments_url: string;
icon: string[]; icon: string[];
} }
export interface NIP05 { export interface NIP05 {
names: { names: {
[key: string]: string; [key: string]: string;
}; };
nip46: { nip46: {
[key: string]: { [key: string]: {
[key: string]: string[]; [key: string]: string[];
}; };
}; };
} }

View File

@@ -23,6 +23,7 @@ export function NoteReply({ large = false }: { large?: boolean }) {
)} )}
> >
<ReplyIcon className="shrink-0 size-4" /> <ReplyIcon className="shrink-0 size-4" />
{large ? "Reply" : null}
</button> </button>
</Tooltip.Trigger> </Tooltip.Trigger>
<Tooltip.Portal> <Tooltip.Portal>

View File

@@ -187,9 +187,9 @@ pub async fn publish(
state: State<'_, Nostr>, state: State<'_, Nostr>,
) -> Result<String, String> { ) -> Result<String, String> {
let client = &state.client; let client = &state.client;
let final_tags = tags.into_iter().map(|val| Tag::parse(&val).unwrap()); let event_tags = tags.into_iter().map(|val| Tag::parse(&val).unwrap());
match client.publish_text_note(content, final_tags).await { match client.publish_text_note(content, event_tags).await {
Ok(event_id) => Ok(event_id.to_bech32().unwrap()), Ok(event_id) => Ok(event_id.to_bech32().unwrap()),
Err(err) => Err(err.to_string()), Err(err) => Err(err.to_string()),
} }

View File

@@ -1,7 +1,10 @@
use std::path::PathBuf; use std::path::PathBuf;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
use tauri::TitleBarStyle; use tauri::TitleBarStyle;
use tauri::{Manager, Runtime, WebviewUrl, WebviewWindowBuilder}; use tauri::{
utils::config::WindowEffectsConfig, window::Effect, Manager, Runtime, WebviewUrl,
WebviewWindowBuilder,
};
use tauri_plugin_shell::ShellExt; use tauri_plugin_shell::ShellExt;
pub fn create_tray<R: Runtime>(app: &tauri::AppHandle<R>) -> tauri::Result<()> { pub fn create_tray<R: Runtime>(app: &tauri::AppHandle<R>) -> tauri::Result<()> {
@@ -60,18 +63,25 @@ pub fn create_tray<R: Runtime>(app: &tauri::AppHandle<R>) -> tauri::Result<()> {
let _ = let _ =
WebviewWindowBuilder::new(app, "editor-0", WebviewUrl::App(PathBuf::from("editor"))) WebviewWindowBuilder::new(app, "editor-0", WebviewUrl::App(PathBuf::from("editor")))
.title("Editor") .title("Editor")
.min_inner_size(500., 400.) .min_inner_size(560., 340.)
.inner_size(600., 400.) .inner_size(560., 340.)
.hidden_title(true) .hidden_title(true)
.title_bar_style(TitleBarStyle::Overlay) .title_bar_style(TitleBarStyle::Overlay)
.transparent(true)
.effects(WindowEffectsConfig {
state: None,
effects: vec![Effect::WindowBackground],
radius: None,
color: None,
})
.build() .build()
.unwrap(); .unwrap();
#[cfg(not(target_os = "macos"))] #[cfg(not(target_os = "macos"))]
let _ = let _ =
WebviewWindowBuilder::new(app, "editor-0", WebviewUrl::App(PathBuf::from("editor"))) WebviewWindowBuilder::new(app, "editor-0", WebviewUrl::App(PathBuf::from("editor")))
.title("Editor") .title("Editor")
.min_inner_size(500., 400.) .min_inner_size(560., 340.)
.inner_size(600., 400.) .inner_size(560., 340.)
.build() .build()
.unwrap(); .unwrap();
} }
@@ -92,6 +102,13 @@ pub fn create_tray<R: Runtime>(app: &tauri::AppHandle<R>) -> tauri::Result<()> {
.minimizable(false) .minimizable(false)
.resizable(false) .resizable(false)
.title_bar_style(TitleBarStyle::Overlay) .title_bar_style(TitleBarStyle::Overlay)
.transparent(true)
.effects(WindowEffectsConfig {
state: None,
effects: vec![Effect::WindowBackground],
radius: None,
color: None,
})
.build() .build()
.unwrap(); .unwrap();
#[cfg(not(target_os = "macos"))] #[cfg(not(target_os = "macos"))]
@@ -131,6 +148,13 @@ pub fn create_tray<R: Runtime>(app: &tauri::AppHandle<R>) -> tauri::Result<()> {
.hidden_title(true) .hidden_title(true)
.resizable(false) .resizable(false)
.minimizable(false) .minimizable(false)
.transparent(true)
.effects(WindowEffectsConfig {
state: None,
effects: vec![Effect::WindowBackground],
radius: None,
color: None,
})
.build() .build()
.unwrap(); .unwrap();
#[cfg(not(target_os = "macos"))] #[cfg(not(target_os = "macos"))]