diff --git a/app/src/Tags.js b/app/src/Tags.js index 33fd2858..ad0ec9d0 100644 --- a/app/src/Tags.js +++ b/app/src/Tags.js @@ -1,7 +1,14 @@ import React, {useEffect} from "react"; import {deleteTag, getTags, saveTag} from "./api"; import {Dimmer, Divider, Form, Grid, GridColumn, GridRow, Label, TableCell, TableRow,} from "semantic-ui-react"; -import {APIButton, contrastingColor, ErrorMessage, fuzzyMatch, scrollToTopOfElement} from "./components/Common"; +import { + APIButton, + contrastingColor, + ErrorMessage, + fuzzyMatch, + getDistinctColor, + scrollToTopOfElement +} from "./components/Common"; import { Button, FormInput, @@ -200,6 +207,9 @@ function EditTagsModal() { const {fetchTags, tags} = React.useContext(TagsContext); const {inverted} = React.useContext(ThemeContext); + // Return a random, but distinct Hex color. + const getRandomColor = () => getDistinctColor((tags || []).map(i => i.color)); + const [open, setOpen] = React.useState(false); const [tagId, setTagId] = React.useState(null); const [tagName, setTagName] = React.useState(''); @@ -208,6 +218,13 @@ function EditTagsModal() { const [tagNameError, setTagNameError] = React.useState(null); const disabled = !!!tagName || !!tagNameError; + const setRandomColor = () => setTagColor(getRandomColor()); + + // Open modal with random color. + React.useEffect(() => { + setRandomColor(); + }, [open]); + const localOnClose = () => { setOpen(false); setTagName(''); @@ -238,7 +255,8 @@ function EditTagsModal() { } setTagName(''); setTagId(null); - setTagColor(DEFAULT_TAG_COLOR); + // Change suggested color after save. + setRandomColor(); } const handleTagNameChange = (e, {value}) => { @@ -282,13 +300,27 @@ function EditTagsModal() { - Save + + + + + + + Save + + + diff --git a/app/src/components/Common.js b/app/src/components/Common.js index febe6811..49367058 100644 --- a/app/src/components/Common.js +++ b/app/src/components/Common.js @@ -1876,3 +1876,75 @@ export function mergeDeep(target, source) { return result; } +export function getDistinctColor(hexColors) { + function hexToHSL(hex) { + let r = parseInt(hex.slice(1, 3), 16) / 255; + let g = parseInt(hex.slice(3, 5), 16) / 255; + let b = parseInt(hex.slice(5, 7), 16) / 255; + + let cmax = Math.max(r, g, b), cmin = Math.min(r, g, b); + let delta = cmax - cmin; + let h, s, l = (cmax + cmin) / 2; + + if (delta === 0) h = 0; + else if (cmax === r) h = ((g - b) / delta) % 6; + else if (cmax === g) h = (b - r) / delta + 2; + else h = (r - g) / delta + 4; + + h = Math.round(h * 60); + if (h < 0) h += 360; + + s = delta === 0 ? 0 : delta / (1 - Math.abs(2 * l - 1)); + + return {h: h, s: s, l: l}; + } + + function hslToHex(h, s, l) { + l /= 100; + const a = s * Math.min(l, 1 - l) / 100; + const f = n => { + const k = (n + h / 30) % 12; + const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1); + return Math.round(255 * color).toString(16).padStart(2, '0'); + }; + return `#${f(0)}${f(8)}${f(4)}`; + } + + function generateRandomHSL() { + return { + h: Math.random() * 360, + s: Math.random() * 100, + l: Math.random() * 100 + }; + } + + function isDistinct(newColor, existingColors, threshold) { + const hslNew = newColor; + for (let color of existingColors) { + let hslColor = hexToHSL(color); + if (Math.abs(hslNew.h - hslColor.h) < threshold && + Math.abs(hslNew.s * 100 - hslColor.s * 100) < threshold && + Math.abs(hslNew.l - hslColor.l * 100) < threshold) { + return false; + } + } + return true; + } + + let newColor, attempt = 0; + const maxAttempts = 1000; // Max attempts before returning any color + const baseThreshold = 30; // Starting threshold + + do { + newColor = generateRandomHSL(); + // Decreasing threshold as attempts increase, but never below 10 for distinctiveness + let threshold = Math.max(baseThreshold - (attempt / 10), 10); + if (isDistinct(newColor, hexColors, threshold)) { + return hslToHex(newColor.h, newColor.s, newColor.l); + } + attempt++; + } while (attempt < maxAttempts); + + // If we've tried maxAttempts times, return the last generated color regardless + return hslToHex(newColor.h, newColor.s, newColor.l); +}