mirror of
https://github.com/lumehq/lume.git
synced 2025-07-09 13:10:22 +02:00
feat: improve editor
This commit is contained in:
@ -10,9 +10,8 @@ import { useSlateStatic } from "slate-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export function MediaButton({ className }: { className?: string }) {
|
||||
const { ark } = useRouteContext({ strict: false });
|
||||
const editor = useSlateStatic();
|
||||
|
||||
const { ark } = useRouteContext({ strict: false });
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const uploadToNostrBuild = async () => {
|
||||
|
83
apps/desktop2/src/routes/editor/-components/mention.tsx
Normal file
83
apps/desktop2/src/routes/editor/-components/mention.tsx
Normal 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>
|
||||
);
|
||||
}
|
40
apps/desktop2/src/routes/editor/-components/pow.tsx
Normal file
40
apps/desktop2/src/routes/editor/-components/pow.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -1,27 +1,18 @@
|
||||
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 {
|
||||
Portal,
|
||||
cn,
|
||||
insertImage,
|
||||
insertMention,
|
||||
insertNostrEvent,
|
||||
isImageUrl,
|
||||
sendNativeNotification,
|
||||
} from "@lume/utils";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { nip19 } from "nostr-tools";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
type Descendant,
|
||||
Editor,
|
||||
Node,
|
||||
Range,
|
||||
Transforms,
|
||||
createEditor,
|
||||
} from "slate";
|
||||
import { type Descendant, Node, Transforms, createEditor } from "slate";
|
||||
import {
|
||||
Editable,
|
||||
ReactEditor,
|
||||
@ -33,6 +24,7 @@ import {
|
||||
} from "slate-react";
|
||||
import { MediaButton } from "./-components/media";
|
||||
import { NsfwToggle } from "./-components/nsfw";
|
||||
import { MentionButton } from "./-components/mention";
|
||||
|
||||
type EditorSearch = {
|
||||
reply_to: string;
|
||||
@ -73,32 +65,20 @@ export const Route = createFileRoute("/editor/")({
|
||||
};
|
||||
},
|
||||
component: Screen,
|
||||
pendingComponent: Pending,
|
||||
});
|
||||
|
||||
function Screen() {
|
||||
const ref = useRef<HTMLDivElement | null>();
|
||||
const { reply_to, quote } = Route.useSearch();
|
||||
const { ark, initialValue, contacts } = Route.useRouteContext();
|
||||
const { ark, initialValue } = Route.useRouteContext();
|
||||
|
||||
const [t] = useTranslation();
|
||||
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 [nsfw, setNsfw] = useState(false);
|
||||
const [editor] = useState(() =>
|
||||
withMentions(withNostrEvent(withImages(withReact(createEditor())))),
|
||||
);
|
||||
|
||||
const filters =
|
||||
contacts
|
||||
?.filter((c) =>
|
||||
c?.profile.name?.toLowerCase().startsWith(search.toLowerCase()),
|
||||
)
|
||||
?.slice(0, 5) ?? [];
|
||||
|
||||
const reset = () => {
|
||||
// @ts-expect-error, backlog
|
||||
editor.children = [{ type: "paragraph", children: [{ text: "" }] }];
|
||||
@ -138,11 +118,15 @@ function Screen() {
|
||||
const eventId = await ark.publish(content, reply_to, quote);
|
||||
|
||||
if (eventId) {
|
||||
await sendNativeNotification("You've publish new post successfully.");
|
||||
await sendNativeNotification(
|
||||
"Your note has been published successfully.",
|
||||
"Lume",
|
||||
);
|
||||
}
|
||||
|
||||
// stop loading
|
||||
setLoading(false);
|
||||
|
||||
// reset form
|
||||
reset();
|
||||
} 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 (
|
||||
<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">
|
||||
<Slate
|
||||
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 className="w-full h-full">
|
||||
<Slate editor={editor} initialValue={editorValue}>
|
||||
<div
|
||||
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
|
||||
nsfw={nsfw}
|
||||
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
|
||||
type="button"
|
||||
onClick={() => publish()}
|
||||
@ -216,10 +162,13 @@ function Screen() {
|
||||
{t("global.post")}
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex h-full min-h-0 w-full">
|
||||
<div className="flex h-full w-full flex-1 flex-col gap-2 px-2 pb-2">
|
||||
{reply_to && !quote ? <MentionNote eventId={reply_to} /> : null}
|
||||
<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">
|
||||
<div className="flex h-full w-full flex-1 flex-col">
|
||||
{reply_to && !quote ? (
|
||||
<div className="px-4 py-2">
|
||||
<MentionNote eventId={reply_to} />
|
||||
</div>
|
||||
) : null}
|
||||
<div className="overflow-y-auto p-4">
|
||||
<Editable
|
||||
key={JSON.stringify(editorValue)}
|
||||
autoFocus={true}
|
||||
@ -232,37 +181,6 @@ function Screen() {
|
||||
}
|
||||
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>
|
||||
</div>
|
||||
</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 { insertData, isVoid } = editor;
|
||||
|
||||
@ -429,7 +333,7 @@ const Element = (props) => {
|
||||
return <Event {...props} />;
|
||||
default:
|
||||
return (
|
||||
<p {...attributes} className="text-lg">
|
||||
<p {...attributes} className="text-[15px]">
|
||||
{children}
|
||||
</p>
|
||||
);
|
||||
|
@ -66,7 +66,7 @@ function Screen() {
|
||||
<div className="flex items-center justify-center gap-6">
|
||||
{loading ? (
|
||||
<div className="inline-flex size-6 items-center justify-center">
|
||||
<Spinner className="size-6" />
|
||||
<Spinner className="size-6 text-white" />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
|
@ -276,6 +276,7 @@ export class Ark {
|
||||
|
||||
if (quote) {
|
||||
eventTags.push(["e", replyEvent.id, relayHint, "mention"]);
|
||||
eventTags.push(["q", replyEvent.id]);
|
||||
} else {
|
||||
const rootEvent = replyEvent.tags.find((ev) => ev[3] === "root");
|
||||
|
||||
@ -809,8 +810,8 @@ export class Ark {
|
||||
label,
|
||||
title: "Editor",
|
||||
url,
|
||||
width: 500,
|
||||
height: 360,
|
||||
width: 560,
|
||||
height: 340,
|
||||
});
|
||||
} catch (e) {
|
||||
throw new Error(String(e));
|
||||
|
@ -4,21 +4,13 @@ export function MentionIcon(
|
||||
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
|
||||
) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
{...props}
|
||||
>
|
||||
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" {...props}>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
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"
|
||||
></path>
|
||||
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"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ export function NoteReply({ large = false }: { large?: boolean }) {
|
||||
)}
|
||||
>
|
||||
<ReplyIcon className="shrink-0 size-4" />
|
||||
{large ? "Reply" : null}
|
||||
</button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Portal>
|
||||
|
@ -187,9 +187,9 @@ pub async fn publish(
|
||||
state: State<'_, Nostr>,
|
||||
) -> Result<String, String> {
|
||||
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()),
|
||||
Err(err) => Err(err.to_string()),
|
||||
}
|
||||
|
@ -1,7 +1,10 @@
|
||||
use std::path::PathBuf;
|
||||
#[cfg(target_os = "macos")]
|
||||
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;
|
||||
|
||||
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 _ =
|
||||
WebviewWindowBuilder::new(app, "editor-0", WebviewUrl::App(PathBuf::from("editor")))
|
||||
.title("Editor")
|
||||
.min_inner_size(500., 400.)
|
||||
.inner_size(600., 400.)
|
||||
.min_inner_size(560., 340.)
|
||||
.inner_size(560., 340.)
|
||||
.hidden_title(true)
|
||||
.title_bar_style(TitleBarStyle::Overlay)
|
||||
.transparent(true)
|
||||
.effects(WindowEffectsConfig {
|
||||
state: None,
|
||||
effects: vec![Effect::WindowBackground],
|
||||
radius: None,
|
||||
color: None,
|
||||
})
|
||||
.build()
|
||||
.unwrap();
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let _ =
|
||||
WebviewWindowBuilder::new(app, "editor-0", WebviewUrl::App(PathBuf::from("editor")))
|
||||
.title("Editor")
|
||||
.min_inner_size(500., 400.)
|
||||
.inner_size(600., 400.)
|
||||
.min_inner_size(560., 340.)
|
||||
.inner_size(560., 340.)
|
||||
.build()
|
||||
.unwrap();
|
||||
}
|
||||
@ -92,6 +102,13 @@ pub fn create_tray<R: Runtime>(app: &tauri::AppHandle<R>) -> tauri::Result<()> {
|
||||
.minimizable(false)
|
||||
.resizable(false)
|
||||
.title_bar_style(TitleBarStyle::Overlay)
|
||||
.transparent(true)
|
||||
.effects(WindowEffectsConfig {
|
||||
state: None,
|
||||
effects: vec![Effect::WindowBackground],
|
||||
radius: None,
|
||||
color: None,
|
||||
})
|
||||
.build()
|
||||
.unwrap();
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
@ -131,6 +148,13 @@ pub fn create_tray<R: Runtime>(app: &tauri::AppHandle<R>) -> tauri::Result<()> {
|
||||
.hidden_title(true)
|
||||
.resizable(false)
|
||||
.minimizable(false)
|
||||
.transparent(true)
|
||||
.effects(WindowEffectsConfig {
|
||||
state: None,
|
||||
effects: vec![Effect::WindowBackground],
|
||||
radius: None,
|
||||
color: None,
|
||||
})
|
||||
.build()
|
||||
.unwrap();
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
|
Reference in New Issue
Block a user