Settings Manager (#211)

* refactor: landing screen

* fix: code debt

* feat: add settings screen

* chore: clean up

* feat: settings

* feat: small updates
This commit is contained in:
雨宮蓮
2024-06-19 14:00:58 +07:00
committed by GitHub
parent 0061ecea78
commit 18c133d096
50 changed files with 937 additions and 1167 deletions

View File

@@ -44,9 +44,9 @@ export function Column({
useEffect(() => {
if (!isCreated) return;
const unlisten = listen<WindowEvent>("window", (data) => {
const unlisten = listen<WindowEvent>("child-webview", (data) => {
if (data.payload.scroll) repositionWebview();
if (data.payload.resize) resizeWebview();
if (data.payload.resize) repositionWebview().then(() => resizeWebview());
});
return () => {

View File

@@ -1,15 +1,16 @@
import { RepostIcon } from "@lume/icons";
import { cn } from "@lume/utils";
import { useCallback, useState } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import { Spinner } from "@lume/ui";
import { useNoteContext } from "../provider";
import { LumeWindow } from "@lume/system";
import { Spinner } from "@lume/ui";
import { cn } from "@lume/utils";
import { useRouteContext } from "@tanstack/react-router";
import { Menu, MenuItem } from "@tauri-apps/api/menu";
import { useCallback, useState } from "react";
import { toast } from "sonner";
import { useNoteContext } from "../provider";
export function NoteRepost({ large = false }: { large?: boolean }) {
const event = useNoteContext();
const { settings } = useRouteContext({ strict: false });
const [loading, setLoading] = useState(false);
const [isRepost, setIsRepost] = useState(false);
@@ -56,6 +57,8 @@ export function NoteRepost({ large = false }: { large?: boolean }) {
await menu.popup().catch((e) => console.error(e));
}, []);
if (!settings.display_repost_button) return null;
return (
<button
type="button"

View File

@@ -1,10 +1,14 @@
import { ZapIcon } from "@lume/icons";
import { useNoteContext } from "../provider";
import { cn } from "@lume/utils";
import { LumeWindow } from "@lume/system";
import { cn } from "@lume/utils";
import { useRouteContext } from "@tanstack/react-router";
import { useNoteContext } from "../provider";
export function NoteZap({ large = false }: { large?: boolean }) {
const event = useNoteContext();
const { settings } = useRouteContext({ strict: false });
if (!settings.display_zap_button) return null;
return (
<button

View File

@@ -1,4 +1,6 @@
import { cn } from "@lume/utils";
import { useRouteContext } from "@tanstack/react-router";
import { nanoid } from "nanoid";
import { type ReactNode, useMemo } from "react";
import reactStringReplace from "react-string-replace";
import { Hashtag } from "./mentions/hashtag";
@@ -7,7 +9,6 @@ import { MentionUser } from "./mentions/user";
import { Images } from "./preview/images";
import { Videos } from "./preview/videos";
import { useNoteContext } from "./provider";
import { nanoid } from "nanoid";
export function NoteContent({
quote = true,
@@ -20,6 +21,7 @@ export function NoteContent({
clean?: boolean;
className?: string;
}) {
const { settings } = useRouteContext({ strict: false });
const event = useNoteContext();
const content = useMemo(() => {
try {
@@ -27,7 +29,9 @@ export function NoteContent({
const { content, hashtags, events, mentions } = event.meta;
// Define rich content
let richContent: ReactNode[] | string = content;
let richContent: ReactNode[] | string = settings.display_media
? content
: event.content;
for (const hashtag of hashtags) {
const regex = new RegExp(`(|^)${hashtag}\\b`, "g");
@@ -98,8 +102,12 @@ export function NoteContent({
>
{content}
</div>
{event.meta?.images.length ? <Images urls={event.meta.images} /> : null}
{event.meta?.videos.length ? <Videos urls={event.meta.videos} /> : null}
{settings.display_media && event.meta?.images.length ? (
<Images urls={event.meta.images} />
) : null}
{settings.display_media && event.meta?.videos.length ? (
<Videos urls={event.meta.videos} />
) : null}
</div>
);
}

View File

@@ -1,33 +1,36 @@
import { WebviewWindow } from "@tauri-apps/api/webviewWindow";
import { useRouteContext } from "@tanstack/react-router";
import { open } from "@tauri-apps/plugin-shell";
import { useMemo } from "react";
export function ImagePreview({ url }: { url: string }) {
const open = async (url: string) => {
const name = new URL(url).pathname
.split("/")
.pop()
.replace(/[^a-zA-Z ]/g, "");
const label = `viewer-${name}`;
const window = WebviewWindow.getByLabel(label);
const { settings } = useRouteContext({ strict: false });
if (!window) {
const newWindow = new WebviewWindow(label, {
url,
title: "Image Viewer",
width: 800,
height: 800,
titleBarStyle: "overlay",
});
return newWindow;
const imageUrl = useMemo(() => {
if (settings.image_resize_service.length) {
const newUrl = `${settings.image_resize_service}?url=${url}&ll&af&default=1&n=-1`;
return newUrl;
} else {
return url;
}
}, [settings.image_resize_service]);
return await window.setFocus();
};
if (!settings.display_media) {
return (
<a
href={url}
target="_blank"
rel="noreferrer"
className="inline text-blue-500 hover:text-blue-600"
>
{url}
</a>
);
}
return (
<div className="relative my-1 group">
<div className="my-1">
<img
src={url}
src={imageUrl}
alt={url}
loading="lazy"
decoding="async"

View File

@@ -1,42 +1,35 @@
import { WebviewWindow } from "@tauri-apps/api/webviewWindow";
import { Carousel, CarouselItem } from "@lume/ui";
import { useRouteContext } from "@tanstack/react-router";
import { open } from "@tauri-apps/plugin-shell";
import { useMemo } from "react";
export function Images({ urls }: { urls: string[] }) {
const open = async (url: string) => {
const name = new URL(url).pathname
.split("/")
.pop()
.replace(/[^a-zA-Z ]/g, "");
const label = `viewer-${name}`;
const window = WebviewWindow.getByLabel(label);
const { settings } = useRouteContext({ strict: false });
if (!window) {
const newWindow = new WebviewWindow(label, {
url,
title: "Image Viewer",
width: 800,
height: 800,
titleBarStyle: "overlay",
});
return newWindow;
const imageUrls = useMemo(() => {
if (settings.image_resize_service.length) {
const newUrls = urls.map(
(url) =>
`${settings.image_resize_service}?url=${url}&ll&af&default=1&n=-1`,
);
return newUrls;
} else {
return urls;
}
return await window.setFocus();
};
}, [settings.image_resize_service]);
if (urls.length === 1) {
return (
<div className="px-3 group">
<img
src={urls[0]}
src={imageUrls[0]}
alt={urls[0]}
loading="lazy"
decoding="async"
style={{ contentVisibility: "auto" }}
className="max-h-[400px] w-auto object-cover rounded-lg outline outline-1 -outline-offset-1 outline-black/15"
onClick={() => open(urls[0])}
onKeyDown={() => open(urls[0])}
onClick={() => urls[0]}
onKeyDown={() => urls[0]}
onError={({ currentTarget }) => {
currentTarget.onerror = null;
currentTarget.src = "/404.jpg";
@@ -48,7 +41,7 @@ export function Images({ urls }: { urls: string[] }) {
return (
<Carousel
items={urls}
items={imageUrls}
renderItem={({ item, isSnapPoint }) => (
<CarouselItem key={item} isSnapPoint={isSnapPoint}>
<img

View File

@@ -1,87 +0,0 @@
import { useOpenGraph } from "@lume/utils";
function isImage(url: string) {
return /^https?:\/\/.+\.(jpg|jpeg|png|webp|avif)$/.test(url);
}
export function LinkPreview({ url }: { url: string }) {
const domain = new URL(url);
const { isLoading, isError, data } = useOpenGraph(url);
if (isLoading) {
return (
<div className="my-1.5 flex w-full flex-col overflow-hidden rounded-2xl border border-black/10 p-3 dark:border-white/10">
<div className="h-48 w-full shrink-0 animate-pulse bg-neutral-300 dark:bg-neutral-700" />
<div className="flex flex-col gap-2 px-3 py-3">
<div className="h-3 w-2/3 animate-pulse rounded bg-neutral-300 dark:bg-neutral-700" />
<div className="h-3 w-3/4 animate-pulse rounded bg-neutral-300 dark:bg-neutral-700" />
<span className="mt-2.5 text-sm leading-none text-neutral-600 dark:text-neutral-400">
{domain.hostname}
</span>
</div>
</div>
);
}
if (!data.title && !data.image && !data.description) {
return (
<a
href={url}
target="_blank"
rel="noreferrer"
className="inline-block text-blue-500 hover:text-blue-600"
>
{url}
</a>
);
}
if (isError) {
return (
<a
href={url}
target="_blank"
rel="noreferrer"
className="inline-block text-blue-500 hover:text-blue-600"
>
{url}
</a>
);
}
return (
<a
href={url}
target="_blank"
rel="noreferrer"
className="my-1 flex w-full flex-col overflow-hidden rounded-2xl border border-black/10 dark:border-white/10"
>
{isImage(data.image) ? (
<img
src={data.image}
alt={url}
loading="lazy"
decoding="async"
className="h-48 w-full shrink-0 rounded-t-lg bg-white object-cover"
/>
) : null}
<div className="flex flex-col items-start p-3">
<div className="flex flex-col items-start text-left">
{data.title ? (
<div className="content-break line-clamp-1 text-base font-semibold text-neutral-900 dark:text-neutral-100">
{data.title}
</div>
) : null}
{data.description ? (
<div className="content-break mb-2 line-clamp-3 text-balance text-sm text-neutral-700 dark:text-neutral-400">
{data.description}
</div>
) : null}
</div>
<div className="break-all text-sm font-semibold text-blue-500">
{domain.hostname}
</div>
</div>
</a>
);
}

View File

@@ -1,4 +1,21 @@
import { useRouteContext } from "@tanstack/react-router";
export function VideoPreview({ url }: { url: string }) {
const { settings } = useRouteContext({ strict: false });
if (settings.display_media) {
return (
<a
href={url}
target="_blank"
rel="noreferrer"
className="inline text-blue-500 hover:text-blue-600"
>
{url}
</a>
);
}
return (
<div className="my-1">
<video

View File

@@ -1,5 +1,6 @@
import { cn } from "@lume/utils";
import * as Avatar from "@radix-ui/react-avatar";
import { useRouteContext } from "@tanstack/react-router";
import { minidenticon } from "minidenticons";
import { nanoid } from "nanoid";
import { useMemo } from "react";
@@ -7,19 +8,46 @@ import { useUserContext } from "./provider";
export function UserAvatar({ className }: { className?: string }) {
const user = useUserContext();
const { settings } = useRouteContext({ strict: false });
const picture = useMemo(() => {
if (
settings?.image_resize_service?.length &&
user.profile?.picture?.length
) {
const url = `${settings.image_resize_service}?url=${user.profile?.picture}&w=100&h=100&default=1&n=-1`;
return url;
} else {
return user.profile?.picture;
}
}, [user.profile?.picture]);
const fallbackAvatar = useMemo(
() =>
`data:image/svg+xml;utf8,${encodeURIComponent(
minidenticon(user.pubkey || nanoid(), 90, 50),
)}`,
[user],
[user.pubkey],
);
if (settings && !settings.display_avatar) {
return (
<Avatar.Root className="shrink-0">
<Avatar.Fallback delayMs={120}>
<img
src={fallbackAvatar}
alt={user.pubkey}
className={cn("bg-black dark:bg-white", className)}
/>
</Avatar.Fallback>
</Avatar.Root>
);
}
return (
<Avatar.Root className="shrink-0">
<Avatar.Image
src={user.profile?.picture}
src={picture}
alt={user.pubkey}
loading="eager"
decoding="async"

View File

@@ -15,7 +15,7 @@ export function UserCover({ className }: { className?: string }) {
);
}
if (user && !user.profile.banner) {
if (user && !user.profile?.banner) {
return (
<div
className={cn("bg-gradient-to-b from-blue-400 to-teal-200", className)}
@@ -25,7 +25,7 @@ export function UserCover({ className }: { className?: string }) {
return (
<img
src={user.profile.banner}
src={user?.profile?.banner}
alt="banner"
loading="lazy"
decoding="async"