Ugly Progress Commit
All checks were successful
timw4mail/HummingBirdAnimeClient/develop This commit looks good

* Update Person pages to have series organized by character for Voice
Acting
* Miscellaneous style updates
* Add placeholder images for items missing images
This commit is contained in:
Timothy Warren 2018-10-26 13:08:45 -04:00
parent 14be365a16
commit 50b65d66e1
16 changed files with 316 additions and 67 deletions

View File

@ -1,6 +1,6 @@
<main class="details fixed">
<section class="flex flex-no-wrap">
<div>
<section class="flex">
<aside class="info">
<picture class="cover">
<source srcset="<?= $urlGenerator->assetUrl("images/anime/{$show_data['id']}-original.webp") ?>" type="image/webp">
<source srcset="<?= $urlGenerator->assetUrl("images/anime/{$show_data['id']}-original.jpg") ?>" type="image/jpeg">
@ -40,8 +40,8 @@
</td>
</tr>
</table>
</div>
<div>
</aside>
<article class="text">
<h2><a rel="external" href="<?= $show_data['url'] ?>"><?= $show_data['title'] ?></a></h2>
<?php foreach ($show_data['titles'] as $title): ?>
<h3><?= $title ?></h3>
@ -85,13 +85,13 @@
<h4>Trailer</h4>
<iframe width="560" height="315" src="https://www.youtube.com/embed/<?= $show_data['trailer_id'] ?>" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen></iframe>
<?php endif ?>
</div>
</article>
</section>
<?php if (count($characters) > 0): ?>
<hr />
<br />
<h2>Characters</h2>
<section class="align_center media-wrap">
<section class="media-wrap flex flex-wrap flex-justify-start">
<?php foreach($characters as $id => $char): ?>
<?php if ( ! empty($char['image']['original'])): ?>
<article class="character">

View File

@ -4,18 +4,29 @@ use Aviat\AnimeClient\API\Kitsu;
?>
<main class="details fixed">
<section class="flex flex-no-wrap">
<div>
<picture class="cover">
<aside class="info cover">
<picture>
<source srcset="<?= $urlGenerator->assetUrl("images/characters/{$data[0]['id']}-original.webp") ?>" type="image/webp">
<source srcset="<?= $urlGenerator->assetUrl("images/characters/{$data[0]['id']}-original.jpg") ?>" type="image/jpeg">
<img src="<?= $urlGenerator->assetUrl("images/characters/{$data[0]['id']}-original.jpg") ?>" alt="" />
</picture>
</div>
<div>
<h2><?= $data[0]['attributes']['name'] ?></h2>
<?php if ( ! empty($data[0]['attributes']['otherNames'])): ?>
<h3>Nicknames / Other names</h3>
<?php foreach ($data[0]['attributes']['otherNames'] as $name): ?>
<h4><?= $name ?></h4>
<?php endforeach ?>
<?php endif ?>
</aside>
<article class="text">
<h2><?= $data['name'] ?></h2>
<?php foreach ($data['names'] as $name): ?>
<h3><?= $name ?></h3>
<?php endforeach ?>
<hr />
<p class="description"><?= $data[0]['attributes']['description'] ?></p>
</div>
</article>
</section>
<?php if (array_key_exists('anime', $data['included']) || array_key_exists('manga', $data['included'])): ?>

View File

@ -1,6 +1,6 @@
<main class="details fixed">
<section class="flex flex-no-wrap">
<div>
<aside class="info">
<picture class="cover">
<source srcset="<?= $urlGenerator->assetUrl("images/manga/{$data['id']}-original.webp") ?>" type="image/webp">
<source srcset="<?= $urlGenerator->assetUrl("images/manga/{$data['id']}-original.jpg") ?>" type="image/jpeg">
@ -28,8 +28,8 @@
</td>
</tr>
</table>
</div>
<div>
</aside>
<article class="text">
<h2><a rel="external" href="<?= $data['url'] ?>"><?= $data['title'] ?></a></h2>
<?php foreach($data['titles'] as $title): ?>
<h3><?= $title ?></h3>
@ -37,12 +37,12 @@
<br />
<p><?= nl2br($data['synopsis']) ?></p>
</div>
</article>
</section>
<?php if (count($characters) > 0): ?>
<h2>Characters</h2>
<section class="media-wrap">
<section class="media-wrap flex flex-wrap flex-justify-start">
<?php foreach($characters as $id => $char): ?>
<?php if ( ! empty($char['image']['original'])): ?>
<article class="character">

