added basic manga list

This commit is contained in:
wea_ondara
2023-09-22 14:48:18 +02:00
parent f9d3ff29ed
commit a37d46fde4
12 changed files with 323 additions and 8 deletions

View File

@@ -18,9 +18,9 @@ export default class App extends Vue {
</script>
<template>
<div>
<div class="d-flex flex-column h-100 w-100">
<DocumentLocaleSetter/>
<NavBar/>
<RouterView/>
<RouterView class="flex-grow-1"/>
</div>
</template>

View File

@@ -0,0 +1,99 @@
<script lang="ts">
import {Options, Vue} from 'vue-class-component';
import type {MangaListModel} from '@/models/MangaListCollection';
import {Prop} from 'vue-property-decorator';
import {BTable, type TableField} from 'bootstrap-vue-next';
@Options({
name: 'MangaList',
components: {BTable},
})
export default class MangaList extends Vue {
@Prop({required: true})
readonly list!: MangaListModel;
private get listName(): string {
const key = 'manga.status.' + this.list.name.toLocaleLowerCase();
const res = this.$t(key);
return res == 'key' ? this.list.name : res;
}
private get fields(): TableField[] {
return [{
key: 'media.coverImage.large',
label: '',
sortable: true,
}, {
key: 'media.title.userPreferred',
label: this.$t('manga.title'),
sortable: true,
}, {
key: 'score',
label: this.$t('manga.score'),
sortable: true,
tdClass: 'text-end',
thClass: 'text-end',
}, {
key: 'progress',
label: this.$t('manga.progress'),
sortable: true,
tdClass: 'text-end',
thClass: 'text-end',
}];
}
}
</script>
<template>
<div class="card">
<h3 class="card-title p-3">{{ listName }}</h3>
<BTable :fields="fields" :items="list.entries" :primary-key="'id'" class="manga-table" hover striped>
<template #cell(media.coverImage.large)="data">
<img :src="data.value as string" alt="cover-img" class="list-cover"/>
</template>
<template #cell(progress)="data">
{{ data.value + ' / ' + (data.item.media.chapters ?? '?') }}
</template>
</BTable>
<!-- <table class="table table-striped">-->
<!-- <thead>-->
<!-- <tr>-->
<!-- <th></th>-->
<!-- <th>title</th>-->
<!-- <th>score</th>-->
<!-- <th>chapter</th>-->
<!-- </tr>-->
<!-- </thead>-->
<!-- <tbody>-->
<!-- <tr v-for="manga in list.entries">-->
<!-- <td><img :src="manga.media.coverImage.large" class="list-cover" alt="cover-img"/></td>-->
<!-- <td>{{ manga.media.title.userPreferred }}</td>-->
<!-- <td>{{ manga.score }}</td>-->
<!-- <td class="text-end" style="min-width: 5rem">-->
<!-- {{ manga.progress + ' / ' + (manga.media.chapters ?? '?') }}-->
<!-- </td>-->
<!-- </tr>-->
<!-- </tbody>-->
<!-- </table>-->
</div>
</template>
<style>
.manga-table .list-cover {
width: 4rem;
height: 4rem;
object-fit: cover;
border-radius: 0.25rem;
}
.manga-table tr td:first-child,
.manga-table tr th:first-child {
padding-inline-start: 1rem;
}
.manga-table tr td:last-child,
.manga-table tr th:last-child {
padding-inline-end: 1rem;
}
</style>

View File

@@ -0,0 +1,25 @@
<script lang="ts">
import {Options, Vue} from 'vue-class-component';
import type {MangaListModel} from '@/models/MangaListCollection';
import {AniListStore} from '@/stores/AniListStore';
import MangaList from '@/components/MangaList.vue';
@Options({
name: 'MangaLists',
components: {MangaList},
})
export default class MangaLists extends Vue {
private aniListStore = new AniListStore();
get lists(): MangaListModel[] {
return this.aniListStore.manga?.lists ?? [];
}
}
</script>
<template>
<div class="overflow-y-auto p-3">
<MangaList v-for="list in lists" :key="list.name" :list="list"/>
</div>
</template>

View File

@@ -1,20 +1,40 @@
<script lang="ts">
import {Options, Vue} from 'vue-class-component';
import BootstrapThemeSwitch from '@/components/bootstrapThemeSwitch/BootstrapThemeSwitch.vue';
import {AniListStore} from '@/stores/AniListStore';
@Options({
name: 'UserSearch',
components: {},
})
export default class UserSearch extends Vue {
private aniListStore = new AniListStore();
private get userName(): string {
return this.aniListStore.userName ?? '';
}
private set userName(val: string) {
this.aniListStore.setUserName(val);
}
mounted(): void {
if (this.userName) {
this.onSearch();
}
}
private onSearch(): void {
this.aniListStore.reload();
}
}
</script>
<template>
<div class="input-group">
<input class="form-control" type="search" placeholder="Search" aria-label="Search">
<button class="btn btn-primary">
<input class="form-control" type="search" :placeholder="$t('search')" aria-label="Search" v-model="userName"
@keydown.enter="onSearch">
<button class="btn btn-primary" @click="onSearch">
<i class="fa fa-search"></i>
</button>
</div>

View File

@@ -1,4 +1,18 @@
export const messagesDe = {};
export const messagesDe = {
manga: {
progress: 'Fortschritt',
score: 'Bewertung',
status: {
'paused': 'Pausiert',
'completed': 'Vollendet',
'reading': 'Lesen',
'planning': 'Geplant',
'dropped': 'Abgesetzt',
},
title: 'Titel',
},
search: 'Suche',
};
export const datetimeFormatsDe = {
date: {

View File

@@ -1,4 +1,18 @@
export const messagesEn = {};
export const messagesEn = {
manga: {
progress: 'Progress',
score: 'Score',
status: {
'paused': 'Paused',
'completed': 'Completed',
'reading': 'Reading',
'planning': 'Planning',
'dropped': 'Dropped',
},
title: 'Title',
},
search: 'Search',
};
export const datetimeFormatsEn = {
date: {

View File

@@ -0,0 +1,57 @@
export type MangaListCollection = {
lists: MangaListModel[]
}
export type MangaListModel = {
name: 'Paused' | 'Completed' | 'Planning' | 'Dropped' | 'Reading',
isCustomList: boolean,
isCompletedList: boolean,
entries: MangaListEntry[]
}
export type MangaListEntry = {
id: number,
mediaId: number,
status: 'PAUSED' | 'COMPLETED' | 'PLANNING' | 'DROPPED' | 'READING',
score: number,
progress: number,
progressVolumes: number,
repeat: number,
priority: number,
private: boolean,
hiddenFromStatusLists: boolean,
customLists: any,
advancedScores: any,
notes: any,
updatedAt: number
startedAt: any,
completedAt: any,
media: Media,
}
export type Media = {
id: 86399
title: {
userPreferred: string,
romaji: string,
english: string,
native: string,
}
coverImage: {
extraLarge: string, //url
large: string, //url
}
type: 'MANGA',
format: 'MANGA' | any,
status: 'RELEASING' | any,
episodes: number | null,
volumes: number | null,
chapters: number | null,
averageScore: number,
popularity: number,
isAdult: boolean,
countryOfOrigin: 'JP' | 'KR' | any,
genres: string[],
bannerImage: string, //url
startDate: any,
}

5
src/models/User.ts Normal file
View File

@@ -0,0 +1,5 @@
export type User = {
id: number,
name: string,
//...
}

View File

@@ -0,0 +1,70 @@
import {Pinia, Store} from 'pinia-class-component';
import type {User} from '@/models/User';
import type {MangaListCollection} from '@/models/MangaListCollection';
@Store({
id: 'AniListStore',
name: 'AniListStore',
})
export class AniListStore extends Pinia {
//data
private x_manga: MangaListCollection | null = null;
private userNameUpdateTrigger: string | null = null;
private x_user: User | null = null;
//getter
get manga(): MangaListCollection | null {
return this.x_manga;
}
get userName(): string | null {
this.userNameUpdateTrigger;
return window.localStorage.getItem('userName');
}
get user(): User | null {
return this.x_user;
}
//actions
setUserName(userName: string | null) {
this.userNameUpdateTrigger = userName;
if (userName) {
window.localStorage.setItem('userName', userName ?? '');
} else {
window.localStorage.removeItem('userName');
}
}
async reload(): Promise<void> {
if (!this.userName) {
return;
}
let res = await fetch('/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
'query': 'query($id:Int,$name:String){User(id:$id,name:$name){id name previousNames{name updatedAt}avatar{large}bannerImage about isFollowing isFollower donatorTier donatorBadge createdAt moderatorRoles isBlocked bans options{profileColor restrictMessagesToFollowing}mediaListOptions{scoreFormat}statistics{anime{count meanScore standardDeviation minutesWatched episodesWatched genrePreview:genres(limit:10,sort:COUNT_DESC){genre count}}manga{count meanScore standardDeviation chaptersRead volumesRead genrePreview:genres(limit:10,sort:COUNT_DESC){genre count}}}stats{activityHistory{date amount level}}favourites{anime{edges{favouriteOrder node{id type status(version:2)format isAdult bannerImage title{userPreferred}coverImage{large}startDate{year}}}}manga{edges{favouriteOrder node{id type status(version:2)format isAdult bannerImage title{userPreferred}coverImage{large}startDate{year}}}}characters{edges{favouriteOrder node{id name{userPreferred}image{large}}}}staff{edges{favouriteOrder node{id name{userPreferred}image{large}}}}studios{edges{favouriteOrder node{id name}}}}}}',
'variables': {'name': this.userName},
}),
});
this.x_user = (await res.json()).data.User;
console.log(this.user);
res = await fetch('/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
'query': 'query($userId:Int,$userName:String,$type:MediaType){MediaListCollection(userId:$userId,userName:$userName,type:$type){lists{name isCustomList isCompletedList:isSplitCompletedList entries{...mediaListEntry}}user{id name avatar{large}mediaListOptions{scoreFormat rowOrder animeList{sectionOrder customLists splitCompletedSectionByFormat theme}mangaList{sectionOrder customLists splitCompletedSectionByFormat theme}}}}}fragment mediaListEntry on MediaList{id mediaId status score progress progressVolumes repeat priority private hiddenFromStatusLists customLists advancedScores notes updatedAt startedAt{year month day}completedAt{year month day}media{id title{userPreferred romaji english native}coverImage{extraLarge large}type format status(version:2)episodes volumes chapters averageScore popularity isAdult countryOfOrigin genres bannerImage startDate{year month day}}}',
'variables': {'userId': this.user!.id, 'type': 'MANGA'},
}),
});
this.x_manga = (await res.json()).data.MediaListCollection;
console.log(this.manga);
}
}

View File

@@ -1,13 +1,15 @@
<script lang="ts">
import {Options, Vue} from 'vue-class-component';
import MangaLists from '@/components/MangaLists.vue';
@Options({
name: 'HomeView',
components: {MangaLists},
})
export default class HomeView extends Vue {
}
</script>
<template>
main
<MangaLists/>
</template>