mirror of
https://github.com/gabehf/Koito.git
synced 2026-04-22 12:01:52 -07:00
feat: add minutes listened to ui and fix image drop
This commit is contained in:
parent
0042e3f3bd
commit
d84519fa21
11 changed files with 39 additions and 22 deletions
|
|
@ -1,7 +1,12 @@
|
||||||
# v0.1.0
|
# v0.1.0
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
- You can now search and merge items by their ID! Just preface the id with `id:`. E.g. `id:123` (#26)
|
|
||||||
|
|
||||||
## Enhancements
|
## Enhancements
|
||||||
- Track durations will now be updated using MusicBrainz data where possible, if the duration was not provided by the request. (#27)
|
- Track durations will now be updated using MusicBrainz data where possible, if the duration was not provided by the request. (#27)
|
||||||
|
- You can now search and merge items by their ID! Just preface the id with `id:`. E.g. `id:123` (#26)
|
||||||
|
- Hovering over any "hours listened" statistic will now also show the minutes listened.
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
- Navigating from one page directly to another and then changing the image via drag-and-drop now works as expected. (#25)
|
||||||
|
- Fixed a bug that caused updated usernames with uppercase letters to create login failures.
|
||||||
9
Makefile
9
Makefile
|
|
@ -12,6 +12,9 @@ postgres.schemadump:
|
||||||
postgres.run:
|
postgres.run:
|
||||||
docker run --name koito-db -p 5432:5432 -e POSTGRES_PASSWORD=secret -d postgres
|
docker run --name koito-db -p 5432:5432 -e POSTGRES_PASSWORD=secret -d postgres
|
||||||
|
|
||||||
|
postgres.run-scratch:
|
||||||
|
docker run --name koito-scratch -p 5433:5432 -e POSTGRES_PASSWORD=secret -d postgres
|
||||||
|
|
||||||
postgres.start:
|
postgres.start:
|
||||||
docker start koito-db
|
docker start koito-db
|
||||||
|
|
||||||
|
|
@ -21,9 +24,15 @@ postgres.stop:
|
||||||
postgres.remove:
|
postgres.remove:
|
||||||
docker stop koito-db && docker rm koito-db
|
docker stop koito-db && docker rm koito-db
|
||||||
|
|
||||||
|
postgres.remove-scratch:
|
||||||
|
docker stop koito-scratch && docker rm koito-scratch
|
||||||
|
|
||||||
api.debug:
|
api.debug:
|
||||||
KOITO_ALLOWED_HOSTS=* KOITO_LOG_LEVEL=debug KOITO_CONFIG_DIR=test_config_dir KOITO_DATABASE_URL=postgres://postgres:secret@localhost:5432?sslmode=disable go run cmd/api/main.go
|
KOITO_ALLOWED_HOSTS=* KOITO_LOG_LEVEL=debug KOITO_CONFIG_DIR=test_config_dir KOITO_DATABASE_URL=postgres://postgres:secret@localhost:5432?sslmode=disable go run cmd/api/main.go
|
||||||
|
|
||||||
|
api.scratch:
|
||||||
|
KOITO_ALLOWED_HOSTS=* KOITO_LOG_LEVEL=debug KOITO_CONFIG_DIR=test_config_dir/scratch KOITO_DATABASE_URL=postgres://postgres:secret@localhost:5433?sslmode=disable go run cmd/api/main.go
|
||||||
|
|
||||||
api.test:
|
api.test:
|
||||||
go test ./... -timeout 60s
|
go test ./... -timeout 60s
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -288,7 +288,7 @@ type Stats = {
|
||||||
track_count: number
|
track_count: number
|
||||||
album_count: number
|
album_count: number
|
||||||
artist_count: number
|
artist_count: number
|
||||||
hours_listened: number
|
minutes_listened: number
|
||||||
}
|
}
|
||||||
type SearchResponse = {
|
type SearchResponse = {
|
||||||
albums: Album[]
|
albums: Album[]
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ export default function AllTimeStats() {
|
||||||
<div>
|
<div>
|
||||||
<h2>All Time Stats</h2>
|
<h2>All Time Stats</h2>
|
||||||
<div>
|
<div>
|
||||||
<span className={numberClasses}>{data.hours_listened}</span> Hours Listened
|
<span className={numberClasses} title={data.minutes_listened + " minutes"}>{Math.floor(data.minutes_listened / 60)}</span> Hours Listened
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className={numberClasses}>{data.listen_count}</span> Plays
|
<span className={numberClasses}>{data.listen_count}</span> Plays
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,10 @@ import { useEffect } from 'react';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
itemType: string,
|
itemType: string,
|
||||||
id: number,
|
|
||||||
onComplete: Function
|
onComplete: Function
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ImageDropHandler({ itemType, id, onComplete }: Props) {
|
export default function ImageDropHandler({ itemType, onComplete }: Props) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleDragOver = (e: DragEvent) => {
|
const handleDragOver = (e: DragEvent) => {
|
||||||
console.log('dragover!!')
|
console.log('dragover!!')
|
||||||
|
|
@ -25,7 +24,11 @@ export default function ImageDropHandler({ itemType, id, onComplete }: Props) {
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('image', imageFile);
|
formData.append('image', imageFile);
|
||||||
formData.append(itemType.toLowerCase()+'_id', String(id))
|
const pathname = window.location.pathname;
|
||||||
|
const segments = pathname.split('/');
|
||||||
|
const filteredSegments = segments.filter(segment => segment !== '');
|
||||||
|
const lastSegment = filteredSegments[filteredSegments.length - 1];
|
||||||
|
formData.append(itemType.toLowerCase()+'_id', lastSegment)
|
||||||
replaceImage(formData).then((r) => {
|
replaceImage(formData).then((r) => {
|
||||||
if (r.status >= 200 && r.status < 300) {
|
if (r.status >= 200 && r.status < 300) {
|
||||||
onComplete()
|
onComplete()
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ export default function Album() {
|
||||||
}}
|
}}
|
||||||
subContent={<div className="flex flex-col gap-2 items-start">
|
subContent={<div className="flex flex-col gap-2 items-start">
|
||||||
{album.listen_count && <p>{album.listen_count} play{ album.listen_count > 1 ? 's' : ''}</p>}
|
{album.listen_count && <p>{album.listen_count} play{ album.listen_count > 1 ? 's' : ''}</p>}
|
||||||
{<p>{timeListenedString(album.time_listened)}</p>}
|
{<p title={Math.floor(album.time_listened / 60) + " minutes"}>{timeListenedString(album.time_listened)}</p>}
|
||||||
</div>}
|
</div>}
|
||||||
>
|
>
|
||||||
<div className="mt-10">
|
<div className="mt-10">
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ export default function Artist() {
|
||||||
}}
|
}}
|
||||||
subContent={<div className="flex flex-col gap-2 items-start">
|
subContent={<div className="flex flex-col gap-2 items-start">
|
||||||
{artist.listen_count && <p>{artist.listen_count} play{ artist.listen_count > 1 ? 's' : ''}</p>}
|
{artist.listen_count && <p>{artist.listen_count} play{ artist.listen_count > 1 ? 's' : ''}</p>}
|
||||||
{<p>{timeListenedString(artist.time_listened)}</p>}
|
{<p title={Math.floor(artist.time_listened / 60) + " minutes"}>{timeListenedString(artist.time_listened)}</p>}
|
||||||
</div>}
|
</div>}
|
||||||
>
|
>
|
||||||
<div className="mt-10">
|
<div className="mt-10">
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ export default function MediaLayout(props: Props) {
|
||||||
transition: '1000',
|
transition: '1000',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ImageDropHandler itemType={props.type.toLowerCase() === 'artist' ? 'artist' : 'album'} id={props.imgItemId} onComplete={replaceImageCallback} />
|
<ImageDropHandler itemType={props.type.toLowerCase() === 'artist' ? 'artist' : 'album'} onComplete={replaceImageCallback} />
|
||||||
<title>{title}</title>
|
<title>{title}</title>
|
||||||
<meta property="og:title" content={title} />
|
<meta property="og:title" content={title} />
|
||||||
<meta
|
<meta
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ export default function Track() {
|
||||||
subContent={<div className="flex flex-col gap-2 items-start">
|
subContent={<div className="flex flex-col gap-2 items-start">
|
||||||
<Link to={`/album/${track.album_id}`}>appears on {album.title}</Link>
|
<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>}
|
{track.listen_count && <p>{track.listen_count} play{ track.listen_count > 1 ? 's' : ''}</p>}
|
||||||
{<p>{timeListenedString(track.time_listened)}</p>}
|
{<p title={Math.floor(track.time_listened / 60) + " minutes"}>{timeListenedString(track.time_listened)}</p>}
|
||||||
</div>}
|
</div>}
|
||||||
>
|
>
|
||||||
<div className="mt-10">
|
<div className="mt-10">
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ type StatsResponse struct {
|
||||||
TrackCount int64 `json:"track_count"`
|
TrackCount int64 `json:"track_count"`
|
||||||
AlbumCount int64 `json:"album_count"`
|
AlbumCount int64 `json:"album_count"`
|
||||||
ArtistCount int64 `json:"artist_count"`
|
ArtistCount int64 `json:"artist_count"`
|
||||||
HoursListened int64 `json:"hours_listened"`
|
MinutesListened int64 `json:"minutes_listened"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func StatsHandler(store db.DB) http.HandlerFunc {
|
func StatsHandler(store db.DB) http.HandlerFunc {
|
||||||
|
|
@ -83,7 +83,7 @@ func StatsHandler(store db.DB) http.HandlerFunc {
|
||||||
TrackCount: tracks,
|
TrackCount: tracks,
|
||||||
AlbumCount: albums,
|
AlbumCount: albums,
|
||||||
ArtistCount: artists,
|
ArtistCount: artists,
|
||||||
HoursListened: timeListenedS / 60 / 60,
|
MinutesListened: timeListenedS / 60,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -447,7 +447,7 @@ func TestStats(t *testing.T) {
|
||||||
assert.EqualValues(t, 3, actual.TrackCount)
|
assert.EqualValues(t, 3, actual.TrackCount)
|
||||||
assert.EqualValues(t, 3, actual.AlbumCount)
|
assert.EqualValues(t, 3, actual.AlbumCount)
|
||||||
assert.EqualValues(t, 3, actual.ArtistCount)
|
assert.EqualValues(t, 3, actual.ArtistCount)
|
||||||
assert.EqualValues(t, 0, actual.HoursListened)
|
assert.EqualValues(t, 11, actual.MinutesListened)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestListenActivity(t *testing.T) {
|
func TestListenActivity(t *testing.T) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue