feat: settings screens
This commit is contained in:
@@ -37,6 +37,7 @@ const router = createRouter({
|
||||
ark: undefined!,
|
||||
platform: platformName,
|
||||
locale: osLocale,
|
||||
settings: null,
|
||||
queryClient,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -7,12 +7,14 @@ import {
|
||||
import { type Ark } from "@lume/ark";
|
||||
import { type QueryClient } from "@tanstack/react-query";
|
||||
import { type Platform } from "@tauri-apps/plugin-os";
|
||||
import { Settings } from "@lume/types";
|
||||
|
||||
interface RouterContext {
|
||||
ark: Ark;
|
||||
queryClient: QueryClient;
|
||||
platform: Platform;
|
||||
locale: string;
|
||||
settings: Settings;
|
||||
}
|
||||
|
||||
export const Route = createRootRouteWithContext<RouterContext>()({
|
||||
|
||||
@@ -68,7 +68,7 @@ function Screen() {
|
||||
</div>
|
||||
<div className="flex w-full flex-col gap-5">
|
||||
<div className="flex flex-col gap-2">
|
||||
<label htmlFor="nsec" className="font-medium">
|
||||
<label htmlFor="passphase" className="font-medium">
|
||||
Set a passphase to secure your key
|
||||
</label>
|
||||
<div className="relative">
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { CheckIcon } from "@lume/icons";
|
||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||
import { createLazyFileRoute, useNavigate } from "@tanstack/react-router";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import * as Switch from "@radix-ui/react-switch";
|
||||
import { Link } from "@tanstack/react-router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Settings } from "@lume/types";
|
||||
import { useArk } from "@lume/ark";
|
||||
@@ -18,6 +17,7 @@ export const Route = createLazyFileRoute("/auth/settings")({
|
||||
|
||||
function Screen() {
|
||||
const ark = useArk();
|
||||
const navigate = useNavigate();
|
||||
|
||||
// @ts-ignore, magic!!!
|
||||
const { account } = Route.useSearch();
|
||||
@@ -51,10 +51,12 @@ function Screen() {
|
||||
}));
|
||||
};
|
||||
|
||||
const saveSettings = async () => {
|
||||
const submit = async () => {
|
||||
try {
|
||||
const eventId = await ark.set_settings(settings);
|
||||
if (eventId) toast.success("Settings have been updated successfully.");
|
||||
if (eventId) {
|
||||
navigate({ to: "/$account/home", params: { account }, replace: true });
|
||||
}
|
||||
} catch (e) {
|
||||
toast.error(e);
|
||||
}
|
||||
@@ -142,22 +144,13 @@ function Screen() {
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={saveSettings}
|
||||
className="inline-flex h-11 flex-1 items-center justify-center rounded-lg bg-neutral-100 font-medium hover:bg-neutral-200 disabled:opacity-50 dark:bg-neutral-900 dark:hover:bg-neutral-800"
|
||||
>
|
||||
Save settings
|
||||
</button>
|
||||
<Link
|
||||
to="/$account/home"
|
||||
params={{ account }}
|
||||
className="inline-flex h-11 flex-1 items-center justify-center rounded-lg bg-blue-500 font-semibold text-white hover:bg-blue-600 disabled:opacity-50"
|
||||
>
|
||||
{t("global.continue")}
|
||||
</Link>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={submit}
|
||||
className="inline-flex h-11 flex-1 items-center justify-center rounded-lg bg-blue-500 font-semibold text-white hover:bg-blue-600 disabled:opacity-50"
|
||||
>
|
||||
{t("global.continue")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -24,6 +24,10 @@ export const Route = createFileRoute("/")({
|
||||
|
||||
const account = accounts[0].npub;
|
||||
const loadedAccount = await ark.load_selected_account(account);
|
||||
const settings = await ark.get_settings(account);
|
||||
|
||||
// Update settings
|
||||
context.settings = settings;
|
||||
|
||||
if (loadedAccount) {
|
||||
throw redirect({
|
||||
@@ -43,12 +47,15 @@ export const Route = createFileRoute("/")({
|
||||
function Screen() {
|
||||
const ark = useArk();
|
||||
const navigate = useNavigate();
|
||||
const context = Route.useRouteContext();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const select = async (npub: string) => {
|
||||
setLoading(true);
|
||||
const loadAccount = await ark.load_selected_account(npub);
|
||||
context.settings = await ark.get_settings(npub);
|
||||
|
||||
if (loadAccount) {
|
||||
navigate({
|
||||
to: "/$account/home",
|
||||
|
||||
106
apps/desktop2/src/routes/settings.tsx
Normal file
106
apps/desktop2/src/routes/settings.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
import { SettingsIcon, UserIcon, ZapIcon, SecureIcon } from "@lume/icons";
|
||||
import { cn } from "@lume/utils";
|
||||
import { Link } from "@tanstack/react-router";
|
||||
import { Outlet, createFileRoute } from "@tanstack/react-router";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export const Route = createFileRoute("/settings")({
|
||||
component: Screen,
|
||||
});
|
||||
|
||||
function Screen() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col bg-neutral-100 dark:bg-neutral-950">
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
className="flex h-20 w-full shrink-0 items-center justify-center border-b border-neutral-200 dark:border-neutral-800"
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
<Link to="/settings/general">
|
||||
{({ isActive }) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-14 w-20 shrink-0 flex-col items-center justify-center rounded-lg p-2",
|
||||
isActive
|
||||
? "bg-neutral-200 hover:bg-neutral-300 dark:bg-neutral-800 dark:text-neutral-100 dark:hover:bg-neutral-700"
|
||||
: "text-neutral-700 hover:bg-neutral-200 dark:text-neutral-300 dark:hover:bg-neutral-800",
|
||||
)}
|
||||
>
|
||||
<SettingsIcon className="size-5 shrink-0" />
|
||||
<p className="text-sm font-medium">
|
||||
{t("settings.general.title")}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Link>
|
||||
<Link to="/settings/user">
|
||||
{({ isActive }) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-14 w-20 shrink-0 flex-col items-center justify-center rounded-lg p-2",
|
||||
isActive
|
||||
? "bg-neutral-200 hover:bg-neutral-300 dark:bg-neutral-800 dark:text-neutral-100 dark:hover:bg-neutral-700"
|
||||
: "text-neutral-700 hover:bg-neutral-200 dark:text-neutral-300 dark:hover:bg-neutral-800",
|
||||
)}
|
||||
>
|
||||
<UserIcon className="size-5 shrink-0" />
|
||||
<p className="text-sm font-medium">
|
||||
{t("settings.user.title")}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Link>
|
||||
<Link to="/settings/zap">
|
||||
{({ isActive }) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-14 w-20 shrink-0 flex-col items-center justify-center rounded-lg p-2",
|
||||
isActive
|
||||
? "bg-neutral-200 hover:bg-neutral-300 dark:bg-neutral-800 dark:text-neutral-100 dark:hover:bg-neutral-700"
|
||||
: "text-neutral-700 hover:bg-neutral-200 dark:text-neutral-300 dark:hover:bg-neutral-800",
|
||||
)}
|
||||
>
|
||||
<ZapIcon className="size-5 shrink-0" />
|
||||
<p className="text-sm font-medium">
|
||||
{t("settings.zap.title")}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Link>
|
||||
<Link to="/settings/backup">
|
||||
{({ isActive }) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-14 w-20 shrink-0 flex-col items-center justify-center rounded-lg p-2",
|
||||
isActive
|
||||
? "bg-neutral-200 hover:bg-neutral-300 dark:bg-neutral-800 dark:text-neutral-100 dark:hover:bg-neutral-700"
|
||||
: "text-neutral-700 hover:bg-neutral-200 dark:text-neutral-300 dark:hover:bg-neutral-800",
|
||||
)}
|
||||
>
|
||||
<SecureIcon className="size-5 shrink-0" />
|
||||
<p className="text-sm font-medium">
|
||||
{t("settings.backup.title")}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full flex-1 overflow-y-auto px-5 py-4">
|
||||
<div className="mx-auto w-full max-w-xl">
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
122
apps/desktop2/src/routes/settings/backup.tsx
Normal file
122
apps/desktop2/src/routes/settings/backup.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
import { type Account } from "@lume/types";
|
||||
import { User } from "@lume/ui";
|
||||
import { displayNsec } from "@lume/utils";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { writeText } from "@tauri-apps/plugin-clipboard-manager";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export const Route = createFileRoute("/settings/backup")({
|
||||
component: Screen,
|
||||
loader: async ({ context }) => {
|
||||
const ark = context.ark;
|
||||
const npubs = await ark.get_all_accounts();
|
||||
|
||||
let accounts: Account[] = [];
|
||||
|
||||
for (const account of npubs) {
|
||||
const nsec: string = await invoke("get_stored_nsec", {
|
||||
npub: account.npub,
|
||||
});
|
||||
accounts.push({ ...account, nsec });
|
||||
}
|
||||
|
||||
return accounts;
|
||||
},
|
||||
});
|
||||
|
||||
function Screen() {
|
||||
const accounts = Route.useLoaderData();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3 divide-y divide-neutral-300 dark:divide-neutral-700">
|
||||
{accounts.map((account, index) => (
|
||||
<div key={account.npub} className="flex items-start gap-6 py-3">
|
||||
<div className="w-36 shrink-0 text-end font-medium">
|
||||
Account {index}
|
||||
</div>
|
||||
<Account account={account} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Account({ account }: { account: Account }) {
|
||||
const [key, setKey] = useState(account.nsec);
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [passphase, setPassphase] = useState("");
|
||||
|
||||
const encrypt = async () => {
|
||||
const encrypted: string = await invoke("get_encrypted_key", {
|
||||
npub: account.npub,
|
||||
password: passphase,
|
||||
});
|
||||
setKey(encrypted);
|
||||
};
|
||||
|
||||
const copyKey = async () => {
|
||||
try {
|
||||
await writeText(key);
|
||||
setCopied(true);
|
||||
} catch (e) {
|
||||
toast.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-1 flex-col gap-2">
|
||||
<User.Provider pubkey={account.npub}>
|
||||
<User.Root className="flex items-center gap-2">
|
||||
<User.Avatar className="size-8 rounded-full object-cover" />
|
||||
<div className="flex flex-col">
|
||||
<User.Name className="text-sm leading-tight" />
|
||||
<User.NIP05 className="text-sm leading-tight text-neutral-700 dark:text-neutral-300" />
|
||||
</div>
|
||||
</User.Root>
|
||||
</User.Provider>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
readOnly
|
||||
type="text"
|
||||
value={displayNsec(key, 36)}
|
||||
className="h-9 w-full rounded-lg border-neutral-300 bg-transparent px-3 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:border-neutral-700 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={copyKey}
|
||||
className="inline-flex h-9 w-24 items-center justify-center rounded-lg bg-neutral-200 text-sm font-medium hover:bg-neutral-300 dark:bg-neutral-900 dark:hover:bg-neutral-700"
|
||||
>
|
||||
{copied ? "Copied" : "Copy"}
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex w-full flex-col gap-1">
|
||||
<label
|
||||
htmlFor="passphase"
|
||||
className="text-sm font-medium text-neutral-700 dark:text-neutral-300"
|
||||
>
|
||||
Set a passphase to secure your key
|
||||
</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
name="passphase"
|
||||
type="password"
|
||||
value={passphase}
|
||||
onChange={(e) => setPassphase(e.target.value)}
|
||||
className="h-9 w-full rounded-lg border-neutral-300 bg-transparent px-3 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:border-neutral-700 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={encrypt}
|
||||
className="inline-flex h-9 w-24 items-center justify-center rounded-lg bg-neutral-200 text-sm font-medium hover:bg-neutral-300 dark:bg-neutral-900 dark:hover:bg-neutral-700"
|
||||
>
|
||||
Update
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
5
apps/desktop2/src/routes/settings/general.lazy.tsx
Normal file
5
apps/desktop2/src/routes/settings/general.lazy.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { createLazyFileRoute } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createLazyFileRoute('/settings/general')({
|
||||
component: () => <div>Hello /settings/general!</div>
|
||||
})
|
||||
@@ -1,9 +0,0 @@
|
||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createLazyFileRoute("/settings/")({
|
||||
component: Screen,
|
||||
});
|
||||
|
||||
function Screen() {
|
||||
return <div>Settings</div>;
|
||||
}
|
||||
5
apps/desktop2/src/routes/settings/user.lazy.tsx
Normal file
5
apps/desktop2/src/routes/settings/user.lazy.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { createLazyFileRoute } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createLazyFileRoute('/settings/user')({
|
||||
component: () => <div>Hello /settings/user!</div>
|
||||
})
|
||||
96
apps/desktop2/src/routes/settings/zap.lazy.tsx
Normal file
96
apps/desktop2/src/routes/settings/zap.lazy.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export const Route = createLazyFileRoute("/settings/zap")({
|
||||
component: Screen,
|
||||
});
|
||||
|
||||
function Screen() {
|
||||
return (
|
||||
<div className="flex flex-col gap-3 divide-y divide-neutral-300 dark:divide-neutral-700">
|
||||
<div className="flex flex-col gap-6 py-3">
|
||||
<Connection />
|
||||
<DefaultAmount />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Connection() {
|
||||
const [uri, setUri] = useState("");
|
||||
|
||||
const connect = async () => {
|
||||
try {
|
||||
await invoke("set_nwc", { uri });
|
||||
} catch (e) {
|
||||
toast.error(String(e));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-start gap-6">
|
||||
<div className="w-36 shrink-0 text-end font-medium">Connection</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex w-full flex-col gap-1">
|
||||
<label
|
||||
htmlFor="nwc"
|
||||
className="text-sm font-medium text-neutral-700 dark:text-neutral-300"
|
||||
>
|
||||
Nostr Wallet Connect
|
||||
</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
name="nwc"
|
||||
type="text"
|
||||
value={uri}
|
||||
onChange={(e) => setUri(e.target.value)}
|
||||
placeholder="nostrconnect://"
|
||||
className="h-9 w-full rounded-lg border-neutral-300 bg-transparent px-3 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:border-neutral-700 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={connect}
|
||||
className="inline-flex h-9 w-24 items-center justify-center rounded-lg bg-neutral-200 text-sm font-medium hover:bg-neutral-300 dark:bg-neutral-900 dark:hover:bg-neutral-700"
|
||||
>
|
||||
Connect
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DefaultAmount() {
|
||||
return (
|
||||
<div className="flex items-start gap-6">
|
||||
<div className="w-36 shrink-0 text-end font-medium">Default amount</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex w-full flex-col gap-1">
|
||||
<label
|
||||
htmlFor="amount"
|
||||
className="text-sm font-medium text-neutral-700 dark:text-neutral-300"
|
||||
>
|
||||
Set default amount for quick zapping
|
||||
</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
name="amount"
|
||||
type="number"
|
||||
value={21}
|
||||
className="h-9 w-full rounded-lg border-neutral-300 bg-transparent px-3 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:border-neutral-700 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex h-9 w-24 items-center justify-center rounded-lg bg-neutral-200 text-sm font-medium hover:bg-neutral-300 dark:bg-neutral-900 dark:hover:bg-neutral-700"
|
||||
>
|
||||
Update
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user