added manga details modal to manga tables
This commit is contained in:
126
src/components/manga/MangaEntryDetailsModal.vue
Normal file
126
src/components/manga/MangaEntryDetailsModal.vue
Normal 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>
|
||||||
@@ -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,9 +108,10 @@ 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
|
||||||
|
@row-clicked="onRowClicked">
|
||||||
<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>
|
||||||
@@ -142,14 +138,16 @@ export default class MangaListTable extends Vue {
|
|||||||
</template>
|
</template>
|
||||||
<template #cell(entry.score)="data">
|
<template #cell(entry.score)="data">
|
||||||
<div class="table-data-mobile">{{ cd(data).value }}</div>
|
<div class="table-data-mobile">{{ cd(data).value }}</div>
|
||||||
<div class="table-data-desktop"><i class="fa fa-star" style="color: var(--bs-orange)"/> {{ cd(data).value }}</div>
|
<div class="table-data-desktop">
|
||||||
|
<i class="fa fa-star" style="color: var(--bs-orange)"/> {{ cd(data).value }}
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #head(entry.progress)="data">
|
<template #head(entry.progress)="data">
|
||||||
<div class="table-header-mobile"><i class="fa fa-bars-progress"/></div>
|
<div class="table-header-mobile"><i class="fa fa-bars-progress"/></div>
|
||||||
<div class="table-header-desktop"> {{ hd(data).label }}</div>
|
<div class="table-header-desktop"> {{ hd(data).label }}</div>
|
||||||
</template>
|
</template>
|
||||||
<template #cell(entry.progress)="data">
|
<template #cell(entry.progress)="data">
|
||||||
{{ cd(data).value + '/' + lastestChapterString(cd(data).item) }}
|
{{ cd(data).value + '/' + latestChapterString(cd(data).item) }}
|
||||||
</template>
|
</template>
|
||||||
<template #head(newChapters)="data">
|
<template #head(newChapters)="data">
|
||||||
<div class="table-header-mobile"><i class="fa fa-plus" style="color: var(--bs-success)"/></div>
|
<div class="table-header-mobile"><i class="fa fa-plus" style="color: var(--bs-success)"/></div>
|
||||||
@@ -161,6 +159,8 @@ export default class MangaListTable extends Vue {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</BTable>
|
</BTable>
|
||||||
|
<MangaEntryDetailsModal ref="detailsModal"/>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|||||||
20
src/components/manga/util.manga.ts
Normal file
20
src/components/manga/util.manga.ts
Normal 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);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user