mirror of
https://github.com/gabehf/Koito.git
synced 2026-04-22 20:11:50 -07:00
feat: make all activity grids configurable
This commit is contained in:
parent
7ed60b3785
commit
5a2cb437a1
7 changed files with 141 additions and 108 deletions
|
|
@ -3,5 +3,12 @@
|
||||||
## Features
|
## Features
|
||||||
- Allow loading environment variables from files using the _FILE suffix (#20)
|
- Allow loading environment variables from files using the _FILE suffix (#20)
|
||||||
|
|
||||||
|
## Enhancements
|
||||||
|
|
||||||
## Fixes
|
## Fixes
|
||||||
- Sub-second precision is stripped from incoming listens to ensure they can be deleted reliably
|
- Sub-second precision is stripped from incoming listens to ensure they can be deleted reliably
|
||||||
|
|
||||||
|
## Updates
|
||||||
|
- Adjusted colors for the "Yuu" theme
|
||||||
|
- Themes now have a single source of truth in themes.css.ts
|
||||||
|
- Basline support for custom themes added
|
||||||
|
|
@ -45,13 +45,17 @@ export default function ActivityGrid({
|
||||||
albumId = 0,
|
albumId = 0,
|
||||||
trackId = 0,
|
trackId = 0,
|
||||||
configurable = false,
|
configurable = false,
|
||||||
autoAdjust = false,
|
|
||||||
}: Props) {
|
}: Props) {
|
||||||
|
|
||||||
const [color, setColor] = useState(getPrimaryColor())
|
const [color, setColor] = useState(getPrimaryColor())
|
||||||
const [stepState, setStep] = useState(step)
|
const [stepState, setStep] = useState(step)
|
||||||
const [rangeState, setRange] = useState(range)
|
const [rangeState, setRange] = useState(range)
|
||||||
|
|
||||||
|
// sometimes, a little bit of a lie for the sake of better design is necessary
|
||||||
|
if (rangeState === 365) {
|
||||||
|
setRange(rangeState - 1)
|
||||||
|
}
|
||||||
|
|
||||||
const { isPending, isError, data, error } = useQuery({
|
const { isPending, isError, data, error } = useQuery({
|
||||||
queryKey: [
|
queryKey: [
|
||||||
'listen-activity',
|
'listen-activity',
|
||||||
|
|
@ -111,24 +115,26 @@ export default function ActivityGrid({
|
||||||
|
|
||||||
const getDarkenAmount = (v: number, t: number): number => {
|
const getDarkenAmount = (v: number, t: number): number => {
|
||||||
|
|
||||||
if (autoAdjust) {
|
// really ugly way to just check if this is for all items and not a specific item.
|
||||||
// automatically adjust the target value based on step
|
// is it jsut better to just pass the target in as a var? probably.
|
||||||
// the smartest way to do this would be to have the api return the
|
const adjustment = artistId == albumId && albumId == trackId && trackId == 0 ? 10 : 1
|
||||||
// highest value in the range. too bad im not smart
|
|
||||||
switch (stepState) {
|
// automatically adjust the target value based on step
|
||||||
case 'day':
|
// the smartest way to do this would be to have the api return the
|
||||||
t = 10
|
// highest value in the range. too bad im not smart
|
||||||
break;
|
switch (stepState) {
|
||||||
case 'week':
|
case 'day':
|
||||||
t = 20
|
t = 10 * adjustment
|
||||||
break;
|
break;
|
||||||
case 'month':
|
case 'week':
|
||||||
t = 50
|
t = 20 * adjustment
|
||||||
break;
|
break;
|
||||||
case 'year':
|
case 'month':
|
||||||
t = 100
|
t = 50 * adjustment
|
||||||
break;
|
break;
|
||||||
}
|
case 'year':
|
||||||
|
t = 100 * adjustment
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
v = Math.min(v, t)
|
v = Math.min(v, t)
|
||||||
|
|
@ -142,45 +148,58 @@ export default function ActivityGrid({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (<div className="flex flex-col items-start">
|
const CHUNK_SIZE = 26 * 7;
|
||||||
<h2>Activity</h2>
|
const chunks = [];
|
||||||
{configurable ? (
|
|
||||||
<ActivityOptsSelector
|
for (let i = 0; i < data.length; i += CHUNK_SIZE) {
|
||||||
rangeSetter={setRange}
|
chunks.push(data.slice(i, i + CHUNK_SIZE));
|
||||||
currentRange={rangeState}
|
}
|
||||||
stepSetter={setStep}
|
|
||||||
currentStep={stepState}
|
return (
|
||||||
/>
|
<div className="flex flex-col items-start">
|
||||||
) : (
|
<h2>Activity</h2>
|
||||||
''
|
{configurable ? (
|
||||||
)}
|
<ActivityOptsSelector
|
||||||
<div className="w-auto grid grid-flow-col grid-rows-7 gap-[3px] md:gap-[5px]">
|
rangeSetter={setRange}
|
||||||
{data.map((item) => (
|
currentRange={rangeState}
|
||||||
|
stepSetter={setStep}
|
||||||
|
currentStep={stepState}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{chunks.map((chunk, index) => (
|
||||||
<div
|
<div
|
||||||
key={new Date(item.start_time).toString()}
|
key={index}
|
||||||
className="w-[10px] sm:w-[12px] h-[10px] sm:h-[12px]"
|
className="w-auto grid grid-flow-col grid-rows-7 gap-[3px] md:gap-[5px] mb-4"
|
||||||
>
|
>
|
||||||
<Popup
|
{chunk.map((item) => (
|
||||||
position="top"
|
|
||||||
space={12}
|
|
||||||
extraClasses="left-2"
|
|
||||||
inner={`${new Date(item.start_time).toLocaleDateString()} ${item.listens} plays`}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
style={{
|
key={new Date(item.start_time).toString()}
|
||||||
display: 'inline-block',
|
className="w-[10px] sm:w-[12px] h-[10px] sm:h-[12px]"
|
||||||
background:
|
>
|
||||||
item.listens > 0
|
<Popup
|
||||||
? LightenDarkenColor(color, getDarkenAmount(item.listens, 100))
|
position="top"
|
||||||
: 'var(--color-bg-secondary)',
|
space={12}
|
||||||
}}
|
extraClasses="left-2"
|
||||||
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)'}`}
|
inner={`${new Date(item.start_time).toLocaleDateString()} ${item.listens} plays`}
|
||||||
></div>
|
>
|
||||||
</Popup>
|
<div
|
||||||
|
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)'
|
||||||
|
}`}
|
||||||
|
></div>
|
||||||
|
</Popup>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { useEffect } from "react";
|
import { ChevronDown, ChevronUp } from "lucide-react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
stepSetter: (value: string) => void;
|
stepSetter: (value: string) => void;
|
||||||
|
|
@ -18,16 +19,15 @@ export default function ActivityOptsSelector({
|
||||||
const stepPeriods = ['day', 'week', 'month', 'year'];
|
const stepPeriods = ['day', 'week', 'month', 'year'];
|
||||||
const rangePeriods = [105, 182, 365];
|
const rangePeriods = [105, 182, 365];
|
||||||
|
|
||||||
const stepDisplay = (str: string): string => {
|
const [collapsed, setCollapsed] = useState(true);
|
||||||
return str.split('_').map(w =>
|
|
||||||
|
const stepDisplay = (str: string): string =>
|
||||||
|
str.split('_').map(w =>
|
||||||
w.split('').map((char, index) =>
|
w.split('').map((char, index) =>
|
||||||
index === 0 ? char.toUpperCase() : char).join('')
|
index === 0 ? char.toUpperCase() : char).join('')
|
||||||
).join(' ');
|
).join(' ');
|
||||||
};
|
|
||||||
|
|
||||||
const rangeDisplay = (r: number): string => {
|
const rangeDisplay = (r: number): string => `${r}`;
|
||||||
return `${r}`
|
|
||||||
}
|
|
||||||
|
|
||||||
const setStep = (val: string) => {
|
const setStep = (val: string) => {
|
||||||
stepSetter(val);
|
stepSetter(val);
|
||||||
|
|
@ -46,53 +46,60 @@ export default function ActivityOptsSelector({
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!disableCache) {
|
if (!disableCache) {
|
||||||
const cachedRange = parseInt(localStorage.getItem('activity_range_' + window.location.pathname.split('/')[1]) ?? '35');
|
const cachedRange = parseInt(localStorage.getItem('activity_range_' + window.location.pathname.split('/')[1]) ?? '35');
|
||||||
if (cachedRange) {
|
if (cachedRange) rangeSetter(cachedRange);
|
||||||
rangeSetter(cachedRange);
|
|
||||||
}
|
|
||||||
const cachedStep = localStorage.getItem('activity_step_' + window.location.pathname.split('/')[1]);
|
const cachedStep = localStorage.getItem('activity_step_' + window.location.pathname.split('/')[1]);
|
||||||
if (cachedStep) {
|
if (cachedStep) stepSetter(cachedStep);
|
||||||
stepSetter(cachedStep);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col gap-2 relative">
|
||||||
<div className="flex gap-2 items-center">
|
<button
|
||||||
<p>Step:</p>
|
onClick={() => setCollapsed(!collapsed)}
|
||||||
{stepPeriods.map((p, i) => (
|
className="text-sm underline self-start color-fg-secondary hover:color-fg transition absolute -top-9 left-20"
|
||||||
<div key={`step_selector_${p}`}>
|
>
|
||||||
<button
|
{collapsed ? <ChevronDown size={18} /> : <ChevronUp size={18} />}
|
||||||
className={`period-selector ${p === currentStep ? 'color-fg' : 'color-fg-secondary'} ${i !== stepPeriods.length - 1 ? 'pr-2' : ''}`}
|
</button>
|
||||||
onClick={() => setStep(p)}
|
|
||||||
disabled={p === currentStep}
|
|
||||||
>
|
|
||||||
{stepDisplay(p)}
|
|
||||||
</button>
|
|
||||||
<span className="color-fg-secondary">
|
|
||||||
{i !== stepPeriods.length - 1 ? '|' : ''}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex gap-2 items-center">
|
{!collapsed && (
|
||||||
<p>Range:</p>
|
<>
|
||||||
{rangePeriods.map((r, i) => (
|
<div className="flex gap-2 items-center">
|
||||||
<div key={`range_selector_${r}`}>
|
<p>Step:</p>
|
||||||
<button
|
{stepPeriods.map((p, i) => (
|
||||||
className={`period-selector ${r === currentRange ? 'color-fg' : 'color-fg-secondary'} ${i !== rangePeriods.length - 1 ? 'pr-2' : ''}`}
|
<div key={`step_selector_${p}`}>
|
||||||
onClick={() => setRange(r)}
|
<button
|
||||||
disabled={r === currentRange}
|
className={`period-selector ${p === currentStep ? 'color-fg' : 'color-fg-secondary'} ${i !== stepPeriods.length - 1 ? 'pr-2' : ''}`}
|
||||||
>
|
onClick={() => setStep(p)}
|
||||||
{rangeDisplay(r)}
|
disabled={p === currentStep}
|
||||||
</button>
|
>
|
||||||
<span className="color-fg-secondary">
|
{stepDisplay(p)}
|
||||||
{i !== rangePeriods.length - 1 ? '|' : ''}
|
</button>
|
||||||
</span>
|
<span className="color-fg-secondary">
|
||||||
|
{i !== stepPeriods.length - 1 ? '|' : ''}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
))}
|
|
||||||
</div>
|
<div className="flex gap-2 items-center">
|
||||||
|
<p>Range:</p>
|
||||||
|
{rangePeriods.map((r, i) => (
|
||||||
|
<div key={`range_selector_${r}`}>
|
||||||
|
<button
|
||||||
|
className={`period-selector ${r === currentRange ? 'color-fg' : 'color-fg-secondary'} ${i !== rangePeriods.length - 1 ? 'pr-2' : ''}`}
|
||||||
|
onClick={() => setRange(r)}
|
||||||
|
disabled={r === currentRange}
|
||||||
|
>
|
||||||
|
{rangeDisplay(r)}
|
||||||
|
</button>
|
||||||
|
<span className="color-fg-secondary">
|
||||||
|
{i !== rangePeriods.length - 1 ? '|' : ''}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ export default function Home() {
|
||||||
<div className="flex-1 flex flex-col items-center gap-16 min-h-0 mt-20">
|
<div className="flex-1 flex flex-col items-center gap-16 min-h-0 mt-20">
|
||||||
<div className="flex flex-col md:flex-row gap-10 md:gap-20">
|
<div className="flex flex-col md:flex-row gap-10 md:gap-20">
|
||||||
<AllTimeStats />
|
<AllTimeStats />
|
||||||
<ActivityGrid />
|
<ActivityGrid configurable />
|
||||||
</div>
|
</div>
|
||||||
<PeriodSelector setter={setPeriod} current={period} />
|
<PeriodSelector setter={setPeriod} current={period} />
|
||||||
<div className="flex flex-wrap gap-10 2xl:gap-20 xl:gap-10 justify-between mx-5 md:gap-5">
|
<div className="flex flex-wrap gap-10 2xl:gap-20 xl:gap-10 justify-between mx-5 md:gap-5">
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ export default function Album() {
|
||||||
<div className="flex flex-wrap gap-20 mt-10">
|
<div className="flex flex-wrap gap-20 mt-10">
|
||||||
<LastPlays limit={30} albumId={album.id} />
|
<LastPlays limit={30} albumId={album.id} />
|
||||||
<TopTracks limit={12} period={period} albumId={album.id} />
|
<TopTracks limit={12} period={period} albumId={album.id} />
|
||||||
<ActivityGrid autoAdjust configurable albumId={album.id} />
|
<ActivityGrid configurable albumId={album.id} />
|
||||||
</div>
|
</div>
|
||||||
</MediaLayout>
|
</MediaLayout>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ export default function Artist() {
|
||||||
<div className="flex gap-15 mt-10 flex-wrap">
|
<div className="flex gap-15 mt-10 flex-wrap">
|
||||||
<LastPlays limit={20} artistId={artist.id} />
|
<LastPlays limit={20} artistId={artist.id} />
|
||||||
<TopTracks limit={8} period={period} artistId={artist.id} />
|
<TopTracks limit={8} period={period} artistId={artist.id} />
|
||||||
<ActivityGrid configurable autoAdjust artistId={artist.id} />
|
<ActivityGrid configurable artistId={artist.id} />
|
||||||
</div>
|
</div>
|
||||||
<ArtistAlbums period={period} artistId={artist.id} name={artist.name} />
|
<ArtistAlbums period={period} artistId={artist.id} name={artist.name} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ export default function Track() {
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap gap-20 mt-10">
|
<div className="flex flex-wrap gap-20 mt-10">
|
||||||
<LastPlays limit={20} trackId={track.id}/>
|
<LastPlays limit={20} trackId={track.id}/>
|
||||||
<ActivityGrid trackId={track.id} configurable autoAdjust />
|
<ActivityGrid trackId={track.id} configurable />
|
||||||
</div>
|
</div>
|
||||||
</MediaLayout>
|
</MediaLayout>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue