1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157 import { ref, watch } from 'vue'
import { useRoute } from 'vue-router'
import { orderBy } from 'lodash-es'
import type { Album, Genre } from '../types'
import { useSubsonicApi } from '../subsonicApi'
import { useMainStore } from '../store/main'
import { usePlayerStore } from '../store/player'
import { useRadioStore } from '../store/radio'
import { AlbumList } from '../components/AlbumList.vine'
interface GenreWithAlbums {
id: string
name: string
albumCount: number
albums: Album[]
}
export const GenreView = ({ id }: { id: string }) => {
const
route = useRoute(),
subsonicApi = useSubsonicApi(),
mainStore = useMainStore(),
playerStore = usePlayerStore(),
radioStore = useRadioStore(),
sort = ref<string>('a-z'),
plainGenres = ref<Genre[]>([]),
genres = ref<GenreWithAlbums[]>([]),
albums = ref<Album[]>([]),
hasMore = ref<boolean>(true),
shuffleNow = () => (!!id && radioStore.shuffleGenre(id)),
fetchGenres = async () => {
mainStore.isLoading = true
genres.value = []
try {
const response = await subsonicApi.getGenres()
plainGenres.value = (
(sort.value !== 'a-z')
? orderBy(response, 'albumCount', 'desc')
: orderBy(response, 'name')
)
fetchGenreAlbums()
} catch (error) {
console.error('Failed to load genres or albums:', error)
} finally {
mainStore.isLoading = false
}
},
fetchGenreAlbums = async () => {
try {
const numCurrentItems = genres.value.length
mainStore.isLoading = true
Promise.all(
plainGenres.value.slice(
numCurrentItems,
numCurrentItems + 5
)
.map(async (genre: Genre) => {
return {
id: genre.id,
name: genre.name,
albumCount: genre.albumCount,
albums: await subsonicApi.getAlbumsByGenre(genre.id, 20, 0, true),
} as GenreWithAlbums
})
).then(
result => genres.value.push(...result)
)
hasMore.value = genres.value.length < plainGenres.value.length
} finally {
mainStore.isLoading = false
}
},
fetchAlbums = async() => {
try {
mainStore.isLoading = true
const newAlbums = await subsonicApi.getAlbumsByGenre(id, 30, albums.value.length)
albums.value.push(...newAlbums)
hasMore.value = !!newAlbums.length
} catch (error) {
console.log(error)
} finally {
mainStore.isLoading = false
}
}
watch(
() => [ id, sort.value ],
async () => {
if (!id) {
fetchGenres()
} else {
albums.value = []
fetchAlbums()
}
},
{ immediate: true }
)
return vine`
<template v-if="!id">
<div class="row align-items-center justify-content-space-between">
<span class="main-title">
<Icon icon="genres" />
Genres
</span>
<div>
<select v-model="sort" name="sort" title="Sort by...">
<option value="most">Most albums</option>
<option value="a-z">Alphabetically</option>
</select>
</div>
</div>
<div v-for="genre in genres" :key="genre.id">
<div class="row align-items-center justify-content-space-between">
<router-link class="section-title" :to="{ name: 'genre', params: { id: genre.id } }">
{{ genre.name }} - <small>{{ genre.albumCount }} Albums</small>
</router-link>
<button data-tooltip="Genre Radio" @click="radio.shuffleGenre(genre.id)">
<Icon icon="radio" />
</button>
</div>
<AlbumList :items="genre.albums" tile-size="100" two-rows allow-h-scroll />
</div>
<InfiniteLoader :is-loading="mainStore.isLoading" :has-more="hasMore" @load-more="fetchGenreAlbums" />
</template>
<template v-else>
<div class="row align-items-center">
<div class="main-title flex-grow">{{ id }}</div>
<button class="title-color" data-tooltip="Genre Radio" @click="shuffleNow">
<Icon icon="radio" />
</button>
</div>
<AlbumList :items="albums" />
<InfiniteLoader :is-loading="mainStore.isLoading" :has-more="hasMore" @load-more="fetchAlbums" />
</template>
`
}