mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-04-09 20:29:17 +02:00
add option to change primary color
This commit is contained in:
parent
a209b9d2fe
commit
66c6b4d73f
5
.changeset/chilled-impalas-wonder.md
Normal file
5
.changeset/chilled-impalas-wonder.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"nostrudel": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Add option to change primary color for theme
|
@ -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);
|
||||||
|
31
src/hooks/use-app-settings.ts
Normal file
31
src/hooks/use-app-settings.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
@ -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();
|
||||||
|
|
||||||
|
@ -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",
|
||||||
});
|
});
|
||||||
|
@ -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;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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;
|
|
||||||
|
@ -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 }) => (
|
||||||
|
@ -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>
|
||||||
|
@ -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(",")]);
|
||||||
|
@ -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>
|
||||||
|
@ -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 }) => (
|
||||||
|
Loading…
x
Reference in New Issue
Block a user