feat: Rewind (#116)

* wip

* chore: update counts to allow unix timeframe

* feat: add db functions for counting new items

* wip: endpoint working

* wip

* wip: initial ui done

* add header, adjust ui

* add time listened toggle

* fix layout, year param

* param fixes
This commit is contained in:
Gabe Farrell 2025-12-31 18:44:55 -05:00 committed by GitHub
parent c0a8c64243
commit d4ac96f780
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
64 changed files with 2252 additions and 1055 deletions

View file

@ -2,128 +2,158 @@ import { useEffect, useState } from "react";
import { Modal } from "./Modal";
import { search, type SearchResponse } from "api/api";
import SearchResults from "../SearchResults";
import type { MergeFunc, MergeSearchCleanerFunc } from "~/routes/MediaItems/MediaLayout";
import type {
MergeFunc,
MergeSearchCleanerFunc,
} from "~/routes/MediaItems/MediaLayout";
import { useNavigate } from "react-router";
interface Props {
open: boolean
setOpen: Function
type: string
currentId: number
currentTitle: string
mergeFunc: MergeFunc
mergeCleanerFunc: MergeSearchCleanerFunc
open: boolean;
setOpen: Function;
type: string;
currentId: number;
currentTitle: string;
mergeFunc: MergeFunc;
mergeCleanerFunc: MergeSearchCleanerFunc;
}
export default function MergeModal(props: Props) {
const [query, setQuery] = useState('');
const [data, setData] = useState<SearchResponse>();
const [debouncedQuery, setDebouncedQuery] = useState(query);
const [mergeTarget, setMergeTarget] = useState<{title: string, id: number}>({title: '', id: 0})
const [mergeOrderReversed, setMergeOrderReversed] = useState(false)
const [replaceImage, setReplaceImage] = useState(false)
const navigate = useNavigate()
const [query, setQuery] = useState("");
const [data, setData] = useState<SearchResponse>();
const [debouncedQuery, setDebouncedQuery] = useState(query);
const [mergeTarget, setMergeTarget] = useState<{ title: string; id: number }>(
{ title: "", id: 0 }
);
const [mergeOrderReversed, setMergeOrderReversed] = useState(false);
const [replaceImage, setReplaceImage] = useState(false);
const navigate = useNavigate();
const closeMergeModal = () => {
props.setOpen(false);
setQuery("");
setData(undefined);
setMergeOrderReversed(false);
setMergeTarget({ title: "", id: 0 });
};
const closeMergeModal = () => {
props.setOpen(false)
setQuery('')
setData(undefined)
setMergeOrderReversed(false)
setMergeTarget({title: '', id: 0})
const toggleSelect = ({ title, id }: { title: string; id: number }) => {
setMergeTarget({ title: title, id: id });
};
useEffect(() => {
console.log("mergeTarget", mergeTarget);
}, [mergeTarget]);
const doMerge = () => {
let from, to;
if (!mergeOrderReversed) {
from = mergeTarget;
to = { id: props.currentId, title: props.currentTitle };
} else {
from = { id: props.currentId, title: props.currentTitle };
to = mergeTarget;
}
const toggleSelect = ({title, id}: {title: string, id: number}) => {
setMergeTarget({title: title, id: id})
}
useEffect(() => {
console.log("mergeTarget",mergeTarget)
}, [mergeTarget])
const doMerge = () => {
let from, to
if (!mergeOrderReversed) {
from = mergeTarget
to = {id: props.currentId, title: props.currentTitle}
props
.mergeFunc(from.id, to.id, replaceImage)
.then((r) => {
if (r.ok) {
if (mergeOrderReversed) {
navigate(`/${props.type.toLowerCase()}/${mergeTarget.id}`);
closeMergeModal();
} else {
window.location.reload();
}
} else {
from = {id: props.currentId, title: props.currentTitle}
to = mergeTarget
// TODO: handle error
console.log(r);
}
props.mergeFunc(from.id, to.id, replaceImage)
.then(r => {
if (r.ok) {
if (mergeOrderReversed) {
navigate(`/${props.type.toLowerCase()}/${mergeTarget.id}`)
closeMergeModal()
} else {
window.location.reload()
}
} else {
// TODO: handle error
console.log(r)
}
})
.catch((err) => console.log(err))
})
.catch((err) => console.log(err));
};
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedQuery(query);
if (query === "") {
setData(undefined);
}
}, 300);
return () => {
clearTimeout(handler);
};
}, [query]);
useEffect(() => {
if (debouncedQuery) {
search(debouncedQuery).then((r) => {
r = props.mergeCleanerFunc(r, props.currentId);
setData(r);
});
}
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedQuery(query);
if (query === '') {
setData(undefined)
}
}, 300);
}, [debouncedQuery]);
return () => {
clearTimeout(handler);
};
}, [query]);
useEffect(() => {
if (debouncedQuery) {
search(debouncedQuery).then((r) => {
r = props.mergeCleanerFunc(r, props.currentId)
setData(r);
});
}
}, [debouncedQuery]);
return (
return (
<Modal isOpen={props.open} onClose={closeMergeModal}>
<h2>Merge {props.type}s</h2>
<div className="flex flex-col items-center">
<input
type="text"
autoFocus
// i find my stupid a(n) logic to be a little silly so im leaving it in even if its not optimal
placeholder={`Search for a${props.type.toLowerCase()[0] === 'a' ? 'n' : ''} ${props.type.toLowerCase()} to be merged into the current ${props.type.toLowerCase()}`}
className="w-full mx-auto fg bg rounded p-2"
onChange={(e) => setQuery(e.target.value)}
/>
<SearchResults selectorMode data={data} onSelect={toggleSelect}/>
{ mergeTarget.id !== 0 ?
<>
{mergeOrderReversed ?
<p className="mt-5"><strong>{props.currentTitle}</strong> will be merged into <strong>{mergeTarget.title}</strong></p>
:
<p className="mt-5"><strong>{mergeTarget.title}</strong> will be merged into <strong>{props.currentTitle}</strong></p>
}
<button className="hover:cursor-pointer px-5 py-2 rounded-md mt-5 bg-(--color-bg) hover:bg-(--color-bg-tertiary)" onClick={doMerge}>Merge Items</button>
<h3>Merge {props.type}s</h3>
<div className="flex flex-col items-center">
<input
type="text"
autoFocus
// i find my stupid a(n) logic to be a little silly so im leaving it in even if its not optimal
placeholder={`Search for a${
props.type.toLowerCase()[0] === "a" ? "n" : ""
} ${props.type.toLowerCase()} to be merged into the current ${props.type.toLowerCase()}`}
className="w-full mx-auto fg bg rounded p-2"
onChange={(e) => setQuery(e.target.value)}
/>
<SearchResults selectorMode data={data} onSelect={toggleSelect} />
{mergeTarget.id !== 0 ? (
<>
{mergeOrderReversed ? (
<p className="mt-5">
<strong>{props.currentTitle}</strong> will be merged into{" "}
<strong>{mergeTarget.title}</strong>
</p>
) : (
<p className="mt-5">
<strong>{mergeTarget.title}</strong> will be merged into{" "}
<strong>{props.currentTitle}</strong>
</p>
)}
<button
className="hover:cursor-pointer px-5 py-2 rounded-md mt-5 bg-(--color-bg) hover:bg-(--color-bg-tertiary)"
onClick={doMerge}
>
Merge Items
</button>
<div className="flex gap-2 mt-3">
<input type="checkbox" name="reverse-merge-order" checked={mergeOrderReversed} onChange={() => setMergeOrderReversed(!mergeOrderReversed)} />
<label htmlFor="reverse-merge-order">Reverse merge order</label>
<input
type="checkbox"
name="reverse-merge-order"
checked={mergeOrderReversed}
onChange={() => setMergeOrderReversed(!mergeOrderReversed)}
/>
<label htmlFor="reverse-merge-order">Reverse merge order</label>
</div>
{
(props.type.toLowerCase() === "album" || props.type.toLowerCase() === "artist") &&
<div className="flex gap-2 mt-3">
<input type="checkbox" name="replace-image" checked={replaceImage} onChange={() => setReplaceImage(!replaceImage)} />
{(props.type.toLowerCase() === "album" ||
props.type.toLowerCase() === "artist") && (
<div className="flex gap-2 mt-3">
<input
type="checkbox"
name="replace-image"
checked={replaceImage}
onChange={() => setReplaceImage(!replaceImage)}
/>
<label htmlFor="replace-image">Replace image</label>
</div>
}
</> :
''}
</div>
</div>
)}
</>
) : (
""
)}
</div>
</Modal>
)
);
}