Version 5.1 - All the GraphQL #32

Closed
timw4mail wants to merge 1160 commits from develop into master
97 changed files with 1710 additions and 2588 deletions
Showing only changes of commit 49dc661de1 - Show all commits

View File

@ -181,6 +181,7 @@ $routes = [
'user_info' => [
'path' => '/user/{username}',
'controller' => 'user',
'action' => 'about',
'tokens' => [
'username' => '.*?'
]

View File

@ -20,5 +20,3 @@ host = "127.0.0.1"
# Database number
database = 2
[options]

View File

@ -2,52 +2,55 @@
<main class="details fixed">
<section class="flex">
<aside class="info">
<?= $helper->picture("images/anime/{$show_data['id']}-original.webp") ?>
<?= $helper->picture("images/anime/{$data['id']}-original.webp") ?>
<br />
<table class="media-details">
<tr>
<td class="align-right">Airing Status</td>
<td><?= $show_data['status'] ?></td>
<td><?= $data['status'] ?></td>
</tr>
<tr>
<td>Show Type</td>
<td><?= $show_data['show_type'] ?></td>
<td><?= $data['show_type'] ?></td>
</tr>
<tr>
<td>Episode Count</td>
<td><?= $show_data['episode_count'] ?? '-' ?></td>
<td><?= $data['episode_count'] ?? '-' ?></td>
</tr>
<?php if ( ! empty($show_data['episode_length'])): ?>
<?php if ( ! empty($data['episode_length'])): ?>
<tr>
<td>Episode Length</td>
<td><?= $show_data['episode_length'] ?> minutes</td>
<td><?= $data['episode_length'] ?> minutes</td>
</tr>
<?php endif ?>
<?php if ( ! empty($show_data['age_rating'])): ?>
<?php if ( ! empty($data['age_rating'])): ?>
<tr>
<td>Age Rating</td>
<td><abbr title="<?= $show_data['age_rating_guide'] ?>"><?= $show_data['age_rating'] ?></abbr>
<td><abbr title="<?= $data['age_rating_guide'] ?>"><?= $data['age_rating'] ?></abbr>
</td>
</tr>
<?php endif ?>
<tr>
<td>Genres</td>
<td>
<?= implode(', ', $show_data['genres']) ?>
<?= implode(', ', $data['genres']) ?>
</td>
</tr>
</table>
<br />
</aside>
<article class="text">
<h2 class="toph"><a rel="external" href="<?= $show_data['url'] ?>"><?= $show_data['title'] ?></a></h2>
<?php foreach ($show_data['titles'] as $title): ?>
<h2 class="toph"><a rel="external" href="<?= $data['url'] ?>"><?= $data['title'] ?></a></h2>
<?php foreach ($data['titles'] as $title): ?>
<h3><?= $title ?></h3>
<?php endforeach ?>
<br />
<p class="description"><?= nl2br($show_data['synopsis']) ?></p>
<?php if (count($show_data['streaming_links']) > 0): ?>
<p class="description"><?= nl2br($data['synopsis']) ?></p>
<?php if (count($data['streaming_links']) > 0): ?>
<hr />
<h4>Streaming on:</h4>
<table class="full-width invisible streaming-links">
@ -59,13 +62,13 @@
</tr>
</thead>
<tbody>
<?php foreach ($show_data['streaming_links'] as $link): ?>
<?php foreach ($data['streaming_links'] as $link): ?>
<tr>
<td class="align-left">
<?php if ($link['meta']['link'] !== FALSE): ?>
<a
href="<?= $link['link'] ?>"
title="Stream '<?= $show_data['title'] ?>' on <?= $link['meta']['name'] ?>"
title="Stream '<?= $data['title'] ?>' on <?= $link['meta']['name'] ?>"
>
<?= $helper->picture("images/{$link['meta']['image']}", 'svg', [
'class' => 'streaming-logo',
@ -92,13 +95,13 @@
</tbody>
</table>
<?php endif ?>
<?php if ( ! empty($show_data['trailer_id'])): ?>
<?php if ( ! empty($data['trailer_id'])): ?>
<div class="responsive-iframe">
<h4>Trailer</h4>
<iframe
width="560"
height="315"
src="https://www.youtube.com/embed/<?= $show_data['trailer_id'] ?>"
src="https://www.youtube.com/embed/<?= $data['trailer_id'] ?>"
frameborder="0"
allow="autoplay; encrypted-media"
allowfullscreen
@ -108,13 +111,13 @@
</article>
</section>
<?php if (count($characters) > 0): ?>
<?php if (count($data['characters']) > 0): ?>
<section>
<h2>Characters</h2>
<div class="tabs">
<?php $i = 0 ?>
<?php foreach ($characters as $role => $list): ?>
<?php foreach ($data['characters'] as $role => $list): ?>
<input
type="radio" name="character-types"
id="character-types-<?= $i ?>" <?= ($i === 0) ? 'checked' : '' ?> />
@ -140,14 +143,13 @@
</section>
<?php endif ?>
<?php if (count($staff) > 0): ?>
<?php //dump($staff); ?>
<?php if (count($data['staff']) > 0): ?>
<section>
<h2>Staff</h2>
<div class="vertical-tabs">
<?php $i = 0; ?>
<?php foreach ($staff as $role => $people): ?>
<?php foreach ($data['staff'] as $role => $people): ?>
<div class="tab">
<input type="radio" name="staff-roles" id="staff-role<?= $i ?>" <?= $i === 0 ? 'checked' : '' ?> />
<label for="staff-role<?= $i ?>"><?= $role ?></label>

View File

@ -7,10 +7,10 @@ use Aviat\AnimeClient\API\Kitsu;
<main class="details fixed">
<section class="flex flex-no-wrap">
<div>
<?= $helper->picture("images/characters/{$data[0]['id']}-original.webp") ?>
<?php if ( ! empty($data[0]['attributes']['otherNames'])): ?>
<?= $helper->picture("images/characters/{$data['id']}-original.webp") ?>
<?php if ( ! empty($data['otherNames'])): ?>
<h3>Nicknames / Other names</h3>
<?php foreach ($data[0]['attributes']['otherNames'] as $name): ?>
<?php foreach ($data['otherNames'] as $name): ?>
<h4><?= $name ?></h4>
<?php endforeach ?>
<?php endif ?>
@ -23,19 +23,19 @@ use Aviat\AnimeClient\API\Kitsu;
<hr />
<p class="description"><?= $data[0]['attributes']['description'] ?></p>
<p class="description"><?= $data['description'] ?></p>
</div>
</section>
<?php if (array_key_exists('anime', $data['included']) || array_key_exists('manga', $data['included'])): ?>
<?php if ( ! (empty($data['media']['anime']) || empty($data['media']['manga']))): ?>
<h3>Media</h3>
<div class="tabs">
<?php if (array_key_exists('anime', $data['included'])): ?>
<?php if ( ! empty($data['media']['anime'])): ?>
<input checked="checked" type="radio" id="media-anime" name="media-tabs" />
<label for="media-anime">Anime</label>
<section class="media-wrap content">
<?php foreach ($data['included']['anime'] as $id => $anime): ?>
<?php foreach ($data['media']['anime'] as $id => $anime): ?>
<article class="media">
<?php
$link = $url->generate('anime.details', ['id' => $anime['attributes']['slug']]);
@ -58,12 +58,12 @@ use Aviat\AnimeClient\API\Kitsu;
</section>
<?php endif ?>
<?php if (array_key_exists('manga', $data['included'])): ?>
<?php if ( ! empty($data['media']['manga'])): ?>
<input type="radio" id="media-manga" name="media-tabs" />
<label for="media-manga">Manga</label>
<section class="media-wrap content">
<?php foreach ($data['included']['manga'] as $id => $manga): ?>
<?php foreach ($data['media']['manga'] as $id => $manga): ?>
<article class="media">
<?php
$link = $url->generate('manga.details', ['id' => $manga['attributes']['slug']]);
@ -89,14 +89,68 @@ use Aviat\AnimeClient\API\Kitsu;
<?php endif ?>
<section>
<?php if ($castCount > 0): ?>
<?php if (count($data['castings']) > 0): ?>
<h3>Castings</h3>
<?php
$vas = $castings['Voice Actor'];
unset($castings['Voice Actor']);
ksort($vas)
$vas = $data['castings']['Voice Actor'];
unset($data['castings']['Voice Actor']);
ksort($vas)
?>
<?php foreach ($data['castings'] as $role => $entries): ?>
<h4><?= $role ?></h4>
<?php foreach ($entries as $language => $casting): ?>
<h5><?= $language ?></h5>
<table class="min-table">
<tr>
<th>Cast Member</th>
<th>Series</th>
</tr>
<?php foreach ($casting as $cid => $c): ?>
<tr>
<td>
<article class="character">
<?php
$link = $url->generate('person', ['id' => $c['person']['id']]);
?>
<a href="<?= $link ?>">
<?= $helper->picture(getLocalImg($c['person']['image'], TRUE)) ?>
<div class="name">
<?= $c['person']['name'] ?>
</div>
</a>
</article>
</td>
<td>
<section class="align-left media-wrap">
<?php foreach ($c['series'] as $series): ?>
<article class="media">
<?php
$link = $url->generate('anime.details', ['id' => $series['attributes']['slug']]);
$titles = Kitsu::filterTitles($series['attributes']);
?>
<a href="<?= $link ?>">
<?= $helper->picture(getLocalImg($series['attributes']['posterImage']['small'], TRUE)) ?>
</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 endforeach ?>
<?php endforeach ?>
<?php if ( ! empty($vas)): ?>
<h4>Voice Actors</h4>
@ -161,61 +215,6 @@ use Aviat\AnimeClient\API\Kitsu;
<?php endforeach ?>
</div>
<?php endif ?>
<?php foreach ($castings as $role => $entries): ?>
<h4><?= $role ?></h4>
<?php foreach ($entries as $language => $casting): ?>
<h5><?= $language ?></h5>
<table class="min-table">
<tr>
<th>Cast Member</th>
<th>Series</th>
</tr>
<?php foreach ($casting as $cid => $c): ?>
<tr>
<td style="width:229px">
<article class="character">
<?php
$link = $url->generate('person', ['id' => $c['person']['id']]);
?>
<a href="<?= $link ?>">
<?= $helper->picture(getLocalImg($c['person']['image'], TRUE)) ?>
<div class="name">
<?= $c['person']['name'] ?>
</div>
</a>
</article>
</td>
<td>
<section class="align-left media-wrap">
<?php foreach ($c['series'] as $series): ?>
<article class="media">
<?php
$link = $url->generate('anime.details', ['id' => $series['attributes']['slug']]);
$titles = Kitsu::filterTitles($series['attributes']);
?>
<a href="<?= $link ?>">
<?= $helper->picture(getLocalImg($series['attributes']['posterImage']['small'], TRUE)) ?>
</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 endforeach ?>
<?php endforeach ?>
<?php endif ?>
</section>
</main>

View File

@ -2,7 +2,7 @@
<main>
<h2>Edit Anime Collection Item</h2>
<form action="<?= $action_url ?>" method="post">
<table class="invisible form" style="border:0">
<table class="invisible form">
<tbody>
<tr>
<td rowspan="6" class="align-center">
@ -28,7 +28,7 @@
<td class="align-left">
<select name="media_id" id="media_id">
<?php foreach($media_items as $id => $name): ?>
<option <?= $item['media_id'] == $id ? 'selected="selected"' : '' ?> value="<?= $id ?>"><?= $name ?></option>
<option <?= $item['media_id'] === $id ? 'selected="selected"' : '' ?> value="<?= $id ?>"><?= $name ?></option>
<?php endforeach ?>
</select>
</td>

View File

@ -9,7 +9,7 @@
<a href="<?= $url->generate('anime.details', ['id' => $item['slug']]) ?>">
<?= $item['title'] ?>
</a>
<?= (!empty($item['alternate_title'])) ? " <br /><small> " . $item['alternate_title'] . "</small>" : "" ?>
<?= ! empty($item['alternate_title']) ? ' <br /><small> ' . $item['alternate_title'] . '</small>' : '' ?>
</td>
<td><?= $item['episode_count'] ?></td>
<td><?= $item['episode_length'] ?></td>

View File

@ -7,6 +7,7 @@
<meta http-equiv="Content-Security-Policy" content="script-src 'self'" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1, user-scalable=1" />
<link rel="stylesheet" href="<?= $urlGenerator->assetUrl('css/app.min.css') ?>" />
<link rel="<?= $config->get('dark_theme') ? '' : 'alternate ' ?>stylesheet" title="Dark Theme" href="<?= $urlGenerator->assetUrl('css/dark.min.css') ?>" />
<link rel="icon" href="<?= $urlGenerator->assetUrl('images/icons/favicon.ico') ?>" />
<link rel="apple-touch-icon" sizes="57x57" href="<?= $urlGenerator->assetUrl('images/icons/apple-icon-57x57.png') ?>">
<link rel="apple-touch-icon" sizes="60x60" href="<?= $urlGenerator->assetUrl('images/icons/apple-icon-60x60.png') ?>">
@ -38,4 +39,5 @@
}
}
?>
</header>

View File

@ -25,6 +25,8 @@
</td>
</tr>
</table>
<br />
</aside>
<article class="text">
<h2 class="toph"><a rel="external" href="<?= $data['url'] ?>"><?= $data['title'] ?></a></h2>
@ -37,11 +39,12 @@
</article>
</section>
<?php if (count($characters) > 0): ?>
<?php if (count($data['characters']) > 0): ?>
<h2>Characters</h2>
<div class="tabs">
<?php $i = 0 ?>
<?php foreach ($characters as $role => $list): ?>
<?php foreach ($data['characters'] as $role => $list): ?>
<input
type="radio" name="character-role-tabs"
id="character-tabs<?= $i ?>" <?= $i === 0 ? 'checked' : '' ?> />
@ -66,12 +69,12 @@
</div>
<?php endif ?>
<?php if (count($staff) > 0): ?>
<?php if (count($data['staff']) > 0): ?>
<h2>Staff</h2>
<div class="vertical-tabs">
<?php $i = 0 ?>
<?php foreach ($staff as $role => $people): ?>
<?php foreach ($data['staff'] as $role => $people): ?>
<div class="tab">
<input
type="radio" name="staff-roles" id="staff-role<?= $i ?>" <?= $i === 0 ? 'checked' : '' ?> />

View File

@ -5,7 +5,7 @@ use Aviat\AnimeClient\API\Kitsu;
<h3>Voice Acting Roles</h3>
<div class="tabs">
<?php $i = 0; ?>
<?php foreach($characters as $role => $characterList): ?>
<?php foreach($data['characters'] as $role => $characterList): ?>
<input <?= $i === 0 ? 'checked="checked"' : '' ?> type="radio" name="character-type-tabs" id="character-type-<?= $i ?>" />
<label for="character-type-<?= $i ?>"><h5><?= ucfirst($role) ?></h5></label>
<section class="content">
@ -16,7 +16,7 @@ use Aviat\AnimeClient\API\Kitsu;
</tr>
<?php foreach ($characterList as $cid => $character): ?>
<tr>
<td style="width:229px">
<td>
<article class="character">
<?php
$link = $url->generate('character', ['slug' => $character['character']['slug']]);

View File

@ -9,16 +9,16 @@ use Aviat\AnimeClient\API\Kitsu;
<?= $helper->picture("images/people/{$data['id']}-original.webp", 'jpg', ['class' => 'cover' ]) ?>
</div>
<div>
<h2 class="toph"><?= $data['attributes']['name'] ?></h2>
<h2 class="toph"><?= $data['name'] ?></h2>
</div>
</section>
<?php if ( ! empty($staff)): ?>
<?php if ( ! empty($data['staff'])): ?>
<section>
<h3>Castings</h3>
<div class="vertical-tabs">
<?php $i = 0 ?>
<?php foreach ($staff as $role => $entries): ?>
<?php foreach ($data['staff'] as $role => $entries): ?>
<div class="tab">
<input
type="radio" name="staff-roles" id="staff-role<?= $i ?>" <?= $i === 0 ? 'checked' : '' ?> />
@ -59,7 +59,7 @@ use Aviat\AnimeClient\API\Kitsu;
</section>
<?php endif ?>
<?php if ( ! (empty($characters['main']) || empty($characters['supporting']))): ?>
<?php if ( ! (empty($data['characters']['main']) || empty($data['characters']['supporting']))): ?>
<section>
<?php include 'character-mapping.php' ?>
</section>

View File

@ -1,45 +1,39 @@
<?php
use function Aviat\AnimeClient\getLocalImg;
use Aviat\AnimeClient\API\Kitsu;
?>
<main class="user-page details">
<h2 class="toph">
<?= $helper->a(
"https://kitsu.io/users/{$attributes['slug']}",
$attributes['name'], [
"https://kitsu.io/users/{$data['slug']}",
$data['name'], [
'title' => 'View profile on Kitsu'
])
?>
</h2>
<p><?= $escape->html($attributes['about']) ?></p>
<p><?= $escape->html($data['about']) ?></p>
<section class="flex flex-no-wrap">
<aside class="info">
<center>
<?php
$avatar = $urlGenerator->assetUrl(
getLocalImg($attributes['avatar']['original'], FALSE)
);
echo $helper->img($avatar, ['alt' => '']);
?>
<?= $helper->img($urlGenerator->assetUrl($data['avatar']), ['alt' => '']); ?>
</center>
<br />
<table class="media-details">
<tr>
<td>Location</td>
<td><?= $attributes['location'] ?></td>
<td><?= $data['location'] ?></td>
</tr>
<tr>
<td>Website</td>
<td><?= $helper->a($attributes['website'], $attributes['website']) ?></td>
<td><?= $helper->a($data['website'], $data['website']) ?></td>
</tr>
<?php if (array_key_exists('waifu', $relationships)): ?>
<?php if ( ! empty($data['waifu'])): ?>
<tr>
<td><?= $escape->html($attributes['waifuOrHusbando']) ?></td>
<td><?= $escape->html($data['waifu']['label']) ?></td>
<td>
<?php
$character = $relationships['waifu']['attributes'];
$character = $data['waifu']['character'];
echo $helper->a(
$url->generate('character', ['slug' => $character['slug']]),
$character['canonicalName']
@ -52,42 +46,24 @@ use Aviat\AnimeClient\API\Kitsu;
<h3>User Stats</h3><br />
<table class="media-details">
<?php foreach($data['stats'] as $label => $stat): ?>
<tr>
<td>Time spent watching anime:</td>
<td><?= $timeOnAnime ?></td>
</tr>
<tr>
<td># of Anime episodes watched</td>
<td><?= number_format($stats['anime-amount-consumed']['units']) ?></td>
</tr>
<tr>
<td># of Manga chapters read</td>
<td><?= number_format($stats['manga-amount-consumed']['units']) ?></td>
</tr>
<tr>
<td># of Posts</td>
<td><?= number_format($attributes['postsCount']) ?></td>
</tr>
<tr>
<td># of Comments</td>
<td><?= number_format($attributes['commentsCount']) ?></td>
</tr>
<tr>
<td># of Media Rated</td>
<td><?= number_format($attributes['ratingsCount']) ?></td>
<td><?= $label ?></td>
<td><?= $stat ?></td>
</tr>
<?php endforeach ?>
</table>
</aside>
<article>
<?php if ( ! empty($favorites)): ?>
<?php if ( ! empty($data['favorites'])): ?>
<h3>Favorites</h3>
<div class="tabs">
<?php $i = 0 ?>
<?php if ( ! empty($favorites['characters'])): ?>
<?php if ( ! empty($data['favorites']['characters'])): ?>
<input type="radio" name="user-favorites" id="user-fav-chars" <?= $i === 0 ? 'checked' : '' ?> />
<label for="user-fav-chars">Characters</label>
<section class="content full-width media-wrap">
<?php foreach($favorites['characters'] as $id => $char): ?>
<?php foreach($data['favorites']['characters'] as $id => $char): ?>
<?php if ( ! empty($char['image']['original'])): ?>
<article class="character">
<?php $link = $url->generate('character', ['slug' => $char['slug']]) ?>
@ -101,11 +77,11 @@ use Aviat\AnimeClient\API\Kitsu;
</section>
<?php $i++; ?>
<?php endif ?>
<?php if ( ! empty($favorites['anime'])): ?>
<?php if ( ! empty($data['favorites']['anime'])): ?>
<input type="radio" name="user-favorites" id="user-fav-anime" <?= $i === 0 ? 'checked' : '' ?> />
<label for="user-fav-anime">Anime</label>
<section class="content full-width media-wrap">
<?php foreach($favorites['anime'] as $anime): ?>
<?php foreach($data['favorites']['anime'] as $anime): ?>
<article class="media">
<?php
$link = $url->generate('anime.details', ['id' => $anime['slug']]);
@ -127,11 +103,11 @@ use Aviat\AnimeClient\API\Kitsu;
</section>
<?php $i++; ?>
<?php endif ?>
<?php if ( ! empty($favorites['manga'])): ?>
<?php if ( ! empty($data['favorites']['manga'])): ?>
<input type="radio" name="user-favorites" id="user-fav-manga" <?= $i === 0 ? 'checked' : '' ?> />
<label for="user-fav-manga">Manga</label>
<section class="content full-width media-wrap">
<?php foreach($favorites['manga'] as $manga): ?>
<?php foreach($data['favorites']['manga'] as $manga): ?>
<article class="media">
<?php
$link = $url->generate('manga.details', ['id' => $manga['slug']]);

View File

@ -22,7 +22,7 @@
"aura/html": "^2.0",
"aura/router": "^3.0",
"aura/session": "^2.0",
"aviat/banker": "^1.0.0",
"aviat/banker": "^2.0.0",
"aviat/ion": "^2.4.1",
"ext-iconv": "*",
"ext-json": "*",
@ -37,28 +37,30 @@
},
"require-dev": {
"consolidation/robo": "~1.0",
"filp/whoops": "^2.1",
"henrikbjorn/lurker": "^1.1.0",
"pdepend/pdepend": "^2.2",
"phploc/phploc": "^4.0",
"phpmd/phpmd": "^2.4",
"phpstan/phpstan": "^0.9.1",
"phpunit/phpunit": "^6.0",
"phpstan/phpstan": "^0.10.5",
"phpunit/phpunit": "^7.4.3",
"roave/security-advisories": "dev-master",
"robmorgan/phinx": "^0.10.6",
"sebastian/phpcpd": "^3.0",
"sebastian/phpcpd": "^4.1.0",
"spatie/phpunit-snapshot-assertions": "^1.2.0",
"squizlabs/php_codesniffer": "^3.2.2",
"symfony/var-dumper": "^4.0.1",
"theseer/phpdox": "^0.11.0",
"filp/whoops": "^2.1"
"theseer/phpdox": "*"
},
"scripts": {
"build": "vendor/bin/robo build",
"build:css": "cd public && npm run build && cd ..",
"build:css": "cd public && npm run build:css && cd ..",
"build:js": "cd public && npm run build:js && cd ..",
"clean": "vendor/bin/robo clean",
"coverage": "phpdbg -qrr -- vendor/bin/phpunit -c build",
"phpstan": "phpstan analyse -l 4 -c phpstan.neon src tests ./console index.php",
"watch:css": "cd public && npm run watch",
"watch:css": "cd public && npm run watch:css",
"watch:js": "cd public && npm run watch:js",
"test": "vendor/bin/phpunit"
},
"scripts-descriptions": {

View File

@ -47,12 +47,12 @@ $CONF_DIR = _dir($APP_DIR, 'config');
// -----------------------------------------------------------------------------
// Dependency Injection setup
// -----------------------------------------------------------------------------
$baseConfig = require $APPCONF_DIR . '/base_config.php';
$di = require $APP_DIR . '/bootstrap.php';
$baseConfig = require "{$APPCONF_DIR}/base_config.php";
$di = require "{$APP_DIR}/bootstrap.php";
$config = loadToml($CONF_DIR);
$overrideFile = $CONF_DIR . '/admin-override.toml';
$overrideFile = "{$CONF_DIR}/admin-override.toml";
$overrideConfig = file_exists($overrideFile)
? loadTomlFile($overrideFile)
: [];

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
undefined

View File

@ -0,0 +1,135 @@
a {
color: rgb(25, 120, 226);
text-shadow: var(--link-shadow);
}
a:hover {
color: #9e34fd;
}
body,
legend,
nav ul li a {
background: #333;
color: #eee;
}
nav a:hover, nav li.selected a {
border-color: #fff;
}
header button {
background: transparent;
}
table {
box-shadow: none;
}
td, th {
border-color: #111;
}
thead td,
thead th {
background: #333;
color: #eee;
}
tbody > tr:nth-child(2n) {
background: #555;
color: #eee;
}
tbody > tr:nth-child(2n+1) {
background: #333;
}
footer, legend, hr {
border-color: #ddd;
}
small {
color: #fff;
}
input, select, textarea {
color: #111;
}
.message, .static-message {
text-shadow: var(--white-link-shadow);
}
.message.success, .static-message.success {
background: #1f8454;
border-color: #70dda9;
}
.message.error, .static-message.error {
border-color:#f3e6e6;
background: #924949;
}
.message.info, .static-message.info {
border-color: #FFFFCC;
background: #bfbe3a;
}
.invisible tr,
.invisible td,
.invisible th,
.invisible tbody > tr:nth-child(2n),
.invisible tbody > tr:nth-child(2n+1) {
background: transparent;
}
#main-nav {
border-bottom: .1rem solid #ddd;
}
.tabs,
.vertical-tabs{
background: #333;
}
.tabs > label,
.vertical-tabs .tab label {
background: #222;
border: 0;
color: #eee;
}
.vertical-tabs .tab label {
width: 100%;
}
.tabs > label:hover,
.vertical-tabs .tab > label:hover {
background: #888;
}
.tabs > label:active,
.vertical-tabs .tab > label:active {
background: #999;
}
.tabs > [type="radio"]:checked + label,
.tabs > [type="radio"]:checked + label + .content,
.vertical-tabs [type="radio"]:checked + label,
.vertical-tabs [type="radio"]:checked ~ .content {
/* border-color: #333; */
border: 0;
background: #666;
color: #eee;
}
.vertical-tabs {
background: #222;
border: 1px solid #444;
}
.vertical-tabs .tab {
background: #666;
border-bottom: 1px solid #444;
}

1
public/css/dark.min.css vendored Normal file
View File

@ -0,0 +1 @@
a{color:#1978e2;text-shadow:var(--link-shadow)}a:hover{color:#9e34fd}body,legend,nav ul li a{background:#333;color:#eee}nav a:hover,nav li.selected a{border-color:#fff}header button{background:transparent}table{-webkit-box-shadow:none;box-shadow:none}td,th{border-color:#111}thead td,thead th{background:#333;color:#eee}tbody>tr:nth-child(2n){background:#555;color:#eee}tbody>tr:nth-child(odd){background:#333}footer,hr,legend{border-color:#ddd}small{color:#fff}input,select,textarea{color:#111}.message,.static-message{text-shadow:var(--white-link-shadow)}.message.success,.static-message.success{background:#1f8454;border-color:#70dda9}.message.error,.static-message.error{border-color:#f3e6e6;background:#924949}.message.info,.static-message.info{border-color:#ffc;background:#bfbe3a}.invisible tbody>tr:nth-child(2n),.invisible tbody>tr:nth-child(odd),.invisible td,.invisible th,.invisible tr{background:transparent}#main-nav{border-bottom:.1rem solid #ddd}.tabs,.vertical-tabs{background:#333}.tabs>label,.vertical-tabs .tab label{background:#222;border:0;color:#eee}.vertical-tabs .tab label{width:100%}.tabs>label:hover,.vertical-tabs .tab>label:hover{background:#888}.tabs>label:active,.vertical-tabs .tab>label:active{background:#999}.tabs>[type=radio]:checked+label,.tabs>[type=radio]:checked+label+.content,.vertical-tabs [type=radio]:checked+label,.vertical-tabs [type=radio]:checked~.content{border:0;background:#666;color:#eee}.vertical-tabs{background:#222;border:1px solid #444}.vertical-tabs .tab{background:#666;border-bottom:1px solid #444}

View File

@ -359,7 +359,7 @@ td .media-wrap-flex {
display: inline-block;
text-align: center;
width: 220px;
height: 311px;
height: 312px;
margin: var(--normal-padding);
z-index: 0;
background: rgba(0, 0, 0, 0.15);
@ -423,7 +423,7 @@ picture.cover {
background: var(--title-overlay); */
content: '';
display: block;
height: 311px;
height: 312px;
left: 0;
position: absolute;
top: 0;

View File

@ -39,23 +39,26 @@
table .align-right,
table.align-center {
border: 0;
display: block;
margin: 0 auto;
/* display: block; */
margin-left: auto;
margin-right: auto;
text-align: left;
width: 100%;
}
table tbody {
width: 100%;
}
table td {
display: inline-block;
}
table tbody,
table.media-details {
width: 100%;
}
table.media-details td {
display: block;
text-align: left !important;
width: 100%;
}
table thead {

View File

@ -7,17 +7,18 @@ sel.addEventListener(event,listener,false)}function delegateEvent(sel,target,eve
"GET")url+=url.match(/\?/)?ajaxSerialize(config.data):"?"+ajaxSerialize(config.data);request.open(method,url);request.onreadystatechange=function(){if(request.readyState===4){var responseText="";if(request.responseType==="json")responseText=JSON.parse(request.responseText);else responseText=request.responseText;if(request.status>299)config.error.call(null,request.status,responseText,request.response);else config.success.call(null,responseText,request.status)}};if(config.dataType==="json"){config.data=
JSON.stringify(config.data);config.mimeType="application/json"}else config.data=ajaxSerialize(config.data);request.setRequestHeader("Content-Type",config.mimeType);switch(method){case "GET":request.send(null);break;default:request.send(config.data);break}};AnimeClient.get=function(url,data,callback){callback=callback===undefined?null:callback;if(callback===null){callback=data;data={}}return AnimeClient.ajax(url,{data:data,success:callback})};AnimeClient.on("header","click",".message",function(e){AnimeClient.hide(e.target)});
AnimeClient.on("form.js-delete","submit",function(event){var proceed=confirm("Are you ABSOLUTELY SURE you want to delete this item?");if(proceed===false){event.preventDefault();event.stopPropagation()}});AnimeClient.on(".js-clear-cache","click",function(){AnimeClient.get("/cache_purge",function(){AnimeClient.showMessage("success","Successfully purged api cache")})});AnimeClient.on(".vertical-tabs input","change",function(event){var el=event.currentTarget.parentElement;var rect=el.getBoundingClientRect();
var top=rect.top+window.pageYOffset;window.scrollTo({top:top,behavior:"smooth"})});AnimeClient.on("main","change",".big-check",function(e){var id=e.target.id;document.getElementById("mal_"+id).checked=true});function renderAnimeSearchResults(data){var results=[];data.forEach(function(x){var item=x.attributes;var titles=item.titles.reduce(function(prev,current){return prev+(current+"<br />")},[]);results.push('\n\t\t\t<article class="media search">\n\t\t\t\t<div class="name">\n\t\t\t\t\t<input type="radio" class="mal-check" id="mal_'+
item.slug+'" name="mal_id" value="'+x.mal_id+'" />\n\t\t\t\t\t<input type="radio" class="big-check" id="'+item.slug+'" name="id" value="'+x.id+'" />\n\t\t\t\t\t<label for="'+item.slug+'">\n\t\t\t\t\t\t<picture width="220">\n\t\t\t\t\t\t\t<source srcset="/public/images/anime/'+x.id+'.webp" type="image/webp" />\n\t\t\t\t\t\t\t<source srcset="/public/images/anime/'+x.id+'.jpg" type="image/jpeg" />\n\t\t\t\t\t\t\t<img src="/public/images/anime/'+x.id+'.jpg" alt="" width="220" />\n\t\t\t\t\t\t</picture>\n\t\t\t\t\t\t\n\t\t\t\t\t\t<span class="name">\n\t\t\t\t\t\t\t'+
item.canonicalTitle+"<br />\n\t\t\t\t\t\t\t<small>"+titles+'</small>\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</label>\n\t\t\t\t</div>\n\t\t\t\t<div class="table">\n\t\t\t\t\t<div class="row">\n\t\t\t\t\t\t<span class="edit">\n\t\t\t\t\t\t\t<a class="bracketed" href="/anime/details/'+item.slug+'">Info Page</a>\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</article>\n\t\t')});return results.join("")}function renderMangaSearchResults(data){var results=[];data.forEach(function(x){var item=x.attributes;
var titles=item.titles.reduce(function(prev,current){return prev+(current+"<br />")},[]);results.push('\n\t\t\t<article class="media search">\n\t\t\t\t<div class="name">\n\t\t\t\t\t<input type="radio" id="mal_'+item.slug+'" name="mal_id" value="'+x.mal_id+'" />\n\t\t\t\t\t<input type="radio" class="big-check" id="'+item.slug+'" name="id" value="'+x.id+'" />\n\t\t\t\t\t<label for="'+item.slug+'">\n\t\t\t\t\t\t<picture width="220">\n\t\t\t\t\t\t\t<source srcset="/public/images/manga/'+x.id+'.webp" type="image/webp" />\n\t\t\t\t\t\t\t<source srcset="/public/images/manga/'+
x.id+'.jpg" type="image/jpeg" />\n\t\t\t\t\t\t\t<img src="/public/images/manga/'+x.id+'.jpg" alt="" width="220" />\n\t\t\t\t\t\t</picture>\n\t\t\t\t\t\t<span class="name">\n\t\t\t\t\t\t\t'+item.canonicalTitle+"<br />\n\t\t\t\t\t\t\t<small>"+titles+'</small>\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</label>\n\t\t\t\t</div>\n\t\t\t\t<div class="table">\n\t\t\t\t\t<div class="row">\n\t\t\t\t\t\t<span class="edit">\n\t\t\t\t\t\t\t<a class="bracketed" href="/manga/details/'+item.slug+'">Info Page</a>\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</article>\n\t\t')});
return results.join("")}var search=function(query){AnimeClient.$(".cssload-loader")[0].removeAttribute("hidden");AnimeClient.get(AnimeClient.url("/anime-collection/search"),{query:query},function(searchResults,status){searchResults=JSON.parse(searchResults);AnimeClient.$(".cssload-loader")[0].setAttribute("hidden","hidden");AnimeClient.$("#series-list")[0].innerHTML=renderAnimeSearchResults(searchResults.data)})};if(AnimeClient.hasElement(".anime #search"))AnimeClient.on("#search","keyup",AnimeClient.throttle(250,
function(e){var query=encodeURIComponent(e.target.value);if(query==="")return;search(query)}));AnimeClient.on("body.anime.list","click",".plus-one",function(e){var parentSel=AnimeClient.closestParent(e.target,"article");var watchedCount=parseInt(AnimeClient.$(".completed_number",parentSel)[0].textContent,10)||0;var totalCount=parseInt(AnimeClient.$(".total_number",parentSel)[0].textContent,10);var title=AnimeClient.$(".name a",parentSel)[0].textContent;var data={id:parentSel.dataset.kitsuId,mal_id:parentSel.dataset.malId,
data:{progress:watchedCount+1}};if(isNaN(watchedCount)||watchedCount===0)data.data.status="current";if(!isNaN(watchedCount)&&watchedCount+1===totalCount)data.data.status="completed";AnimeClient.show(AnimeClient.$("#loading-shadow")[0]);AnimeClient.ajax(AnimeClient.url("/anime/increment"),{data:data,dataType:"json",type:"POST",success:function(res){var resData=JSON.parse(res);if(resData.errors){AnimeClient.hide(AnimeClient.$("#loading-shadow")[0]);AnimeClient.showMessage("error","Failed to update "+
title+". ");AnimeClient.scrollToTop();return}if(resData.data.attributes.status==="completed")AnimeClient.hide(parentSel);AnimeClient.hide(AnimeClient.$("#loading-shadow")[0]);AnimeClient.showMessage("success","Successfully updated "+title);AnimeClient.$(".completed_number",parentSel)[0].textContent=++watchedCount;AnimeClient.scrollToTop()},error:function(){AnimeClient.hide(AnimeClient.$("#loading-shadow")[0]);AnimeClient.showMessage("error","Failed to update "+title+". ");AnimeClient.scrollToTop()}})});
var search$1=function(query){AnimeClient.$(".cssload-loader")[0].removeAttribute("hidden");AnimeClient.get(AnimeClient.url("/manga/search"),{query:query},function(searchResults,status){searchResults=JSON.parse(searchResults);AnimeClient.$(".cssload-loader")[0].setAttribute("hidden","hidden");AnimeClient.$("#series-list")[0].innerHTML=renderMangaSearchResults(searchResults.data)})};if(AnimeClient.hasElement(".manga #search"))AnimeClient.on("#search","keyup",AnimeClient.throttle(250,function(e){var query=
encodeURIComponent(e.target.value);if(query==="")return;search$1(query)}));AnimeClient.on(".manga.list","click",".edit-buttons button",function(e){var thisSel=e.target;var parentSel=AnimeClient.closestParent(e.target,"article");var type=thisSel.classList.contains("plus-one-chapter")?"chapter":"volume";var completed=parseInt(AnimeClient.$("."+type+"s_read",parentSel)[0].textContent,10)||0;var total=parseInt(AnimeClient.$("."+type+"_count",parentSel)[0].textContent,10);var mangaName=AnimeClient.$(".name",
parentSel)[0].textContent;if(isNaN(completed))completed=0;var data={id:parentSel.dataset.kitsuId,mal_id:parentSel.dataset.malId,data:{progress:completed}};if(isNaN(completed)||completed===0)data.data.status="current";if(!isNaN(completed)&&completed+1===total)data.data.status="completed";data.data.progress=++completed;AnimeClient.show(AnimeClient.$("#loading-shadow")[0]);AnimeClient.ajax(AnimeClient.url("/manga/increment"),{data:data,dataType:"json",type:"POST",mimeType:"application/json",success:function(){if(data.data.status===
"completed")AnimeClient.hide(parentSel);AnimeClient.hide(AnimeClient.$("#loading-shadow")[0]);AnimeClient.$("."+type+"s_read",parentSel)[0].textContent=completed;AnimeClient.showMessage("success","Successfully updated "+mangaName);AnimeClient.scrollToTop()},error:function(){AnimeClient.hide(AnimeClient.$("#loading-shadow")[0]);AnimeClient.showMessage("error","Failed to update "+mangaName);AnimeClient.scrollToTop()}})})})();
var top=rect.top+window.pageYOffset;window.scrollTo({top:top,behavior:"smooth"})});if("serviceWorker"in navigator)navigator.serviceWorker.register("/sw.js").then(function(reg){console.log("Service worker registered",reg.scope)})["catch"](function(error){console.error("Failed to register service worker",error)});AnimeClient.on("main","change",".big-check",function(e){var id=e.target.id;document.getElementById("mal_"+id).checked=true});function renderAnimeSearchResults(data){var results=[];data.forEach(function(x){var item=
x.attributes;var titles=item.titles.reduce(function(prev,current){return prev+(current+"<br />")},[]);results.push('\n\t\t\t<article class="media search">\n\t\t\t\t<div class="name">\n\t\t\t\t\t<input type="radio" class="mal-check" id="mal_'+item.slug+'" name="mal_id" value="'+x.mal_id+'" />\n\t\t\t\t\t<input type="radio" class="big-check" id="'+item.slug+'" name="id" value="'+x.id+'" />\n\t\t\t\t\t<label for="'+item.slug+'">\n\t\t\t\t\t\t<picture width="220">\n\t\t\t\t\t\t\t<source srcset="/public/images/anime/'+
x.id+'.webp" type="image/webp" />\n\t\t\t\t\t\t\t<source srcset="/public/images/anime/'+x.id+'.jpg" type="image/jpeg" />\n\t\t\t\t\t\t\t<img src="/public/images/anime/'+x.id+'.jpg" alt="" width="220" />\n\t\t\t\t\t\t</picture>\n\t\t\t\t\t\t\n\t\t\t\t\t\t<span class="name">\n\t\t\t\t\t\t\t'+item.canonicalTitle+"<br />\n\t\t\t\t\t\t\t<small>"+titles+'</small>\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</label>\n\t\t\t\t</div>\n\t\t\t\t<div class="table">\n\t\t\t\t\t<div class="row">\n\t\t\t\t\t\t<span class="edit">\n\t\t\t\t\t\t\t<a class="bracketed" href="/anime/details/'+
item.slug+'">Info Page</a>\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</article>\n\t\t')});return results.join("")}function renderMangaSearchResults(data){var results=[];data.forEach(function(x){var item=x.attributes;var titles=item.titles.reduce(function(prev,current){return prev+(current+"<br />")},[]);results.push('\n\t\t\t<article class="media search">\n\t\t\t\t<div class="name">\n\t\t\t\t\t<input type="radio" id="mal_'+item.slug+'" name="mal_id" value="'+x.mal_id+'" />\n\t\t\t\t\t<input type="radio" class="big-check" id="'+
item.slug+'" name="id" value="'+x.id+'" />\n\t\t\t\t\t<label for="'+item.slug+'">\n\t\t\t\t\t\t<picture width="220">\n\t\t\t\t\t\t\t<source srcset="/public/images/manga/'+x.id+'.webp" type="image/webp" />\n\t\t\t\t\t\t\t<source srcset="/public/images/manga/'+x.id+'.jpg" type="image/jpeg" />\n\t\t\t\t\t\t\t<img src="/public/images/manga/'+x.id+'.jpg" alt="" width="220" />\n\t\t\t\t\t\t</picture>\n\t\t\t\t\t\t<span class="name">\n\t\t\t\t\t\t\t'+item.canonicalTitle+"<br />\n\t\t\t\t\t\t\t<small>"+titles+
'</small>\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</label>\n\t\t\t\t</div>\n\t\t\t\t<div class="table">\n\t\t\t\t\t<div class="row">\n\t\t\t\t\t\t<span class="edit">\n\t\t\t\t\t\t\t<a class="bracketed" href="/manga/details/'+item.slug+'">Info Page</a>\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</article>\n\t\t')});return results.join("")}var search=function(query){AnimeClient.$(".cssload-loader")[0].removeAttribute("hidden");AnimeClient.get(AnimeClient.url("/anime-collection/search"),{query:query},
function(searchResults,status){searchResults=JSON.parse(searchResults);AnimeClient.$(".cssload-loader")[0].setAttribute("hidden","hidden");AnimeClient.$("#series-list")[0].innerHTML=renderAnimeSearchResults(searchResults.data)})};if(AnimeClient.hasElement(".anime #search"))AnimeClient.on("#search","keyup",AnimeClient.throttle(250,function(e){var query=encodeURIComponent(e.target.value);if(query==="")return;search(query)}));AnimeClient.on("body.anime.list","click",".plus-one",function(e){var parentSel=
AnimeClient.closestParent(e.target,"article");var watchedCount=parseInt(AnimeClient.$(".completed_number",parentSel)[0].textContent,10)||0;var totalCount=parseInt(AnimeClient.$(".total_number",parentSel)[0].textContent,10);var title=AnimeClient.$(".name a",parentSel)[0].textContent;var data={id:parentSel.dataset.kitsuId,mal_id:parentSel.dataset.malId,data:{progress:watchedCount+1}};if(isNaN(watchedCount)||watchedCount===0)data.data.status="current";if(!isNaN(watchedCount)&&watchedCount+1===totalCount)data.data.status=
"completed";AnimeClient.show(AnimeClient.$("#loading-shadow")[0]);AnimeClient.ajax(AnimeClient.url("/anime/increment"),{data:data,dataType:"json",type:"POST",success:function(res){var resData=JSON.parse(res);if(resData.errors){AnimeClient.hide(AnimeClient.$("#loading-shadow")[0]);AnimeClient.showMessage("error","Failed to update "+title+". ");AnimeClient.scrollToTop();return}if(resData.data.attributes.status==="completed")AnimeClient.hide(parentSel);AnimeClient.hide(AnimeClient.$("#loading-shadow")[0]);
AnimeClient.showMessage("success","Successfully updated "+title);AnimeClient.$(".completed_number",parentSel)[0].textContent=++watchedCount;AnimeClient.scrollToTop()},error:function(){AnimeClient.hide(AnimeClient.$("#loading-shadow")[0]);AnimeClient.showMessage("error","Failed to update "+title+". ");AnimeClient.scrollToTop()}})});var search$1=function(query){AnimeClient.$(".cssload-loader")[0].removeAttribute("hidden");AnimeClient.get(AnimeClient.url("/manga/search"),{query:query},function(searchResults,
status){searchResults=JSON.parse(searchResults);AnimeClient.$(".cssload-loader")[0].setAttribute("hidden","hidden");AnimeClient.$("#series-list")[0].innerHTML=renderMangaSearchResults(searchResults.data)})};if(AnimeClient.hasElement(".manga #search"))AnimeClient.on("#search","keyup",AnimeClient.throttle(250,function(e){var query=encodeURIComponent(e.target.value);if(query==="")return;search$1(query)}));AnimeClient.on(".manga.list","click",".edit-buttons button",function(e){var thisSel=e.target;var parentSel=
AnimeClient.closestParent(e.target,"article");var type=thisSel.classList.contains("plus-one-chapter")?"chapter":"volume";var completed=parseInt(AnimeClient.$("."+type+"s_read",parentSel)[0].textContent,10)||0;var total=parseInt(AnimeClient.$("."+type+"_count",parentSel)[0].textContent,10);var mangaName=AnimeClient.$(".name",parentSel)[0].textContent;if(isNaN(completed))completed=0;var data={id:parentSel.dataset.kitsuId,mal_id:parentSel.dataset.malId,data:{progress:completed}};if(isNaN(completed)||
completed===0)data.data.status="current";if(!isNaN(completed)&&completed+1===total)data.data.status="completed";data.data.progress=++completed;AnimeClient.show(AnimeClient.$("#loading-shadow")[0]);AnimeClient.ajax(AnimeClient.url("/manga/increment"),{data:data,dataType:"json",type:"POST",mimeType:"application/json",success:function(){if(data.data.status==="completed")AnimeClient.hide(parentSel);AnimeClient.hide(AnimeClient.$("#loading-shadow")[0]);AnimeClient.$("."+type+"s_read",parentSel)[0].textContent=
completed;AnimeClient.showMessage("success","Successfully updated "+mangaName);AnimeClient.scrollToTop()},error:function(){AnimeClient.hide(AnimeClient.$("#loading-shadow")[0]);AnimeClient.showMessage("error","Failed to update "+mangaName);AnimeClient.scrollToTop()}})})})();
//# sourceMappingURL=scripts-authed.min.js.map

File diff suppressed because one or more lines are too long

View File

@ -7,5 +7,5 @@ sel.addEventListener(event,listener,false)}function delegateEvent(sel,target,eve
"GET")url+=url.match(/\?/)?ajaxSerialize(config.data):"?"+ajaxSerialize(config.data);request.open(method,url);request.onreadystatechange=function(){if(request.readyState===4){var responseText="";if(request.responseType==="json")responseText=JSON.parse(request.responseText);else responseText=request.responseText;if(request.status>299)config.error.call(null,request.status,responseText,request.response);else config.success.call(null,responseText,request.status)}};if(config.dataType==="json"){config.data=
JSON.stringify(config.data);config.mimeType="application/json"}else config.data=ajaxSerialize(config.data);request.setRequestHeader("Content-Type",config.mimeType);switch(method){case "GET":request.send(null);break;default:request.send(config.data);break}};AnimeClient.get=function(url,data,callback){callback=callback===undefined?null:callback;if(callback===null){callback=data;data={}}return AnimeClient.ajax(url,{data:data,success:callback})};AnimeClient.on("header","click",".message",function(e){AnimeClient.hide(e.target)});
AnimeClient.on("form.js-delete","submit",function(event){var proceed=confirm("Are you ABSOLUTELY SURE you want to delete this item?");if(proceed===false){event.preventDefault();event.stopPropagation()}});AnimeClient.on(".js-clear-cache","click",function(){AnimeClient.get("/cache_purge",function(){AnimeClient.showMessage("success","Successfully purged api cache")})});AnimeClient.on(".vertical-tabs input","change",function(event){var el=event.currentTarget.parentElement;var rect=el.getBoundingClientRect();
var top=rect.top+window.pageYOffset;window.scrollTo({top:top,behavior:"smooth"})})})();
var top=rect.top+window.pageYOffset;window.scrollTo({top:top,behavior:"smooth"})});if("serviceWorker"in navigator)navigator.serviceWorker.register("/sw.js").then(function(reg){console.log("Service worker registered",reg.scope)})["catch"](function(error){console.error("Failed to register service worker",error)})})();
//# sourceMappingURL=scripts.min.js.map

File diff suppressed because one or more lines are too long

View File

@ -1,10 +1,10 @@
import './base/events.js';
/* if ('serviceWorker' in navigator) {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js').then(reg => {
console.log('Service worker registered', reg.scope);
}).catch(error => {
console.error('Failed to register service worker', error);
});
} */
}

View File

@ -8,7 +8,9 @@ const cssNext = require('postcss-cssnext');
const cssNano = require('cssnano');
const css = fs.readFileSync('css/all.css', 'utf-8');
const darkCss = fs.readFileSync('css/dark-override.css', 'utf-8');
// Basic theme
postcss()
.use(atImport())
.use(cssNext())
@ -24,6 +26,24 @@ postcss()
from: 'css/all.css',
to: 'css/app.min.css'
}).then(result => {
fs.writeFileSync('css/app.min.css', result.css);
fs.writeFileSync('css/app.min.css.map', result.map);
});
fs.writeFileSync('css/app.min.css', result.css);
});
// Dark theme
postcss()
.use(atImport())
.use(cssNext())
.use(cssNano({
autoprefixer: false,
colormin: false,
minifyFontValues: false,
options: {
sourcemap: false
}
}))
.process(darkCss, {
from: 'css/dark-override.css',
to: 'css/dark.min.css'
}).then(result => {
fs.writeFileSync('css/dark.min.css', result.css);
});

View File

