frontend: added filter to filter manga
This commit is contained in:
@@ -25,13 +25,13 @@ export default class App extends Vue {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-100 w-100">
|
||||
<div class="h-100 w-100 overflow-hidden">
|
||||
<LocaleSaver/>
|
||||
<DocumentLocaleSetter/>
|
||||
<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"/>
|
||||
<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"/>
|
||||
<RouterView class="flex-grow-1"/>
|
||||
</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 MangaEntryDetailsModal from '@/components/manga/MangaEntryDetailsModal.vue';
|
||||
import {get, latestChaptersSorted, latestChapterString, newChapterCount} from '@/components/manga/util.manga';
|
||||
import {decode} from 'html-entities';
|
||||
|
||||
type HeadData<I> = {
|
||||
label: string,
|
||||
@@ -27,6 +28,7 @@ type CellData<I, V> = {
|
||||
|
||||
@Options({
|
||||
name: 'MangaListTable',
|
||||
methods: {decode},
|
||||
components: {BTable, MangaEntryDetailsModal},
|
||||
})
|
||||
export default class MangaListTable extends Vue {
|
||||
@@ -170,7 +172,7 @@ export default class MangaListTable extends Vue {
|
||||
</span>
|
||||
<template v-if="cd(data).item.series">
|
||||
<a :href="cd(data).item.series!.url" target="_blank">
|
||||
{{ cd(data).item.series!.title }}
|
||||
{{ decode(cd(data).item.series!.title) }}
|
||||
</a>
|
||||
</template>
|
||||
<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 {MangaUpdatesChapter} from '@/data/models/mangaupdates/MangaUpdatesChapter';
|
||||
import {newChapterCount} from '@/components/manga/util.manga';
|
||||
import FilterBar from '@/components/manga/FilterBar.vue';
|
||||
|
||||
@Options({
|
||||
name: 'MangaLists',
|
||||
components: {MangaList, BTable},
|
||||
components: {FilterBar, MangaList, BTable},
|
||||
})
|
||||
export default class MangaLists extends Vue {
|
||||
filter: string | undefined = '';
|
||||
|
||||
get mangaStore(): MangaStore {
|
||||
return new MangaStore();
|
||||
};
|
||||
@@ -42,33 +45,50 @@ export default class MangaLists extends Vue {
|
||||
const lists = this.mangaStore.aniListLists;
|
||||
const manga = this.mangaStore.aniListManga;
|
||||
|
||||
const filterparts = this.filter?.trim().toLowerCase().split(' ') ?? [];
|
||||
return lists.map(l => ({
|
||||
list: l,
|
||||
entries: (manga.get(l.name) ?? []).map(e => {
|
||||
const media = this.mediaById.get(e.mediaId) ?? null;
|
||||
const relation = this.relationsByAniListMediaId.get(e.mediaId) ?? null;
|
||||
const series = this.seriesById.get(relation?.mangaUpdatesSeriesId as any) ?? null;
|
||||
const chapters = this.chaptersBySeriesId.get(relation?.mangaUpdatesSeriesId as any) ?? [];
|
||||
const viewEntry = {
|
||||
entry: e,
|
||||
media: media,
|
||||
relation: relation,
|
||||
series: series,
|
||||
chapters: chapters,
|
||||
newChapters: 0,
|
||||
} as ViewEntry;
|
||||
viewEntry.newChapters = newChapterCount(viewEntry);
|
||||
return viewEntry;
|
||||
}),
|
||||
entries: (manga.get(l.name) ?? [])
|
||||
.filter(e => { // apply filter if set
|
||||
const media = this.mediaById.get(e.mediaId) ?? null;
|
||||
return !this.filter?.trim().length
|
||||
|| filterparts.some(fp => media!.title.english?.toLowerCase().includes(fp))
|
||||
|| filterparts.some(fp => media!.title.native?.toLowerCase().includes(fp))
|
||||
|| filterparts.some(fp => media!.title.romaji?.toLowerCase().includes(fp));
|
||||
})
|
||||
.map(e => {
|
||||
const media = this.mediaById.get(e.mediaId) ?? null;
|
||||
const relation = this.relationsByAniListMediaId.get(e.mediaId) ?? null;
|
||||
const series = this.seriesById.get(relation?.mangaUpdatesSeriesId as any) ?? null;
|
||||
const chapters = this.chaptersBySeriesId.get(relation?.mangaUpdatesSeriesId as any) ?? [];
|
||||
const 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()));
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="overflow-y-auto manga-lists">
|
||||
<MangaList v-for="viewList in viewLists" :key="viewList.list.name" :viewList="viewList" class="mb-3"/>
|
||||
<div class="d-flex flex-column overflow-hidden">
|
||||
<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>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ export type AniListMedia = {
|
||||
title: {
|
||||
userPreferred: string,
|
||||
romaji: string,
|
||||
english: string,
|
||||
english?: string,
|
||||
native: string,
|
||||
}
|
||||
coverImage: {
|
||||
|
||||
@@ -19,6 +19,7 @@ export const messagesDe = {
|
||||
series: 'Kapitel',
|
||||
},
|
||||
},
|
||||
filter: 'Filter',
|
||||
locale: 'Sprache',
|
||||
locales: messagesEn.locales,
|
||||
localStorage: {
|
||||
|
||||
@@ -17,6 +17,7 @@ export const messagesEn = {
|
||||
series: 'Series',
|
||||
},
|
||||
},
|
||||
filter: 'Filter',
|
||||
locale: 'Language',
|
||||
locales: {
|
||||
'de': 'Deutsch',
|
||||
|
||||
Reference in New Issue
Block a user