feat(webui): alphabetical navigation for libraries

closes #186
This commit is contained in:
Gauthier Roebroeck 2021-07-21 11:13:37 +08:00
parent 9fc98ed9c1
commit 5d747d2cd3
4 changed files with 165 additions and 28 deletions

View file

@ -0,0 +1,63 @@
<template>
<div>
<v-tooltip
v-for="symbol in symbols"
:key="symbol"
:disabled="groupCount === undefined"
top
>
<template v-slot:activator="{ on }">
<v-btn
text
small
icon
@click="clicked(symbol)"
:color="selected === symbol ? 'secondary' : undefined"
:disabled="groupCount ? getCount(symbol) === 0 : false"
v-on="on"
>
<!-- <v-icon>mdi-alpha-{{ symbol.toLowerCase() }}</v-icon>-->
{{ symbol }}
</v-btn>
</template>
{{ getCount(symbol) }}
</v-tooltip>
</div>
</template>
<script lang="ts">
import Vue, {PropType} from 'vue'
import {GroupCountDto} from '@/types/komga-series'
export default Vue.extend({
name: 'AlphabeticalNavigation',
data: function () {
return {}
},
props: {
groupCount: {
type: Array as PropType<GroupCountDto[]>,
required: false,
},
symbols: {
type: Array,
default: () => ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'],
},
selected: {
type: String,
required: false,
},
},
methods: {
clicked(symbol: string) {
this.$emit('clicked', symbol)
},
getCount(symbol: string): number | undefined {
if(!this.groupCount) return undefined
const found = this.groupCount.find(g => g.group.toLowerCase() === symbol.toLowerCase())
return found ? found.count : 0
},
},
})
</script>

View file

