diff --git a/src/App.vue b/src/App.vue index ba82095..7b5e984 100644 --- a/src/App.vue +++ b/src/App.vue @@ -436,17 +436,7 @@ a { .grid { display: inline-grid; row-gap: 30px; - padding: 0 12.5px; - - &--media { - grid-template-columns: repeat(auto-fill, 100%); - margin-bottom: 30px; - } - - &--person { - column-gap: 20px; - grid-template-columns: repeat(auto-fill, 130px); - } + padding-right: 12.5px; .card { a { diff --git a/src/components/search/cards/Media.vue b/src/components/search/cards/Media.vue index 1c30a0f..5bc6ace 100644 --- a/src/components/search/cards/Media.vue +++ b/src/components/search/cards/Media.vue @@ -1,611 +1,37 @@ diff --git a/src/components/search/cards/Person.vue b/src/components/search/cards/Person.vue index da3950f..d516ac7 100644 --- a/src/components/search/cards/Person.vue +++ b/src/components/search/cards/Person.vue @@ -45,7 +45,7 @@ export default class Person extends Mixins(Vue, MixinI18n) { grid-template-rows: min-content auto; position: relative; text-decoration: none; - width: 130px; + width: 100%; &:hover { .name { diff --git a/src/components/search/cards/media/Chart.vue b/src/components/search/cards/media/Chart.vue new file mode 100644 index 0000000..9e423e3 --- /dev/null +++ b/src/components/search/cards/media/Chart.vue @@ -0,0 +1,429 @@ + + + + + diff --git a/src/components/search/cards/media/Cover.vue b/src/components/search/cards/media/Cover.vue new file mode 100644 index 0000000..3aca1f6 --- /dev/null +++ b/src/components/search/cards/media/Cover.vue @@ -0,0 +1,149 @@ + + + + + diff --git a/src/components/search/cards/media/Table.vue b/src/components/search/cards/media/Table.vue new file mode 100644 index 0000000..07c7fbd --- /dev/null +++ b/src/components/search/cards/media/Table.vue @@ -0,0 +1,246 @@ + + + + + diff --git a/src/mixins/CardMedia.ts b/src/mixins/CardMedia.ts new file mode 100644 index 0000000..c39c147 --- /dev/null +++ b/src/mixins/CardMedia.ts @@ -0,0 +1,205 @@ +import { + Component, + Mixins, + Prop, + Vue, +} from 'vue-property-decorator'; +import { State } from 'vuex-class'; +import * as Enum from '@/utils/Enum'; +import Color from '@/utils/Color'; +import MixinI18n from '@/mixins/I18n'; +import MixinSaveActivity from '@/mixins/Activity'; + +@Component +export default class CardMedia extends Mixins(Vue, MixinI18n, MixinSaveActivity) { + @State('settings') settings!: ALSearch.Settings; + + @Prop() readonly data?: ALSearch.AniList.Media; + + /** + * Get media colors overlay. + */ + get mediaColors(): object { + return Color.getMediaCardColors(this.data!.coverImage.color); + } + + /** + * Return media studio if available. + */ + get studio(): any { + return this.data!.studios && this.data!.studios.edges[0] + ? this.data!.studios.edges[0].node + : null; + } + + /** + * Is the media an anime and currently airing? + */ + get isAiring(): boolean { + return this.data!.type === Enum.SearchType.ANIME + && this.data!.status === Enum.Status.RELEASING + && this.data!.nextAiringEpisode !== null; + } + + /** + * Is the media a manga and currently published? + */ + get isPublishing(): boolean { + return this.data!.type === Enum.SearchType.MANGA + && this.data!.status === Enum.Status.RELEASING + && this.data!.startDate.year !== null; + } + + /** + * Return countdown pre content. + */ + get nextEpisodeIn(): string { + return this.i18n('S_NextAiringEpisodeIn', `${this.data!.nextAiringEpisode!.episode}`); + } + + /** + * Return countdown content. + */ + get countdown(): string { + const seconds = this.data!.nextAiringEpisode!.timeUntilAiring; + + const days = Math.floor(seconds / (24 * 3600)); + const hours = Math.floor((seconds % (24 * 3600)) / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + + let str = ''; + if (minutes) str = ` ${minutes}m`; + if (hours) str = ` ${hours}h${str}`; + if (days) str = `${days}d${str}`; + + return str.trim(); + } + + /** + * Return ongoing since content. + */ + get ongoingSince(): string { + return this.data!.type === Enum.SearchType.ANIME + ? this.i18n('S_ReleasingSince', `${this.data!.startDate.year}`) + : this.i18n('S_PublishingSince', `${this.data!.startDate.year}`); + } + + /** + * Return date or status content. + */ + get datesOrStatus(): string { + let str = ''; + + // Cases for anime. + if (this.data!.type === Enum.SearchType.ANIME) { + // If season available, display "Season Year" format. + if (this.data!.season) str = `${this.i18n(`ENUM_${this.data!.season}`)} ${this.data!.startDate.year}`; + // If start date available. + else if (this.data!.startDate.year) str = `${this.data!.startDate.year}`; + } + // Cases for manga. + else if (this.data!.type === Enum.SearchType.MANGA) { + // Try to display a "Start date - End date" format. + if (this.data!.startDate.year) str += this.data!.startDate.year; + if (this.data!.endDate.year && this.data!.startDate.year !== this.data!.endDate.year) { + if (str) str += ' - '; + + str += this.data!.endDate.year; + } + } + + // Fallback to simply displaying status if nothing worked. + if (!str) str = this.i18n(`ENUM_${this.data!.status}`); + + return str; + } + + /** + * Return duration content. + */ + get duration(): string { + let str = ''; + + const episodesFormats = [ + Enum.Format.ONA, + Enum.Format.OVA, + Enum.Format.SPECIAL, + Enum.Format.TV, + Enum.Format.TV_SHORT, + ]; + + // Cases for anime. + if (this.data!.type === Enum.SearchType.ANIME) { + // Display episode count for valid formats. + if (this.data!.format && episodesFormats.includes(this.data!.format) && this.data!.episodes) { + str = `${this.data!.episodes} ${this.i18n('S_Episodes')}`; + } + // Else display duration time. + else str = this.durationTime(); + } + + // Cases for manga. + if (this.data!.type === Enum.SearchType.MANGA) { + // If chapter count. + if (this.data!.chapters) str = this.chapterCount(); + } + + return str; + } + + /** + * Get genre url. + */ + genreUrl(genre: string): string { + return this.data!.type === Enum.SearchType.ANIME + ? `${process.env.VUE_APP_ANILIST_ANIME_URL}/${genre}` + : `${process.env.VUE_APP_ANILIST_MANGA_URL}/${genre}`; + } + + /** + * Return formatted duration time. + */ + durationTime(): string { + const duration = this.data!.duration!; + + const hours = Math.floor(duration / 60); + const minutes = Math.floor(duration % 60); + + let str = ''; + if (minutes) str = ` ${minutes}m`; + if (hours) str = `${hours}h${str}`; + + return str.trim(); + } + + /** + * Return chapters count. + */ + chapterCount(): string { + return `${this.data!.chapters} ${this.i18n('S_Chapters')}`; + } + + handleClick(e: Event): void { + e.stopPropagation(); + e.preventDefault(); + + if (this.settings.activity.visitedPages) { + const activity: ALSearch.Activity.Activity = { + type: Enum.ActivityType.VISITED_PAGE, + label: this.data!.title.userPreferred, + value: this.data!.siteUrl, + params: { + type: this.data!.type as Enum.SearchType, + }, + }; + + this.saveActivity(activity); + } + + window.open(this.data!.siteUrl); + } + + handleStudioClick(e: Event): void { + window.open(this.data!.studios!.edges[0].node.siteUrl); + } +} diff --git a/src/views/Search.vue b/src/views/Search.vue index f3a23f6..ad19345 100644 --- a/src/views/Search.vue +++ b/src/views/Search.vue @@ -1,7 +1,10 @@