From 517cc8ac282ccc3468506fa350ca79424c317df0 Mon Sep 17 00:00:00 2001 From: Michael Landry Date: Fri, 26 Sep 2025 16:52:31 -0400 Subject: [PATCH] Fix race condition with using getComputedStyle primary color for dynamic activity grid darkening Instead just use the color from the current theme directly. Tested works on initial load and theme changes. Fixes https://github.com/gabehf/Koito/issues/75 --- client/app/components/ActivityGrid.tsx | 358 +++++++++++++------------ 1 file changed, 182 insertions(+), 176 deletions(-) diff --git a/client/app/components/ActivityGrid.tsx b/client/app/components/ActivityGrid.tsx index 966b4b5..9628df6 100644 --- a/client/app/components/ActivityGrid.tsx +++ b/client/app/components/ActivityGrid.tsx @@ -1,191 +1,197 @@ -import { useQuery } from "@tanstack/react-query" -import { getActivity, type getActivityArgs, type ListenActivityItem } from "api/api" -import Popup from "./Popup" -import { useState } from "react" -import { useTheme } from "~/hooks/useTheme" -import ActivityOptsSelector from "./ActivityOptsSelector" -import type { Theme } from "~/styles/themes.css" - +import { useQuery } from "@tanstack/react-query"; +import { + getActivity, + type getActivityArgs, + type ListenActivityItem, +} from "api/api"; +import Popup from "./Popup"; +import { useState } from "react"; +import { useTheme } from "~/hooks/useTheme"; +import ActivityOptsSelector from "./ActivityOptsSelector"; +import type { Theme } from "~/styles/themes.css"; function getPrimaryColor(theme: Theme): string { - const value = theme.primary; - const rgbMatch = value.match(/^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$/); - if (rgbMatch) { - const [, r, g, b] = rgbMatch.map(Number); - return ( - '#' + - [r, g, b] - .map((n) => n.toString(16).padStart(2, '0')) - .join('') - ); - } - - return value; + const value = theme.primary; + const rgbMatch = value.match( + /^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$/ + ); + if (rgbMatch) { + const [, r, g, b] = rgbMatch.map(Number); + return "#" + [r, g, b].map((n) => n.toString(16).padStart(2, "0")).join(""); + } + + return value; } interface Props { - step?: string - range?: number - month?: number - year?: number - artistId?: number - albumId?: number - trackId?: number - configurable?: boolean - autoAdjust?: boolean + step?: string; + range?: number; + month?: number; + year?: number; + artistId?: number; + albumId?: number; + trackId?: number; + configurable?: boolean; + autoAdjust?: boolean; } export default function ActivityGrid({ - step = 'day', - range = 182, - month = 0, - year = 0, - artistId = 0, - albumId = 0, - trackId = 0, - configurable = false, - }: Props) { - - const [stepState, setStep] = useState(step) - const [rangeState, setRange] = useState(range) - - const { isPending, isError, data, error } = useQuery({ - queryKey: [ - 'listen-activity', - { - step: stepState, - range: rangeState, - month: month, - year: year, - artist_id: artistId, - album_id: albumId, - track_id: trackId - }, - ], - queryFn: ({ queryKey }) => getActivity(queryKey[1] as getActivityArgs), - }); - - - const { theme, themeName } = useTheme(); - const color = getPrimaryColor(theme); - - - if (isPending) { - return ( -
-

Activity

-

Loading...