@ -1,6 +1,6 @@
import {AxiosInstance} from 'axios'
import {AuthorDto, BookDto} from '@/types/komga-books'
import {SeriesDto, SeriesMetadataUpdateDto} from '@/types/komga-series'
import {GroupCountDto, SeriesDto, SeriesMetadataUpdateDto} from '@/types/komga-series'
const qs = require('qs')
@ -9,17 +9,19 @@ const API_SERIES = '/api/v1/series'
export default class KomgaSeriesService {
private http: AxiosInstance
constructor (http: AxiosInstance) {
constructor(http: AxiosInstance) {
this.http = http
}
async getSeries (libraryId?: string, pageRequest?: PageRequest, search?: string, status?: string[],
readStatus?: string[], genre?: string[], tag?: string[], language?: string[],
publisher?: string[], ageRating?: string[], releaseDate?: string[], authors?: AuthorDto[]): Promise<Page<SeriesDto>> {
async getSeries(libraryId?: string, pageRequest?: PageRequest, search?: string, status?: string[],
readStatus?: string[], genre?: string[], tag?: string[], language?: string[],
publisher?: string[], ageRating?: string[], releaseDate?: string[], authors?: AuthorDto[],
searchRegex?: string): Promise<Page<SeriesDto>> {
try {
const params = { ...pageRequest } as any
const params = {...pageRequest} as any
if (libraryId) params.library_id = libraryId
if (search) params.search = search
if (searchRegex) params.search_regex = searchRegex
if (status) params.status = status
if (readStatus) params.read_status = readStatus
if (genre) params.genre = genre
@ -32,7 +34,7 @@ export default class KomgaSeriesService {
return (await this.http.get(API_SERIES, {
params: params,
paramsSerializer: params => qs.stringify(params, { indices: false }),
paramsSerializer: params => qs.stringify(params, {indices: false}),
})).data
} catch (e) {
let msg = 'An error occurred while trying to retrieve series'
@ -43,9 +45,39 @@ export default class KomgaSeriesService {
}
}
async getNewSeries (libraryId?: string, pageRequest?: PageRequest): Promise<Page<SeriesDto>> {
async getAlphabeticalGroups(libraryId?: string, search?: string, status?: string[],
readStatus?: string[], genre?: string[], tag?: string[], language?: string[],
publisher?: string[], ageRating?: string[], releaseDate?: string[], authors?: AuthorDto[]): Promise<GroupCountDto[]> {
try {
const params = { ...pageRequest } as any
const params = {} as any
if (libraryId) params.library_id = libraryId
if (search) params.search = search
if (status) params.status = status
if (readStatus) params.read_status = readStatus
if (genre) params.genre = genre
if (tag) params.tag = tag
if (language) params.language = language
if (publisher) params.publisher = publisher
if (ageRating) params.age_rating = ageRating
if (releaseDate) params.release_year = releaseDate
if (authors) params.author = authors.map(a => `${a.name},${a.role}`)
return (await this.http.get(`${API_SERIES}/alphabetical-groups`, {
params: params,
paramsSerializer: params => qs.stringify(params, {indices: false}),
})).data
} catch (e) {
let msg = 'An error occurred while trying to retrieve series alphabetical groups'
if (e.response.data.message) {
msg += `: ${e.response.data.message}`
}
throw new Error(msg)
}
}
async getNewSeries(libraryId?: string, pageRequest?: PageRequest): Promise<Page<SeriesDto>> {
try {
const params = {...pageRequest} as any
if (libraryId) {
params.library_id = libraryId
}
@ -61,9 +93,9 @@ export default class KomgaSeriesService {
}
}
async getUpdatedSeries (libraryId?: string, pageRequest?: PageRequest): Promise<Page<SeriesDto>> {
async getUpdatedSeries(libraryId?: string, pageRequest?: PageRequest): Promise<Page<SeriesDto>> {
try {
const params = { ...pageRequest } as any
const params = {...pageRequest} as any
if (libraryId) {
params.library_id = libraryId
}
@ -79,7 +111,7 @@ export default class KomgaSeriesService {
}
}
async getOneSeries (seriesId: string): Promise<SeriesDto> {
async getOneSeries(seriesId: string): Promise<SeriesDto> {
try {
return (await this.http.get(`${API_SERIES}/${seriesId}`)).data
} catch (e) {
@ -91,16 +123,16 @@ export default class KomgaSeriesService {
}
}
async getBooks (seriesId: string, pageRequest?: PageRequest, readStatus?: string[], tag?: string[], authors?: AuthorDto[]): Promise<Page<BookDto>> {
async getBooks(seriesId: string, pageRequest?: PageRequest, readStatus?: string[], tag?: string[], authors?: AuthorDto[]): Promise<Page<BookDto>> {
try {
const params = { ...pageRequest } as any
const params = {...pageRequest} as any
if (readStatus) params.read_status = readStatus
if (tag) params.tag = tag
if (authors) params.author = authors.map(a => `${a.name},${a.role}`)
return (await this.http.get(`${API_SERIES}/${seriesId}/books`, {
params: params,
paramsSerializer: params => qs.stringify(params, { indices: false }),
paramsSerializer: params => qs.stringify(params, {indices: false}),
})).data
} catch (e) {
let msg = 'An error occurred while trying to retrieve books'
@ -111,7 +143,7 @@ export default class KomgaSeriesService {
}
}
async getCollections (seriesId: string): Promise<CollectionDto[]> {
async getCollections(seriesId: string): Promise<CollectionDto[]> {
try {
return (await this.http.get(`${API_SERIES}/${seriesId}/collections`)).data
} catch (e) {
@ -123,7 +155,7 @@ export default class KomgaSeriesService {
}
}
async analyzeSeries (series: SeriesDto) {
async analyzeSeries(series: SeriesDto) {
try {
await this.http.post(`${API_SERIES}/${series.id}/analyze`)
} catch (e) {
@ -135,7 +167,7 @@ export default class KomgaSeriesService {
}
}
async refreshMetadata (series: SeriesDto) {
async refreshMetadata(series: SeriesDto) {
try {
await this.http.post(`${API_SERIES}/${series.id}/metadata/refresh`)
} catch (e) {
@ -147,7 +179,7 @@ export default class KomgaSeriesService {
}
}
async updateMetadata (seriesId: string, metadata: SeriesMetadataUpdateDto) {
async updateMetadata(seriesId: string, metadata: SeriesMetadataUpdateDto) {
try {
await this.http.patch(`${API_SERIES}/${seriesId}/metadata`, metadata)
} catch (e) {
@ -159,7 +191,7 @@ export default class KomgaSeriesService {
}
}
async markAsRead (seriesId: string) {
async markAsRead(seriesId: string) {
try {
await this.http.post(`${API_SERIES}/${seriesId}/read-progress`)
} catch (e) {
@ -171,7 +203,7 @@ export default class KomgaSeriesService {
}
}
async markAsUnread (seriesId: string) {
async markAsUnread(seriesId: string) {
try {
await this.http.delete(`${API_SERIES}/${seriesId}/read-progress`)
} catch (e) {

View file

@ -71,3 +71,8 @@ export interface SeriesMetadataUpdateDto {
tags?: String[],
tagsLock?: boolean
}
export interface GroupCountDto {
group: string,
count: number,
}

View file

@ -86,6 +86,14 @@
/>
<template v-if="totalPages > 0">
<alphabetical-navigation
class="text-center"
:symbols="alphabeticalNavigation"
:selected="selectedSymbol"
:group-count="seriesGroups"
@clicked="filterByStarting"
/>
<v-pagination
v-if="totalPages > 1"
v-model="page"
@ -139,15 +147,17 @@ import SortList from '@/components/SortList.vue'
import FilterPanels from '@/components/FilterPanels.vue'
import FilterList from '@/components/FilterList.vue'
import {mergeFilterParams, sortOrFilterActive, toNameValue} from '@/functions/filter'
import {SeriesDto} from '@/types/komga-series'
import {GroupCountDto, SeriesDto} from '@/types/komga-series'
import {AuthorDto} from '@/types/komga-books'
import {authorRoles} from '@/types/author-roles'
import {LibrarySseDto, ReadProgressSeriesSseDto, SeriesSseDto} from '@/types/komga-sse'
import {throttle} from 'lodash'
import AlphabeticalNavigation from '@/components/AlphabeticalNavigation.vue'
export default Vue.extend({
name: 'BrowseLibraries',
components: {
AlphabeticalNavigation,
LibraryActionsMenu,
EmptyState,
ToolbarSticky,
@ -164,6 +174,10 @@ export default Vue.extend({
return {
library: undefined as LibraryDto | undefined,
series: [] as SeriesDto[],
seriesGroups: [] as GroupCountDto[],
alphabeticalNavigation: ['ALL', '#', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'],
searchRegex: undefined as any,
selectedSymbol: 'ALL',
selectedSeries: [] as SeriesDto[],
page: 1,
pageSize: 20,
@ -234,6 +248,9 @@ export default Vue.extend({
this.totalPages = 1
this.totalElements = null
this.series = []
this.seriesGroups = []
this.selectedSymbol = 'ALL'
this.searchRegex = undefined
this.loadLibrary(to.params.libraryId)
@ -313,6 +330,15 @@ export default Vue.extend({
},
},
methods: {
filterByStarting(symbol: string) {
if (symbol === 'ALL') this.searchRegex = undefined
else if (symbol === '#') this.searchRegex = '^[^a-z],title_sort'
else this.searchRegex = `^${symbol},title_sort`
this.selectedSymbol = symbol
this.page = 1
this.loadPage(this.libraryId, 1, this.sortActive, this.searchRegex)
},
resetSortAndFilters() {
this.drawer = false
for (const prop in this.filters) {
@ -341,7 +367,7 @@ export default Vue.extend({
])
this.$set(this.filterOptions, 'genre', toNameValue(genres))
this.$set(this.filterOptions, 'tag', toNameValue(tags))
this.$set(this.filterOptions, 'publisher', toNameValue(publishers ))
this.$set(this.filterOptions, 'publisher', toNameValue(publishers))
this.$set(this.filterOptions, 'language', (languages))
this.$set(this.filterOptions, 'ageRating', toNameValue(ageRatings))
this.$set(this.filterOptions, 'releaseDate', toNameValue(releaseDates))
@ -406,7 +432,7 @@ export default Vue.extend({
this.pageUnwatch = this.$watch('page', (val) => {
this.updateRoute()
this.loadPage(this.libraryId, val, this.sortActive)
this.loadPage(this.libraryId, val, this.sortActive, this.searchRegex)
})
},
unsetWatches() {
@ -421,7 +447,7 @@ export default Vue.extend({
this.page = 1
this.updateRoute()
this.loadPage(this.libraryId, this.page, this.sortActive)
this.loadPage(this.libraryId, this.page, this.sortActive, this.searchRegex)
this.setWatches()
},
@ -441,7 +467,7 @@ export default Vue.extend({
async loadLibrary(libraryId: string) {
this.library = this.getLibraryLazy(libraryId)
await this.loadPage(libraryId, this.page, this.sortActive)
await this.loadPage(libraryId, this.page, this.sortActive, this.searchRegex)
},
updateRoute() {
const loc = {
@ -460,7 +486,7 @@ export default Vue.extend({
reloadPage: throttle(function (this: any) {
this.loadPage(this.libraryId, this.page, this.sortActive)
}, 1000),
async loadPage(libraryId: string, page: number, sort: SortActive) {
async loadPage(libraryId: string, page: number, sort: SortActive, searchRegex?: string) {
this.selectedSeries = []
const pageRequest = {
@ -481,11 +507,22 @@ export default Vue.extend({
})
const requestLibraryId = libraryId !== LIBRARIES_ALL ? libraryId : undefined
const seriesPage = await this.$komgaSeries.getSeries(requestLibraryId, pageRequest, undefined, this.filters.status, replaceCompositeReadStatus(this.filters.readStatus), this.filters.genre, this.filters.tag, this.filters.language, this.filters.publisher, this.filters.ageRating, this.filters.releaseDate, authorsFilter)
const seriesPage = await this.$komgaSeries.getSeries(requestLibraryId, pageRequest, undefined, this.filters.status, replaceCompositeReadStatus(this.filters.readStatus), this.filters.genre, this.filters.tag, this.filters.language, this.filters.publisher, this.filters.ageRating, this.filters.releaseDate, authorsFilter, searchRegex)
this.totalPages = seriesPage.totalPages
this.totalElements = seriesPage.totalElements
this.series = seriesPage.content
const seriesGroups = await this.$komgaSeries.getAlphabeticalGroups(requestLibraryId, undefined, this.filters.status, replaceCompositeReadStatus(this.filters.readStatus), this.filters.genre, this.filters.tag, this.filters.language, this.filters.publisher, this.filters.ageRating, this.filters.releaseDate, authorsFilter)
const nonAlpha = seriesGroups
.filter((g) => !(/[a-zA-Z]/).test(g.group))
.reduce((a, b) => a + b.count, 0)
const all = seriesGroups.reduce((a, b) => a + b.count, 0)
this.seriesGroups = [
...seriesGroups.filter((g) => (/[a-zA-Z]/).test(g.group)),
{group: '#', count: nonAlpha} as GroupCountDto,
{group: 'ALL', count: all} as GroupCountDto,
]
},
getLibraryLazy(libraryId: string): LibraryDto | undefined {
if (libraryId !== LIBRARIES_ALL) {