View File

@ -0,0 +1,62 @@
<?php
use function Aviat\AnimeClient\getLocalImg;
use Aviat\AnimeClient\API\Kitsu;
?>
<?php foreach ($entries as $type => $casting): ?>
<?php if($type === 'characters'): ?>
<table class="min-table">
<tr>
<th>Character</th>
<th>Series</th>
</tr>
<?php foreach ($casting as $cid => $character): ?>
<tr>
<td style="width:229px">
<article class="character">
<?php
$link = $url->generate('character', ['slug' => $character['character']['slug']]);
?>
<a href="<?= $link ?>">
<?php $imgPath = ($character['character']['image'] === NULL)
? $urlGenerator->assetUrl('images/characters/empty.png')
: $urlGenerator->assetUrl(getLocalImg($character['character']['image']['original']));
?>
<img src="<?= $imgPath ?>" alt="" />
<div class="name">
<?= $character['character']['canonicalName'] ?>
</div>
</a>
</article>
</td>
<td>
<section class="align_left media-wrap">
<?php foreach ($character['media'] as $sid => $series): ?>
<article class="media">
<?php
$link = $url->generate('anime.details', ['id' => $series['slug']]);
$titles = Kitsu::filterTitles($series);
?>
<a href="<?= $link ?>">
<img
src="<?= $urlGenerator->assetUrl("images/anime/{$sid}.jpg") ?>"
width="220" alt=""
/>
</a>
<div class="name">
<a href="<?= $link ?>">
<?= array_shift($titles) ?>
<?php foreach ($titles as $title): ?>
<br />
<small><?= $title ?></small>
<?php endforeach ?>
</a>
</div>
</article>
<?php endforeach ?>
</section>
</td>
</tr>
<?php endforeach; ?>
</table>
<?php endif ?>
<?php endforeach ?>

View File

@ -1,5 +1,4 @@
<?php
use function Aviat\AnimeClient\getLocalImg;
use Aviat\AnimeClient\API\Kitsu;
?>
<main class="details fixed">
@ -27,7 +26,11 @@ use Aviat\AnimeClient\API\Kitsu;
<h3>Castings</h3>
<?php foreach ($castings as $role => $entries): ?>
<h4><?= $role ?></h4>
<?php if($role === 'Voice Actor'): ?>
<?php include 'character-mapping.php' ?>
<?php else: ?>
<?php foreach ($entries as $type => $casting): ?>
<?php if ($type === 'characters') continue; ?>
<?php if ( ! empty($entries['manga'])): ?>
<h5><?= ucfirst($type) ?></h5>
<?php endif ?>
@ -68,6 +71,7 @@ use Aviat\AnimeClient\API\Kitsu;
</section>
<br />
<?php endforeach ?>
<?php endif ?>
<?php endforeach ?>
<?php endif ?>
</section>

View File

@ -24,6 +24,7 @@
"aura/session": "^2.0",
"aviat/banker": "^1.0.0",
"aviat/ion": "^2.4.1",
"ext-iconv": "*",
"ext-json": "*",
"ext-gd":"*",
"ext-pdo": "*",

View File

@ -20,6 +20,8 @@ use Aviat\AnimeClient\Types\Config as ConfigType;
use function Aviat\Ion\_dir;
setlocale(LC_CTYPE, 'en_US');
// Work around the silly timezone error
$timezone = ini_get('date.timezone');
if ($timezone === '' || $timezone === FALSE)

File diff suppressed because one or more lines are too long

View File

