Skip to content

Commit 80b6f4d

Browse files
committed
feat: v0.0.8
1 parent 00e7782 commit 80b6f4d

66 files changed

Lines changed: 1554 additions & 911 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
1-
# v0.0.7
1+
# v0.0.8
2+
## Features
3+
- An album artist can now be set as primary so that they are shown as the album artist in the top albums list
4+
5+
## Enhancements
6+
- Show a few more items under "Last Played" on the home page
7+
- Importing is now 4-5x faster
8+
29
## Fixes
3-
- Login form now correctly handles special characters
4-
- Update User form now correctly handles special characters
5-
- Delete Listen button is now hidden when not logged in
10+
- Merge selections now function correctly when selecting an item while another is selected
11+
- Use anchor tags for top tracks and top albums
12+
- UI fixes
13+
14+
## Updates
15+
- Improved logging and error traces in logs
16+
17+
## Docs
18+
- Add KOITO_FETCH_IMAGES_DURING_IMPORT to config reference

client/api/api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,7 @@ type Artist = {
226226
listen_count: number
227227
musicbrainz_id: string
228228
time_listened: number
229+
is_primary: boolean
229230
}
230231
type Album = {
231232
id: number,

client/app/components/SearchResults.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export default function SearchResults({ data, onSelect, selectorMode }: Props) {
1616
const selectItem = (title: string, id: number) => {
1717
if (selected === id) {
1818
setSelected(0)
19-
onSelect({id: id, title: title})
19+
onSelect({id: 0, title: ''})
2020
} else {
2121
setSelected(id)
2222
onSelect({id: id, title: title})

client/app/components/TopItemList.tsx

Lines changed: 30 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -35,105 +35,65 @@ export default function TopItemList<T extends Item>({ data, separators, type, cl
3535

3636
function ItemCard({ item, type }: { item: Item; type: "album" | "track" | "artist" }) {
3737

38-
const itemClasses = `flex items-center gap-2 hover:text-(--color-fg-secondary)`
39-
40-
const navigate = useNavigate();
41-
42-
const handleItemClick = (type: string, id: number) => {
43-
navigate(`/${type.toLowerCase()}/${id}`);
44-
};
45-
46-
const handleArtistClick = (event: React.MouseEvent) => {
47-
// Stop the click from navigating to the album page
48-
event.stopPropagation();
49-
};
50-
51-
// Also stop keyboard events on the inner links from bubbling up
52-
const handleArtistKeyDown = (event: React.KeyboardEvent) => {
53-
event.stopPropagation();
54-
}
38+
const itemClasses = `flex items-center gap-2`
5539

5640
switch (type) {
5741
case "album": {
5842
const album = item as Album;
5943

60-
const handleKeyDown = (event: React.KeyboardEvent) => {
61-
if (event.key === 'Enter') {
62-
handleItemClick("album", album.id);
63-
}
64-
};
65-
6644
return (
67-
<div style={{fontSize: 12}}>
68-
<div
69-
className={itemClasses}
70-
onClick={() => handleItemClick("album", album.id)}
71-
onKeyDown={handleKeyDown}
72-
role="link"
73-
tabIndex={0}
74-
aria-label={`View album: ${album.title}`}
75-
style={{ cursor: 'pointer' }}
76-
>
77-
<img src={imageUrl(album.image, "small")} alt={album.title} />
78-
<div>
45+
<div style={{fontSize: 12}} className={itemClasses}>
46+
<Link to={`/album/${album.id}`}>
47+
<img loading="lazy" src={imageUrl(album.image, "small")} alt={album.title} className="min-w-[48px]" />
48+
</Link>
49+
<div>
50+
<Link to={`/album/${album.id}`} className="hover:text-(--color-fg-secondary)">
7951
<span style={{fontSize: 14}}>{album.title}</span>
80-
<br />
81-
{album.is_various_artists ?
82-
<span className="color-fg-secondary">Various Artists</span>
83-
:
84-
<div onClick={handleArtistClick} onKeyDown={handleArtistKeyDown}>
85-
<ArtistLinks artists={album.artists ? [album.artists[0]] : [{id: 0, name: 'Unknown Artist'}]}/>
86-
</div>
87-
}
88-
<div className="color-fg-secondary">{album.listen_count} plays</div>
52+
</Link>
53+
<br />
54+
{album.is_various_artists ?
55+
<span className="color-fg-secondary">Various Artists</span>
56+
:
57+
<div>
58+
<ArtistLinks artists={album.artists ? [album.artists[0]] : [{id: 0, name: 'Unknown Artist'}]}/>
8959
</div>
60+
}
61+
<div className="color-fg-secondary">{album.listen_count} plays</div>
9062
</div>
9163
</div>
9264
);
9365
}
9466
case "track": {
9567
const track = item as Track;
96-
97-
const handleKeyDown = (event: React.KeyboardEvent) => {
98-
if (event.key === 'Enter') {
99-
handleItemClick("track", track.id);
100-
}
101-
};
10268

10369
return (
104-
<div style={{fontSize: 12}}>
105-
<div
106-
className={itemClasses}
107-
onClick={() => handleItemClick("track", track.id)}
108-
onKeyDown={handleKeyDown}
109-
role="link"
110-
tabIndex={0}
111-
aria-label={`View track: ${track.title}`}
112-
style={{ cursor: 'pointer' }}
113-
>
114-
<img src={imageUrl(track.image, "small")} alt={track.title} />
70+
<div style={{fontSize: 12}} className={itemClasses}>
71+
<Link to={`/track/${track.id}`}>
72+
<img loading="lazy" src={imageUrl(track.image, "small")} alt={track.title} className="min-w-[48px]" />
73+
</Link>
11574
<div>
116-
<span style={{fontSize: 14}}>{track.title}</span>
75+
<Link to={`/track/${track.id}`} className="hover:text-(--color-fg-secondary)">
76+
<span style={{fontSize: 14}}>{track.title}</span>
77+
</Link>
11778
<br />
118-
<div onClick={handleArtistClick} onKeyDown={handleArtistKeyDown}>
79+
<div>
11980
<ArtistLinks artists={track.artists || [{id: 0, Name: 'Unknown Artist'}]}/>
12081
</div>
12182
<div className="color-fg-secondary">{track.listen_count} plays</div>
12283
</div>
123-
</div>
12484
</div>
12585
);
12686
}
12787
case "artist": {
12888
const artist = item as Artist;
12989
return (
13090
<div style={{fontSize: 12}}>
131-
<Link className={itemClasses+' mt-1 mb-[6px]'} to={`/artist/${artist.id}`}>
132-
<img src={imageUrl(artist.image, "small")} alt={artist.name} />
133-
<div>
134-
<span style={{fontSize: 14}}>{artist.name}</span>
135-
<div className="color-fg-secondary">{artist.listen_count} plays</div>
136-
</div>
91+
<Link className={itemClasses+' mt-1 mb-[6px] hover:text-(--color-fg-secondary)'} to={`/artist/${artist.id}`}>
92+
<img loading="lazy" src={imageUrl(artist.image, "small")} alt={artist.name} className="min-w-[48px]" />
93+
<div>
94+
<span style={{fontSize: 14}}>{artist.name}</span>
95+
<div className="color-fg-secondary">{artist.listen_count} plays</div>
96+
</div>
13797
</Link>
13898
</div>
13999
);

client/app/components/modals/EditModal.tsx renamed to client/app/components/modals/EditModal/EditModal.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { useQuery } from "@tanstack/react-query";
2-
import { createAlias, deleteAlias, getAliases, getAlbum, setPrimaryAlias, type Album, type Alias } from "api/api";
3-
import { Modal } from "./Modal";
4-
import { AsyncButton } from "../AsyncButton";
2+
import { createAlias, deleteAlias, getAliases, setPrimaryAlias, type Alias } from "api/api";
3+
import { Modal } from "../Modal";
4+
import { AsyncButton } from "../../AsyncButton";
55
import { useEffect, useState } from "react";
66
import { Trash } from "lucide-react";
77
import SetVariousArtists from "./SetVariousArtist";
8+
import SetPrimaryArtist from "./SetPrimaryArtist";
89

910
interface Props {
1011
type: string
@@ -18,7 +19,6 @@ export default function EditModal({ open, setOpen, type, id }: Props) {
1819
const [loading, setLoading ] = useState(false)
1920
const [err, setError ] = useState<string>()
2021
const [displayData, setDisplayData] = useState<Alias[]>([])
21-
const [variousArtists, setVariousArtists] = useState(false)
2222

2323
const { isPending, isError, data, error } = useQuery({
2424
queryKey: [
@@ -125,7 +125,10 @@ export default function EditModal({ open, setOpen, type, id }: Props) {
125125
</div>
126126
</div>
127127
{ type.toLowerCase() === "album" &&
128+
<>
128129
<SetVariousArtists id={id} />
130+
<SetPrimaryArtist id={id} type="album" />
131+
</>
129132
}
130133
</div>
131134
</Modal>
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { useQuery } from "@tanstack/react-query";
2+
import { getAlbum, type Artist } from "api/api";
3+
import { useEffect, useState } from "react"
4+
5+
interface Props {
6+
id: number
7+
type: string
8+
}
9+
10+
export default function SetPrimaryArtist({ id, type }: Props) {
11+
const [err, setErr] = useState('')
12+
const [primary, setPrimary] = useState<Artist>()
13+
const [success, setSuccess] = useState('')
14+
15+
const { isPending, isError, data, error } = useQuery({
16+
queryKey: [
17+
'get-artists-'+type.toLowerCase(),
18+
{
19+
id: id
20+
},
21+
],
22+
queryFn: () => {
23+
return fetch('/apis/web/v1/artists?'+type.toLowerCase()+'_id='+id).then(r => r.json()) as Promise<Artist[]>;
24+
},
25+
});
26+
27+
useEffect(() => {
28+
if (data) {
29+
for (let a of data) {
30+
if (a.is_primary) {
31+
setPrimary(a)
32+
break
33+
}
34+
}
35+
}
36+
}, [data])
37+
38+
if (isError) {
39+
return (
40+
<p className="error">Error: {error.message}</p>
41+
)
42+
}
43+
if (isPending) {
44+
return (
45+
<p>Loading...</p>
46+
)
47+
}
48+
49+
const updatePrimary = (artist: number, val: boolean) => {
50+
setErr('');
51+
setSuccess('');
52+
fetch(`/apis/web/v1/artists/primary?artist_id=${artist}&${type.toLowerCase()}_id=${id}&is_primary=${val}`, {
53+
method: 'POST',
54+
headers: {
55+
"Content-Type": "application/x-www-form-urlencoded"
56+
}
57+
})
58+
.then(r => {
59+
if (r.ok) {
60+
setSuccess('successfully updated primary artists');
61+
} else {
62+
r.json().then(r => setErr(r.error));
63+
}
64+
});
65+
}
66+
67+
return (
68+
<div className="w-full">
69+
<h2>Set Primary Artist</h2>
70+
<div className="flex flex-col gap-4">
71+
<select
72+
name="mark-various-artists"
73+
id="mark-various-artists"
74+
className="w-60 px-3 py-2 rounded-md"
75+
value={primary?.name || ""}
76+
onChange={(e) => {
77+
for (let a of data) {
78+
if (a.name === e.target.value) {
79+
setPrimary(a);
80+
updatePrimary(a.id, true);
81+
}
82+
}
83+
}}
84+
>
85+
<option value="" disabled>
86+
Select an artist
87+
</option>
88+
{data.map((a) => (
89+
<option key={a.id} value={a.name}>
90+
{a.name}
91+
</option>
92+
))}
93+
</select>
94+
{err && <p className="error">{err}</p>}
95+
{success && <p className="success">{success}</p>}
96+
</div>
97+
</div>
98+
);
99+
}

client/app/components/modals/SetVariousArtist.tsx renamed to client/app/components/modals/EditModal/SetVariousArtist.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ export default function SetVariousArtists({ id }: Props) {
7373
<option value="false">False</option>
7474
</select>
7575
{err && <p className="error">{err}</p>}
76+
{success && <p className="success">{success}</p>}
7677
</div>
7778
</div>
7879
)

client/app/components/modals/MergeModal.tsx

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,11 @@ export default function MergeModal(props: Props) {
3434
}
3535

3636
const toggleSelect = ({title, id}: {title: string, id: number}) => {
37-
if (mergeTarget.id === 0) {
38-
setMergeTarget({title: title, id: id})
39-
} else {
40-
setMergeTarget({title:"", id: 0})
41-
}
37+
setMergeTarget({title: title, id: id})
4238
}
4339

4440
useEffect(() => {
45-
console.log(mergeTarget)
41+
console.log("mergeTarget",mergeTarget)
4642
}, [mergeTarget])
4743

4844
const doMerge = () => {

client/app/routes/Home.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export default function Home() {
3333
<TopArtists period={period} limit={homeItems} />
3434
<TopAlbums period={period} limit={homeItems} />
3535
<TopTracks period={period} limit={homeItems} />
36-
<LastPlays limit={Math.floor(homeItems * 2.5)} />
36+
<LastPlays limit={Math.floor(homeItems * 2.7)} />
3737
</div>
3838
</div>
3939
</main>

client/app/routes/MediaItems/MediaLayout.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import { useAppContext } from "~/providers/AppProvider";
77
import MergeModal from "~/components/modals/MergeModal";
88
import ImageReplaceModal from "~/components/modals/ImageReplaceModal";
99
import DeleteModal from "~/components/modals/DeleteModal";
10-
import RenameModal from "~/components/modals/EditModal";
11-
import EditModal from "~/components/modals/EditModal";
10+
import RenameModal from "~/components/modals/EditModal/EditModal";
11+
import EditModal from "~/components/modals/EditModal/EditModal";
1212

1313
export type MergeFunc = (from: number, to: number, replaceImage: boolean) => Promise<Response>
1414
export type MergeSearchCleanerFunc = (r: SearchResponse, id: number) => SearchResponse
@@ -69,9 +69,9 @@ export default function MediaLayout(props: Props) {
6969
content={title}
7070
/>
7171
<div className="w-19/20 mx-auto pt-12">
72-
<div className="flex gap-8 flex-wrap relative">
72+
<div className="flex gap-8 flex-wrap md:flex-nowrap relative">
7373
<div className="flex flex-col justify-around">
74-
<img style={{zIndex: 5}} src={imageUrl(props.img, "large")} alt={props.title} className="md:w-sm w-[220px] h-auto shadow-(--color-shadow) shadow-lg" />
74+
<img style={{zIndex: 5}} src={imageUrl(props.img, "large")} alt={props.title} className="md:min-w-[385px] w-[220px] h-auto shadow-(--color-shadow) shadow-lg" />
7575
</div>
7676
<div className="flex flex-col items-start">
7777
<h3>{props.type}</h3>

0 commit comments

Comments
 (0)