frontend: added filter to filter manga
This commit is contained in:
@@ -25,13 +25,13 @@ export default class App extends Vue {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="h-100 w-100">
|
<div class="h-100 w-100 overflow-hidden">
|
||||||
<LocaleSaver/>
|
<LocaleSaver/>
|
||||||
<DocumentLocaleSetter/>
|
<DocumentLocaleSetter/>
|
||||||
<StoragePersist/>
|
<StoragePersist/>
|
||||||
<SideBar ref="sidebar" class="h-100 w-100" :toggled="sidebarToggled"
|
<SideBar ref="sidebar" class="h-100 w-100 overflow-hidden" :toggled="sidebarToggled"
|
||||||
@close="sidebarToggled=false"/>
|
@close="sidebarToggled=false"/>
|
||||||
<div class="d-flex flex-column h-100 w-100">
|
<div class="d-flex flex-column h-100 w-100 overflow-hidden">
|
||||||
<NavBar @toggleSidebar="sidebarToggled = !sidebarToggled"/>
|
<NavBar @toggleSidebar="sidebarToggled = !sidebarToggled"/>
|
||||||
<RouterView class="flex-grow-1"/>
|
<RouterView class="flex-grow-1"/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
43
frontend/src/components/manga/FilterBar.vue
Normal file
43
frontend/src/components/manga/FilterBar.vue
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {Options, Vue} from 'vue-class-component';
|
||||||
|
import {Prop} from 'vue-property-decorator';
|
||||||
|
|
||||||
|
@Options({
|
||||||
|
name: 'FilterBar',
|
||||||
|
components: {},
|
||||||
|
emits: {
|
||||||
|
'update:modelValue': (modelValue: string) => true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
export default class FilterBar extends Vue {
|
||||||
|
@Prop({required: true})
|
||||||
|
modelValue!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<!-- z-index needed, otherwise shadow not showing -->
|
||||||
|
<nav class="navbar border-bottom shadow flex-nowrap filter-bar" style="z-index: 1">
|
||||||
|
<div class="input-group mx-auto">
|
||||||
|
<input class="form-control" :value="modelValue" :placeholder="$t('filter')"
|
||||||
|
@input="$emit('update:modelValue', ($event.target! as HTMLInputElement).value)"/>
|
||||||
|
<button v-if="modelValue?.trim().length" class="btn btn-secondary" @click="$emit('update:modelValue', '')">
|
||||||
|
<i class="fa fa-xmark"/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</template>
|
||||||
|
<style lang="scss">
|
||||||
|
@import 'bootstrap/scss/functions';
|
||||||
|
@import 'bootstrap/scss/variables';
|
||||||
|
@import 'bootstrap/scss/mixins/breakpoints';
|
||||||
|
|
||||||
|
@include media-breakpoint-up(sm) {
|
||||||
|
nav.filter-bar > div {
|
||||||
|
width: 33%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
@@ -7,6 +7,7 @@ import type {TableField, TableFieldObject} from 'bootstrap-vue-next/dist/src/typ
|
|||||||
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 {get, latestChaptersSorted, latestChapterString, newChapterCount} from '@/components/manga/util.manga';
|
import {get, latestChaptersSorted, latestChapterString, newChapterCount} from '@/components/manga/util.manga';
|
||||||
|
import {decode} from 'html-entities';
|
||||||
|
|
||||||
type HeadData<I> = {
|
type HeadData<I> = {
|
||||||
label: string,
|
label: string,
|
||||||
@@ -27,6 +28,7 @@ type CellData<I, V> = {
|
|||||||
|
|
||||||
@Options({
|
@Options({
|
||||||
name: 'MangaListTable',
|
name: 'MangaListTable',
|
||||||
|
methods: {decode},
|
||||||
components: {BTable, MangaEntryDetailsModal},
|
components: {BTable, MangaEntryDetailsModal},
|
||||||
})
|
})
|
||||||
export default class MangaListTable extends Vue {
|
export default class MangaListTable extends Vue {
|
||||||
@@ -170,7 +172,7 @@ export default class MangaListTable extends Vue {
|
|||||||
</span>
|
</span>
|
||||||
<template v-if="cd(data).item.series">
|
<template v-if="cd(data).item.series">
|
||||||
<a :href="cd(data).item.series!.url" target="_blank">
|
<a :href="cd(data).item.series!.url" target="_blank">
|
||||||
{{ cd(data).item.series!.title }}
|
{{ decode(cd(data).item.series!.title) }}
|
||||||
</a>
|
</a>
|
||||||
</template>
|
</template>
|
||||||
<span v-else>{{ $t('mangaupdates.relation.found') }}</span>
|
<span v-else>{{ $t('mangaupdates.relation.found') }}</span>
|
||||||
|
|||||||
@@ -8,12 +8,15 @@ import type {MangaUpdatesRelation} from '@/data/models/mangaupdates/MangaUpdates
|
|||||||
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 {newChapterCount} from '@/components/manga/util.manga';
|
import {newChapterCount} from '@/components/manga/util.manga';
|
||||||
|
import FilterBar from '@/components/manga/FilterBar.vue';
|
||||||
|
|
||||||
@Options({
|
@Options({
|
||||||
name: 'MangaLists',
|
name: 'MangaLists',
|
||||||
components: {MangaList, BTable},
|
components: {FilterBar, MangaList, BTable},
|
||||||
})
|
})
|
||||||
export default class MangaLists extends Vue {
|
export default class MangaLists extends Vue {
|
||||||
|
filter: string | undefined = '';
|
||||||
|
|
||||||
get mangaStore(): MangaStore {
|
get mangaStore(): MangaStore {
|
||||||
return new MangaStore();
|
return new MangaStore();
|
||||||
};
|
};
|
||||||
@@ -42,33 +45,50 @@ export default class MangaLists extends Vue {
|
|||||||
const lists = this.mangaStore.aniListLists;
|
const lists = this.mangaStore.aniListLists;
|
||||||
const manga = this.mangaStore.aniListManga;
|
const manga = this.mangaStore.aniListManga;
|
||||||
|
|
||||||
|
const filterparts = this.filter?.trim().toLowerCase().split(' ') ?? [];
|
||||||
return lists.map(l => ({
|
return lists.map(l => ({
|
||||||
list: l,
|
list: l,
|
||||||
entries: (manga.get(l.name) ?? []).map(e => {
|
entries: (manga.get(l.name) ?? [])
|
||||||
const media = this.mediaById.get(e.mediaId) ?? null;
|
.filter(e => { // apply filter if set
|
||||||
const relation = this.relationsByAniListMediaId.get(e.mediaId) ?? null;
|
const media = this.mediaById.get(e.mediaId) ?? null;
|
||||||
const series = this.seriesById.get(relation?.mangaUpdatesSeriesId as any) ?? null;
|
return !this.filter?.trim().length
|
||||||
const chapters = this.chaptersBySeriesId.get(relation?.mangaUpdatesSeriesId as any) ?? [];
|
|| filterparts.some(fp => media!.title.english?.toLowerCase().includes(fp))
|
||||||
const viewEntry = {
|
|| filterparts.some(fp => media!.title.native?.toLowerCase().includes(fp))
|
||||||
entry: e,
|
|| filterparts.some(fp => media!.title.romaji?.toLowerCase().includes(fp));
|
||||||
media: media,
|
})
|
||||||
relation: relation,
|
.map(e => {
|
||||||
series: series,
|
const media = this.mediaById.get(e.mediaId) ?? null;
|
||||||
chapters: chapters,
|
const relation = this.relationsByAniListMediaId.get(e.mediaId) ?? null;
|
||||||
newChapters: 0,
|
const series = this.seriesById.get(relation?.mangaUpdatesSeriesId as any) ?? null;
|
||||||
} as ViewEntry;
|
const chapters = this.chaptersBySeriesId.get(relation?.mangaUpdatesSeriesId as any) ?? [];
|
||||||
viewEntry.newChapters = newChapterCount(viewEntry);
|
const viewEntry = {
|
||||||
return viewEntry;
|
entry: e,
|
||||||
}),
|
media: media,
|
||||||
|
relation: relation,
|
||||||
|
series: series,
|
||||||
|
chapters: chapters,
|
||||||
|
newChapters: 0,
|
||||||
|
} as ViewEntry;
|
||||||
|
viewEntry.newChapters = newChapterCount(viewEntry);
|
||||||
|
return viewEntry;
|
||||||
|
}),
|
||||||
}))
|
}))
|
||||||
|
.filter(e => !this.filter?.trim().length || e.entries.length) // hide empty lists when filtering
|
||||||
.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()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="overflow-y-auto manga-lists">
|
<div class="d-flex flex-column overflow-hidden">
|
||||||
<MangaList v-for="viewList in viewLists" :key="viewList.list.name" :viewList="viewList" class="mb-3"/>
|
<div>
|
||||||
|
<FilterBar v-model="filter"/>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow-1 overflow-y-auto">
|
||||||
|
<div class="manga-lists">
|
||||||
|
<MangaList v-for="viewList in viewLists" :key="viewList.list.name" :viewList="viewList" class="mb-3"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ export type AniListMedia = {
|
|||||||
title: {
|
title: {
|
||||||
userPreferred: string,
|
userPreferred: string,
|
||||||
romaji: string,
|
romaji: string,
|
||||||
english: string,
|
english?: string,
|
||||||
native: string,
|
native: string,
|
||||||
}
|
}
|
||||||
coverImage: {
|
coverImage: {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export const messagesDe = {
|
|||||||
series: 'Kapitel',
|
series: 'Kapitel',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
filter: 'Filter',
|
||||||
locale: 'Sprache',
|
locale: 'Sprache',
|
||||||
locales: messagesEn.locales,
|
locales: messagesEn.locales,
|
||||||
localStorage: {
|
localStorage: {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export const messagesEn = {
|
|||||||
series: 'Series',
|
series: 'Series',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
filter: 'Filter',
|
||||||
locale: 'Language',
|
locale: 'Language',
|
||||||
locales: {
|
locales: {
|
||||||
'de': 'Deutsch',
|
'de': 'Deutsch',
|
||||||
|
|||||||
Reference in New Issue
Block a user