@ -91,6 +91,10 @@ a:hover, a:active {
flex-wrap: nowrap
}
.flex-align-start {
align-content: flex-start;
}
.flex-align-end {
align-items: flex-end
}
@ -99,6 +103,10 @@ a:hover, a:active {
align-content: space-around
}
.flex-justify-start {
justify-content: flex-start;
}
.flex-justify-space-around {
justify-content: space-around
}
@ -107,6 +115,10 @@ a:hover, a:active {
align-self: center
}
.flex-space-evenly {
justify-content: space-evenly;
}
.flex {
display: flex
}
@ -671,7 +683,13 @@ picture.cover {
}
.fixed {
max-width: 93rem;
max-width: 100rem;
/* max-width: 80%; */
margin: 0 auto;
}
.fixed .text {
max-width: 600px;
}
.details .cover {
@ -684,14 +702,10 @@ picture.cover {
margin-top: 0;
}
.details .flex > div {
.details .flex > * {
margin: 1rem;
}
.details .media_details {
max-width: 300px;
}
.details .media_details td {
padding: 0 1.5rem;
}
@ -752,6 +766,15 @@ picture.cover {
margin-left: 0;
}
aside.info {
max-width: 390px;
}
aside.info picture, aside.info img {
display: block;
margin: 0 auto;
}
/* ----------------------------------------------------------------------------
User page styles
-----------------------------------------------------------------------------*/

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 B

View File

@ -101,27 +101,28 @@ final class JsonAPI {
continue;
}
// Single data item
else if (array_key_exists('id', $props['data']))
if (array_key_exists('id', $props['data']))
{
$idKey = $props['data']['id'];
$typeKey = $props['data']['type'];
$dataType = $props['data']['type'];
$relationship =& $item['relationships'][$relType];
unset($relationship['data']);
if (in_array($relType, $singular))
if (\in_array($relType, $singular, TRUE))
{
$relationship = $included[$typeKey][$idKey];
$relationship = $included[$dataType][$idKey];
continue;
}
if ($relType === $typeKey)
if ($relType === $dataType)
{
$relationship[$idKey] = $included[$typeKey][$idKey];
$relationship[$idKey] = $included[$dataType][$idKey];
continue;
}
$relationship[$typeKey][$idKey] = $included[$typeKey][$idKey];
$relationship[$dataType][$idKey] = $included[$dataType][$idKey];
}
// Multiple data items
else
@ -129,16 +130,16 @@ final class JsonAPI {
foreach($props['data'] as $j => $datum)
{
$idKey = $props['data'][$j]['id'];
$typeKey = $props['data'][$j]['type'];
$dataType = $props['data'][$j]['type'];
$relationship =& $item['relationships'][$relType];
if ($relType === $typeKey)
if ($relType === $dataType)
{
$relationship[$idKey] = $included[$typeKey][$idKey];
$relationship[$idKey] = $included[$dataType][$idKey];
continue;
}
$relationship[$typeKey][$idKey][$j] = $included[$typeKey][$idKey];
$relationship[$dataType][$idKey][$j] = $included[$dataType][$idKey];
}
}
}
@ -201,29 +202,33 @@ final class JsonAPI {
{
foreach($items as $id => $item)
{
if (array_key_exists('relationships', $item) && is_array($item['relationships']))
if (array_key_exists('relationships', $item) && \is_array($item['relationships']))
{
foreach($item['relationships'] as $relType => $props)
{
if (array_key_exists('data', $props) && is_array($props['data']) && array_key_exists('id', $props['data']))
{
if (array_key_exists($props['data']['id'], $organized[$props['data']['type']]))
if (array_key_exists('data', $props) && \is_array($props['data']) && array_key_exists('id', $props['data']))
{
$idKey = $props['data']['id'];
$typeKey = $props['data']['type'];
$dataType = $props['data']['type'];
if ( ! array_key_exists($dataType, $organized))
{
$organized[$dataType] = [];
}
$relationship =& $organized[$type][$id]['relationships'][$relType];
unset($relationship['links']);
unset($relationship['data']);
if ($relType === $typeKey)
if ($relType === $dataType)
{
$relationship[$idKey] = $included[$typeKey][$idKey];
$relationship[$idKey] = $included[$dataType][$idKey];
continue;
}
$relationship[$typeKey][$idKey] = $organized[$typeKey][$idKey];
if (array_key_exists($idKey, $organized[$dataType]))
{
$relationship[$dataType][$idKey] = $organized[$dataType][$idKey];
}
}
}

View File

@ -229,11 +229,26 @@ final class Model {
*/
public function getPerson(string $id): array
{
return $this->getRequest("people/{$id}", [
$cacheItem = $this->cache->getItem("kitsu-person-{$id}");
if ( ! $cacheItem->isHit())
{
$data = $this->getRequest("people/{$id}", [
'query' => [
'include' => 'castings,castings.media,staff,staff.media,voices'
'fields' => [
'characters' => 'canonicalName,slug,image',
'anime' => 'canonicalTitle,titles,slug,posterImage',
'manga' => 'canonicalTitle,titles,slug,posterImage',
],
'include' => 'castings.character,castings.media'
],
]);
$cacheItem->set($data);
$cacheItem->save();
}
return $cacheItem->get();
}
/**

View File

@ -206,14 +206,14 @@ function getLocalImg ($kitsuUrl): string
{
if ( ! is_string($kitsuUrl))
{
return '/404';
return 'images/404/404.png';
}
$parts = parse_url($kitsuUrl);
if ($parts === FALSE)
{
return '/404';
return 'images/404/404.png';
}
$file = basename($parts['path']);
@ -221,11 +221,51 @@ function getLocalImg ($kitsuUrl): string
$ext = array_pop($fileParts);
$segments = explode('/', trim($parts['path'], '/'));
// dump($segments);
$type = $segments[0] === 'users' ? $segments[1] : $segments[0];
$id = $segments[count($segments) - 2];
return implode('/', ['images', $type, "{$id}.{$ext}"]);
}
/**
* Create a transparent placeholder image
*
* @param string $path
* @param int $width
* @param int $height
* @param string $text
*/
function createPlaceholderImage ($path, $width, $height, $text = 'Image Unavailable')
{
$width = $width ?? 200;
$height = $height ?? 200;
$img = imagecreate($width, $height);
$path = rtrim($path, '/');
// Background is the first color by default
imagecolorallocatealpha($img, 255, 255, 255, 127);
$textColor = imagecolorallocate($img, 64, 64, 64);
imagealphablending($img, TRUE);
// Generate placeholder text
$fontSize = 10;
$fontWidth = imagefontwidth($fontSize);
$fontHeight = imagefontheight($fontSize);
$length = strlen($text);
$textWidth = $length * $fontWidth;
$fxPos = (int) ceil((imagesx($img) - $textWidth) / 2);
$fyPos = (int) ceil((imagesy($img) - $fontHeight) / 2);
// Add the image text
imagestring($img, $fontSize, $fxPos, $fyPos, $text, $textColor);
// Save the images
imagesavealpha($img, TRUE);
imagepng($img, $path . '/placeholder.png', 9);
imagedestroy($img);
}

View File

@ -59,6 +59,14 @@ class Character extends BaseController {
$data = JsonAPI::organizeData($rawData);
$data['names'] = array_unique(
array_merge(
[ $data[0]['attributes']['canonicalName'] ],
$data[0]['attributes']['names']
)
);
$data['name'] = array_shift($data['names']);
if (array_key_exists('included', $data))
{
if (array_key_exists('anime', $data['included']))

View File

@ -16,6 +16,7 @@
namespace Aviat\AnimeClient\Controller;
use function Aviat\AnimeClient\createPlaceholderImage;
use function Amp\Promise\wait;
use Aviat\AnimeClient\Controller as BaseController;
@ -269,45 +270,60 @@ final class Index extends BaseController {
$fileName = str_replace('-original', '', $file);
[$id, $ext] = explode('.', basename($fileName));
$baseSavePath = $this->config->get('img_cache_path');
$typeMap = [
'anime' => [
'kitsuUrl' => "anime/poster_images/{$id}/medium.{$ext}",
'width' => 220,
'height' => 312,
],
'avatars' => [
'kitsuUrl' => "users/avatars/{$id}/original.{$ext}",
'width' => null,
'height' => null,
],
'characters' => [
'kitsuUrl' => "characters/images/{$id}/original.{$ext}",
'width' => 225,
'height' => 350,
],
'manga' => [
'kitsuUrl' => "manga/poster_images/{$id}/medium.{$ext}",
'width' => 220,
'height' => 312,
],
'people' => [
'kitsuUrl' => "people/images/{$id}/original.{$ext}",
'width' => null,
'height' => null,
],
];
if ( ! array_key_exists($type, $typeMap))
{
$this->notFound();
$this->getPlaceholder($baseSavePath, 100, 100);
return;
}
$kitsuUrl .= $typeMap[$type]['kitsuUrl'];
$width = $typeMap[$type]['width'];
$height = $typeMap[$type]['height'];
$promise = (new HummingbirdClient)->request($kitsuUrl);
$response = wait($promise);
if ($response->getStatus() !== 200)
{
if ($display)
{
$this->getPlaceholder("{$baseSavePath}/{$type}", $width, $height);
}
return;
}
$data = wait($response->getBody());
// echo "Fetching {$kitsuUrl}\n";
$baseSavePath = $this->config->get('img_cache_path');
$filePrefix = "{$baseSavePath}/{$type}/{$id}";
[$origWidth] = getimagesizefromstring($data);
@ -371,4 +387,19 @@ final class Index extends BaseController {
return $output;
}
private function getPlaceholder (string $path, ?int $width = 200, ?int $height = NULL): void
{
$height = $height ?? $width;
$filename = $path . '/placeholder.png';
if ( ! file_exists($path . '/placeholder.png'))
{
createPlaceholderImage($path, $width, $height);
}
header('Content-Type: image/png');
echo file_get_contents($filename);
}
}

View File

@ -62,11 +62,12 @@ final class People extends BaseController {
if (array_key_exists('included', $data) && array_key_exists('castings', $data['included']))
{
$viewData['included'] = $data['included'];
$viewData['castings'] = $this->organizeCast($data['included']['castings']);
$viewData['castCount'] = count($viewData['castings']);
}
$this->outputHTML('person', $viewData);
$this->outputHTML('person/index', $viewData);
}
protected function organizeCast(array $cast): array
@ -82,6 +83,52 @@ final class People extends BaseController {
$roleName = $role['attributes']['role'];
$media = $role['relationships']['media'];
$chars = $role['relationships']['character']['characters'] ?? [];
if ( ! array_key_exists($roleName, $output))
{
$output[$roleName] = [
'characters' => [],
];
}
if ( ! empty($chars))
{
$relatedMedia = [];
if (array_key_exists('anime', $media))
{
foreach($media['anime'] as $sid => $series)
{
$relatedMedia[$sid] = $series['attributes'];
}
}
foreach($chars as $cid => $character)
{
// To make sure all the media are properly associated,
// merge the found media for this iteration with
// existing media, making sure to preserve array keys
$existingMedia = array_key_exists($cid, $output[$roleName]['characters'])
? $output[$roleName]['characters'][$cid]['media']
: [];
$includedMedia = array_replace_recursive($existingMedia, $relatedMedia);
uasort($includedMedia, function ($a, $b) {
return $a['canonicalTitle'] <=> $b['canonicalTitle'];
});
$output[$roleName]['characters'][$cid] = [
'character' => $character['attributes'],
'media' => $includedMedia,
];
}
uasort($output[$roleName]['characters'], function ($a, $b) {
return $a['character']['canonicalName'] <=> $b['character']['canonicalName'];
});
}
if (array_key_exists('anime', $media))
{
@ -99,7 +146,7 @@ final class People extends BaseController {
{
$output[$roleName]['manga'][$sid] = $series;
}
uasort($output[$roleName]['anime'], function ($a, $b) {
uasort($output[$roleName]['manga'], function ($a, $b) {
return $a['attributes']['canonicalTitle'] <=> $b['attributes']['canonicalTitle'];
});
}