frontend: make manga tables sortable
This commit is contained in:
@@ -19,6 +19,7 @@ export type ViewEntry = {
|
|||||||
relation: MangaUpdatesRelation | null,
|
relation: MangaUpdatesRelation | null,
|
||||||
series: MangaUpdatesSeries | null,
|
series: MangaUpdatesSeries | null,
|
||||||
chapters: MangaUpdatesChapter[],
|
chapters: MangaUpdatesChapter[],
|
||||||
|
newChapters: number,
|
||||||
};
|
};
|
||||||
|
|
||||||
@Options({
|
@Options({
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import {Options, Vue} from 'vue-class-component';
|
|||||||
import {Prop, Watch} from 'vue-property-decorator';
|
import {Prop, Watch} from 'vue-property-decorator';
|
||||||
import {BTable, type TableItem} from 'bootstrap-vue-next';
|
import {BTable, type TableItem} from 'bootstrap-vue-next';
|
||||||
//@ts-ignore TS2307
|
//@ts-ignore TS2307
|
||||||
import type {TableFieldObject} from 'bootstrap-vue-next/dist/src/types';
|
import type {TableField, TableFieldObject} from 'bootstrap-vue-next/dist/src/types';
|
||||||
import type {ViewEntry, ViewList} from '@/components/manga/MangaList.vue';
|
import type {ViewEntry, ViewList} from '@/components/manga/MangaList.vue';
|
||||||
import MangaEntryDetailsModal from '@/components/manga/MangaEntryDetailsModal.vue';
|
import MangaEntryDetailsModal from '@/components/manga/MangaEntryDetailsModal.vue';
|
||||||
import {latestChaptersSorted, latestChapterString, newChapterCount} from '@/components/manga/util.manga';
|
import {latestChaptersSorted, latestChapterString, newChapterCount} from '@/components/manga/util.manga';
|
||||||
@@ -34,6 +34,8 @@ export default class MangaListTable extends Vue {
|
|||||||
readonly viewList!: ViewList;
|
readonly viewList!: ViewList;
|
||||||
|
|
||||||
bTableRefreshHack = true;
|
bTableRefreshHack = true;
|
||||||
|
sortKey: string | null = null;
|
||||||
|
sortAsc: boolean = false;
|
||||||
|
|
||||||
//methods
|
//methods
|
||||||
latestChaptersSorted = latestChaptersSorted;
|
latestChaptersSorted = latestChaptersSorted;
|
||||||
@@ -44,36 +46,36 @@ export default class MangaListTable extends Vue {
|
|||||||
return [{
|
return [{
|
||||||
key: 'media.coverImage.large',
|
key: 'media.coverImage.large',
|
||||||
label: '',
|
label: '',
|
||||||
sortable: true,
|
sortable: false,
|
||||||
tdClass: 'c-pointer',
|
tdClass: 'c-pointer',
|
||||||
}, {
|
}, {
|
||||||
key: 'media.title.userPreferred',
|
key: 'media.title.userPreferred',
|
||||||
label: this.$t('manga.title'),
|
label: this.$t('manga.title'),
|
||||||
sortable: true,
|
sortable: true,
|
||||||
tdClass: 'c-pointer',
|
tdClass: 'c-pointer',
|
||||||
|
thClass: 'c-pointer',
|
||||||
}, {
|
}, {
|
||||||
key: 'entry.score',
|
key: 'entry.score',
|
||||||
label: this.$t('manga.score'),
|
label: this.$t('manga.score'),
|
||||||
sortable: true,
|
sortable: true,
|
||||||
tdClass: 'text-center manga-column-score c-pointer',
|
tdClass: 'text-center manga-column-score c-pointer',
|
||||||
thClass: 'text-center manga-column-score',
|
thClass: 'text-center manga-column-score c-pointer',
|
||||||
}, {
|
}, {
|
||||||
key: 'entry.progress',
|
key: 'entry.progress',
|
||||||
label: this.$t('manga.progress'),
|
label: this.$t('manga.progress'),
|
||||||
sortable: true,
|
sortable: true,
|
||||||
tdClass: 'text-end text-nowrap c-pointer',
|
tdClass: 'text-end text-nowrap c-pointer',
|
||||||
thClass: 'text-end',
|
thClass: 'text-end c-pointer',
|
||||||
}, {
|
}, {
|
||||||
key: 'newChapters',
|
key: 'newChapters',
|
||||||
formatter: (value: never, key: never, item: ViewEntry) => this.newChapterCount(item),
|
|
||||||
label: this.$t('manga.chapters.newCount'),
|
label: this.$t('manga.chapters.newCount'),
|
||||||
sortable: true,
|
sortable: true,
|
||||||
tdClass: 'text-end c-pointer',
|
tdClass: 'text-end c-pointer',
|
||||||
thClass: 'text-end',
|
thClass: 'text-end c-pointer',
|
||||||
}, {
|
}, {
|
||||||
key: 'latestChapters',
|
key: 'latestChapters',
|
||||||
label: this.$t('manga.chapters.latest'),
|
label: this.$t('manga.chapters.latest'),
|
||||||
sortable: true,
|
sortable: false,
|
||||||
tdClass: 'manga-column-latest-chapters c-pointer',
|
tdClass: 'manga-column-latest-chapters c-pointer',
|
||||||
thClass: 'manga-column-latest-chapters',
|
thClass: 'manga-column-latest-chapters',
|
||||||
}];
|
}];
|
||||||
@@ -83,6 +85,34 @@ export default class MangaListTable extends Vue {
|
|||||||
return this.viewList.entries;
|
return this.viewList.entries;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get tableEntriesSorted(): ViewEntry[] {
|
||||||
|
if (!this.sortKey) {
|
||||||
|
return this.tableEntries;
|
||||||
|
}
|
||||||
|
const keyExtractor = (e: ViewEntry) => eval('e.' + this.sortKey);//TODO eval is evil
|
||||||
|
const comparer = (l: ViewEntry, r: ViewEntry) => {
|
||||||
|
const lkey = keyExtractor(l);
|
||||||
|
const rkey = keyExtractor(r);
|
||||||
|
if ([null, undefined].includes(lkey) && [null, undefined].includes(lkey)) {
|
||||||
|
return 0;
|
||||||
|
} else if ([null, undefined].includes(lkey) && ![null, undefined].includes(lkey)) {
|
||||||
|
return -1;
|
||||||
|
} else if (![null, undefined].includes(lkey) && [null, undefined].includes(lkey)) {
|
||||||
|
return 1;
|
||||||
|
} else if (typeof lkey === 'number' && typeof rkey === 'number') {
|
||||||
|
return lkey - rkey;
|
||||||
|
} else if (typeof lkey === 'string' && typeof rkey === 'string') {
|
||||||
|
return lkey.localeCompare(rkey);
|
||||||
|
} else if (lkey < rkey) {
|
||||||
|
return -1;
|
||||||
|
} else if (lkey > rkey) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
return [...this.tableEntries].sort((l, r) => comparer(l, r) * (this.sortAsc ? 1 : -1));
|
||||||
|
}
|
||||||
|
|
||||||
cd<V = any>(data: V): CellData<ViewEntry, V> {
|
cd<V = any>(data: V): CellData<ViewEntry, V> {
|
||||||
return (data as CellData<ViewEntry, V>);
|
return (data as CellData<ViewEntry, V>);
|
||||||
}
|
}
|
||||||
@@ -91,6 +121,19 @@ export default class MangaListTable extends Vue {
|
|||||||
return (data as HeadData<V>);
|
return (data as HeadData<V>);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onHeaderClicked(fieldKey: string, field: TableField<ViewEntry>, event: MouseEvent, isFooter: boolean): void {
|
||||||
|
if (!field.sortable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.sortKey === fieldKey) {
|
||||||
|
this.sortAsc = !this.sortAsc;
|
||||||
|
} else {
|
||||||
|
this.sortAsc = false;
|
||||||
|
}
|
||||||
|
this.sortKey = fieldKey;
|
||||||
|
}
|
||||||
|
|
||||||
onRowClicked(entry: TableItem<ViewEntry>): void {
|
onRowClicked(entry: TableItem<ViewEntry>): void {
|
||||||
(this.$refs.detailsModal as MangaEntryDetailsModal).open(entry);
|
(this.$refs.detailsModal as MangaEntryDetailsModal).open(entry);
|
||||||
}
|
}
|
||||||
@@ -109,9 +152,10 @@ export default class MangaListTable extends Vue {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<BTable ref="table" v-if="bTableRefreshHack" :fields="fields" :items="tableEntries" :primary-key="'id'"
|
<BTable ref="table" v-if="bTableRefreshHack" :fields="fields" :items="tableEntriesSorted" :primary-key="'id'"
|
||||||
class="manga-table" hover striped responsive no-sort-reset sort-by="newChapters" sort-desc
|
class="manga-table" hover striped responsive no-sort-reset sort-by="newChapters" sort-desc
|
||||||
@row-clicked="onRowClicked as any /* TODO dumb typing issue */">
|
@row-clicked="onRowClicked as any /* TODO dumb typing issue */"
|
||||||
|
@head-clicked="onHeaderClicked">
|
||||||
<template #cell(media.coverImage.large)="data">
|
<template #cell(media.coverImage.large)="data">
|
||||||
<img :src="data.value as string" alt="cover-img" class="list-cover"/>
|
<img :src="data.value as string" alt="cover-img" class="list-cover"/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import type {AniListMedia} from '@/data/models/anilist/AniListMedia';
|
|||||||
import type {MangaUpdatesRelation} from '@/data/models/mangaupdates/MangaUpdatesRelation';
|
import type {MangaUpdatesRelation} from '@/data/models/mangaupdates/MangaUpdatesRelation';
|
||||||
import type {MangaUpdatesSeries} from '@/data/models/mangaupdates/MangaUpdatesSeries';
|
import type {MangaUpdatesSeries} from '@/data/models/mangaupdates/MangaUpdatesSeries';
|
||||||
import type {MangaUpdatesChapter} from '@/data/models/mangaupdates/MangaUpdatesChapter';
|
import type {MangaUpdatesChapter} from '@/data/models/mangaupdates/MangaUpdatesChapter';
|
||||||
import groupBy from '@/util';
|
import {newChapterCount} from '@/components/manga/util.manga';
|
||||||
|
|
||||||
@Options({
|
@Options({
|
||||||
name: 'MangaLists',
|
name: 'MangaLists',
|
||||||
@@ -49,13 +49,16 @@ export default class MangaLists extends Vue {
|
|||||||
const relation = this.relationsByAniListMediaId.get(e.mediaId) ?? null;
|
const relation = this.relationsByAniListMediaId.get(e.mediaId) ?? null;
|
||||||
const series = this.seriesById.get(relation?.mangaUpdatesSeriesId as any) ?? null;
|
const series = this.seriesById.get(relation?.mangaUpdatesSeriesId as any) ?? null;
|
||||||
const chapters = this.chaptersBySeriesId.get(relation?.mangaUpdatesSeriesId as any) ?? [];
|
const chapters = this.chaptersBySeriesId.get(relation?.mangaUpdatesSeriesId as any) ?? [];
|
||||||
return ({
|
const viewEntry = {
|
||||||
entry: e,
|
entry: e,
|
||||||
media: media,
|
media: media,
|
||||||
relation: relation,
|
relation: relation,
|
||||||
series: series,
|
series: series,
|
||||||
chapters: chapters,
|
chapters: chapters,
|
||||||
} as ViewEntry);
|
newChapters: 0,
|
||||||
|
} as ViewEntry;
|
||||||
|
viewEntry.newChapters = newChapterCount(viewEntry);
|
||||||
|
return viewEntry;
|
||||||
}),
|
}),
|
||||||
}))
|
}))
|
||||||
.sort((l, r) => order.indexOf(l.list.name.toLowerCase()) - order.indexOf(r.list.name.toLowerCase()));
|
.sort((l, r) => order.indexOf(l.list.name.toLowerCase()) - order.indexOf(r.list.name.toLowerCase()));
|
||||||
|
|||||||
Reference in New Issue
Block a user