Get Person detail pages via GraphQL, resolves #27
timw4mail/HummingBirdAnimeClient/pipeline/pr-master This commit looks good Details

This commit is contained in:
Timothy Warren 2020-08-27 15:01:00 -04:00
parent 1b74df5269
commit e2f29c6731
19 changed files with 349 additions and 198 deletions

View File

@ -186,9 +186,8 @@ $routes = [
] ]
], ],
'person' => [ 'person' => [
'path' => '/people/{id}{/slug}', 'path' => '/people/{slug}',
'tokens' => [ 'tokens' => [
'id' => SLUG_PATTERN,
'slug' => SLUG_PATTERN, 'slug' => SLUG_PATTERN,
] ]
], ],

View File

@ -0,0 +1,5 @@
<section class="<?= $className ?>">
<?php foreach ($data as $tabName => $tabData): ?>
<?= $callback($tabData, $tabName) ?>
<?php endforeach ?>
</section>

View File

@ -186,7 +186,7 @@ use function Aviat\AnimeClient\getLocalImg;
} }
$rendered[] = $component->character( $rendered[] = $component->character(
$person['name'], $person['name'],
$url->generate('person', ['id' => $person['id'], 'slug' => $person['slug']]), $url->generate('person', ['slug' => $person['slug']]),
$helper->picture(getLocalImg($person['image']['original'] ?? NULL)), $helper->picture(getLocalImg($person['image']['original'] ?? NULL)),
'character small-person', 'character small-person',
); );

View File

