added manga details modal to manga tables

This commit is contained in:
wea_ondara
2023-10-08 11:09:06 +02:00
parent 3935c5640b
commit e6e872375f
3 changed files with 217 additions and 71 deletions

View File

@@ -0,0 +1,126 @@
<script lang="ts">
import {Options, Vue} from 'vue-property-decorator';
import {BModal} from 'bootstrap-vue-next';
import type {ViewEntry} from '@/components/manga/MangaList.vue';
import {Modal} from 'bootstrap';
import {latestChaptersSorted, latestChapterString, newChapterCount} from './util.manga';
@Options({
name: 'MangaEntryDetailsModal',
components: {BModal},
})
export default class MangaEntryDetailsModal extends Vue {
entry: ViewEntry | null = null;
//functions
latestChaptersSorted = latestChaptersSorted;
latestChapterString = latestChapterString;
newChapterCount = newChapterCount;
open(entry: ViewEntry): void {
this.entry = entry;
new Modal(this.$refs.modal as Element).show();
}
dismiss(): void {
this.entry = null;
new Modal(this.$refs.modal as Element).hide();
}
}
</script>
<template>
<div ref="modal" class="modal manga-details-modal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content" v-if="entry">
<div class="modal-header">
<div class="modal-title">
<div class="mb-0">{{ entry.media?.title?.native }}</div>
<div style="font-size: 0.5em">{{ entry.media?.title?.english ?? entry.media?.title?.romaji }}</div>
</div>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="d-flex flex-row">
<img :src="entry.media?.coverImage.large as string" alt="cover-img" class="modal-cover"/>
<div class="ms-1">
<!-- <div>-->
<!-- {{ $t('manga.title') + ': ' }}-->
<!-- <span class="mb-0">{{ entry.media?.title?.native }}</span>-->
<!-- <span style="font-size: 0.5em">{{ entry.media?.title?.english ?? entry.media?.title?.romaji }}</span>-->
<!-- </div>-->
<div>
{{ $t('manga.score') + ':' }}
<i class="fa fa-star" style="color: var(--bs-orange)"/>
{{ entry.entry.score }}
</div>
<div>
{{ $t('manga.progress') + ': ' + entry.entry.progress + '/' + latestChapterString(entry) }}
</div>
<div>
{{ $t('manga.chapters.newCount') + ': ' + newChapterCount(entry) }}
</div>
<div v-if="entry.chapters.length">
<div>{{ $t('manga.chapters.latest') + ': ' }}</div>
<ul class="mb-0 ps-3">
<li v-for="c in latestChaptersSorted(entry)">
{{ c.chapter + ' (' + c.group + ')' }}
</li>
</ul>
</div>
<div v-if="entry.relation">
<span>
MangaUpdates:
</span>
<template v-if="entry.series">
<a :href="entry.series!.url" target="_blank">
{{ entry.series!.title }}
</a>
<!-- <span class="ms-auto">-->
<!-- {{ formatDateTimeSeconds(new Date(entry.series!.last_updated.as_rfc3339)) }}-->
<!-- </span>-->
</template>
<span v-else>{{ $t('mangaupdates.relation.found') }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<style lang="scss">
@import 'bootstrap/scss/functions';
@import 'bootstrap/scss/variables';
@import 'bootstrap/scss/mixins/breakpoints';
@include media-breakpoint-down(sm) {
.modal.manga-details-modal {
--manga-cover-size: 3rem;
font-size: 0.8em;
--bs-modal-header-padding: 0.5rem;
--bs-modal-padding: 0.5rem;
--bs-modal-footer-padding: 0.5rem;
}
}
@include media-breakpoint-up(sm) {
.manga-details-modal {
--manga-cover-size: 4rem;
}
}
.manga-details-modal .modal-cover {
max-width: var(--manga-cover-size);
max-height: calc(var(--manga-cover-size) * 2);
object-fit: cover;
border-radius: calc(var(--manga-cover-size) / 16);
}
</style>

View File

@@ -5,6 +5,8 @@ import {BTable, type TableItem} from 'bootstrap-vue-next';
import type {TableFieldObject} from 'bootstrap-vue-next/dist/src/types'; import type {TableFieldObject} from 'bootstrap-vue-next/dist/src/types';
import type {MangaUpdatesChapter} from '@/data/models/mangaupdates/MangaUpdatesChapter'; import type {MangaUpdatesChapter} from '@/data/models/mangaupdates/MangaUpdatesChapter';
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 {latestChaptersSorted, latestChapterString, newChapterCount} from '@/components/manga/util.manga';
type HeadData<I> = { type HeadData<I> = {
label: string, label: string,
@@ -25,7 +27,7 @@ type CellData<I, V> = {
@Options({ @Options({
name: 'MangaListTable', name: 'MangaListTable',
components: {BTable}, components: {BTable, MangaEntryDetailsModal},
}) })
export default class MangaListTable extends Vue { export default class MangaListTable extends Vue {
@Prop({required: true}) @Prop({required: true})
@@ -33,39 +35,46 @@ export default class MangaListTable extends Vue {
bTableRefreshHack = true; bTableRefreshHack = true;
//methods
latestChaptersSorted = latestChaptersSorted;
latestChapterString = latestChapterString;
newChapterCount = newChapterCount;
get fields(): TableFieldObject<ViewEntry>[] { get fields(): TableFieldObject<ViewEntry>[] {
return [{ return [{
key: 'media.coverImage.large', key: 'media.coverImage.large',
label: '', label: '',
sortable: true, sortable: true,
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',
}, { }, {
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', tdClass: 'text-center manga-column-score c-pointer',
thClass: 'text-center manga-column-score', thClass: 'text-center manga-column-score',
}, { }, {
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', tdClass: 'text-end text-nowrap c-pointer',
thClass: 'text-end', thClass: 'text-end',
}, { }, {
key: 'newChapters', key: 'newChapters',
formatter: (value: never, key: never, item: ViewEntry) => this.newChapterCount(item), 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', tdClass: 'text-end c-pointer',
thClass: 'text-end', thClass: 'text-end',
}, { }, {
key: 'latestChapters', key: 'latestChapters',
label: this.$t('manga.chapters.latest'), label: this.$t('manga.chapters.latest'),
sortable: true, sortable: true,
tdClass: 'manga-column-latest-chapters', tdClass: 'manga-column-latest-chapters c-pointer',
thClass: 'manga-column-latest-chapters', thClass: 'manga-column-latest-chapters',
}]; }];
} }
@@ -74,24 +83,6 @@ export default class MangaListTable extends Vue {
return this.viewList.entries; return this.viewList.entries;
} }
newChapterCount(entry: ViewEntry): number {
const max = entry.media?.chapters || entry.chapters.reduce((l, r) => Math.max(l, r.chapter), 0);
return Math.max(0, max - entry.entry.progress);
}
lastestChapterString(entry: ViewEntry): string {
if (entry.media?.chapters) {
return '' + entry.media.chapters;
} else if (entry.chapters.length) {
return entry.chapters.reduce((l, r) => Math.max(l, r.chapter), 0) + '+';
}
return '?';
}
latestChaptersSorted(entry: ViewEntry): MangaUpdatesChapter[] {
return entry.chapters.sort((l, r) => l.chapter - r.chapter);
}
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>);
} }
@@ -100,6 +91,10 @@ export default class MangaListTable extends Vue {
return (data as HeadData<V>); return (data as HeadData<V>);
} }
onRowClicked(entry: TableItem<ViewEntry>): void {
(this.$refs.detailsModal as MangaEntryDetailsModal).open(entry);
}
@Watch('viewList', {deep: true}) @Watch('viewList', {deep: true})
private onViewListChanged(): void { private onViewListChanged(): void {
this.bTableRefreshHack = false; this.bTableRefreshHack = false;
@@ -113,54 +108,59 @@ export default class MangaListTable extends Vue {
</script> </script>
<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="tableEntries" :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
<template #cell(media.coverImage.large)="data"> @row-clicked="onRowClicked">
<img :src="data.value as string" alt="cover-img" class="list-cover"/> <template #cell(media.coverImage.large)="data">
</template> <img :src="data.value as string" alt="cover-img" class="list-cover"/>
<template #cell(media.title.userPreferred)="data"> </template>
<div class="flex-grow-1"> <template #cell(media.title.userPreferred)="data">
<div>{{ cd(data).item.media?.title.native }}</div> <div class="flex-grow-1">
<div>{{ cd(data).item.media?.title.english }}</div> <div>{{ cd(data).item.media?.title.native }}</div>
</div> <div>{{ cd(data).item.media?.title.english }}</div>
<div v-if="cd(data).item.relation" class="d-flex flex-row" style="font-size: 0.5em"> </div>
<span> <div v-if="cd(data).item.relation" class="d-flex flex-row" style="font-size: 0.5em">
MangaUpdates:&nbsp; <span>
</span> MangaUpdates:&nbsp;
<template v-if="cd(data).item.series"> </span>
<a :href="cd(data).item.series!.url" target="_blank"> <template v-if="cd(data).item.series">
{{ cd(data).item.series!.title }} <a :href="cd(data).item.series!.url" target="_blank">
</a> {{ cd(data).item.series!.title }}
</template> </a>
<span v-else>{{ $t('mangaupdates.relation.found') }}</span> </template>
</div> <span v-else>{{ $t('mangaupdates.relation.found') }}</span>
</template> </div>
<template #head(entry.score)="data"> </template>
<div class="table-header-mobile"><i class="fa fa-star" style="color: var(--bs-orange)"/></div> <template #head(entry.score)="data">
<div class="table-header-desktop"> {{ hd(data).label }}</div> <div class="table-header-mobile"><i class="fa fa-star" style="color: var(--bs-orange)"/></div>
</template> <div class="table-header-desktop"> {{ hd(data).label }}</div>
<template #cell(entry.score)="data"> </template>
<div class="table-data-mobile">{{ cd(data).value }}</div> <template #cell(entry.score)="data">
<div class="table-data-desktop"><i class="fa fa-star" style="color: var(--bs-orange)"/> {{ cd(data).value }}</div> <div class="table-data-mobile">{{ cd(data).value }}</div>
</template> <div class="table-data-desktop">
<template #head(entry.progress)="data"> <i class="fa fa-star" style="color: var(--bs-orange)"/> {{ cd(data).value }}
<div class="table-header-mobile"><i class="fa fa-bars-progress"/></div> </div>
<div class="table-header-desktop"> {{ hd(data).label }}</div> </template>
</template> <template #head(entry.progress)="data">
<template #cell(entry.progress)="data"> <div class="table-header-mobile"><i class="fa fa-bars-progress"/></div>
{{ cd(data).value + '/' + lastestChapterString(cd(data).item) }} <div class="table-header-desktop"> {{ hd(data).label }}</div>
</template> </template>
<template #head(newChapters)="data"> <template #cell(entry.progress)="data">
<div class="table-header-mobile"><i class="fa fa-plus" style="color: var(--bs-success)"/></div> {{ cd(data).value + '/' + latestChapterString(cd(data).item) }}
<div class="table-header-desktop"> {{ hd(data).label }}</div> </template>
</template> <template #head(newChapters)="data">
<template #cell(latestChapters)="data"> <div class="table-header-mobile"><i class="fa fa-plus" style="color: var(--bs-success)"/></div>
<div v-for="c in latestChaptersSorted(cd(data).item)"> <div class="table-header-desktop"> {{ hd(data).label }}</div>
{{ c.chapter + ' (' + c.group + ')' }} </template>
</div> <template #cell(latestChapters)="data">
</template> <div v-for="c in latestChaptersSorted(cd(data).item)">
</BTable> {{ c.chapter + ' (' + c.group + ')' }}
</div>
</template>
</BTable>
<MangaEntryDetailsModal ref="detailsModal"/>
</div>
</template> </template>
<style lang="scss"> <style lang="scss">

View File

@@ -0,0 +1,20 @@
import type {ViewEntry} from '@/components/manga/MangaList.vue';
import type {MangaUpdatesChapter} from '@/data/models/mangaupdates/MangaUpdatesChapter';
export function latestChaptersSorted(entry: ViewEntry): MangaUpdatesChapter[] {
return entry.chapters.sort((l, r) => l.chapter - r.chapter);
}
export function latestChapterString(entry: ViewEntry): string {
if (entry.media?.chapters) {
return '' + entry.media.chapters;
} else if (entry.chapters.length) {
return entry.chapters.reduce((l, r) => Math.max(l, r.chapter), 0) + '+';
}
return '?';
}
export function newChapterCount(entry: ViewEntry): number {
const max = entry.media?.chapters || entry.chapters.reduce((l, r) => Math.max(l, r.chapter), 0);
return Math.max(0, max - entry.entry.progress);
}