feat: v0.0.5

This commit is contained in:
Gabe Farrell 2025-06-15 19:09:44 -04:00
parent 4c4ebc593d
commit 242a82ad8c
36 changed files with 694 additions and 174 deletions

View file

@ -74,13 +74,13 @@ function mergeTracks(from: number, to: number): Promise<Response> {
method: "POST",
})
}
function mergeAlbums(from: number, to: number): Promise<Response> {
return fetch(`/apis/web/v1/merge/albums?from_id=${from}&to_id=${to}`, {
function mergeAlbums(from: number, to: number, replaceImage: boolean): Promise<Response> {
return fetch(`/apis/web/v1/merge/albums?from_id=${from}&to_id=${to}&replace_image=${replaceImage}`, {
method: "POST",
})
}
function mergeArtists(from: number, to: number): Promise<Response> {
return fetch(`/apis/web/v1/merge/artists?from_id=${from}&to_id=${to}`, {
function mergeArtists(from: number, to: number, replaceImage: boolean): Promise<Response> {
return fetch(`/apis/web/v1/merge/artists?from_id=${from}&to_id=${to}&replace_image=${replaceImage}`, {
method: "POST",
})
}
@ -200,6 +200,7 @@ type Track = {
image: string
album_id: number
musicbrainz_id: string
time_listened: number
}
type Artist = {
id: number
@ -208,6 +209,7 @@ type Artist = {
aliases: string[]
listen_count: number
musicbrainz_id: string
time_listened: number
}
type Album = {
id: number,
@ -217,6 +219,7 @@ type Album = {
is_various_artists: boolean
artists: SimpleArtists[]
musicbrainz_id: string
time_listened: number
}
type Alias = {
id: number

View file

@ -82,7 +82,7 @@ function ItemCard({ item, type }: { item: Item; type: "album" | "track" | "artis
<span className="color-fg-secondary">Various Artists</span>
:
<div onClick={handleArtistClick} onKeyDown={handleArtistKeyDown}>
<ArtistLinks artists={album.artists || [{id: 0, Name: 'Unknown Artist'}]}/>
<ArtistLinks artists={album.artists ? [album.artists[0]] : [{id: 0, name: 'Unknown Artist'}]}/>
</div>
}
<div className="color-fg-secondary">{album.listen_count} plays</div>

View file

@ -21,6 +21,7 @@ export default function MergeModal(props: Props) {
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()
@ -53,7 +54,7 @@ export default function MergeModal(props: Props) {
from = {id: props.currentId, title: props.currentTitle}
to = mergeTarget
}
props.mergeFunc(from.id, to.id)
props.mergeFunc(from.id, to.id, replaceImage)
.then(r => {
if (r.ok) {
if (mergeOrderReversed) {
@ -117,6 +118,13 @@ export default function MergeModal(props: Props) {
<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)} />
<label htmlFor="replace-image">Replace image</label>
</div>
}
</> :
''}
</div>

View file

@ -6,6 +6,7 @@ import LastPlays from "~/components/LastPlays";
import PeriodSelector from "~/components/PeriodSelector";
import MediaLayout from "./MediaLayout";
import ActivityGrid from "~/components/ActivityGrid";
import { timeListenedString } from "~/utils/utils";
export async function clientLoader({ params }: LoaderFunctionArgs) {
const res = await fetch(`/apis/web/v1/album?id=${params.id}`);
@ -40,9 +41,10 @@ export default function Album() {
}
return r
}}
subContent={<>
subContent={<div className="flex flex-col gap-2 items-start">
{album.listen_count && <p>{album.listen_count} play{ album.listen_count > 1 ? 's' : ''}</p>}
</>}
{<p>{timeListenedString(album.time_listened)}</p>}
</div>}
>
<div className="mt-10">
<PeriodSelector setter={setPeriod} current={period} />

View file

@ -7,6 +7,7 @@ import PeriodSelector from "~/components/PeriodSelector";
import MediaLayout from "./MediaLayout";
import ArtistAlbums from "~/components/ArtistAlbums";
import ActivityGrid from "~/components/ActivityGrid";
import { timeListenedString } from "~/utils/utils";
export async function clientLoader({ params }: LoaderFunctionArgs) {
const res = await fetch(`/apis/web/v1/artist?id=${params.id}`);
@ -46,9 +47,10 @@ export default function Artist() {
}
return r
}}
subContent={<>
subContent={<div className="flex flex-col gap-2 items-start">
{artist.listen_count && <p>{artist.listen_count} play{ artist.listen_count > 1 ? 's' : ''}</p>}
</>}
{<p>{timeListenedString(artist.time_listened)}</p>}
</div>}
>
<div className="mt-10">
<PeriodSelector setter={setPeriod} current={period} />

View file

@ -9,7 +9,7 @@ import ImageReplaceModal from "~/components/modals/ImageReplaceModal";
import DeleteModal from "~/components/modals/DeleteModal";
import RenameModal from "~/components/modals/RenameModal";
export type MergeFunc = (from: number, to: number) => Promise<Response>
export type MergeFunc = (from: number, to: number, replaceImage: boolean) => Promise<Response>
export type MergeSearchCleanerFunc = (r: SearchResponse, id: number) => SearchResponse
interface Props {

View file

@ -5,6 +5,7 @@ import LastPlays from "~/components/LastPlays";
import PeriodSelector from "~/components/PeriodSelector";
import MediaLayout from "./MediaLayout";
import ActivityGrid from "~/components/ActivityGrid";
import { timeListenedString } from "~/utils/utils";
export async function clientLoader({ params }: LoaderFunctionArgs) {
let res = await fetch(`/apis/web/v1/track?id=${params.id}`);
@ -42,9 +43,10 @@ export default function Track() {
}
return r
}}
subContent={<div className="flex flex-col gap-4 items-start">
subContent={<div className="flex flex-col gap-2 items-start">
<Link to={`/album/${track.album_id}`}>appears on {album.title}</Link>
{track.listen_count && <p>{track.listen_count} play{ track.listen_count > 1 ? 's' : ''}</p>}
{<p>{timeListenedString(track.time_listened)}</p>}
</div>}
>
<div className="mt-10">

View file

@ -86,5 +86,17 @@ const hexToHSL = (hex: string): hsl => {
};
};
export {hexToHSL}
const timeListenedString = (seconds: number) => {
if (!seconds) return ""
if (seconds > (120 * 60) - 1) {
let hours = Math.floor(seconds / 60 / 60)
return `${hours} hours listened`
} else {
let minutes = Math.floor(seconds / 60)
return `${minutes} minutes listened`
}
}
export {hexToHSL, timeListenedString}
export type {hsl}