add option to change primary color

This commit is contained in:
hzrd149 2023-04-12 01:19:50 -05:00
parent a209b9d2fe
commit 66c6b4d73f
15 changed files with 160 additions and 72 deletions

View File

@ -0,0 +1,5 @@
---
"nostrudel": minor
---
Add option to change primary color for theme

View File

@ -1,9 +1,7 @@
import { import {
Badge, Badge,
Box,
Button, Button,
Flex, Flex,
Highlight,
IconButton, IconButton,
Input, Input,
InputGroup, InputGroup,
@ -17,13 +15,13 @@ import {
ModalHeader, ModalHeader,
ModalOverlay, ModalOverlay,
ModalProps, ModalProps,
Text,
useDisclosure, useDisclosure,
} from "@chakra-ui/react"; } from "@chakra-ui/react";
import { useState } from "react"; import { useState } from "react";
import { useAsync } from "react-use"; import { useAsync } from "react-use";
import { unique } from "../helpers/array"; import { unique } from "../helpers/array";
import { RelayIcon, SearchIcon } from "./icons"; import { RelayIcon, SearchIcon } from "./icons";
import { safeRelayUrl } from "../helpers/url";
function RelayPickerModal({ function RelayPickerModal({
onSelect, onSelect,
@ -60,9 +58,8 @@ function RelayPickerModal({
</InputGroup> </InputGroup>
<Flex gap="2" direction="column"> <Flex gap="2" direction="column">
{filteredRelays.map((url) => ( {filteredRelays.map((url) => (
<Flex gap="2" alignItems="center"> <Flex key={url} gap="2" alignItems="center">
<Button <Button
key={url}
value={url} value={url}
onClick={() => { onClick={() => {
onSelect(url); onSelect(url);

View File

@ -0,0 +1,31 @@
import { useCallback } from "react";
import appSettings, { replaceSettings } from "../services/app-settings";
import useSubject from "./use-subject";
import { AppSettings } from "../services/user-app-settings";
import { useToast } from "@chakra-ui/react";
export default function useAppSettings() {
const settings = useSubject(appSettings);
const toast = useToast();
const updateSettings = useCallback(
(newSettings: Partial<AppSettings>) => {
try {
return replaceSettings({ ...settings, ...newSettings });
} catch (e) {
if (e instanceof Error) {
toast({
status: "error",
description: e.message,
});
}
}
},
[settings]
);
return {
...settings,
updateSettings,
};
}

View File

@ -1,10 +1,16 @@
import React from "react"; import React, { useMemo } from "react";
import { ChakraProvider, localStorageManager } from "@chakra-ui/react"; import { ChakraProvider, localStorageManager } from "@chakra-ui/react";
import theme from "../theme";
import { SigningProvider } from "./signing-provider"; import { SigningProvider } from "./signing-provider";
import createTheme from "../theme";
import useAppSettings from "../hooks/use-app-settings";
export const Providers = ({ children }: { children: React.ReactNode }) => ( export const Providers = ({ children }: { children: React.ReactNode }) => {
<ChakraProvider theme={theme} colorModeManager={localStorageManager}> const { primaryColor } = useAppSettings();
<SigningProvider>{children}</SigningProvider> const theme = useMemo(() => createTheme(primaryColor), [primaryColor]);
</ChakraProvider>
); return (
<ChakraProvider theme={theme} colorModeManager={localStorageManager}>
<SigningProvider>{children}</SigningProvider>
</ChakraProvider>
);
};

View File

@ -6,27 +6,27 @@ import signingService from "./signing";
import { nostrPostAction } from "../classes/nostr-post-action"; import { nostrPostAction } from "../classes/nostr-post-action";
export let appSettings = new PersistentSubject(defaultSettings); export let appSettings = new PersistentSubject(defaultSettings);
export async function updateSettings(settings: Partial<AppSettings>) { export async function replaceSettings(newSettings: AppSettings) {
try { const account = accountService.current.value;
const account = accountService.current.value; if (!account) return;
if (!account) return;
const json: AppSettings = { ...appSettings.value, ...settings };
if (account.readonly) { if (account.readonly) {
accountService.updateAccountLocalSettings(account.pubkey, json); accountService.updateAccountLocalSettings(account.pubkey, newSettings);
appSettings.next(json); appSettings.next(newSettings);
} else { } else {
const draft = userAppSettings.buildAppSettingsEvent({ ...appSettings.value, ...settings }); const draft = userAppSettings.buildAppSettingsEvent(newSettings);
const event = await signingService.requestSignature(draft, account); const event = await signingService.requestSignature(draft, account);
userAppSettings.receiveEvent(event); userAppSettings.receiveEvent(event);
await nostrPostAction(clientRelaysService.getWriteUrls(), event).onComplete; await nostrPostAction(clientRelaysService.getWriteUrls(), event).onComplete;
} }
} catch (e) {}
} }
export async function loadSettings() { export async function loadSettings() {
const account = accountService.current.value; const account = accountService.current.value;
if (!account) return; if (!account) {
appSettings.next(defaultSettings);
return;
}
appSettings.disconnectAll(); appSettings.disconnectAll();

View File

@ -46,7 +46,11 @@ const db = await openDB<SchemaV2>(dbName, version, {
const v1 = db as unknown as IDBPDatabase<SchemaV1>; const v1 = db as unknown as IDBPDatabase<SchemaV1>;
const v2 = db as unknown as IDBPDatabase<SchemaV2>; const v2 = db as unknown as IDBPDatabase<SchemaV2>;
v1.deleteObjectStore("settings"); // rename the old settings object store to misc
const oldSettings = transaction.objectStore("settings");
oldSettings.name = "misc";
// create new settings object store
const settings = v2.createObjectStore("settings", { const settings = v2.createObjectStore("settings", {
keyPath: "pubkey", keyPath: "pubkey",
}); });

View File

@ -56,4 +56,8 @@ export interface SchemaV2 extends SchemaV1 {
value: NostrEvent; value: NostrEvent;
indexes: { created_at: number }; indexes: { created_at: number };
}; };
misc: {
key: string;
value: any;
};
} }

View File

@ -7,12 +7,12 @@ const decryptedKeys = new Map<string, string>();
class SigningService { class SigningService {
private async getSalt() { private async getSalt() {
let salt = await db.get("settings", "salt"); let salt = await db.get("misc", "salt");
if (salt) { if (salt) {
return salt as Uint8Array; return salt as Uint8Array;
} else { } else {
const newSalt = window.crypto.getRandomValues(new Uint8Array(16)); const newSalt = window.crypto.getRandomValues(new Uint8Array(16));
await db.put("settings", newSalt, "salt"); await db.put("misc", newSalt, "salt");
return newSalt; return newSalt;
} }
} }

View File

@ -24,6 +24,7 @@ export type AppSettings = {
showSignatureVerification: boolean; showSignatureVerification: boolean;
lightningPayMode: LightningPayMode; lightningPayMode: LightningPayMode;
zapAmounts: number[]; zapAmounts: number[];
primaryColor: string;
}; };
export const defaultSettings: AppSettings = { export const defaultSettings: AppSettings = {
@ -35,6 +36,7 @@ export const defaultSettings: AppSettings = {
showSignatureVerification: false, showSignatureVerification: false,
lightningPayMode: LightningPayMode.Prompt, lightningPayMode: LightningPayMode.Prompt,
zapAmounts: [50, 200, 500, 1000], zapAmounts: [50, 200, 500, 1000],
primaryColor: "#8DB600",
}; };
function parseAppSettings(event: NostrEvent): AppSettings { function parseAppSettings(event: NostrEvent): AppSettings {

View File

@ -1,37 +1,24 @@
import { extendTheme } from "@chakra-ui/react"; import { extendTheme } from "@chakra-ui/react";
import { containerTheme } from "./container"; import { containerTheme } from "./container";
const theme = extendTheme({ export default function createTheme(primaryColor: string = "#8DB600") {
colors: { return extendTheme({
// https://hihayk.github.io/scale/#5/4/60/50/0/0/20/-25/8DB600/141/182/0/white colors: {
// brand: { brand: {
// 50: "#334009", 50: primaryColor,
// 100: "#44550A", 100: primaryColor,
// 200: "#556C09", 200: primaryColor,
// 300: "#678307", 300: primaryColor,
// 400: "#7A9C04", 400: primaryColor,
// 500: "#8DB600", 500: primaryColor,
// 600: "#9DC320", 600: primaryColor,
// 700: "#ADCF40", 700: primaryColor,
// 800: "#BCDA60", 800: primaryColor,
// 900: "#CBE480", 900: primaryColor,
// }, },
brand: {
50: "#8DB600",
100: "#8DB600",
200: "#8DB600",
300: "#8DB600",
400: "#8DB600",
500: "#8DB600",
600: "#8DB600",
700: "#8DB600",
800: "#8DB600",
900: "#8DB600",
}, },
}, components: {
components: { Container: containerTheme,
Container: containerTheme, },
}, });
}); }
export default theme;

View File

@ -23,6 +23,7 @@ export default function HomeView() {
isLazy isLazy
index={activeTab} index={activeTab}
onChange={(v) => navigate(tabs[v].path)} onChange={(v) => navigate(tabs[v].path)}
colorScheme="brand"
> >
<TabList> <TabList>
{tabs.map(({ label }) => ( {tabs.map(({ label }) => (

View File

@ -9,12 +9,42 @@ import {
Box, Box,
AccordionIcon, AccordionIcon,
FormHelperText, FormHelperText,
Input,
InputProps,
} from "@chakra-ui/react"; } from "@chakra-ui/react";
import useSubject from "../../hooks/use-subject"; import { useEffect, useRef, useState } from "react";
import appSettings, { updateSettings } from "../../services/app-settings"; import useAppSettings from "../../hooks/use-app-settings";
function ColorPicker({ value, onPickColor, ...props }: { onPickColor?: (color: string) => void } & InputProps) {
const [tmpColor, setTmpColor] = useState(value);
const ref = useRef<HTMLInputElement>();
useEffect(() => setTmpColor(value), [value]);
useEffect(() => {
if (ref.current) {
ref.current.onchange = () => {
if (onPickColor && ref.current?.value) {
onPickColor(ref.current.value);
}
};
}
});
return (
<Input
{...props}
ref={ref}
value={tmpColor}
onChange={(e) => {
setTmpColor(e.target.value);
if (props.onChange) props.onChange(e);
}}
/>
);
}
export default function DisplaySettings() { export default function DisplaySettings() {
const { blurImages, colorMode } = useSubject(appSettings); const { blurImages, colorMode, primaryColor, updateSettings } = useAppSettings();
return ( return (
<AccordionItem> <AccordionItem>
@ -40,7 +70,25 @@ export default function DisplaySettings() {
/> />
</Flex> </Flex>
<FormHelperText> <FormHelperText>
<span>Enabled: hacker mode</span> <span>Enables hacker mode</span>
</FormHelperText>
</FormControl>
<FormControl>
<Flex alignItems="center">
<FormLabel htmlFor="primary-color" mb="0">
Primary Color
</FormLabel>
<ColorPicker
id="primary-color"
type="color"
value={primaryColor}
onPickColor={(color) => updateSettings({ primaryColor: color })}
maxW="120"
size="sm"
/>
</Flex>
<FormHelperText>
<span>The primary color of the theme</span>
</FormHelperText> </FormHelperText>
</FormControl> </FormControl>
<FormControl> <FormControl>

View File

@ -12,13 +12,14 @@ import {
Select, Select,
} from "@chakra-ui/react"; } from "@chakra-ui/react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import appSettings, { updateSettings } from "../../services/app-settings"; import appSettings, { replaceSettings } from "../../services/app-settings";
import useSubject from "../../hooks/use-subject"; import useSubject from "../../hooks/use-subject";
import { LightningIcon } from "../../components/icons"; import { LightningIcon } from "../../components/icons";
import { LightningPayMode } from "../../services/user-app-settings"; import { LightningPayMode } from "../../services/user-app-settings";
import useAppSettings from "../../hooks/use-app-settings";
export default function LightningSettings() { export default function LightningSettings() {
const { lightningPayMode, zapAmounts } = useSubject(appSettings); const { lightningPayMode, zapAmounts, updateSettings } = useAppSettings();
const [zapInput, setZapInput] = useState(zapAmounts.join(",")); const [zapInput, setZapInput] = useState(zapAmounts.join(","));
useEffect(() => setZapInput(zapAmounts.join(",")), [zapAmounts.join(",")]); useEffect(() => setZapInput(zapAmounts.join(",")), [zapAmounts.join(",")]);

View File

@ -10,11 +10,12 @@ import {
AccordionIcon, AccordionIcon,
FormHelperText, FormHelperText,
} from "@chakra-ui/react"; } from "@chakra-ui/react";
import appSettings, { updateSettings } from "../../services/app-settings"; import appSettings, { replaceSettings } from "../../services/app-settings";
import useSubject from "../../hooks/use-subject"; import useSubject from "../../hooks/use-subject";
import useAppSettings from "../../hooks/use-app-settings";
export default function PerformanceSettings() { export default function PerformanceSettings() {
const { autoShowMedia, proxyUserMedia, showReactions, showSignatureVerification } = useSubject(appSettings); const { autoShowMedia, proxyUserMedia, showReactions, showSignatureVerification, updateSettings } = useAppSettings();
return ( return (
<AccordionItem> <AccordionItem>

View File

@ -62,6 +62,7 @@ const UserView = () => {
isLazy isLazy
index={activeTab} index={activeTab}
onChange={(v) => navigate(tabs[v].path)} onChange={(v) => navigate(tabs[v].path)}
colorScheme="brand"
> >
<TabList overflowX="auto" overflowY="hidden" flexShrink={0}> <TabList overflowX="auto" overflowY="hidden" flexShrink={0}>
{tabs.map(({ label }) => ( {tabs.map(({ label }) => (