@ -17,6 +17,7 @@
namespace Aviat\AnimeClient\API;
use function Amp\Promise\wait;
use function Aviat\AnimeClient\getResponse;
use Amp;
use Amp\Artax\{FormBody, Request};
@ -250,7 +251,7 @@ class APIRequestBuilder {
*/
public function getResponseData(Request $request)
{
$response = wait((new HummingbirdClient)->request($request));
$response = getResponse($request);
return wait($response->getBody());
}
@ -315,7 +316,7 @@ class APIRequestBuilder {
* @param string $type
* @return void
*/
private function resetState($url, $type = 'GET')
private function resetState($url, $type = 'GET'): void
{
$requestUrl = $url ?: $this->baseUrl;

View File

@ -65,7 +65,7 @@ final class Anilist {
MangaReadingStatus::PLAN_TO_READ => KMRS::PLAN_TO_READ,
];
public static function getIdToWatchingStatusMap()
public static function getIdToWatchingStatusMap(): array
{
return [
'CURRENT' => AnimeWatchingStatus::WATCHING,
@ -77,7 +77,7 @@ final class Anilist {
];
}
public static function getIdToReadingStatusMap()
public static function getIdToReadingStatusMap(): array
{
return [
'CURRENT' => MangaReadingStatus::READING,

View File

@ -19,14 +19,12 @@ namespace Aviat\AnimeClient\API\Anilist;
use const Aviat\AnimeClient\USER_AGENT;
use function Amp\Promise\wait;
use function Aviat\AnimeClient\getResponse;
use Amp\Artax\Request;
use Amp\Artax\Response;
use Aviat\AnimeClient\API\{
Anilist,
HummingbirdClient
};
use Aviat\AnimeClient\API\Anilist;
use Aviat\Ion\Json;
use Aviat\Ion\Di\ContainerAware;
@ -200,7 +198,7 @@ trait AnilistTrait {
}
$request = $this->setUpRequest($url, $options);
$response = wait((new HummingbirdClient)->request($request));
$response = getResponse($request);
$logger->debug('Anilist response', [
'status' => $response->getStatus(),
@ -221,7 +219,7 @@ trait AnilistTrait {
$logger = $this->container->getLogger('anilist-request');
}
$response = wait((new HummingbirdClient)->request($request));
$response = getResponse($request);
$logger->debug('Anilist response', [
'status' => $response->getStatus(),

View File

@ -100,7 +100,7 @@ final class Model
$config = $this->container->get('config');
$anilistUser = $config->get(['anilist', 'username']);
if ( ! is_string($anilistUser))
if ( ! \is_string($anilistUser))
{
throw new InvalidArgumentException('Anilist username is not defined in config');
}
@ -151,10 +151,9 @@ final class Model
* Create a list item with all the relevant data
*
* @param array $data
* @param string $type
* @return Request
*/
public function createFullListItem(array $data, string $type = 'anime'): Request
public function createFullListItem(array $data): Request
{
$createData = $data['data'];
$mediaId = $this->getMediaIdFromMalId($data['mal_id']);
@ -168,6 +167,7 @@ final class Model
* Get the data for a specific list item, generally for editing
*
* @param string $malId - The unique identifier of that list item
* @param string $type - Them media type (anime/manga)
* @return mixed
*/
public function getListItem(string $malId, string $type): array
@ -185,6 +185,7 @@ final class Model
* Increase the watch count for the current list item
*
* @param FormItem $data
* @param string $type - Them media type (anime/manga)
* @return Request
*/
public function incrementListItem(FormItem $data, string $type): Request
@ -198,7 +199,7 @@ final class Model
* Modify a list item
*
* @param FormItem $data
* @param int [$id]
* @param string $type - Them media type (anime/manga)
* @return Request
*/
public function updateListItem(FormItem $data, string $type): Request
@ -225,6 +226,7 @@ final class Model
* Get the id of the specific list entry from the malId
*
* @param string $malId
* @param string $type - The media type (anime/manga)
* @return string
*/
public function getListIdFromMalId(string $malId, string $type): ?string
@ -234,7 +236,7 @@ final class Model
}
/**
* Get the Anilist media id from its MAL id
* Get the Anilist list item id from the media id from its MAL id
* this way is more accurate than getting the list item id
* directly from the MAL id
*/
@ -248,13 +250,6 @@ final class Model
'userName' => $anilistUser,
]);
/* dump([
'media_id' => $mediaId,
'userName' => $anilistUser,
'response' => $info,
]);
die(); */
return (string)$info['data']['MediaList']['id'];
}
@ -272,12 +267,6 @@ final class Model
'type' => mb_strtoupper($type),
]);
/* dump([
'mal_id' => $malId,
'response' => $info,
]);
die(); */
return (string)$info['data']['Media']['id'];
}
}

View File

@ -17,6 +17,7 @@
namespace Aviat\AnimeClient\API\Anilist\Transformer;
use Aviat\AnimeClient\API\Enum\AnimeWatchingStatus\Anilist as AnilistStatus;
use Aviat\AnimeClient\API\Enum\AnimeWatchingStatus\Kitsu as KitsuStatus;
use Aviat\AnimeClient\API\Mapping\AnimeWatchingStatus;
use Aviat\AnimeClient\Types\{AnimeListItem, FormItem};
@ -39,6 +40,8 @@ class AnimeListTransformer extends AbstractTransformer {
*/
public function untransform(array $item): FormItem
{
$reconsuming = $item['status'] === AnilistStatus::REPEATING;
return new FormItem([
'id' => $item['id'],
'mal_id' => $item['media']['idMal'],
@ -46,26 +49,16 @@ class AnimeListTransformer extends AbstractTransformer {
'notes' => $item['notes'] ?? '',
'private' => $item['private'],
'progress' => $item['progress'],
'rating' => $item['score'],
'rating' => $item['score'] ?? NULL,
'reconsumeCount' => $item['repeat'],
'reconsuming' => $item['status'] === AnilistStatus::REPEATING,
'status' => AnimeWatchingStatus::ANILIST_TO_KITSU[$item['status']],
'reconsuming' => $reconsuming,
'status' => $reconsuming
? KitsuStatus::WATCHING
:AnimeWatchingStatus::ANILIST_TO_KITSU[$item['status']],
'updatedAt' => (new DateTime())
->setTimestamp($item['updatedAt'])
->format(DateTime::W3C)
],
]);
}
/**
* Transform a set of structures
*
* @param array|object $collection
* @return array
*/
public function untransformCollection($collection): array
{
$list = (array)$collection;
return array_map([$this, 'untransform'], $list);
}
}

View File

@ -18,6 +18,7 @@ namespace Aviat\AnimeClient\API\Anilist\Transformer;
use Aviat\AnimeClient\API\Enum\MangaReadingStatus\Anilist as AnilistStatus;
use Aviat\AnimeClient\API\Mapping\MangaReadingStatus;
use Aviat\AnimeClient\Types\MangaListItem;
use Aviat\AnimeClient\Types\FormItem;
use Aviat\Ion\Transformer\AbstractTransformer;
@ -28,7 +29,7 @@ class MangaListTransformer extends AbstractTransformer {
public function transform($item)
{
return new MangaListItem([]);
}
/**
@ -56,16 +57,4 @@ class MangaListTransformer extends AbstractTransformer {
]
]);
}
/**
* Transform a set of structures
*
* @param array|object $collection
* @return array
*/
public function untransformCollection($collection): array
{
$list = (array)$collection;
return array_map([$this, 'untransform'], $list);
}
}

View File

@ -22,10 +22,10 @@ use Aviat\Ion\Enum;
* Possible values for watching status for the current anime
*/
final class Anilist extends Enum {
const WATCHING = 'CURRENT';
const COMPLETED = 'COMPLETED';
const ON_HOLD = 'PAUSED';
const DROPPED = 'DROPPED';
const PLAN_TO_WATCH = 'PLANNING';
const REPEATING = 'REPEATING';
public const WATCHING = 'CURRENT';
public const COMPLETED = 'COMPLETED';
public const ON_HOLD = 'PAUSED';
public const DROPPED = 'DROPPED';
public const PLAN_TO_WATCH = 'PLANNING';
public const REPEATING = 'REPEATING';
}

View File

@ -22,9 +22,9 @@ use Aviat\Ion\Enum;
* Possible values for watching status for the current anime
*/
final class Kitsu extends Enum {
const WATCHING = 'current';
const PLAN_TO_WATCH = 'planned';
const ON_HOLD = 'on_hold';
const DROPPED = 'dropped';
const COMPLETED = 'completed';
public const WATCHING = 'current';
public const PLAN_TO_WATCH = 'planned';
public const ON_HOLD = 'on_hold';
public const DROPPED = 'dropped';
public const COMPLETED = 'completed';
}

View File

@ -16,16 +16,16 @@
namespace Aviat\AnimeClient\API\Enum\AnimeWatchingStatus;
use Aviat\Ion\Enum as Enum;
use Aviat\Ion\Enum;
/**
* Possible values for current watching status of anime
*/
final class Route extends Enum {
const ALL = 'all';
const WATCHING = 'watching';
const PLAN_TO_WATCH = 'plan_to_watch';
const DROPPED = 'dropped';
const ON_HOLD = 'on_hold';
const COMPLETED = 'completed';
public const ALL = 'all';
public const WATCHING = 'watching';
public const PLAN_TO_WATCH = 'plan_to_watch';
public const DROPPED = 'dropped';
public const ON_HOLD = 'on_hold';
public const COMPLETED = 'completed';
}

View File

@ -16,16 +16,16 @@
namespace Aviat\AnimeClient\API\Enum\AnimeWatchingStatus;
use Aviat\Ion\Enum as Enum;
use Aviat\Ion\Enum;
/**
* Possible values for current watching status of anime
*/
final class Title extends Enum {
const ALL = 'All';
const WATCHING = 'Currently Watching';
const PLAN_TO_WATCH = 'Plan to Watch';
const DROPPED = 'Dropped';
const ON_HOLD = 'On Hold';
const COMPLETED = 'Completed';
public const ALL = 'All';
public const WATCHING = 'Currently Watching';
public const PLAN_TO_WATCH = 'Plan to Watch';
public const DROPPED = 'Dropped';
public const ON_HOLD = 'On Hold';
public const COMPLETED = 'Completed';
}

View File

@ -22,10 +22,10 @@ use Aviat\Ion\Enum;
* Possible values for watching status for the current anime
*/
final class Anilist extends Enum {
const READING = 'CURRENT';
const COMPLETED = 'COMPLETED';
const ON_HOLD = 'PAUSED';
const DROPPED = 'DROPPED';
const PLAN_TO_READ = 'PLANNING';
const REPEATING = 'REPEATING';
public const READING = 'CURRENT';
public const COMPLETED = 'COMPLETED';
public const ON_HOLD = 'PAUSED';
public const DROPPED = 'DROPPED';
public const PLAN_TO_READ = 'PLANNING';
public const REPEATING = 'REPEATING';
}

View File

@ -22,9 +22,9 @@ use Aviat\Ion\Enum;
* Possible values for current reading status of manga
*/
final class Kitsu extends Enum {
const READING = 'current';
const PLAN_TO_READ = 'planned';
const DROPPED = 'dropped';
const ON_HOLD = 'on_hold';
const COMPLETED = 'completed';
public const READING = 'current';
public const PLAN_TO_READ = 'planned';
public const DROPPED = 'dropped';
public const ON_HOLD = 'on_hold';
public const COMPLETED = 'completed';
}

View File

@ -22,10 +22,10 @@ use Aviat\Ion\Enum;
* Possible values for current reading status of manga
*/
final class Route extends Enum {
const ALL = 'all';
const READING = 'reading';
const PLAN_TO_READ = 'plan_to_read';
const DROPPED = 'dropped';
const ON_HOLD = 'on_hold';
const COMPLETED = 'completed';
public const ALL = 'all';
public const READING = 'reading';
public const PLAN_TO_READ = 'plan_to_read';
public const DROPPED = 'dropped';
public const ON_HOLD = 'on_hold';
public const COMPLETED = 'completed';
}

View File

@ -22,10 +22,10 @@ use Aviat\Ion\Enum;
* Possible values for current reading status of manga
*/
final class Title extends Enum {
const ALL = 'All';
const READING = 'Currently Reading';
const PLAN_TO_READ = 'Plan to Read';
const DROPPED = 'Dropped';
const ON_HOLD = 'On Hold';
const COMPLETED = 'Completed';
public const ALL = 'All';
public const READING = 'Currently Reading';
public const PLAN_TO_READ = 'Plan to Read';
public const DROPPED = 'Dropped';
public const ON_HOLD = 'On Hold';
public const COMPLETED = 'Completed';
}

File diff suppressed because it is too large Load Diff

View File

@ -21,9 +21,7 @@ namespace Aviat\AnimeClient\API;
*/
final class JsonAPI {
/**
* The full data array
*
/*
* Basic structure is generally like so:
* [
* 'id' => '12016665',
@ -35,10 +33,7 @@ final class JsonAPI {
*
* ]
* ]
*
* @var array
*/
protected $data = [];
/**
* Inline all included data
@ -214,8 +209,7 @@ final class JsonAPI {
$dataType = $props['data']['type'];
$relationship =& $organized[$type][$id]['relationships'][$relType];
unset($relationship['links']);
unset($relationship['data']);
unset($relationship['links'], $relationship['data']);
if ($relType === $dataType)
{

View File

@ -23,11 +23,11 @@ use DateTimeImmutable;
* Data massaging helpers for the Kitsu API
*/
final class Kitsu {
const AUTH_URL = 'https://kitsu.io/api/oauth/token';
const AUTH_USER_ID_KEY = 'kitsu-auth-userid';
const AUTH_TOKEN_CACHE_KEY = 'kitsu-auth-token';
const AUTH_TOKEN_EXP_CACHE_KEY = 'kitsu-auth-token-expires';
const AUTH_TOKEN_REFRESH_CACHE_KEY = 'kitsu-auth-token-refresh';
public const AUTH_URL = 'https://kitsu.io/api/oauth/token';
public const AUTH_USER_ID_KEY = 'kitsu-auth-userid';
public const AUTH_TOKEN_CACHE_KEY = 'kitsu-auth-token';
public const AUTH_TOKEN_EXP_CACHE_KEY = 'kitsu-auth-token-expires';
public const AUTH_TOKEN_REFRESH_CACHE_KEY = 'kitsu-auth-token-refresh';
/**
* Determine whether an anime is airing, finished airing, or has not yet aired
@ -163,7 +163,7 @@ final class Kitsu {
'dubs' => $streamingLink['dubs']
];
}
usort($links, function ($a, $b) {
return $a['meta']['name'] <=> $b['meta']['name'];
});

View File

@ -22,8 +22,8 @@ use Aviat\Ion\Enum as BaseEnum;
* Status of when anime is being/was/will be aired
*/
final class AnimeAiringStatus extends BaseEnum {
const NOT_YET_AIRED = 'Not Yet Aired';
const AIRING = 'Currently Airing';
const FINISHED_AIRING = 'Finished Airing';
public const NOT_YET_AIRED = 'Not Yet Aired';
public const AIRING = 'Currently Airing';
public const FINISHED_AIRING = 'Finished Airing';
}
// End of AnimeAiringStatus.php

View File

@ -16,6 +16,7 @@
namespace Aviat\AnimeClient\API\Kitsu;
use const Aviat\AnimeClient\USER_AGENT;
use Aviat\AnimeClient\API\APIRequestBuilder;
final class KitsuRequestBuilder extends APIRequestBuilder {
@ -32,7 +33,7 @@ final class KitsuRequestBuilder extends APIRequestBuilder {
* @var array
*/
protected $defaultHeaders = [
'User-Agent' => "Tim's Anime Client/4.0",
'User-Agent' => USER_AGENT,
'Accept' => 'application/vnd.api+json',
'Content-Type' => 'application/vnd.api+json',
'CLIENT_ID' => 'dd031b32d2f56c990b1425efe6c42ad847e7fe3ab46bf1299f05ecd856bdb7dd',

View File

@ -19,12 +19,12 @@ namespace Aviat\AnimeClient\API\Kitsu;
use const Aviat\AnimeClient\SESSION_SEGMENT;
use function Amp\Promise\wait;
use function Aviat\AnimeClient\getResponse;
use Amp\Artax\Request;
use Aviat\AnimeClient\AnimeClient;
use Amp\Artax\Response;
use Aviat\AnimeClient\API\{
FailedResponseException,
HummingbirdClient,
Kitsu as K
};
use Aviat\Ion\Json;
@ -121,7 +121,7 @@ trait KitsuTrait {
* @param array $options
* @return Response
*/
private function getResponse(string $type, string $url, array $options = [])
private function getResponse(string $type, string $url, array $options = []): Response
{
$logger = NULL;
if ($this->getContainer())
@ -131,7 +131,7 @@ trait KitsuTrait {
$request = $this->setUpRequest($type, $url, $options);
$response = wait((new HummingbirdClient)->request($request));
$response = getResponse($request);
if ($logger)
{

View File

@ -19,12 +19,10 @@ namespace Aviat\AnimeClient\API\Kitsu;
use const Aviat\AnimeClient\SESSION_SEGMENT;
use function Amp\Promise\wait;
use function Aviat\AnimeClient\getResponse;
use Amp\Artax\Request;
use Aviat\AnimeClient\API\{
HummingbirdClient,
ListItemInterface
};
use Aviat\AnimeClient\API\ListItemInterface;
use Aviat\AnimeClient\Types\FormItemData;
use Aviat\Ion\Di\ContainerAware;
use Aviat\Ion\Json;
@ -37,7 +35,7 @@ final class ListItem implements ListItemInterface {
use KitsuTrait;
public function create(array $data): Request
{
{
$body = [
'data' => [
'type' => 'libraryEntries',
@ -61,7 +59,7 @@ final class ListItem implements ListItemInterface {
]
]
];
if (array_key_exists('notes', $data))
{
$body['data']['attributes']['notes'] = $data['notes'];
@ -78,8 +76,6 @@ final class ListItem implements ListItemInterface {
return $request->setJsonBody($body)
->getFullRequest();
// return ($response->getStatus() === 201);
}
public function delete(string $id): Request
@ -93,8 +89,6 @@ final class ListItem implements ListItemInterface {
}
return $request->getFullRequest();
// return ($response->getStatus() === 204);
}
public function get(string $id): array
@ -112,8 +106,7 @@ final class ListItem implements ListItemInterface {
}
$request = $request->getFullRequest();
$response = wait((new HummingbirdClient)->request($request));
$response = getResponse($request);
return Json::decode(wait($response->getBody()));
}

View File

@ -91,9 +91,10 @@ final class Model {
{
$this->animeTransformer = new AnimeTransformer();
$this->animeListTransformer = new AnimeListTransformer();
$this->listItem = $listItem;
$this->mangaTransformer = new MangaTransformer();
$this->mangaListTransformer = new MangaListTransformer();
$this->listItem = $listItem;
}
/**
@ -265,7 +266,7 @@ final class Model {
public function getUserData(string $username): array
{
// $userId = $this->getUserIdByUsername($username);
$data = $this->getRequest("users", [
$data = $this->getRequest('users', [
'query' => [
'filter' => [
'name' => $username,
@ -334,7 +335,7 @@ final class Model {
* @param string $type "anime" or "manga"
* @return string|NULL
*/
public function getKitsuIdFromMALId(string $malId, string $type="anime")
public function getKitsuIdFromMALId(string $malId, string $type='anime'): ?string
{
$options = [
'query' => [
@ -369,7 +370,7 @@ final class Model {
* @param string $slug
* @return Anime
*/
public function getAnime(string $slug)
public function getAnime(string $slug): Anime
{
$baseData = $this->getRawMediaData('anime', $slug);
@ -523,7 +524,7 @@ final class Model {
* @param string $kitsuAnimeId The id of the anime on Kitsu
* @return string|null Returns the mal id if it exists, otherwise null
*/
public function getMalIdForAnime(string $kitsuAnimeId)
public function getMalIdForAnime(string $kitsuAnimeId): ?string
{
$options = [
'query' => [
@ -625,7 +626,7 @@ final class Model {
* Get information about a particular manga
*
* @param string $mangaId
* @return array
* @return MangaPage
*/
public function getMangaById(string $mangaId): MangaPage
{
@ -808,7 +809,7 @@ final class Model {
* @param string $kitsuMangaId The id of the manga on Kitsu
* @return string|null Returns the mal id if it exists, otherwise null
*/
public function getMalIdForManga(string $kitsuMangaId)
public function getMalIdForManga(string $kitsuMangaId): ?string
{
$options = [
'query' => [
@ -920,7 +921,7 @@ final class Model {
}
/**
* Get the raw data for the anime id
* Get the raw data for the anime/manga id
*
* @param string $type
* @param string $id

View File

@ -18,7 +18,6 @@ namespace Aviat\AnimeClient\API\Kitsu\Transformer;
use Aviat\AnimeClient\API\Kitsu;
use Aviat\AnimeClient\Types\{
Anime,
FormItem,
AnimeListItem
};
@ -95,7 +94,7 @@ final class AnimeListTransformer extends AbstractTransformer {
'started' => $anime['startDate'],
'ended' => $anime['endDate']
],
'anime' => new Anime([
'anime' => [
'id' => $animeId,
'age_rating' => $anime['ageRating'],
'title' => $title,
@ -105,7 +104,7 @@ final class AnimeListTransformer extends AbstractTransformer {
'cover_image' => $anime['posterImage']['small'],
'genres' => $genres,
'streaming_links' => $streamingLinks,
]),
],
'watching_status' => $item['attributes']['status'],
'notes' => $item['attributes']['notes'],
'rewatching' => (bool) $item['attributes']['reconsuming'],

View File

@ -17,7 +17,7 @@
namespace Aviat\AnimeClient\API\Kitsu\Transformer;
use Aviat\AnimeClient\API\{JsonAPI, Kitsu};
use Aviat\AnimeClient\Types\Anime;
use Aviat\AnimeClient\Types\AnimePage;
use Aviat\Ion\Transformer\AbstractTransformer;
/**
@ -30,9 +30,9 @@ final class AnimeTransformer extends AbstractTransformer {
* logical and workable structure
*
* @param array $item API library item
* @return Anime
* @return AnimePage
*/
public function transform($item): Anime
public function transform($item): AnimePage
{
$item['included'] = JsonAPI::organizeIncludes($item['included']);
$genres = $item['included']['categories'] ?? [];
@ -40,13 +40,74 @@ final class AnimeTransformer extends AbstractTransformer {
sort($item['genres']);
$title = $item['canonicalTitle'];
$titles = Kitsu::filterTitles($item);
// $titles = array_unique(array_diff($item['titles'], [$title]));
return new Anime([
$characters = [];
$staff = [];
if (array_key_exists('animeCharacters', $item['included']))
{
$animeCharacters = $item['included']['animeCharacters'];
foreach ($animeCharacters as $rel)
{
$charId = $rel['relationships']['character']['data']['id'];
$role = $rel['role'];
if (array_key_exists($charId, $item['included']['characters']))
{
$characters[$role][$charId] = $item['included']['characters'][$charId];
}
}
}
if (array_key_exists('mediaStaff', $item['included']))
{
foreach ($item['included']['mediaStaff'] as $id => $staffing)
{
$personId = $staffing['relationships']['person']['data']['id'];
$personDetails = $item['included']['people'][$personId];
$role = $staffing['role'];
if ( ! array_key_exists($role, $staff))
{
$staff[$role] = [];
}
$staff[$role][$personId] = [
'id' => $personId,
'name' => $personDetails['name'] ?? '??',
'image' => $personDetails['image'],
];
usort($staff[$role], function ($a, $b) {
return $a['name'] <=> $b['name'];
});
}
}
if ( ! empty($characters['main']))
{
uasort($characters['main'], function ($a, $b) {
return $a['name'] <=> $b['name'];
});
}
if ( ! empty($characters['supporting']))
{
uasort($characters['supporting'], function ($a, $b) {
return $a['name'] <=> $b['name'];
});
}
ksort($characters);
ksort($staff);
return new AnimePage([
'age_rating' => $item['ageRating'],
'age_rating_guide' => $item['ageRatingGuide'],
'characters' => $characters,
'cover_image' => $item['posterImage']['small'],
'episode_count' => $item['episodeCount'],
'episode_length' => $item['episodeLength'],
@ -55,6 +116,7 @@ final class AnimeTransformer extends AbstractTransformer {
'included' => $item['included'],
'show_type' => $this->string($item['showType'])->upperCaseFirst()->__toString(),
'slug' => $item['slug'],
'staff' => $staff,
'status' => Kitsu::getAiringStatus($item['startDate'], $item['endDate']),
'streaming_links' => Kitsu::parseStreamingLinks($item['included']),
'synopsis' => $item['synopsis'],

View File

@ -0,0 +1,173 @@
<?php declare(strict_types=1);
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu to manage anime and manga watch lists
*
* PHP version 7.1
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2018 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.1
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\API\Kitsu\Transformer;
use Aviat\AnimeClient\API\JsonAPI;
use Aviat\AnimeClient\Types\Character;
use Aviat\Ion\Transformer\AbstractTransformer;
/**
* Data transformation class for character pages
*/
final class CharacterTransformer extends AbstractTransformer {
public function transform($characterData): Character
{
$data = JsonAPI::organizeData($characterData);
$attributes = $data[0]['attributes'];
$castings = [];
$names = array_unique(
array_merge(
[$attributes['canonicalName']],
$attributes['names']
)
);
$name = array_shift($names);
if (array_key_exists('included', $data))
{
if (array_key_exists('anime', $data['included']))
{
uasort($data['included']['anime'], function ($a, $b) {
return $a['attributes']['canonicalTitle'] <=> $b['attributes']['canonicalTitle'];
});
}
if (array_key_exists('manga', $data['included']))
{
uasort($data['included']['manga'], function ($a, $b) {
return $a['attributes']['canonicalTitle'] <=> $b['attributes']['canonicalTitle'];
});
}
if (array_key_exists('castings', $data['included']))
{
$castings = $this->organizeCast($data['included']['castings']);
}
}
return new Character([
'castings' => $castings,
'description' => $attributes['description'],
'id' => $data[0]['id'],
'media' => [
'anime' => $data['included']['anime'] ?? [],
'manga' => $data['included']['manga'] ?? [],
],
'name' => $name,
'names' => $names,
'otherNames' => $attributes['otherNames'],
]);
}
/**
* Organize VA => anime relationships
*
* @param array $cast
* @return array
*/
private function dedupeCast(array $cast): array
{
$output = [];
$people = [];
$i = 0;
foreach ($cast as &$role)
{
if (empty($role['attributes']['role']))
{
continue;
}
$person = current($role['relationships']['person']['people'])['attributes'];
$hasName = array_key_exists($person['name'], $people);
if ( ! $hasName)
{
$people[$person['name']] = $i;
$role['relationships']['media']['anime'] = [current($role['relationships']['media']['anime'])];
$output[$i] = $role;
$i++;
continue;
}
if (array_key_exists('anime', $role['relationships']['media']))
{
$key = $people[$person['name']];
$output[$key]['relationships']['media']['anime'][] = current($role['relationships']['media']['anime']);
}
continue;
}
return $output;
}
protected function organizeCast(array $cast): array
{
$cast = $this->dedupeCast($cast);
$output = [];
foreach ($cast as $id => $role)
{
if (empty($role['attributes']['role']))
{
continue;
}
$language = $role['attributes']['language'];
$roleName = $role['attributes']['role'];
$isVA = $role['attributes']['voiceActor'];
if ($isVA)
{
foreach ($role['relationships']['person']['people'] as $pid => $peoples)
{
$p = $peoples;
}
$person = $p['attributes'];
$person['id'] = $pid;
$person['image'] = $person['image']['original'];
uasort($role['relationships']['media']['anime'], function ($a, $b) {
return $a['attributes']['canonicalTitle'] <=> $b['attributes']['canonicalTitle'];
});
$item = [
'person' => $person,
'series' => $role['relationships']['media']['anime']
];
$output[$roleName][$language][] = $item;
} else
{
foreach ($role['relationships']['person']['people'] as $pid => $person)
{
$person['id'] = $pid;
$output[$roleName][$pid] = $person;
}
}
}
return $output;
}
}

View File

@ -51,13 +51,77 @@ final class MangaTransformer extends AbstractTransformer {
$rawTitles = array_values($item['titles']);
$titles = array_unique(array_diff($rawTitles, [$title]));
$characters = [];
$staff = [];
if (array_key_exists('mediaCharacters', $item['included']))
{
$mediaCharacters = $item['included']['mediaCharacters'];
foreach ($mediaCharacters as $rel)
{
// dd($rel);
// $charId = $rel['relationships']['character']['data']['id'];
$role = $rel['attributes']['role'];
foreach ($rel['relationships']['character']['characters'] as $charId => $char)
{
if (array_key_exists($charId, $item['included']['characters']))
{
$characters[$role][$charId] = $char['attributes'];
}
}
}
}
if (array_key_exists('mediaStaff', $item['included']))
{
foreach ($item['included']['mediaStaff'] as $id => $staffing)
{
$role = $staffing['attributes']['role'];
foreach ($staffing['relationships']['person']['people'] as $personId => $personDetails)
{
if ( ! array_key_exists($role, $staff))
{
$staff[$role] = [];
}
$staff[$role][$personId] = [
'id' => $personId,
'name' => $personDetails['attributes']['name'] ?? '??',
'image' => $personDetails['attributes']['image'],
];
}
}
}
if ( ! empty($characters['main']))
{
uasort($characters['main'], function ($a, $b) {
return $a['name'] <=> $b['name'];
});
}
if ( ! empty($characters['supporting']))
{
uasort($characters['supporting'], function ($a, $b) {
return $a['name'] <=> $b['name'];
});
}
ksort($characters);
ksort($staff);
return new MangaPage([
'characters' => $characters,
'chapter_count' => $this->count($item['chapterCount']),
'cover_image' => $item['posterImage']['small'],
'genres' => $genres,
'id' => $item['id'],
'included' => $item['included'],
'manga_type' => $item['mangaType'],
'staff' => $staff,
'synopsis' => $item['synopsis'],
'title' => $title,
'titles' => $titles,

View File

@ -0,0 +1,132 @@
<?php declare(strict_types=1);
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu to manage anime and manga watch lists
*
* PHP version 7.1
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2018 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.1
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\API\Kitsu\Transformer;
use Aviat\AnimeClient\API\JsonAPI;
use Aviat\AnimeClient\Types\Person;
use Aviat\Ion\Transformer\AbstractTransformer;
/**
* Data transformation class for people pages
*/
final class PersonTransformer extends AbstractTransformer {
public function transform($personData): Person
{
$data = JsonAPI::organizeData($personData);
$included = JsonAPI::organizeIncludes($personData['included']);
$orgData = $this->organizeData($included);
return new Person([
'id' => $data['id'],
'name' => $data['attributes']['name'],
'characters' => $orgData['characters'],
'staff' => $orgData['staff'],
]);
}
protected function organizeData(array $data): array
{
$output = [
'characters' => [
'main' => [],
'supporting' => [],
],
'staff' => [],
];
if (array_key_exists('characterVoices', $data))
{
foreach ($data['characterVoices'] as $cv)
{
$mcId = $cv['relationships']['mediaCharacter']['data']['id'];
if ( ! array_key_exists($mcId, $data['mediaCharacters']))
{
continue;
}
$mc = $data['mediaCharacters'][$mcId];
$role = $mc['role'];
$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, 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'], function ($a, $b) {
return $a['character']['canonicalName'] <=> $b['character']['canonicalName'];
});
uasort($output['characters']['supporting'], 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'], function ($a, $b) {
return $a['canonicalTitle'] <=> $b['canonicalTitle'];
});
}
if (array_key_exists('manga', $media))
{
uasort($media['manga'], function ($a, $b) {
return $a['canonicalTitle'] <=> $b['canonicalTitle'];
});
}
}
return $output;
}
}

View File

@ -0,0 +1,168 @@
<?php declare(strict_types=1);
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu to manage anime and manga watch lists
*
* PHP version 7.1
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2018 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.1
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\API\Kitsu\Transformer;
use function Aviat\AnimeClient\getLocalImg;
use Aviat\AnimeClient\API\JsonAPI;
use Aviat\AnimeClient\Types\User;
use Aviat\Ion\Transformer\AbstractTransformer;
/**
* Transform user profile data for display
*/
final class UserTransformer extends AbstractTransformer {
public function transform($profileData): User
{
$orgData = JsonAPI::organizeData($profileData)[0];
$attributes = $orgData['attributes'];
$rels = $orgData['relationships'] ?? [];
$favorites = array_key_exists('favorites', $rels) ? $rels['favorites'] : [];
$stats = [];
foreach ($rels['stats'] as $sid => &$item)
{
$key = $item['attributes']['kind'];
$stats[$key] = $item['attributes']['statsData'];
unset($item);
}
$waifu = [];
if (array_key_exists('waifu', $rels))
{
$waifu = [
'label' => $attributes['waifuOrHusbando'],
'character' => $rels['waifu']['attributes'],
];
}
return new User([
'about' => $attributes['about'],
'avatar' => getLocalImg($attributes['avatar']['original'], FALSE),
'favorites' => $this->organizeFavorites($favorites),
'location' => $attributes['location'],
'name' => $attributes['name'],
'slug' => $attributes['slug'],
'stats' => $this->organizeStats($stats, $attributes),
'waifu' => $waifu,
'website' => $attributes['website'],
]);
}
/**
* Reorganize favorites data to be more useful
*
* @param array $rawFavorites
* @return array
*/
private function organizeFavorites(array $rawFavorites): array
{
$output = [];
unset($rawFavorites['data']);
foreach ($rawFavorites as $item)
{
$rank = $item['attributes']['favRank'];
foreach ($item['relationships']['item'] as $key => $fav)
{
$output[$key] = $output[$key] ?? [];
foreach ($fav as $id => $data)
{
$output[$key][$rank] = array_merge(['id' => $id], $data['attributes']);
}
ksort($output[$key]);
}
}
return $output;
}
/**
* Format the time spent on anime in a more readable format
*
* @param int $seconds
* @return string
*/
private function formatAnimeTime(int $seconds): string
{
// All the seconds left
$remSeconds = $seconds % 60;
$minutes = ($seconds - $remSeconds) / 60;
$minutesPerDay = 1440;
$minutesPerYear = $minutesPerDay * 365;
// Minutes short of a year
$years = (int)floor($minutes / $minutesPerYear);
$minutes %= $minutesPerYear;
// Minutes short of a day
$extraMinutes = $minutes % $minutesPerDay;
$days = ($minutes - $extraMinutes) / $minutesPerDay;
// Minutes short of an hour
$remMinutes = $extraMinutes % 60;
$hours = ($extraMinutes - $remMinutes) / 60;
$output = "{$days} days, {$hours} hours, {$remMinutes} minutes, and {$remSeconds} seconds.";
if ($years > 0)
{
$output = "{$years} year(s),{$output}";
}
return $output;
}
private function organizeStats($stats, $data = []): array
{
$animeStats = [];
$mangaStats = [];
$otherStats = [];
if (array_key_exists('anime-amount-consumed', $stats))
{
$animeStats = [
'Time spent watching anime:' => $this->formatAnimeTime($stats['anime-amount-consumed']['time']),
'Anime series watched:' => number_format($stats['anime-amount-consumed']['media']),
'Anime episodes watched:' => number_format($stats['anime-amount-consumed']['units']),
];
}
if (array_key_exists('manga-amount-consumed', $stats))
{
$mangaStats = [
'Manga series read:' => number_format($stats['manga-amount-consumed']['media']),
'Manga chapters read:' => number_format($stats['manga-amount-consumed']['units']),
];
}
if ( ! empty($data))
{
$otherStats = [
'Posts:' => number_format($data['postsCount']),
'Comments:' => number_format($data['commentsCount']),
'Media Rated:' => number_format($data['ratingsCount']),
];
}
return array_merge($animeStats, $mangaStats, $otherStats);
}
}

View File

@ -24,7 +24,7 @@ use Aviat\Ion\Enum;
* and url route segments
*/
final class AnimeWatchingStatus extends Enum {
const ANILIST_TO_KITSU = [
public const ANILIST_TO_KITSU = [
Anilist::WATCHING => Kitsu::WATCHING,
Anilist::PLAN_TO_WATCH => Kitsu::PLAN_TO_WATCH,
Anilist::COMPLETED => Kitsu::COMPLETED,
@ -32,7 +32,7 @@ final class AnimeWatchingStatus extends Enum {
Anilist::DROPPED => Kitsu::DROPPED
];
const KITSU_TO_ANILIST = [
public const KITSU_TO_ANILIST = [
Kitsu::WATCHING => Anilist::WATCHING,
Kitsu::PLAN_TO_WATCH => Anilist::PLAN_TO_WATCH,
Kitsu::COMPLETED => Anilist::COMPLETED,
@ -40,7 +40,7 @@ final class AnimeWatchingStatus extends Enum {
Kitsu::DROPPED => Anilist::DROPPED
];
const KITSU_TO_TITLE = [
public const KITSU_TO_TITLE = [
Kitsu::WATCHING => Title::WATCHING,
Kitsu::PLAN_TO_WATCH => Title::PLAN_TO_WATCH,
Kitsu::ON_HOLD => Title::ON_HOLD,
@ -48,7 +48,7 @@ final class AnimeWatchingStatus extends Enum {
Kitsu::COMPLETED => Title::COMPLETED
];
const ROUTE_TO_KITSU = [
public const ROUTE_TO_KITSU = [
Route::WATCHING => Kitsu::WATCHING,
Route::PLAN_TO_WATCH => Kitsu::PLAN_TO_WATCH,
Route::ON_HOLD => Kitsu::ON_HOLD,
@ -56,7 +56,7 @@ final class AnimeWatchingStatus extends Enum {
Route::COMPLETED => Kitsu::COMPLETED
];
const ROUTE_TO_TITLE = [
public const ROUTE_TO_TITLE = [
Route::ALL => Title::ALL,
Route::WATCHING => Title::WATCHING,
Route::PLAN_TO_WATCH => Title::PLAN_TO_WATCH,
@ -65,7 +65,7 @@ final class AnimeWatchingStatus extends Enum {
Route::COMPLETED => Title::COMPLETED
];
const TITLE_TO_ROUTE = [
public const TITLE_TO_ROUTE = [
Title::ALL => Route::ALL,
Title::WATCHING => Route::WATCHING,
Title::PLAN_TO_WATCH => Route::PLAN_TO_WATCH,

View File

@ -24,7 +24,7 @@ use Aviat\Ion\Enum;
* and url route segments
*/
final class MangaReadingStatus extends Enum {
const ANILIST_TO_KITSU = [
public const ANILIST_TO_KITSU = [
Anilist::READING => Kitsu::READING,
Anilist::PLAN_TO_READ => Kitsu::PLAN_TO_READ,
Anilist::COMPLETED => Kitsu::COMPLETED,
@ -32,7 +32,7 @@ final class MangaReadingStatus extends Enum {
Anilist::DROPPED => Kitsu::DROPPED
];
const KITSU_TO_ANILIST = [
public const KITSU_TO_ANILIST = [
Kitsu::READING => Anilist::READING,
Kitsu::PLAN_TO_READ => Anilist::PLAN_TO_READ,
Kitsu::COMPLETED => Anilist::COMPLETED,
@ -40,7 +40,7 @@ final class MangaReadingStatus extends Enum {
Kitsu::DROPPED => Anilist::DROPPED
];
const KITSU_TO_TITLE = [
public const KITSU_TO_TITLE = [
Kitsu::READING => Title::READING,
Kitsu::PLAN_TO_READ => Title::PLAN_TO_READ,
Kitsu::COMPLETED => Title::COMPLETED,
@ -48,7 +48,7 @@ final class MangaReadingStatus extends Enum {
Kitsu::DROPPED => Title::DROPPED,
];
const ROUTE_TO_KITSU = [
public const ROUTE_TO_KITSU = [
Route::PLAN_TO_READ => Kitsu::PLAN_TO_READ,
Route::READING => Kitsu::READING,
Route::COMPLETED => Kitsu::COMPLETED,
@ -56,7 +56,7 @@ final class MangaReadingStatus extends Enum {
Route::ON_HOLD => Kitsu::ON_HOLD,
];
const ROUTE_TO_TITLE = [
public const ROUTE_TO_TITLE = [
Route::ALL => Title::ALL,
Route::PLAN_TO_READ => Title::PLAN_TO_READ,
Route::READING => Title::READING,
@ -65,7 +65,7 @@ final class MangaReadingStatus extends Enum {
Route::ON_HOLD => Title::ON_HOLD,
];
const TITLE_TO_KITSU = [
public const TITLE_TO_KITSU = [
Title::PLAN_TO_READ => Kitsu::PLAN_TO_READ,
Title::READING => Kitsu::READING,
Title::COMPLETED => Kitsu::COMPLETED,

View File

@ -18,6 +18,7 @@ namespace Aviat\AnimeClient\API;
use function Amp\call;
use function Amp\Promise\{all, wait};
use function Aviat\AnimeClient\getApiClient;
/**
* Class to simplify making and validating simultaneous requests
@ -70,7 +71,8 @@ final class ParallelAPIRequest {
*/
public function makeRequests(): array
{
$client = new HummingbirdClient();
$client = getApiClient();
$promises = [];
foreach ($this->requests as $key => $url)
@ -92,7 +94,8 @@ final class ParallelAPIRequest {
*/
public function getResponses(): array
{
$client = new HummingbirdClient();
$client = getApiClient();
$promises = [];
foreach ($this->requests as $key => $url)

View File

@ -16,6 +16,10 @@
namespace Aviat\AnimeClient;
use function Amp\Promise\wait;
use Amp\Artax\{Client, DefaultClient, Response};
use Aviat\Ion\ConfigInterface;
use Yosymfony\Toml\{Toml, TomlBuilder};
@ -203,6 +207,37 @@ function checkFolderPermissions(ConfigInterface $config): array
return $errors;
}
/**
* Get an API Client, with better defaults
*
* @return DefaultClient
*/
function getApiClient ()
{
static $client;
if ($client === NULL)
{
$client = new DefaultClient;
$client->setOption(Client::OP_TRANSFER_TIMEOUT, 0);
}
return $client;
}
/**
* Simplify making a request with Artax
*
* @param $request
* @return Response
* @throws \Throwable
*/
function getResponse ($request): Response
{
$client = getApiClient();
return wait($client->request($request));
}
/**
* Generate the path for the cached image from the original image
*
@ -246,7 +281,7 @@ function getLocalImg ($kitsuUrl, $webp = TRUE): string
* @param int $height
* @param string $text
*/
function createPlaceholderImage ($path, $width, $height, $text = 'Image Unavailable')
function createPlaceholderImage ($path, $width, $height, $text = 'Image Unavailable'): void
{
$width = $width ?? 200;
$height = $height ?? 200;
@ -259,7 +294,7 @@ function createPlaceholderImage ($path, $width, $height, $text = 'Image Unavaila
// Background is the first color by default
$fillColor = imagecolorallocatealpha($img, 255, 255, 255, 127);
imagefill($img, 0, 0, $fillColor);
$textColor = imagecolorallocate($img, 64, 64, 64);
imagealphablending($img, TRUE);
@ -268,7 +303,7 @@ function createPlaceholderImage ($path, $width, $height, $text = 'Image Unavaila
$fontSize = 10;
$fontWidth = imagefontwidth($fontSize);
$fontHeight = imagefontheight($fontSize);
$length = strlen($text);
$length = \strlen($text);
$textWidth = $length * $fontWidth;
$fxPos = (int) ceil((imagesx($img) - $textWidth) / 2);
$fyPos = (int) ceil((imagesy($img) - $fontHeight) / 2);
@ -280,11 +315,11 @@ function createPlaceholderImage ($path, $width, $height, $text = 'Image Unavaila
imagesavealpha($img, TRUE);
imagepng($img, $path . '/placeholder.png', 9);
imagedestroy($img);
$pngImage = imagecreatefrompng($path . '/placeholder.png');
imagealphablending($pngImage, TRUE);
imagesavealpha($pngImage, TRUE);
imagewebp($pngImage, $path . '/placeholder.webp');
imagedestroy($pngImage);

View File

@ -21,13 +21,9 @@ use function Aviat\AnimeClient\loadTomlFile;
use Aura\Router\RouterContainer;
use Aura\Session\SessionFactory;
use Aviat\AnimeClient\UrlGenerator;
use Aviat\AnimeClient\Util;
use Aviat\AnimeClient\API\CacheTrait;
use Aviat\AnimeClient\API\Anilist;
use Aviat\AnimeClient\API\Kitsu;
use Aviat\AnimeClient\{Model, UrlGenerator, Util};
use Aviat\AnimeClient\API\{Anilist, CacheTrait, Kitsu};
use Aviat\AnimeClient\API\Kitsu\KitsuRequestBuilder;
use Aviat\AnimeClient\Model;
use Aviat\Banker\Pool;
use Aviat\Ion\Config;
use Aviat\Ion\Di\{Container, ContainerAware};

View File

@ -1,59 +1,59 @@
<?php declare(strict_types=1);
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu to manage anime and manga watch lists
*
* PHP version 7.1
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2018 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.1
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Command;
/**
* Clears out image cache directories
*/
class ClearThumbnails extends BaseCommand {
public function execute(array $args, array $options = []): void
{
$this->clearThumbs();
$this->echoBox('All cached images have been removed');
}
public function clearThumbs()
{
$imgDir = realpath(__DIR__ . '/../../public/images');
$paths = [
'avatars/*.gif',
'avatars/*.jpg',
'avatars/*.png',
'avatars/*.webp',
'anime/*.jpg',
'anime/*.png',
'anime/*.webp',
'manga/*.jpg',
'manga/*.png',
'manga/*.webp',
'characters/*.jpg',
'characters/*.png',
'characters/*.webp',
'people/*.jpg',
'people/*.png',
'people/*.webp',
];
foreach($paths as $path)
{
$cmd = "rm -rf {$imgDir}/{$path}";
exec($cmd);
}
}
<?php declare(strict_types=1);
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu to manage anime and manga watch lists
*
* PHP version 7.1
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2018 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.1
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Command;
/**
* Clears out image cache directories
*/
class ClearThumbnails extends BaseCommand {
public function execute(array $args, array $options = []): void
{
$this->clearThumbs();
$this->echoBox('All cached images have been removed');
}
private function clearThumbs(): void
{
$imgDir = realpath(__DIR__ . '/../../public/images');
$paths = [
'avatars/*.gif',
'avatars/*.jpg',
'avatars/*.png',
'avatars/*.webp',
'anime/*.jpg',
'anime/*.png',
'anime/*.webp',
'manga/*.jpg',
'manga/*.png',
'manga/*.webp',
'characters/*.jpg',
'characters/*.png',
'characters/*.webp',
'people/*.jpg',
'people/*.png',
'people/*.webp',
];
foreach($paths as $path)
{
$cmd = "rm -rf {$imgDir}/{$path}";
exec($cmd);
}
}
}

View File

@ -264,6 +264,11 @@ final class SyncLists extends BaseCommand {
foreach ($potentialMappings as $mappingId)
{
if (\is_array($mappingId))
{
continue;
}
if (array_key_exists($mappingId, $includes['mappings']))
{
$malId = $includes['mappings'][$mappingId]['externalId'];
@ -505,12 +510,16 @@ final class SyncLists extends BaseCommand {
// Use the first set rating, otherwise use the newer rating
if ( ! $sameRating)
{
if ($kitsuItem['data']['rating'] !== 0 && $dateDiff === 1)
if (
$dateDiff === 1 &&
$kitsuItem['data']['rating'] !== 0 &&
$kitsuItem['data']['ratingTwenty'] !== 0
)
{
$update['data']['ratingTwenty'] = $kitsuItem['data']['ratingTwenty'];
$return['updateType'][] = 'anilist';
}
else if($dateDiff === -1)
else if($dateDiff === -1 && $anilistItem['data']['rating'] !== 0)
{
$update['data']['ratingTwenty'] = $anilistItem['data']['rating'] * 2;
$return['updateType'][] = 'kitsu';
@ -547,6 +556,12 @@ final class SyncLists extends BaseCommand {
}
}
// No changes? Let's bail!
if (empty($return['updateType']))
{
return $return;
}
$return['meta'] = [
'kitsu' => $kitsuItem['data'],
'anilist' => $anilistItem['data'],

View File

@ -183,7 +183,7 @@ class Controller {
* @throws \Aviat\Ion\Di\Exception\NotFoundException
* @return void
*/
public function sessionRedirect()
public function sessionRedirect(): void
{
$target = $this->session->get('redirect_url');
if (empty($target))
@ -208,7 +208,7 @@ class Controller {
* @throws \Aviat\Ion\Di\Exception\NotFoundException
* @return string
*/
protected function loadPartial($view, string $template, array $data = [])
protected function loadPartial($view, string $template, array $data = []): string
{
$router = $this->container->get('dispatcher');
@ -242,7 +242,7 @@ class Controller {
* @throws \Aviat\Ion\Di\Exception\NotFoundException
* @return void
*/
protected function renderFullPage($view, string $template, array $data)
protected function renderFullPage($view, string $template, array $data): void
{
$csp = [
"default-src 'self'",
@ -275,7 +275,7 @@ class Controller {
public function notFound(
string $title = 'Sorry, page not found',
string $message = 'Page Not Found'
)
): void
{
$this->outputHTML('404', [
'title' => $title,
@ -383,7 +383,7 @@ class Controller {
* @throws \Aviat\Ion\Di\Exception\NotFoundException
* @return void
*/
protected function outputHTML(string $template, array $data = [], $view = NULL, int $code = 200)
protected function outputHTML(string $template, array $data = [], $view = NULL, int $code = 200): void
{
if (null === $view)
{

View File

@ -23,15 +23,11 @@ use Aviat\AnimeClient\API\Mapping\AnimeWatchingStatus;
use Aviat\AnimeClient\Types\FormItem;
use Aviat\Ion\Di\ContainerInterface;
use Aviat\Ion\Json;
use Aviat\Ion\StringWrapper;
/**
* Controller for Anime-related pages
*/
final class Anime extends BaseController {
use StringWrapper;
/**
* The anime list model
* @var \Aviat\AnimeClient\Model\Anime $model
@ -276,8 +272,6 @@ final class Anime extends BaseController {
public function details(string $animeId): void
{
$data = $this->model->getAnime($animeId);
$characters = [];
$staff = [];
if (empty($data))
{
@ -291,77 +285,13 @@ final class Anime extends BaseController {
return;
}
if (array_key_exists('animeCharacters', $data['included']))
{
$animeCharacters = $data['included']['animeCharacters'];
foreach ($animeCharacters as $rel)
{
$charId = $rel['relationships']['character']['data']['id'];
$role = $rel['role'];
if (array_key_exists($charId, $data['included']['characters']))
{
$characters[$role][$charId] = $data['included']['characters'][$charId];
}
}
}
if (array_key_exists('mediaStaff', $data['included']))
{
foreach ($data['included']['mediaStaff'] as $id => $staffing)
{
$personId = $staffing['relationships']['person']['data']['id'];
$personDetails = $data['included']['people'][$personId];
$role = $staffing['role'];
if ( ! array_key_exists($role, $staff))
{
$staff[$role] = [];
}
$staff[$role][$personId] = [
'id' => $personId,
'name' => $personDetails['name'] ?? '??',
'image' => $personDetails['image'],
];
usort($staff[$role], function ($a, $b) {
return $a['name'] <=> $b['name'];
});
}
}
if ( ! empty($characters['main']))
{
uasort($characters['main'], function ($a, $b) {
return $a['name'] <=> $b['name'];
});
}
if ( ! empty($characters['supporting']))
{
uasort($characters['supporting'], function ($a, $b) {
return $a['name'] <=> $b['name'];
});
}
ksort($characters);
ksort($staff);
// dump($characters);
// dump($staff);
$this->outputHTML('anime/details', [
'title' => $this->formatTitle(
$this->config->get('whose_list') . "'s Anime List",
'Anime',
$data->title
),
'characters' => $characters,
'show_data' => $data,
'staff' => $staff,
'data' => $data,
]);
}

View File

@ -68,7 +68,7 @@ final class AnimeCollection extends BaseController {
* @throws \Aviat\Ion\Exception\DoubleRenderException
* @return void
*/
public function search()
public function search(): void
{
$queryParams = $this->request->getQueryParams();
$query = $queryParams['query'];
@ -84,7 +84,7 @@ final class AnimeCollection extends BaseController {
* @throws \InvalidArgumentException
* @return void
*/
public function index($view)
public function index($view): void
{
$viewMap = [
'' => 'cover',
@ -110,7 +110,7 @@ final class AnimeCollection extends BaseController {
* @throws \InvalidArgumentException
* @return void
*/
public function form($id = NULL)
public function form($id = NULL): void
{
$this->setSessionRedirect();
@ -137,7 +137,7 @@ final class AnimeCollection extends BaseController {
* @throws \InvalidArgumentException
* @return void
*/
public function edit()
public function edit(): void
{
$data = $this->request->getParsedBody();
if (array_key_exists('hummingbird_id', $data))
@ -161,7 +161,7 @@ final class AnimeCollection extends BaseController {
* @throws \InvalidArgumentException
* @return void
*/
public function add()
public function add(): void
{
$data = $this->request->getParsedBody();
if (array_key_exists('id', $data))
@ -182,7 +182,7 @@ final class AnimeCollection extends BaseController {
*
* @return void
*/
public function delete()
public function delete(): void
{
$data = $this->request->getParsedBody();
if ( ! array_key_exists('hummingbird_id', $data))

View File

@ -17,30 +17,42 @@
namespace Aviat\AnimeClient\Controller;
use Aviat\AnimeClient\Controller as BaseController;
use Aviat\AnimeClient\API\JsonAPI;
use Aviat\Ion\ArrayWrapper;
use Aviat\AnimeClient\API\Kitsu\Transformer\CharacterTransformer;
use Aviat\Ion\Di\ContainerInterface;
/**
* Controller for character description pages
*/
class Character extends BaseController {
use ArrayWrapper;
/**
* @var \Aviat\AnimeClient\API\Kitsu\Model
*/
private $model;
/**
* Character constructor.
*
* @param ContainerInterface $container
* @throws \Aviat\Ion\Di\Exception\ContainerException
* @throws \Aviat\Ion\Di\Exception\NotFoundException
*/
public function __construct(ContainerInterface $container)
{
parent::__construct($container);
$this->model = $container->get('kitsu-model');
}
/**
* Show information about a character
*
* @param string $slug
* @throws \Aviat\Ion\Di\ContainerException
* @throws \Aviat\Ion\Di\NotFoundException
* @throws \InvalidArgumentException
* @return void
*/
public function index(string $slug): void
{
$model = $this->container->get('kitsu-model');
$rawData = $model->getCharacter($slug);
$rawData = $this->model->getCharacter($slug);
if (( ! array_key_exists('data', $rawData)) || empty($rawData['data']))
{
@ -55,167 +67,14 @@ class Character extends BaseController {
return;
}
$data = JsonAPI::organizeData($rawData);
$data = (new CharacterTransformer())->transform($rawData)->toArray();
$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']))
{
uasort($data['included']['anime'], function ($a, $b) {
return $a['attributes']['canonicalTitle'] <=> $b['attributes']['canonicalTitle'];
});
}
if (array_key_exists('manga', $data['included']))
{
uasort($data['included']['manga'], function ($a, $b) {
return $a['attributes']['canonicalTitle'] <=> $b['attributes']['canonicalTitle'];
});
}
}
$viewData = [
$this->outputHTML('character/details', [
'title' => $this->formatTitle(
'Characters',
$data[0]['attributes']['name']
$data['name']
),
'data' => $data,
'castCount' => 0,
'castings' => []
];
if (array_key_exists('included', $data))
{
if (array_key_exists('castings', $data['included']))
{
$viewData['castings'] = $this->organizeCast($data['included']['castings']);
$viewData['castCount'] = $this->getCastCount($viewData['castings']);
}
}
$this->outputHTML('character/details', $viewData);
}
/**
* Organize VA => anime relationships
*
* @param array $cast
* @return array
*/
private function dedupeCast(array $cast): array
{
$output = [];
$people = [];
$i = 0;
foreach ($cast as &$role)
{
if (empty($role['attributes']['role']))
{
continue;
}
$person = current($role['relationships']['person']['people'])['attributes'];
$hasName = array_key_exists($person['name'], $people);
if ( ! $hasName)
{
$people[$person['name']] = $i;
$role['relationships']['media']['anime'] = [current($role['relationships']['media']['anime'])];
$output[$i] = $role;
$i++;
continue;
}
if (array_key_exists('anime', $role['relationships']['media']))
{
$key = $people[$person['name']];
$output[$key]['relationships']['media']['anime'][] = current($role['relationships']['media']['anime']);
}
continue;
}
return $output;
}
protected function getCastCount(array $cast): int
{
$count = 0;
foreach($cast as $role)
{
$count++;
/* if (
array_key_exists('attributes', $role) &&
array_key_exists('role', $role['attributes']) &&
$role['attributes']['role'] !== NULL
) {
$count++;
} */
}
return $count;
}
protected function organizeCast(array $cast): array
{
$cast = $this->dedupeCast($cast);
$output = [];
foreach($cast as $id => $role)
{
if (empty($role['attributes']['role']))
{
continue;
}
$language = $role['attributes']['language'];
$roleName = $role['attributes']['role'];
$isVA = $role['attributes']['voiceActor'];
if ($isVA)
{
foreach($role['relationships']['person']['people'] as $pid => $peoples)
{
$p = $peoples;
}
$person = $p['attributes'];
$person['id'] = $pid;
$person['image'] = $person['image']['original'];
uasort($role['relationships']['media']['anime'], function ($a, $b) {
return $a['attributes']['canonicalTitle'] <=> $b['attributes']['canonicalTitle'];
});
$item = [
'person' => $person,
'series' => $role['relationships']['media']['anime']
];
$output[$roleName][$language][] = $item;
}
else
{
foreach($role['relationships']['person']['people'] as $pid => $person)
{
$person['id'] = $pid;
$output[$roleName][$pid] = $person;
}
}
}
return $output;
]);
}
}

View File

@ -1,198 +1,195 @@
<?php declare(strict_types=1);
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu to manage anime and manga watch lists
*
* PHP version 7.1
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2018 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.1
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Controller;
use function Aviat\AnimeClient\createPlaceholderImage;
use function Amp\Promise\wait;
use Aviat\AnimeClient\Controller as BaseController;
use Aviat\AnimeClient\API\{HummingbirdClient, JsonAPI};
use Aviat\Ion\Di\ContainerInterface;
use Aviat\Ion\View\HtmlView;
/**
* Controller for handling routes that don't fit elsewhere
*/
final class Images extends BaseController {
/**
* Get image covers from kitsu
*
* @param string $type The category of image
* @param string $file The filename to look for
* @param bool $display Whether to output the image to the server
* @throws \Aviat\Ion\Di\ContainerException
* @throws \Aviat\Ion\Di\NotFoundException
* @throws \InvalidArgumentException
* @throws \TypeError
* @throws \Error
* @throws \Throwable
* @return void
*/
public function cache(string $type, string $file, $display = TRUE): void
{
$currentUrl = $this->request->getUri()->__toString();
$kitsuUrl = 'https://media.kitsu.io/';
$fileName = str_replace('-original', '', $file);
[$id, $ext] = explode('.', basename($fileName));
$baseSavePath = $this->config->get('img_cache_path');
// Kitsu doesn't serve webp, but for most use cases,
// jpg is a safe assumption
$tryJpg = ['anime','characters','manga','people'];
if ($ext === 'webp' && in_array($type, $tryJpg, TRUE))
{
$ext = 'jpg';
$currentUrl = str_replace('webp', 'jpg', $currentUrl);
}
$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,
],
];
$imageType = $typeMap[$type] ?? NULL;
if (NULL === $imageType)
{
$this->getPlaceholder($baseSavePath, 200, 200);
return;
}
$kitsuUrl .= $imageType['kitsuUrl'];
$width = $imageType['width'];
$height = $imageType['height'];
$filePrefix = "{$baseSavePath}/{$type}/{$id}";
$promise = (new HummingbirdClient)->request($kitsuUrl);
$response = wait($promise);
if ($response->getStatus() !== 200)
{
// Try a few different file types before giving up
// webm => jpg => png => gif
$nextType = [
'jpg' => 'png',
'png' => 'gif',
];
if (array_key_exists($ext, $nextType))
{
$newUrl = str_replace($ext, $nextType[$ext], $currentUrl);
$this->redirect($newUrl, 303);
return;
}
if ($display)
{
$this->getPlaceholder("{$baseSavePath}/{$type}", $width, $height);
}
else
{
createPlaceholderImage("{$baseSavePath}/{$type}", $width, $height);
}
return;
}
$data = wait($response->getBody());
[$origWidth] = getimagesizefromstring($data);
$gdImg = imagecreatefromstring($data);
$resizedImg = imagescale($gdImg, $width ?? $origWidth);
if ($ext === 'gif')
{
file_put_contents("{$filePrefix}.gif", $data);
imagepalletetotruecolor($gdImg);
}
// save the webp versions
imagewebp($gdImg, "{$filePrefix}-original.webp");
imagewebp($resizedImg, "{$filePrefix}.webp");
// save the scaled jpeg file
imagejpeg($resizedImg, "{$filePrefix}.jpg");
// And the original
file_put_contents("{$filePrefix}-original.jpg", $data);
imagedestroy($gdImg);
imagedestroy($resizedImg);
if ($display)
{
$contentType = ($ext === 'webp')
? "image/webp"
: $response->getHeader('content-type')[0];
$outputFile = (strpos($file, '-original') !== FALSE)
? "{$filePrefix}-original.{$ext}"
: "{$filePrefix}.{$ext}";
header("Content-Type: {$contentType}");
echo file_get_contents($outputFile);
}
}
/**
* Get a placeholder for a missing image
*
* @param string $path
* @param int|null $width
* @param int|null $height
*/
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);
}
<?php declare(strict_types=1);
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu to manage anime and manga watch lists
*
* PHP version 7.1
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2018 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.1
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Controller;
use function Amp\Promise\wait;
use function Aviat\AnimeClient\getResponse;
use function Aviat\AnimeClient\createPlaceholderImage;
use Aviat\AnimeClient\Controller as BaseController;
/**
* Controller for handling routes that don't fit elsewhere
*/
final class Images extends BaseController {
/**
* Get image covers from kitsu
*
* @param string $type The category of image
* @param string $file The filename to look for
* @param bool $display Whether to output the image to the server
* @throws \Aviat\Ion\Di\ContainerException
* @throws \Aviat\Ion\Di\NotFoundException
* @throws \InvalidArgumentException
* @throws \TypeError
* @throws \Error
* @throws \Throwable
* @return void
*/
public function cache(string $type, string $file, $display = TRUE): void
{
$currentUrl = $this->request->getUri()->__toString();
$kitsuUrl = 'https://media.kitsu.io/';
$fileName = str_replace('-original', '', $file);
[$id, $ext] = explode('.', basename($fileName));
$baseSavePath = $this->config->get('img_cache_path');
// Kitsu doesn't serve webp, but for most use cases,
// jpg is a safe assumption
$tryJpg = ['anime','characters','manga','people'];
if ($ext === 'webp' && \in_array($type, $tryJpg, TRUE))
{
$ext = 'jpg';
$currentUrl = str_replace('webp', 'jpg', $currentUrl);
}
$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,
],
];
$imageType = $typeMap[$type] ?? NULL;
if (NULL === $imageType)
{
$this->getPlaceholder($baseSavePath, 200, 200);
return;
}
$kitsuUrl .= $imageType['kitsuUrl'];
$width = $imageType['width'];
$height = $imageType['height'];
$filePrefix = "{$baseSavePath}/{$type}/{$id}";
$response = getResponse($kitsuUrl);
if ($response->getStatus() !== 200)
{
// Try a few different file types before giving up
// webm => jpg => png => gif
$nextType = [
'jpg' => 'png',
'png' => 'gif',
];
if (array_key_exists($ext, $nextType))
{
$newUrl = str_replace($ext, $nextType[$ext], $currentUrl);
$this->redirect($newUrl, 303);
return;
}
if ($display)
{
$this->getPlaceholder("{$baseSavePath}/{$type}", $width, $height);
}
else
{
createPlaceholderImage("{$baseSavePath}/{$type}", $width, $height);
}
return;
}
$data = wait($response->getBody());
[$origWidth] = getimagesizefromstring($data);
$gdImg = imagecreatefromstring($data);
$resizedImg = imagescale($gdImg, $width ?? $origWidth);
if ($ext === 'gif')
{
file_put_contents("{$filePrefix}.gif", $data);
\imagepalletetotruecolor($gdImg);
}
// save the webp versions
imagewebp($gdImg, "{$filePrefix}-original.webp");
imagewebp($resizedImg, "{$filePrefix}.webp");
// save the scaled jpeg file
imagejpeg($resizedImg, "{$filePrefix}.jpg");
// And the original
file_put_contents("{$filePrefix}-original.jpg", $data);
imagedestroy($gdImg);
imagedestroy($resizedImg);
if ($display)
{
$contentType = ($ext === 'webp')
? 'image/webp'
: $response->getHeader('content-type')[0];
$outputFile = (strpos($file, '-original') !== FALSE)
? "{$filePrefix}-original.{$ext}"
: "{$filePrefix}.{$ext}";
header("Content-Type: {$contentType}");
echo file_get_contents($outputFile);
}
}
/**
* Get a placeholder for a missing image
*
* @param string $path
* @param int|null $width
* @param int|null $height
*/
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

@ -294,65 +294,6 @@ final class Manga extends Controller {
return;
}
if (array_key_exists('mediaCharacters', $data['included']))
{
$mediaCharacters = $data['included']['mediaCharacters'];
foreach ($mediaCharacters as $rel)
{
// dd($rel);
// $charId = $rel['relationships']['character']['data']['id'];
$role = $rel['attributes']['role'];
foreach($rel['relationships']['character']['characters'] as $charId => $char)
{
if (array_key_exists($charId, $data['included']['characters']))
{
$characters[$role][$charId] = $char['attributes'];
}
}
}
}
if (array_key_exists('mediaStaff', $data['included']))
{
foreach ($data['included']['mediaStaff'] as $id => $staffing)
{
$role = $staffing['attributes']['role'];
foreach($staffing['relationships']['person']['people'] as $personId => $personDetails)
{
if ( ! array_key_exists($role, $staff))
{
$staff[$role] = [];
}
$staff[$role][$personId] = [
'id' => $personId,
'name' => $personDetails['attributes']['name'] ?? '??',
'image' => $personDetails['attributes']['image'],
];
}
}
}
if ( ! empty($characters['main']))
{
uasort($characters['main'], function ($a, $b) {
return $a['name'] <=> $b['name'];
});
}
if ( ! empty($characters['supporting']))
{
uasort($characters['supporting'], function ($a, $b) {
return $a['name'] <=> $b['name'];
});
}
ksort($characters);
ksort($staff);
$this->outputHTML('manga/details', [
'title' => $this->formatTitle(
$this->config->get('whose_list') . "'s Manga List",

View File

@ -138,7 +138,7 @@ final class MangaCollection extends BaseController {
* @throws \InvalidArgumentException
* @return void
*/
public function edit()
public function edit(): void
{
$data = $this->request->getParsedBody();
if (array_key_exists('hummingbird_id', $data))
@ -162,7 +162,7 @@ final class MangaCollection extends BaseController {
* @throws \InvalidArgumentException
* @return void
*/
public function add()
public function add(): void
{
$data = $this->request->getParsedBody();
if (array_key_exists('id', $data))
@ -183,7 +183,7 @@ final class MangaCollection extends BaseController {
*
* @return void
*/
public function delete()
public function delete(): void
{
$data = $this->request->getParsedBody();
if ( ! array_key_exists('hummingbird_id', $data))

View File

@ -17,7 +17,6 @@
namespace Aviat\AnimeClient\Controller;
use Aviat\AnimeClient\Controller as BaseController;
use Aviat\Ion\Di\ContainerInterface;
use Aviat\Ion\View\HtmlView;
/**
@ -29,7 +28,7 @@ final class Misc extends BaseController {
*
* @return void
*/
public function clearCache()
public function clearCache(): void
{
$this->cache->clear();
$this->outputHTML('blank', [
@ -43,7 +42,7 @@ final class Misc extends BaseController {
* @param string $status
* @return void
*/
public function login(string $status = '')
public function login(string $status = ''): void
{
$message = '';
@ -68,7 +67,7 @@ final class Misc extends BaseController {
*
* @return void
*/
public function loginAction()
public function loginAction(): void
{
$auth = $this->container->get('auth');
$post = $this->request->getParsedBody();
@ -88,7 +87,7 @@ final class Misc extends BaseController {
*
* @return void
*/
public function logout()
public function logout(): void
{
$auth = $this->container->get('auth');
$auth->logout();

View File

@ -17,12 +17,33 @@
namespace Aviat\AnimeClient\Controller;
use Aviat\AnimeClient\Controller as BaseController;
use Aviat\AnimeClient\API\JsonAPI;
use Aviat\AnimeClient\API\Kitsu\Transformer\PersonTransformer;
use Aviat\Ion\Di\ContainerInterface;
/**
* Controller for People pages
*/
final class People extends BaseController {
/**
* @var \Aviat\AnimeClient\API\Kitsu\Model
*/
private $model;
/**
* People constructor.
*
* @param ContainerInterface $container
* @throws \Aviat\Ion\Di\Exception\ContainerException
* @throws \Aviat\Ion\Di\Exception\NotFoundException
*/
public function __construct(ContainerInterface $container)
{
parent::__construct($container);
$this->model = $container->get('kitsu-model');
}
/**
* Show information about a person
*
@ -31,9 +52,8 @@ final class People extends BaseController {
*/
public function index(string $id): void
{
$model = $this->container->get('kitsu-model');
$rawData = $model->getPerson($id);
$rawData = $this->model->getPerson($id);
$data = (new PersonTransformer())->transform($rawData)->toArray();
if (( ! array_key_exists('data', $rawData)) || empty($rawData['data']))
{
@ -48,114 +68,12 @@ final class People extends BaseController {
return;
}
$data = JsonAPI::organizeData($rawData);
$included = JsonAPI::organizeIncludes($rawData['included']);
$orgData = $this->organizeData($included);
$viewData = [
'included' => $included,
$this->outputHTML('person/details', [
'title' => $this->formatTitle(
'People',
$data['attributes']['name']
$data['name']
),
'data' => $data,
'castCount' => 0,
'castings' => [],
'characters' => $orgData['characters'],
'staff' => $orgData['staff'],
];
$this->outputHTML('person/details', $viewData);
}
protected function organizeData(array $data): array
{
$output = [
'characters' => [
'main' => [],
'supporting' => [],
],
'staff' => [],
];
if (array_key_exists('characterVoices', $data))
{
foreach ($data['characterVoices'] as $cv)
{
$mcId = $cv['relationships']['mediaCharacter']['data']['id'];
if ( ! array_key_exists($mcId, $data['mediaCharacters']))
{
continue;
}
$mc = $data['mediaCharacters'][$mcId];
$role = $mc['role'];
$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, 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'], function ($a, $b) {
return $a['character']['canonicalName'] <=> $b['character']['canonicalName'];
});
uasort($output['characters']['supporting'], 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'], function ($a, $b) {
return $a['canonicalTitle'] <=> $b['canonicalTitle'];
});
}
if (array_key_exists('manga', $media))
{
uasort($media['manga'], function ($a, $b) {
return $a['canonicalTitle'] <=> $b['canonicalTitle'];
});
}
}
return $output;
]);
}
}

View File

@ -24,7 +24,7 @@ use Aviat\Ion\Di\ContainerInterface;
*/
final class Settings extends BaseController {
/**
* @var \Aviat\API\Anilist\Model
* @var \Aviat\AnimeClient\API\Anilist\Model
*/
private $anilistModel;
@ -33,6 +33,13 @@ final class Settings extends BaseController {
*/
private $settingsModel;
/**
* Settings constructor.
*
* @param ContainerInterface $container
* @throws \Aviat\Ion\Di\Exception\ContainerException
* @throws \Aviat\Ion\Di\Exception\NotFoundException
*/
public function __construct(ContainerInterface $container)
{
parent::__construct($container);
@ -44,7 +51,7 @@ final class Settings extends BaseController {
/**
* Show the user settings, if logged in
*/
public function index()
public function index(): void
{
$auth = $this->container->get('auth');
$form = $this->settingsModel->getSettingsForm();
@ -66,7 +73,7 @@ final class Settings extends BaseController {
*
* @throws \Aura\Router\Exception\RouteNotFound
*/
public function update()
public function update(): void
{
$post = $this->request->getParsedBody();
unset($post['settings-tabs']);
@ -88,14 +95,15 @@ final class Settings extends BaseController {
/**
* Redirect to Anilist to start Oauth flow
*/
public function anilistRedirect()
public function anilistRedirect(): void
{
$redirectUrl = 'https://anilist.co/api/v2/oauth/authorize?' .
http_build_query([
'client_id' => $this->config->get(['anilist', 'client_id']),
'redirect_uri' => $this->urlGenerator->url('/anilist-oauth'),
'response_type' => 'code',
]);
$query = http_build_query([
'client_id' => $this->config->get(['anilist', 'client_id']),
'redirect_uri' => $this->urlGenerator->url('/anilist-oauth'),
'response_type' => 'code',
]);
$redirectUrl = "https://anilist.co/api/v2/oauth/authorize?{$query}";
$this->redirect($redirectUrl, 303);
}
@ -103,7 +111,7 @@ final class Settings extends BaseController {
/**
* Oauth callback for Anilist API
*/
public function anilistCallback()
public function anilistCallback(): void
{
$query = $this->request->getQueryParams();
$authCode = $query['code'];

View File

@ -16,8 +16,9 @@
namespace Aviat\AnimeClient\Controller;
use Aviat\AnimeClient\API\Kitsu\Transformer\UserTransformer;
use Aviat\AnimeClient\Controller as BaseController;
use Aviat\AnimeClient\API\JsonAPI;
use Aviat\Ion\Di\ContainerInterface;
/**
@ -25,8 +26,18 @@ use Aviat\Ion\Di\ContainerInterface;
*/
final class User extends BaseController {
/**
* @var \Aviat\AnimeClient\API\Kitsu\Model
*/
private $kitsuModel;
/**
* User constructor.
*
* @param ContainerInterface $container
* @throws \Aviat\Ion\Di\Exception\ContainerException
* @throws \Aviat\Ion\Di\Exception\NotFoundException
*/
public function __construct(ContainerInterface $container)
{
parent::__construct($container);
@ -56,103 +67,16 @@ final class User extends BaseController {
? $this->config->get(['kitsu_username'])
: $username;
$data = $this->kitsuModel->getUserData($username);
$orgData = JsonAPI::organizeData($data)[0];
$rels = $orgData['relationships'] ?? [];
$favorites = array_key_exists('favorites', $rels) ? $rels['favorites'] : [];
$stats = [];
foreach ($rels['stats'] as $sid => &$item)
{
$key = $item['attributes']['kind'];
$stats[$key] = $item['attributes']['statsData'];
unset($item);
}
//dump($orgData);
// dump($stats);
// $timeOnAnime = $this->formatAnimeTime($orgData['attributes']['lifeSpentOnAnime']);
$timeOnAnime = $this->formatAnimeTime($stats['anime-amount-consumed']['time']);
$whom = $isMainUser
? $this->config->get('whose_list')
: $username;
$rawData = $this->kitsuModel->getUserData($username);
$data = (new UserTransformer())->transform($rawData)->toArray();
$this->outputHTML('user/details', [
'title' => 'About ' . $whom,
'data' => $orgData,
'attributes' => $orgData['attributes'],
'relationships' => $rels,
'favorites' => $this->organizeFavorites($favorites),
'stats' => $stats,
'timeOnAnime' => $timeOnAnime,
'data' => $data,
]);
}
/**
* Reorganize favorites data to be more useful
*
* @param array $rawFavorites
* @return array
*/
private function organizeFavorites(array $rawFavorites): array
{
$output = [];
unset($rawFavorites['data']);
foreach ($rawFavorites as $item)
{
$rank = $item['attributes']['favRank'];
foreach ($item['relationships']['item'] as $key => $fav)
{
$output[$key] = $output[$key] ?? [];
foreach ($fav as $id => $data)
{
$output[$key][$rank] = array_merge(['id' => $id], $data['attributes']);
}
}
ksort($output[$key]);
}
return $output;
}
/**
* Format the time spent on anime in a more readable format
*
* @param int $minutes
* @return string
*/
private function formatAnimeTime(int $minutes): string
{
$minutesPerDay = 1440;
$minutesPerYear = $minutesPerDay * 365;
// Minutes short of a year
$years = (int)floor($minutes / $minutesPerYear);
$minutes %= $minutesPerYear;
// Minutes short of a day
$extraMinutes = $minutes % $minutesPerDay;
$days = ($minutes - $extraMinutes) / $minutesPerDay;
// Minutes short of an hour
$remMinutes = $extraMinutes % 60;
$hours = ($extraMinutes - $remMinutes) / 60;
$output = "{$days} days, {$hours} hours, and {$remMinutes} minutes.";
if ($years > 0)
{
$output = "{$years} year(s),{$output}";
}
return $output;
}
}

View File

@ -64,8 +64,9 @@ final class Dispatcher extends RoutingBase {
public function __construct(ContainerInterface $container)
{
parent::__construct($container);
$this->router = $container->get('aura-router')->getMap();
$this->matcher = $container->get('aura-router')->getMatcher();
$router = $this->container->get('aura-router');
$this->router = $router->getMap();
$this->matcher = $router->getMatcher();
$this->request = $container->get('request');
$this->outputRoutes = $this->setupRoutes();
@ -99,7 +100,7 @@ final class Dispatcher extends RoutingBase {
*
* @return array
*/
public function getOutputRoutes()
public function getOutputRoutes(): array
{
return $this->outputRoutes;
}
@ -171,7 +172,7 @@ final class Dispatcher extends RoutingBase {
$controllerName = $map[$controllerName];
}
$actionMethod = (array_key_exists('action', $route->attributes))
$actionMethod = array_key_exists('action', $route->attributes)
? $route->attributes['action']
: NOT_FOUND_METHOD;
@ -205,9 +206,9 @@ final class Dispatcher extends RoutingBase {
*
* @return string
*/
public function getController()
public function getController(): string
{
$routeType = $this->__get('default_list');
$routeType = $this->config->get('default_list');
$requestUri = $this->request->getUri()->getPath();
$path = trim($requestUri, '/');
@ -225,7 +226,7 @@ final class Dispatcher extends RoutingBase {
$controller = $routeType;
}
return $controller;
return $controller ?? '';
}
/**
@ -233,11 +234,13 @@ final class Dispatcher extends RoutingBase {
*
* @return array
*/
public function getControllerList()
public function getControllerList(): array
{
$defaultNamespace = DEFAULT_CONTROLLER_NAMESPACE;
$path = str_replace('\\', '/', $defaultNamespace);
$path = str_replace('Aviat/AnimeClient/', '', $path);
$find = ['\\', 'Aviat/AnimeClient/'];
$replace = ['/', ''];
$path = str_replace($find, $replace, $defaultNamespace);
$path = trim($path, '/');
$actualPath = realpath(_dir(SRC_DIR, $path));
$classFiles = glob("{$actualPath}/*.php");
@ -265,7 +268,7 @@ final class Dispatcher extends RoutingBase {
* @param array $params
* @return void
*/
protected function call($controllerName, $method, array $params)
protected function call($controllerName, $method, array $params): void
{
$logger = $this->container->getLogger('default');
@ -347,7 +350,7 @@ final class Dispatcher extends RoutingBase {
*
* @return array
*/
protected function setupRoutes()
protected function setupRoutes(): array
{
$routeType = $this->getController();
@ -359,7 +362,7 @@ final class Dispatcher extends RoutingBase {
unset($route['path']);
$controllerMap = $this->getControllerList();
$controllerClass = (array_key_exists($routeType, $controllerMap))
$controllerClass = array_key_exists($routeType, $controllerMap)
? $controllerMap[$routeType]
: DEFAULT_CONTROLLER;

View File

@ -16,35 +16,28 @@
namespace Aviat\AnimeClient;
use Aviat\Ion\
{
ArrayWrapper, StringWrapper
};
use Aviat\Ion\Di\ContainerInterface;
/**
* Helper object to manage form generation, especially for config editing
*/
final class FormGenerator {
use ArrayWrapper;
use StringWrapper;
/**
* Injection Container
* @var ContainerInterface $container
*/
protected $container;
/**
* Html generation helper
*
* @var \Aura\Html\HelperLocator
*/
protected $helper;
private $helper;
/**
* FormGenerator constructor.
*
* @param ContainerInterface $container
* @throws \Aviat\Ion\Di\Exception\ContainerException
* @throws \Aviat\Ion\Di\Exception\NotFoundException
*/
public function __construct(ContainerInterface $container)
{
$this->container = $container;
$this->helper = $container->get('html-helper');
}
@ -55,7 +48,7 @@ final class FormGenerator {
* @param array $form
* @return string
*/
public function generate(string $name, array $form)
public function generate(string $name, array $form): string
{
$type = $form['type'];
@ -105,6 +98,6 @@ final class FormGenerator {
}
}
return $this->helper->input($params);
return (string)$this->helper->input($params);
}
}

View File

@ -45,9 +45,11 @@ final class MenuGenerator extends UrlGenerator {
protected $request;
/**
* Create menu generator
* MenuGenerator constructor.
*
* @param ContainerInterface $container
* @throws \Aviat\Ion\Di\Exception\ContainerException
* @throws \Aviat\Ion\Di\Exception\NotFoundException
*/
public function __construct(ContainerInterface $container)
{
@ -106,7 +108,7 @@ final class MenuGenerator extends UrlGenerator {
$link = $this->helper->a($this->url($path), $title);
$attrs = ($selected)
$attrs = $selected
? ['class' => 'selected']
: [];

View File

@ -16,14 +16,10 @@
namespace Aviat\AnimeClient\Model;
use Aviat\Ion\StringWrapper;
/**
* Base model for api interaction
*/
class API {
use StringWrapper;
/**
* Sort the list entries by their title
*
@ -31,7 +27,7 @@ class API {
* @param string $sortKey
* @return void
*/
protected function sortByName(array &$array, string $sortKey)
protected function sortByName(array &$array, string $sortKey): void
{
$sort = [];

View File

@ -108,7 +108,7 @@ class Anime extends API {
* @param string $slug
* @return AnimeType
*/
public function getAnime(string $slug)
public function getAnime(string $slug): AnimeType
{
return $this->kitsuModel->getAnime($slug);
}
@ -147,7 +147,7 @@ class Anime extends API {
$item = $this->kitsuModel->getListItem($itemId);
$array = $item->toArray();
if (is_array($array['notes']))
if (\is_array($array['notes']))
{
$array['notes'] = '';
}

View File

@ -226,6 +226,11 @@ final class AnimeCollection extends Collection {
return $query->fetch(PDO::FETCH_ASSOC);
}
/**
* Get the list of genres from the database
*
* @return array
*/
private function getGenresForList(): array
{
$query = $this->db->select('hummingbird_id, genre')

View File

@ -83,11 +83,11 @@ class Collection extends DB {
if ( ! empty($filter))
{
$this->db->where_in('hummingbird_id', $filter);
$this->db->whereIn('hummingbird_id', $filter);
}
$query = $this->db->order_by('hummingbird_id')
->order_by('genre')
$query = $this->db->orderBy('hummingbird_id')
->orderBy('genre')
->get();
$output = [];

View File

@ -17,15 +17,12 @@
namespace Aviat\AnimeClient\Model;
use Aviat\Ion\Di\{ContainerAware, ContainerInterface};
use Aviat\Ion\{ArrayWrapper, StringWrapper};
/**
* Base model for database interaction
*/
class DB {
use ArrayWrapper;
use ContainerAware;
use StringWrapper;
/**
* The query builder object

View File

@ -41,7 +41,7 @@ final class Settings {
$this->config = $config;
}
public function getSettings()
public function getSettings(): array
{
$settings = [
'config' => [],
@ -66,7 +66,7 @@ final class Settings {
return $settings;
}
public function getSettingsForm()
public function getSettingsForm(): array
{
$output = [];
@ -124,7 +124,7 @@ final class Settings {
return $output;
}
public function validateSettings(array $settings)
public function validateSettings(array $settings): array
{
$config = (new Config($settings))->toArray();
@ -150,7 +150,7 @@ final class Settings {
$looseConfig[$key] = $val;
}
}
elseif (is_array($val) && ! empty($val))
elseif (\is_array($val) && ! empty($val))
{
foreach($val as $k => $v)
{
@ -204,7 +204,6 @@ final class Settings {
{
dump($e);
dump($settings);
die();
return FALSE;
}

View File

@ -59,20 +59,6 @@ class RoutingBase {
$this->routes = $this->config->get('routes');
}
/**
* Retrieve the appropriate value for the routing key
*
* @param string|int|array $key
* @return mixed
*/
public function __get($key)
{
if ($this->config->has($key))
{
return $this->config->get($key);
}
}
/**
* Get the current url path
* @throws \Aviat\Ion\Di\ContainerException

View File

@ -17,9 +17,9 @@
namespace Aviat\AnimeClient\Types;
/**
* Type representing an Anime object for display
* Type representing an anime within a watch list
*/
final class Anime extends AbstractType {
class Anime extends AbstractType {
public $age_rating;
public $age_rating_guide;
public $cover_image;

View File

@ -17,7 +17,7 @@
namespace Aviat\AnimeClient\Types;
/**
* Type representing an Anime object for display
* Type representing an anime watch list item
*/
final class AnimeListItem extends AbstractType {
public $id;
@ -40,4 +40,9 @@ final class AnimeListItem extends AbstractType {
public $rewatched;
public $user_rating;
public $watching_status;
public function setAnime($anime): void
{
$this->anime = new Anime($anime);
}
}

25
src/Types/AnimePage.php Normal file
View File

@ -0,0 +1,25 @@
<?php declare(strict_types=1);
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu to manage anime and manga watch lists
*
* PHP version 7.1
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2018 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.1
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Types;
/**
* Type representing an Anime object for a detail page
*/
final class AnimePage extends Anime {
public $characters;
public $staff;
}

39
src/Types/Character.php Normal file
View File

@ -0,0 +1,39 @@
<?php declare(strict_types=1);
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu to manage anime and manga watch lists
*
* PHP version 7.1
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2018 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.1
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Types;
/**
* Type representing a character for display
*/
final class Character extends AbstractType {
public $castings;
public $description;
public $id;
public $included;
public $media;
public $name;
public $names;
public $otherNames;
public function setMedia ($media): void
{
$this->media = new class($media) extends AbstractType {
public $anime;
public $manga;
};
}
}

View File

@ -24,6 +24,7 @@ class Config extends AbstractType {
// Settings in config.toml
public $asset_path; // Path to public folder for urls
public $dark_theme;
public $default_anime_list_path;
public $default_list;
public $default_manga_list_path;

View File

@ -20,12 +20,14 @@ namespace Aviat\AnimeClient\Types;
* Type representing an Anime object for display
*/
final class MangaPage extends AbstractType {
public $characters;
public $chapter_count;
public $cover_image;
public $genres;
public $id;
public $included;
public $manga_type;
public $staff;
public $synopsis;
public $title;
public $titles;

35
src/Types/Person.php Normal file
View File

@ -0,0 +1,35 @@
<?php declare(strict_types=1);
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu to manage anime and manga watch lists
*
* PHP version 7.1
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2018 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.1
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Types;
/**
* Type representing a person for display
*/
final class Person extends AbstractType {
public $id;
public $name;
public $characters;
public $staff;
public function setCharacters($characters): void
{
$this->characters = new class($characters) extends AbstractType {
public $main;
public $supporting;
};
}
}

32
src/Types/User.php Normal file
View File

@ -0,0 +1,32 @@
<?php declare(strict_types=1);
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu to manage anime and manga watch lists
*
* PHP version 7.1
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2018 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.1
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Types;
/**
* Type representing a Kitsu user for display
*/
final class User extends AbstractType {
public $about;
public $avatar;
public $favorites;
public $location;
public $name;
public $slug;
public $stats;
public $waifu;
public $website;
}

View File

@ -47,13 +47,13 @@ class UrlGenerator extends RoutingBase {
/**
* Get the base url for css/js/images
*
* @param string ...$args
* @param string[] $args
* @return string
*/
public function assetUrl(string ...$args): string
{
$baseUrl = rtrim($this->url(''), '/')
. $this->__get('asset_path');
. $this->config->get('asset_path');
array_unshift($args, $baseUrl);
@ -82,7 +82,7 @@ class UrlGenerator extends RoutingBase {
{
if ( ! array_key_exists($i + 1, $segments))
{
$segments[$i + 1] = "";
$segments[$i + 1] = '';
}
$path_segments[$i] = preg_replace('`{.*?}`', $segments[$i + 1], $path_segments[$i]);
@ -104,7 +104,7 @@ class UrlGenerator extends RoutingBase {
public function defaultUrl(string $type): string
{
$type = trim($type);
$defaultPath = $this->__get("default_{$type}_list_path");
$defaultPath = $this->config->get("default_{$type}_list_path");
if ($defaultPath !== NULL)
{

View File

@ -16,7 +16,6 @@
namespace Aviat\AnimeClient;
use Aviat\Ion\ConfigInterface;
use Aviat\Ion\Di\{ContainerAware, ContainerInterface};
/**
@ -42,12 +41,6 @@ class Util {
'me'
];
/**
* The config manager
* @var ConfigInterface
*/
private $config;
/**
* Set up the Util class
*
@ -58,7 +51,6 @@ class Util {
public function __construct(ContainerInterface $container)
{
$this->setContainer($container);
$this->config = $container->get('config');
}
/**
@ -68,7 +60,7 @@ class Util {
* @param string $b - Second item to compare
* @return string
*/
public static function isSelected($a, $b)
public static function isSelected(string $a, string $b): string
{
return ($a === $b) ? 'selected' : '';
}
@ -80,7 +72,7 @@ class Util {
* @param string $b - Second item to compare
* @return string
*/
public static function isNotSelected($a, $b)
public static function isNotSelected(string $a, string $b): string
{
return ($a !== $b) ? 'selected' : '';
}
@ -108,7 +100,7 @@ class Util {
*
* @throws \Aviat\Ion\Di\ContainerException
* @throws \Aviat\Ion\Di\NotFoundException
* @return boolean
* @return bool
*/
public function isFormPage(): bool
{

View File

@ -153,6 +153,12 @@ const SETTINGS_MAP = [
'default' => 'Somebody',
'description' => 'Name of the owner of the list data.',
],
'dark_theme' => [
'type' => 'boolean',
'title' => 'Use Dark Theme',
'default' => FALSE,
'description' => 'Use a darker background theme?',
],
'show_anime_collection' => [
'type' => 'boolean',
'title' => 'Show Anime Collection',

31
sw.js
View File

@ -5,36 +5,17 @@ async function fromCache (request) {
return await cache.match(request);
}
async function fromNetwork (request) {
return await fetch(request);
}
async function update (request) {
async function updateCache (request) {
const cache = await caches.open(CACHE_NAME);
const response = await fetch(request);
if (request.url.includes('/public/images/')) {
console.log('Saving to cache: ', request.url);
await cache.put(request, response.clone());
}
return response;
}
/* function refresh (response) {
return self.clients.matchAll().then(clients => {
clients.forEach(client => {
const message = {
type: 'refresh',
url: response.url,
eTag: response.headers.get('ETag')
};
client.postMessage(JSON.stringify(message));
})
});
} */
self.addEventListener('install', event => {
console.log('Public Folder Worker installed');
@ -55,8 +36,8 @@ self.addEventListener('install', event => {
)
});
self.addEventListener('activate', event => {
console.log('Public Folder Worker activated');
self.addEventListener('activate', () => {
console.info('Public Folder Worker activated');
});
// Pull css, images, and javascript from cache
@ -71,11 +52,7 @@ self.addEventListener('fetch', event => {
if (cached !== undefined) {
event.respondWith(cached);
} else {
event.respondWith(fromNetwork(event.request));
event.respondWith(updateCache(event.request));
}
});
event.waitUntil(
update(event.request)
);
});

View File

@ -17,7 +17,9 @@
namespace Aviat\AnimeClient\Tests\API;
use function Amp\Promise\wait;
use Aviat\AnimeClient\API\{APIRequestBuilder, HummingbirdClient};
use function Aviat\AnimeClient\getResponse;
use Aviat\AnimeClient\API\APIRequestBuilder;
use Aviat\Ion\Json;
use PHPUnit\Framework\TestCase;
use Psr\Log\NullLogger;
@ -37,35 +39,35 @@ class APIRequestBuilderTest extends TestCase {
$this->builder->setLogger(new NullLogger);
}
public function testGzipRequest()
public function testGzipRequest(): void
{
$request = $this->builder->newRequest('GET', 'gzip')
->getFullRequest();
$response = wait((new HummingbirdClient)->request($request));
$response = getResponse($request);
$body = Json::decode(wait($response->getBody()));
$this->assertEquals(1, $body['gzipped']);
}
public function testInvalidRequestMethod()
public function testInvalidRequestMethod(): void
{
$this->expectException(\InvalidArgumentException::class);
$this->builder->newRequest('FOO', 'gzip')
->getFullRequest();
}
public function testRequestWithBasicAuth()
public function testRequestWithBasicAuth(): void
{
$request = $this->builder->newRequest('GET', 'headers')
->setBasicAuth('username', 'password')
->getFullRequest();
$response = wait((new HummingbirdClient)->request($request));
$response = getResponse($request);
$body = Json::decode(wait($response->getBody()));
$this->assertEquals('Basic dXNlcm5hbWU6cGFzc3dvcmQ=', $body['headers']['Authorization']);
}
public function testRequestWithQueryString()
public function testRequestWithQueryString(): void
{
$query = [
'foo' => 'bar',
@ -87,13 +89,13 @@ class APIRequestBuilderTest extends TestCase {
->setQuery($query)
->getFullRequest();
$response = wait((new HummingbirdClient)->request($request));
$response = getResponse($request);
$body = Json::decode(wait($response->getBody()));
$this->assertEquals($expected, $body['args']);
}
public function testFormValueRequest()
public function testFormValueRequest(): void
{
$formValues = [
'foo' => 'bar',
@ -104,13 +106,13 @@ class APIRequestBuilderTest extends TestCase {
->setFormFields($formValues)
->getFullRequest();
$response = wait((new HummingbirdClient)->request($request));
$response = getResponse($request);
$body = Json::decode(wait($response->getBody()));
$this->assertEquals($formValues, $body['form']);
}
public function testFullUrlRequest()
public function testFullUrlRequest(): void
{
$data = [
'foo' => [
@ -128,7 +130,7 @@ class APIRequestBuilderTest extends TestCase {
->setJsonBody($data)
->getFullRequest();
$response = wait((new HummingbirdClient)->request($request));
$response = getResponse($request);
$body = Json::decode(wait($response->getBody()));
$this->assertEquals($data, $body['json']);

View File

@ -18,7 +18,6 @@ namespace Aviat\AnimeClient\Tests\API\Kitsu\Transformer;
use Aviat\AnimeClient\API\Kitsu\Transformer\AnimeTransformer;
use Aviat\AnimeClient\Tests\AnimeClientTestCase;
use Aviat\Ion\Friend;
use Aviat\Ion\Json;
class AnimeTransformerTest extends AnimeClientTestCase {

View File

@ -1,4 +1,10 @@
<?php return Aviat\AnimeClient\Types\Anime::__set_state(array(
<?php return Aviat\AnimeClient\Types\AnimePage::__set_state(array(
'characters' =>
array (
),
'staff' =>
array (
),
'age_rating' => 'R',
'age_rating_guide' => 'Violence, Profanity',
'cover_image' => 'https://media.kitsu.io/anime/poster_images/7442/small.jpg?1418580054',

View File

@ -1,4 +1,7 @@
<?php return Aviat\AnimeClient\Types\MangaPage::__set_state(array(
'characters' =>
array (
),
'chapter_count' => '-',
'cover_image' => 'https://media.kitsu.io/manga/poster_images/20286/small.jpg?1434293999',
'genres' =>
@ -68,6 +71,9 @@
),
),
'manga_type' => 'manga',
'staff' =>
array (
),
'synopsis' => 'Usa, a high-school student aspiring to begin a bachelor lifestyle, moves into a new apartment only to discover that he not only shares a room with a perverted roommate that has an obsession for underaged girls, but also that another girl, Ritsu, a love-at-first-sight, is living in the same building as well!
(Source: Kirei Cake)',
'title' => 'Bokura wa Minna Kawaisou',