@ -120,10 +120,7 @@ use Aviat\AnimeClient\Kitsu;
foreach ($casting as $id => $c): foreach ($casting as $id => $c):
$person = $component->character( $person = $component->character(
$c['person']['name'], $c['person']['name'],
$url->generate('person', [ $url->generate('person', ['slug' => $c['person']['slug']]),
'id' => $c['person']['id'],
'slug' => $c['person']['slug']
]),
$helper->picture(getLocalImg($c['person']['image'])) $helper->picture(getLocalImg($c['person']['image']))
); );
$medias = array_map(fn ($series) => $component->media( $medias = array_map(fn ($series) => $component->media(

View File

@ -95,7 +95,7 @@
fn($people) => implode('', array_map( fn($people) => implode('', array_map(
fn ($person) => $component->character( fn ($person) => $component->character(
$person['name'], $person['name'],
$url->generate('person', ['id' => $person['id'], 'slug' => $person['slug']]), $url->generate('person', ['slug' => $person['slug']]),
$helper->picture("images/people/{$person['id']}.webp") $helper->picture("images/people/{$person['id']}.webp")
), ),
$people $people

View File

@ -1,6 +1,5 @@
<?php <?php
use function Aviat\AnimeClient\getLocalImg; use function Aviat\AnimeClient\getLocalImg;
use Aviat\AnimeClient\Kitsu;
?> ?>
<main class="details fixed"> <main class="details fixed">
<section class="flex flex-no-wrap"> <section class="flex flex-no-wrap">
@ -9,6 +8,14 @@ use Aviat\AnimeClient\Kitsu;
</div> </div>
<div> <div>
<h2 class="toph"><?= $data['name'] ?></h2> <h2 class="toph"><?= $data['name'] ?></h2>
<?php foreach ($data['names'] as $name): ?>
<h3><?= $name ?></h3>
<?php endforeach ?>
<br />
<hr />
<div class="description">
<p><?= str_replace("\n", '</p><p>', $data['description']) ?></p>
</div>
</div> </div>
</section> </section>
@ -24,7 +31,6 @@ use Aviat\AnimeClient\Kitsu;
type="radio" name="staff-roles" id="staff-role<?= $i ?>" <?= $i === 0 ? 'checked' : '' ?> /> type="radio" name="staff-roles" id="staff-role<?= $i ?>" <?= $i === 0 ? 'checked' : '' ?> />
<label for="staff-role<?= $i ?>"><?= $role ?></label> <label for="staff-role<?= $i ?>"><?= $role ?></label>
<?php foreach ($entries as $type => $casting): ?> <?php foreach ($entries as $type => $casting): ?>
<?php if ($type === 'characters') continue; ?>
<?php if (isset($entries['manga'], $entries['anime'])): ?> <?php if (isset($entries['manga'], $entries['anime'])): ?>
<h4><?= ucfirst($type) ?></h4> <h4><?= ucfirst($type) ?></h4>
<?php endif ?> <?php endif ?>
@ -32,7 +38,7 @@ use Aviat\AnimeClient\Kitsu;
<?php foreach ($casting as $sid => $series): ?> <?php foreach ($casting as $sid => $series): ?>
<?php $mediaType = in_array($type, ['anime', 'manga'], TRUE) ? $type : 'anime'; ?> <?php $mediaType = in_array($type, ['anime', 'manga'], TRUE) ? $type : 'anime'; ?>
<?= $component->media( <?= $component->media(
Kitsu::filterTitles($series), $series['titles'],
$url->generate("{$mediaType}.details", ['id' => $series['slug']]), $url->generate("{$mediaType}.details", ['id' => $series['slug']]),
$helper->picture("images/{$type}/{$sid}.webp") $helper->picture("images/{$type}/{$sid}.webp")
) ?> ) ?>
@ -46,7 +52,7 @@ use Aviat\AnimeClient\Kitsu;
</section> </section>
<?php endif ?> <?php endif ?>
<?php if ( ! (empty($data['characters']['main']) || empty($data['characters']['supporting']))): ?> <?php if ( ! empty($data['characters'])): ?>
<section> <section>
<h3>Voice Acting Roles</h3> <h3>Voice Acting Roles</h3>
<?= $component->tabs('voice-acting-roles', $data['characters'], static function ($characterList) use ($component, $helper, $url) { <?= $component->tabs('voice-acting-roles', $data['characters'], static function ($characterList) use ($component, $helper, $url) {
@ -61,7 +67,7 @@ use Aviat\AnimeClient\Kitsu;
foreach ($item['media'] as $sid => $series) foreach ($item['media'] as $sid => $series)
{ {
$medias[] = $component->media( $medias[] = $component->media(
Kitsu::filterTitles($series), $series['titles'],
$url->generate('anime.details', ['id' => $series['slug']]), $url->generate('anime.details', ['id' => $series['slug']]),
$helper->picture("images/anime/{$sid}.webp") $helper->picture("images/anime/{$sid}.webp")
); );

View File

@ -163,7 +163,7 @@ CSS Tabs
/* text-align: center; */ /* text-align: center; */
} }
.tabs .content { .tabs .content, .single-tab {
display: none; display: none;
max-height: 950px; max-height: 950px;
border: 1px solid #e5e5e5; border: 1px solid #e5e5e5;
@ -175,7 +175,14 @@ CSS Tabs
overflow: auto; overflow: auto;
} }
.tabs .content.full-height { .single-tab {
display: block;
border: 1px solid #e5e5e5;
box-shadow: 0 48px 80px -32px rgba(0, 0, 0, 0.3);
margin-top: 1.5em;
}
.tabs .content.full-height, .single-tab.full-height {
max-height: none; max-height: none;
} }

View File

@ -147,7 +147,8 @@ button:active {
.tabs > [type="radio"]:checked + label, .tabs > [type="radio"]:checked + label,
.tabs > [type="radio"]:checked + label + .content, .tabs > [type="radio"]:checked + label + .content,
.vertical-tabs [type="radio"]:checked + label, .vertical-tabs [type="radio"]:checked + label,
.vertical-tabs [type="radio"]:checked ~ .content { .vertical-tabs [type="radio"]:checked ~ .content,
.single-tab {
/* border-color: #333; */ /* border-color: #333; */
border: 0; border: 0;
background: #666; background: #666;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -199,27 +199,14 @@ final class Model {
/** /**
* Get information about a person * Get information about a person
* *
* @param string $id * @param string $slug
* @return array * @return array
* @throws InvalidArgumentException * @throws InvalidArgumentException
*/ */
public function getPerson(string $id): array public function getPerson(string $slug): array
{ {
return $this->getCached("kitsu-person-{$id}", fn () => $this->requestBuilder->getRequest("people/{$id}", [ return $this->getCached("kitsu-person-{$slug}", fn () => $this->requestBuilder->runQuery('PersonDetails', [
'query' => [ 'slug' => $slug
'filter' => [
'id' => $id,
],
'fields' => [
'characters' => 'canonicalName,slug,image',
'characterVoices' => 'mediaCharacter',
'anime' => 'canonicalTitle,abbreviatedTitles,titles,slug,posterImage',
'manga' => 'canonicalTitle,abbreviatedTitles,titles,slug,posterImage',
'mediaCharacters' => 'role,media,character',
'mediaStaff' => 'role,media,person',
],
'include' => 'voices.mediaCharacter.media,voices.mediaCharacter.character,staff.media',
],
])); ]));
} }

View File

@ -1,5 +1,5 @@
query ($id: ID!) { query ($slug: String!) {
findPersonById(id: $id) { findPersonBySlug(slug: $slug) {
id id
description description
birthday birthday
@ -22,6 +22,36 @@ query ($id: ID!) {
canonical canonical
localized localized
} }
mediaStaff {
nodes {
id
role
media {
id
slug
type
posterImage {
original {
height
name
url
width
}
views {
height
name
url
width
}
}
titles {
alternatives
canonical
localized
}
}
}
}
voices { voices {
nodes { nodes {
locale locale
@ -29,6 +59,7 @@ query ($id: ID!) {
role role
character { character {
id id
slug
image { image {
original { original {
height height
@ -43,6 +74,7 @@ query ($id: ID!) {
} }
media { media {
id id
slug
posterImage { posterImage {
original { original {
height height

View File

@ -50,7 +50,7 @@ final class CharacterTransformer extends AbstractTransformer {
if (isset($data['media']['nodes'])) if (isset($data['media']['nodes']))
{ {
[$media, $castings] = $this->organizeMediaAndVoices($data['media']['nodes']); [$media, $castings] = $this->organizeMediaAndVoices($data['media']['nodes'] ?? []);
} }
return Character::from([ return Character::from([

View File

@ -16,7 +16,7 @@
namespace Aviat\AnimeClient\API\Kitsu\Transformer; namespace Aviat\AnimeClient\API\Kitsu\Transformer;
use Aviat\AnimeClient\API\JsonAPI; use Aviat\AnimeClient\Kitsu;
use Aviat\AnimeClient\Types\Person; use Aviat\AnimeClient\Types\Person;
use Aviat\Ion\Transformer\AbstractTransformer; use Aviat\Ion\Transformer\AbstractTransformer;
@ -31,14 +31,17 @@ final class PersonTransformer extends AbstractTransformer {
*/ */
public function transform($personData): Person public function transform($personData): Person
{ {
$data = JsonAPI::organizeData($personData); $data = $personData['data']['findPersonBySlug'] ?? [];
$included = JsonAPI::organizeIncludes($personData['included']); $canonicalName = $data['names']['localized'][$data['names']['canonical']]
?? array_shift($data['names']['localized']);
$orgData = $this->organizeData($included); $orgData = $this->organizeData($data);
return Person::from([ return Person::from([
'id' => $data['id'], 'id' => $data['id'],
'name' => $data['attributes']['name'], 'name' => $canonicalName,
'names' => array_diff($data['names']['localized'], [$canonicalName]),
'description' => $data['description']['en'] ?? '',
'characters' => $orgData['characters'], 'characters' => $orgData['characters'],
'staff' => $orgData['staff'], 'staff' => $orgData['staff'],
]); ]);
@ -47,88 +50,98 @@ final class PersonTransformer extends AbstractTransformer {
protected function organizeData(array $data): array protected function organizeData(array $data): array
{ {
$output = [ $output = [
'characters' => [ 'characters' => [],
'main' => [],
'supporting' => [],
],
'staff' => [], 'staff' => [],
]; ];
if (array_key_exists('characterVoices', $data)) $characters = [];
{ $staff = [];
foreach ($data['characterVoices'] as $cv)
{
$mcId = $cv['relationships']['mediaCharacter']['data']['id'];
if ( ! array_key_exists($mcId, $data['mediaCharacters'])) if (count($data['mediaStaff']['nodes']) > 0)
{
$roles = array_unique(array_column($data['mediaStaff']['nodes'], 'role'));
foreach ($roles as $role)
{
$staff[$role] = [];
}
ksort($staff);
foreach ($data['mediaStaff']['nodes'] as $staffing)
{
$media = $staffing['media'];
$role = $staffing['role'];
$title = $media['titles']['canonical'];
$type = strtolower($media['type']);
$staff[$role][$type][$media['id']] = [
'id' => $media['id'],
'title' => $title,
'titles' => array_merge([$title], Kitsu::getFilteredTitles($media['titles'])),
'image' => [
'original' => $media['posterImage']['views'][1]['url'],
],
'slug' => $media['slug'],
];
uasort($staff[$role][$type], fn ($a, $b) => $a['title'] <=> $b['title']);
}
$output['staff'] = $staff;
}
if (count($data['voices']['nodes']) > 0)
{
foreach ($data['voices']['nodes'] as $voicing)
{
$character = $voicing['mediaCharacter']['character'];
$charId = $character['id'];
$rawMedia = $voicing['mediaCharacter']['media'];
$role = strtolower($voicing['mediaCharacter']['role']);
$media = [
'id' => $rawMedia['id'],
'slug' => $rawMedia['slug'],
'titles' => array_merge(
[$rawMedia['titles']['canonical']],
Kitsu::getFilteredTitles($rawMedia['titles']),
),
];
if ( ! isset($characters[$role][$charId]))
{ {
continue; if ( ! array_key_exists($role, $characters))
{
$characters[$role] = [];
}
$characters[$role][$charId] = [
'character' => [
'id' => $character['id'],
'slug' => $character['slug'],
'image' => [
'original' => $character['image']['original']['url'],
],
'canonicalName' => $character['names']['canonical'],
],
'media' => [
$media['id'] => $media
],
];
}
else
{
$characters[$role][$charId]['media'][$media['id']] = $media;
} }
$mc = $data['mediaCharacters'][$mcId]; uasort(
$characters[$role][$charId]['media'],
$role = $mc['role']; fn ($a, $b) => $a['titles'][0] <=> $b['titles'][0]
);
$charId = $mc['relationships']['character']['data']['id'];
$mediaId = $mc['relationships']['media']['data']['id'];
$existingMedia = array_key_exists($charId, $output['characters'][$role])
? $output['characters'][$role][$charId]['media']
: [];
$relatedMedia = [
$mediaId => $data['anime'][$mediaId],
];
$includedMedia = array_replace_recursive($existingMedia, $relatedMedia);
uasort($includedMedia, static function ($a, $b) {
return $a['canonicalTitle'] <=> $b['canonicalTitle'];
});
$character = $data['characters'][$charId];
$output['characters'][$role][$charId] = [
'character' => $character,
'media' => $includedMedia,
];
}
}
if (array_key_exists('mediaStaff', $data))
{
foreach ($data['mediaStaff'] as $rid => $role)
{
$roleName = $role['role'];
$mediaType = $role['relationships']['media']['data']['type'];
$mediaId = $role['relationships']['media']['data']['id'];
$media = $data[$mediaType][$mediaId];
$output['staff'][$roleName][$mediaType][$mediaId] = $media;
}
}
uasort($output['characters']['main'], static function ($a, $b) {
return $a['character']['canonicalName'] <=> $b['character']['canonicalName'];
});
uasort($output['characters']['supporting'], static function ($a, $b) {
return $a['character']['canonicalName'] <=> $b['character']['canonicalName'];
});
ksort($output['staff']);
foreach ($output['staff'] as $role => &$media)
{
if (array_key_exists('anime', $media))
{
uasort($media['anime'], static function ($a, $b) {
return $a['canonicalTitle'] <=> $b['canonicalTitle'];
});
} }
if (array_key_exists('manga', $media)) krsort($characters);
{
uasort($media['manga'], static function ($a, $b) { $output['characters'] = $characters;
return $a['canonicalTitle'] <=> $b['canonicalTitle'];
});
}
} }
return $output; return $output;

View File

@ -402,10 +402,9 @@ type Anime implements Episodic & Media & WithTimestamps {
youtubeTrailerVideoId: String youtubeTrailerVideoId: String
} }
type AnimeAmountConsumed implements AmountConsumed & WithTimestamps { type AnimeAmountConsumed implements AmountConsumed {
"Total media completed atleast once." "Total media completed atleast once."
completed: Int! completed: Int!
createdAt: ISO8601DateTime!
id: ID! id: ID!
"Total amount of media." "Total amount of media."
media: Int! media: Int!
@ -417,13 +416,11 @@ type AnimeAmountConsumed implements AmountConsumed & WithTimestamps {
time: Int! time: Int!
"Total progress of library including reconsuming." "Total progress of library including reconsuming."
units: Int! units: Int!
updatedAt: ISO8601DateTime!
} }
type AnimeCategoryBreakdown implements CategoryBreakdown & WithTimestamps { type AnimeCategoryBreakdown implements CategoryBreakdown {
"A Map of category_id -> count for all categories present on the library entries" "A Map of category_id -> count for all categories present on the library entries"
categories: Map! categories: Map!
createdAt: ISO8601DateTime!
id: ID! id: ID!
"The profile related to the user for this stat." "The profile related to the user for this stat."
profile: Profile! profile: Profile!
@ -431,7 +428,6 @@ type AnimeCategoryBreakdown implements CategoryBreakdown & WithTimestamps {
recalculatedAt: ISO8601Date! recalculatedAt: ISO8601Date!
"The total amount of library entries." "The total amount of library entries."
total: Int! total: Int!
updatedAt: ISO8601DateTime!
} }
"The connection type for Anime." "The connection type for Anime."
@ -468,13 +464,12 @@ type AnimeEdge {
node: Anime node: Anime
} }
type AnimeMutation implements WithTimestamps { type AnimeMutation {
"Create an Anime." "Create an Anime."
create( create(
"Create an Anime." "Create an Anime."
input: AnimeCreateInput! input: AnimeCreateInput!
): AnimeCreatePayload ): AnimeCreatePayload
createdAt: ISO8601DateTime!
"Delete an Anime." "Delete an Anime."
delete( delete(
"Delete an Anime." "Delete an Anime."
@ -485,7 +480,6 @@ type AnimeMutation implements WithTimestamps {
"Update an Anime." "Update an Anime."
input: AnimeUpdateInput! input: AnimeUpdateInput!
): AnimeUpdatePayload ): AnimeUpdatePayload
updatedAt: ISO8601DateTime!
} }
"Autogenerated return type of AnimeUpdate" "Autogenerated return type of AnimeUpdate"
@ -739,6 +733,20 @@ type EpisodeConnection {
totalCount: Int! totalCount: Int!
} }
"Autogenerated return type of EpisodeCreate"
type EpisodeCreatePayload {
episode: Episode
"Graphql Errors"
errors: [Generic!]
}
"Autogenerated return type of EpisodeDelete"
type EpisodeDeletePayload {
episode: GenericDelete
"Graphql Errors"
errors: [Generic!]
}
"An edge in a connection." "An edge in a connection."
type EpisodeEdge { type EpisodeEdge {
"A cursor for use in pagination." "A cursor for use in pagination."
@ -747,6 +755,31 @@ type EpisodeEdge {
node: Episode node: Episode
} }
type EpisodeMutation {
"Create an Episode."
create(
"Create an Episode"
input: EpisodeCreateInput!
): EpisodeCreatePayload
"Delete an Episode."
delete(
"Delete an Episode"
input: GenericDeleteInput!
): EpisodeDeletePayload
"Update an Episode."
update(
"Update an Episode"
input: EpisodeUpdateInput!
): EpisodeUpdatePayload
}
"Autogenerated return type of EpisodeUpdate"
type EpisodeUpdatePayload {
episode: Episode
"Graphql Errors"
errors: [Generic!]
}
"Favorite media, characters, and people for a user" "Favorite media, characters, and people for a user"
type Favorite implements WithTimestamps { type Favorite implements WithTimestamps {
createdAt: ISO8601DateTime! createdAt: ISO8601DateTime!
@ -778,30 +811,24 @@ type FavoriteEdge {
node: Favorite node: Favorite
} }
type Generic implements Base & WithTimestamps { type Generic implements Base {
"The error code." "The error code."
code: String code: String
createdAt: ISO8601DateTime!
"A description of the error" "A description of the error"
message: String! message: String!
"Which input value this error came from" "Which input value this error came from"
path: [String!] path: [String!]
updatedAt: ISO8601DateTime!
} }
type GenericDelete implements WithTimestamps { type GenericDelete {
createdAt: ISO8601DateTime!
id: ID! id: ID!
updatedAt: ISO8601DateTime!
} }
type Image implements WithTimestamps { type Image {
"A blurhash-encoded version of this image" "A blurhash-encoded version of this image"
blurhash: String blurhash: String
createdAt: ISO8601DateTime!
"The original image" "The original image"
original: ImageView! original: ImageView!
updatedAt: ISO8601DateTime!
"The various generated views of this image" "The various generated views of this image"
views(names: [String!]): [ImageView!]! views(names: [String!]): [ImageView!]!
} }
@ -820,7 +847,7 @@ type ImageView implements WithTimestamps {
} }
"The user library filterable by media_type and status" "The user library filterable by media_type and status"
type Library implements WithTimestamps { type Library {
"All Library Entries for a specific Media" "All Library Entries for a specific Media"
all( all(
"Returns the elements in the list that come after the specified cursor." "Returns the elements in the list that come after the specified cursor."
@ -846,7 +873,6 @@ type Library implements WithTimestamps {
last: Int, last: Int,
mediaType: media_type! mediaType: media_type!
): LibraryEntryConnection! ): LibraryEntryConnection!
createdAt: ISO8601DateTime!
"Library Entries for a specific Media filtered by the current status" "Library Entries for a specific Media filtered by the current status"
current( current(
"Returns the elements in the list that come after the specified cursor." "Returns the elements in the list that come after the specified cursor."
@ -895,7 +921,6 @@ type Library implements WithTimestamps {
last: Int, last: Int,
mediaType: media_type! mediaType: media_type!
): LibraryEntryConnection! ): LibraryEntryConnection!
updatedAt: ISO8601DateTime!
} }
"Information about a specific media entry for a user" "Information about a specific media entry for a user"
@ -984,13 +1009,12 @@ type LibraryEntryEdge {
node: LibraryEntry node: LibraryEntry
} }
type LibraryEntryMutation implements WithTimestamps { type LibraryEntryMutation {
"Create a library entry" "Create a library entry"
create( create(
"Create a Library Entry" "Create a Library Entry"
input: LibraryEntryCreateInput! input: LibraryEntryCreateInput!
): LibraryEntryCreatePayload ): LibraryEntryCreatePayload
createdAt: ISO8601DateTime!
"Delete a library entry" "Delete a library entry"
delete( delete(
"Delete Library Entry" "Delete Library Entry"
@ -1001,17 +1025,36 @@ type LibraryEntryMutation implements WithTimestamps {
"Update Library Entry" "Update Library Entry"
input: LibraryEntryUpdateInput! input: LibraryEntryUpdateInput!
): LibraryEntryUpdatePayload ): LibraryEntryUpdatePayload
"Update a library entry status by id" "Update library entry progress by id"
updateProgressById(
"Update library entry progress by id"
input: UpdateProgressByIdInput!
): LibraryEntryUpdateProgressByIdPayload
"Update library entry progress by media"
updateProgressByMedia(
"Update library entry progress by media"
input: UpdateProgressByMediaInput!
): LibraryEntryUpdateProgressByMediaPayload
"Update library entry rating by id"
updateRatingById(
"Update library entry rating by id"
input: UpdateRatingByIdInput!
): LibraryEntryUpdateRatingByIdPayload
"Update library entry rating by media"
updateRatingByMedia(
"Update library entry rating by media"
input: UpdateRatingByMediaInput!
): LibraryEntryUpdateRatingByMediaPayload
"Update library entry status by id"
updateStatusById( updateStatusById(
"Update a library entry status by id" "Update library entry status by id"
input: UpdateStatusByIdInput! input: UpdateStatusByIdInput!
): LibraryEntryUpdateStatusByIdPayload ): LibraryEntryUpdateStatusByIdPayload
"Update a library entry status by media" "Update library entry status by media"
updateStatusByMedia( updateStatusByMedia(
"Update a library entry status by media" "Update library entry status by media"
input: UpdateStatusByMediaInput! input: UpdateStatusByMediaInput!
): LibraryEntryUpdateStatusByMediaPayload ): LibraryEntryUpdateStatusByMediaPayload
updatedAt: ISO8601DateTime!
} }
"Autogenerated return type of LibraryEntryUpdate" "Autogenerated return type of LibraryEntryUpdate"
@ -1021,6 +1064,34 @@ type LibraryEntryUpdatePayload {
libraryEntry: LibraryEntry libraryEntry: LibraryEntry
} }
"Autogenerated return type of LibraryEntryUpdateProgressById"
type LibraryEntryUpdateProgressByIdPayload {
"Graphql Errors"
errors: [Generic!]
libraryEntry: LibraryEntry
}
"Autogenerated return type of LibraryEntryUpdateProgressByMedia"
type LibraryEntryUpdateProgressByMediaPayload {
"Graphql Errors"
errors: [Generic!]
libraryEntry: LibraryEntry
}
"Autogenerated return type of LibraryEntryUpdateRatingById"
type LibraryEntryUpdateRatingByIdPayload {
"Graphql Errors"
errors: [Generic!]
libraryEntry: LibraryEntry
}
"Autogenerated return type of LibraryEntryUpdateRatingByMedia"
type LibraryEntryUpdateRatingByMediaPayload {
"Graphql Errors"
errors: [Generic!]
libraryEntry: LibraryEntry
}
"Autogenerated return type of LibraryEntryUpdateStatusById" "Autogenerated return type of LibraryEntryUpdateStatusById"
type LibraryEntryUpdateStatusByIdPayload { type LibraryEntryUpdateStatusByIdPayload {
"Graphql Errors" "Graphql Errors"
@ -1210,10 +1281,9 @@ type Manga implements Media & WithTimestamps {
volumeCount: Int volumeCount: Int
} }
type MangaAmountConsumed implements AmountConsumed & WithTimestamps { type MangaAmountConsumed implements AmountConsumed {
"Total media completed atleast once." "Total media completed atleast once."
completed: Int! completed: Int!
createdAt: ISO8601DateTime!
id: ID! id: ID!
"Total amount of media." "Total amount of media."
media: Int! media: Int!
@ -1223,13 +1293,11 @@ type MangaAmountConsumed implements AmountConsumed & WithTimestamps {
recalculatedAt: ISO8601Date! recalculatedAt: ISO8601Date!
"Total progress of library including reconsuming." "Total progress of library including reconsuming."
units: Int! units: Int!
updatedAt: ISO8601DateTime!
} }
type MangaCategoryBreakdown implements CategoryBreakdown & WithTimestamps { type MangaCategoryBreakdown implements CategoryBreakdown {
"A Map of category_id -> count for all categories present on the library entries" "A Map of category_id -> count for all categories present on the library entries"
categories: Map! categories: Map!
createdAt: ISO8601DateTime!
id: ID! id: ID!
"The profile related to the user for this stat." "The profile related to the user for this stat."
profile: Profile! profile: Profile!
@ -1237,7 +1305,6 @@ type MangaCategoryBreakdown implements CategoryBreakdown & WithTimestamps {
recalculatedAt: ISO8601Date! recalculatedAt: ISO8601Date!
"The total amount of library entries." "The total amount of library entries."
total: Int! total: Int!
updatedAt: ISO8601DateTime!
} }
"The connection type for Manga." "The connection type for Manga."
@ -1470,12 +1537,11 @@ type MediaStaffEdge {
node: MediaStaff node: MediaStaff
} }
type Mutation implements WithTimestamps { type Mutation {
anime: AnimeMutation anime: AnimeMutation
createdAt: ISO8601DateTime! episode: EpisodeMutation
libraryEntry: LibraryEntryMutation libraryEntry: LibraryEntryMutation
pro: ProMutation! pro: ProMutation!
updatedAt: ISO8601DateTime!
} }
"Information about pagination in a connection." "Information about pagination in a connection."
@ -1490,11 +1556,7 @@ type PageInfo {
startCursor: String startCursor: String
} }
""" "A Voice Actor, Director, Animator, or other person who works in the creation and localization of media"
A Voice Actor, Director, Animator, or other person who works in the creation and\
localization of media
"""
type Person implements WithTimestamps { type Person implements WithTimestamps {
"The day when this person was born" "The day when this person was born"
birthday: Date birthday: Date
@ -1504,6 +1566,17 @@ type Person implements WithTimestamps {
id: ID! id: ID!
"An image of the person" "An image of the person"
image: Image image: Image
"Information about the person working on specific media"
mediaStaff(
"Returns the elements in the list that come after the specified cursor."
after: String,
"Returns the elements in the list that come before the specified cursor."
before: String,
"Returns the first _n_ elements from the list."
first: Int,
"Returns the last _n_ elements from the list."
last: Int
): MediaStaffConnection
"The primary name of this person." "The primary name of this person."
name: String! name: String!
"The name of this person in various languages" "The name of this person in various languages"
@ -1596,8 +1669,7 @@ type PostEdge {
node: Post node: Post
} }
type ProMutation implements WithTimestamps { type ProMutation {
createdAt: ISO8601DateTime!
"Set the user's discord tag" "Set the user's discord tag"
setDiscord( setDiscord(
"Your discord tag (Name#1234)" "Your discord tag (Name#1234)"
@ -1610,7 +1682,6 @@ type ProMutation implements WithTimestamps {
): SetMessagePayload ): SetMessagePayload
"End the user's pro subscription" "End the user's pro subscription"
unsubscribe: UnsubscribePayload unsubscribe: UnsubscribePayload
updatedAt: ISO8601DateTime!
} }
"A subscription to Kitsu PRO" "A subscription to Kitsu PRO"
@ -1719,11 +1790,7 @@ type Profile implements WithTimestamps {
"Returns the last _n_ elements from the list." "Returns the last _n_ elements from the list."
last: Int last: Int
): MediaReactionConnection! ): MediaReactionConnection!
""" "A non-unique publicly visible name for the profile. Minimum of 3 characters and any valid Unicode character"
A non-unique publicly visible name for the profile.
Minimum of 3 characters and any valid Unicode character
"""
name: String! name: String!
"Post pinned to the user profile" "Post pinned to the user profile"
pinnedPost: Post pinnedPost: Post
@ -1787,17 +1854,15 @@ type ProfileEdge {
} }
"The different types of user stats that we calculate." "The different types of user stats that we calculate."
type ProfileStats implements WithTimestamps { type ProfileStats {
"The total amount of anime you have watched over your whole life." "The total amount of anime you have watched over your whole life."
animeAmountConsumed: AnimeAmountConsumed! animeAmountConsumed: AnimeAmountConsumed!
"The breakdown of the different categories related to the anime you have completed" "The breakdown of the different categories related to the anime you have completed"
animeCategoryBreakdown: AnimeCategoryBreakdown! animeCategoryBreakdown: AnimeCategoryBreakdown!
createdAt: ISO8601DateTime!
"The total amount of manga you ahve read over your whole life." "The total amount of manga you ahve read over your whole life."
mangaAmountConsumed: MangaAmountConsumed! mangaAmountConsumed: MangaAmountConsumed!
"The breakdown of the different categories related to the manga you have completed" "The breakdown of the different categories related to the manga you have completed"
mangaCategoryBreakdown: MangaCategoryBreakdown! mangaCategoryBreakdown: MangaCategoryBreakdown!
updatedAt: ISO8601DateTime!
} }
type Query { type Query {
@ -2017,13 +2082,11 @@ type QuoteLineEdge {
} }
"Information about a user session" "Information about a user session"
type Session implements WithTimestamps { type Session {
"The account associated with this session" "The account associated with this session"
account: Account account: Account
createdAt: ISO8601DateTime!
"The profile associated with this session" "The profile associated with this session"
profile: Profile profile: Profile
updatedAt: ISO8601DateTime!
} }
"Autogenerated return type of SetDiscord" "Autogenerated return type of SetDiscord"
@ -2141,17 +2204,15 @@ type StreamingLinkEdge {
node: StreamingLink node: StreamingLink
} }
type TitlesList implements WithTimestamps { type TitlesList {
"A list of additional, alternative, abbreviated, or unofficial titles" "A list of additional, alternative, abbreviated, or unofficial titles"
alternatives: [String!] alternatives: [String!]
"The official or de facto international title" "The official or de facto international title"
canonical: String canonical: String
"The locale code that identifies which title is used as the canonical title" "The locale code that identifies which title is used as the canonical title"
canonicalLocale: String canonicalLocale: String
createdAt: ISO8601DateTime!
"The list of localized titles keyed by locale" "The list of localized titles keyed by locale"
localized(locales: [String!]): Map! localized(locales: [String!]): Map!
updatedAt: ISO8601DateTime!
} }
"Autogenerated return type of Unsubscribe" "Autogenerated return type of Unsubscribe"
@ -2424,6 +2485,27 @@ input AnimeUpdateInput {
youtubeTrailerVideoId: String youtubeTrailerVideoId: String
} }
input EpisodeCreateInput {
description: Map
length: Int
mediaId: ID!
mediaType: media_type!
number: Int!
releasedAt: Date
thumbnailImage: Upload
titles: TitlesListInput!
}
input EpisodeUpdateInput {
description: Map
id: ID!
length: Int
number: Int
releasedAt: Date
thumbnailImage: Upload
titles: TitlesListInput
}
input GenericDeleteInput { input GenericDeleteInput {
id: ID! id: ID!
} }
@ -2464,6 +2546,30 @@ input TitlesListInput {
localized: Map localized: Map
} }
input UpdateProgressByIdInput {
id: ID!
progress: Int!
}
input UpdateProgressByMediaInput {
mediaId: ID!
mediaType: media_type!
progress: Int!
}
input UpdateRatingByIdInput {
id: ID!
"A number between 2 - 20"
rating: Int!
}
input UpdateRatingByMediaInput {
mediaId: ID!
mediaType: media_type!
"A number between 2 - 20"
rating: Int!
}
input UpdateStatusByIdInput { input UpdateStatusByIdInput {
id: ID! id: ID!
status: LibraryEntryStatus! status: LibraryEntryStatus!

View File

@ -38,6 +38,17 @@ final class Tabs {
bool $hasSectionWrapper = false bool $hasSectionWrapper = false
): string ): string
{ {
if (count($tabData) < 2)
{
return $this->render('single-tab.php', [
'name' => $name,
'data' => $tabData,
'callback' => $cb,
'className' => $className . ' single-tab',
'hasSectionWrapper' => $hasSectionWrapper,
]);
}
return $this->render('tabs.php', [ return $this->render('tabs.php', [
'name' => $name, 'name' => $name,
'data' => $tabData, 'data' => $tabData,

View File

@ -50,15 +50,14 @@ final class People extends BaseController {
/** /**
* Show information about a person * Show information about a person
* *
* @param string $id * @param string $slug
* @param string|null $slug
* @return void * @return void
* @throws ContainerException * @throws ContainerException
* @throws NotFoundException * @throws NotFoundException
*/ */
public function index(string $id, ?string $slug = NULL): void public function index(string $slug): void
{ {
$rawData = $this->model->getPerson($id, $slug); $rawData = $this->model->getPerson($slug);
$data = (new PersonTransformer())->transform($rawData)->toArray(); $data = (new PersonTransformer())->transform($rawData)->toArray();
if (( ! array_key_exists('data', $rawData)) || empty($rawData['data'])) if (( ! array_key_exists('data', $rawData)) || empty($rawData['data']))

View File

@ -20,28 +20,16 @@ namespace Aviat\AnimeClient\Types;
* Type representing a person for display * Type representing a person for display
*/ */
final class Person extends AbstractType { final class Person extends AbstractType {
/**
* @var string
*/
public $id; public $id;
/**
* @var string
*/
public ?string $name; public ?string $name;
/** public array $names = [];
* @var Characters
*/ public ?string $description;
public ?Characters $characters;
public array $characters = [];
/**
* @var array
*/
public array $staff = []; public array $staff = [];
public function setCharacters($characters): void
{
$this->characters = Characters::from($characters);
}
} }