-
- ) + step = "day", + range = 182, + month = 0, + year = 0, + artistId = 0, + albumId = 0, + trackId = 0, + configurable = false, +}: Props) { + const [stepState, setStep] = useState(step); + const [rangeState, setRange] = useState(range); + + const { isPending, isError, data, error } = useQuery({ + queryKey: [ + "listen-activity", + { + step: stepState, + range: rangeState, + month: month, + year: year, + artist_id: artistId, + album_id: albumId, + track_id: trackId, + }, + ], + queryFn: ({ queryKey }) => getActivity(queryKey[1] as getActivityArgs), + }); + + const { theme, themeName } = useTheme(); + const color = getPrimaryColor(theme); + + if (isPending) { + return ( +
+

Activity

+

Loading...

+
+ ); + } + if (isError) return

Error:{error.message}

; + + // from https://css-tricks.com/snippets/javascript/lighten-darken-color/ + function LightenDarkenColor(hex: string, lum: number) { + // validate hex string + hex = String(hex).replace(/[^0-9a-f]/gi, ""); + if (hex.length < 6) { + hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]; } - if (isError) return

Error:{error.message}

- - // from https://css-tricks.com/snippets/javascript/lighten-darken-color/ - function LightenDarkenColor(hex: string, lum: number) { - // validate hex string - hex = String(hex).replace(/[^0-9a-f]/gi, ''); - if (hex.length < 6) { - hex = hex[0]+hex[0]+hex[1]+hex[1]+hex[2]+hex[2]; - } - lum = lum || 0; - - // convert to decimal and change luminosity - var rgb = "#", c, i; - for (i = 0; i < 3; i++) { - c = parseInt(hex.substring(i*2,(i*2)+2), 16); - c = Math.round(Math.min(Math.max(0, c + (c * lum)), 255)).toString(16); - rgb += ("00"+c).substring(c.length); - } - - return rgb; + lum = lum || 0; + + // convert to decimal and change luminosity + var rgb = "#", + c, + i; + for (i = 0; i < 3; i++) { + c = parseInt(hex.substring(i * 2, i * 2 + 2), 16); + c = Math.round(Math.min(Math.max(0, c + c * lum), 255)).toString(16); + rgb += ("00" + c).substring(c.length); } - const getDarkenAmount = (v: number, t: number): number => { - - // really ugly way to just check if this is for all items and not a specific item. - // is it jsut better to just pass the target in as a var? probably. - const adjustment = artistId == albumId && albumId == trackId && trackId == 0 ? 10 : 1 - - // automatically adjust the target value based on step - // the smartest way to do this would be to have the api return the - // highest value in the range. too bad im not smart - switch (stepState) { - case 'day': - t = 10 * adjustment - break; - case 'week': - t = 20 * adjustment - break; - case 'month': - t = 50 * adjustment - break; - case 'year': - t = 100 * adjustment - break; - } - - v = Math.min(v, t) - if (themeName === "pearl") { - // special case for the only light theme lol - // could be generalized by pragmatically comparing the - // lightness of the bg vs the primary but eh - return ((t-v) / t) - } else { - return ((v-t) / t) * .8 - } + return rgb; + } + + const getDarkenAmount = (v: number, t: number): number => { + // really ugly way to just check if this is for all items and not a specific item. + // is it jsut better to just pass the target in as a var? probably. + const adjustment = + artistId == albumId && albumId == trackId && trackId == 0 ? 10 : 1; + + // automatically adjust the target value based on step + // the smartest way to do this would be to have the api return the + // highest value in the range. too bad im not smart + switch (stepState) { + case "day": + t = 10 * adjustment; + break; + case "week": + t = 20 * adjustment; + break; + case "month": + t = 50 * adjustment; + break; + case "year": + t = 100 * adjustment; + break; } - const CHUNK_SIZE = 26 * 7; - const chunks = []; - - for (let i = 0; i < data.length; i += CHUNK_SIZE) { - chunks.push(data.slice(i, i + CHUNK_SIZE)); + v = Math.min(v, t); + if (themeName === "pearl") { + // special case for the only light theme lol + // could be generalized by pragmatically comparing the + // lightness of the bg vs the primary but eh + return (t - v) / t; + } else { + return ((v - t) / t) * 0.8; } - - return ( -
-

Activity

- {configurable ? ( - - ) : null} - - {chunks.map((chunk, index) => ( + }; + + const CHUNK_SIZE = 26 * 7; + const chunks = []; + + for (let i = 0; i < data.length; i += CHUNK_SIZE) { + chunks.push(data.slice(i, i + CHUNK_SIZE)); + } + + return ( +
+

Activity

+ {configurable ? ( + + ) : null} + + {chunks.map((chunk, index) => ( +
+ {chunk.map((item) => ( +
+
- {chunk.map((item) => ( -
- -
0 - ? LightenDarkenColor(color, getDarkenAmount(item.listens, 100)) - : 'var(--color-bg-secondary)', - }} - className={`w-[10px] sm:w-[12px] h-[10px] sm:h-[12px] rounded-[2px] md:rounded-[3px] ${ - item.listens > 0 ? '' : 'border-[0.5px] border-(--color-bg-tertiary)' - }`} - >
-
-
- ))} -
- ))} + style={{ + display: "inline-block", + background: + item.listens > 0 + ? LightenDarkenColor( + color, + getDarkenAmount(item.listens, 100) + ) + : "var(--color-bg-secondary)", + }} + className={`w-[10px] sm:w-[12px] h-[10px] sm:h-[12px] rounded-[2px] md:rounded-[3px] ${ + item.listens > 0 + ? "" + : "border-[0.5px] border-(--color-bg-tertiary)" + }`} + >
+ +
+ ))}
- ); -} + ))} +
+ ); +}