added basic manga list
This commit is contained in:
@@ -19,6 +19,7 @@
|
||||
"@intlify/unplugin-vue-i18n": "^0.11.0",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"bootstrap": "^5.3.1",
|
||||
"bootstrap-vue-next": "^0.13.3",
|
||||
"pinia": "^2.1.6",
|
||||
"pinia-class-component": "^0.9.4",
|
||||
"vue": "^3.3.4",
|
||||
|
||||
@@ -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>
|
||||
|
||||
99
src/components/MangaList.vue
Normal file
99
src/components/MangaList.vue
Normal 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>
|
||||
25
src/components/MangaLists.vue
Normal file
25
src/components/MangaLists.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
57
src/models/MangaListCollection.ts
Normal file
57
src/models/MangaListCollection.ts
Normal 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
5
src/models/User.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export type User = {
|
||||
id: number,
|
||||
name: string,
|
||||
//...
|
||||
}
|
||||
70
src/stores/AniListStore.ts
Normal file
70
src/stores/AniListStore.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -22,4 +22,12 @@ export default defineConfig({
|
||||
},
|
||||
},
|
||||
clearScreen: false,
|
||||
server: {
|
||||
proxy: {
|
||||
'/graphql': {
|
||||
target: 'https://graphql.anilist.co',
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user