All in GraphQL #34

Merged
timw4mail merged 87 commits from develop into master 2020-12-01 10:07:49 -05:00
309 changed files with 17064 additions and 9621 deletions

View File

@ -1,5 +1,10 @@
# Changelog # Changelog
## Version 5.1
* Added session check, so when coming back to a page, if the session is expired, the page will refresh.
* Updated logging config so that much fewer, much smaller files are generated.
* Updated Kitsu integration to use GraphQL API, reducing a lot of internal complexity.
## Version 5 ## Version 5
* Updated PHP requirement to 7.4 * Updated PHP requirement to 7.4
* Added anime watching history view * Added anime watching history view

View File

@ -227,7 +227,7 @@ class RoboFile extends Tasks {
{ {
$this->lint(); $this->lint();
$this->_run(['phpunit']); $this->_run(['vendor/bin/phpunit']);
} }
/** /**

View File

@ -28,6 +28,17 @@ use const Aviat\AnimeClient\{
// Maps paths to controllers and methods // Maps paths to controllers and methods
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
$routes = [ $routes = [
// ---------------------------------------------------------------------
// AJAX Routes
// ---------------------------------------------------------------------
'cache_purge' => [
'path' => '/cache_purge',
'action' => 'clearCache',
],
'heartbeat' => [
'path' => '/heartbeat',
'action' => 'heartbeat',
],
// --------------------------------------------------------------------- // ---------------------------------------------------------------------
// Anime List Routes // Anime List Routes
// --------------------------------------------------------------------- // ---------------------------------------------------------------------
@ -175,9 +186,9 @@ $routes = [
] ]
], ],
'person' => [ 'person' => [
'path' => '/people/{id}', 'path' => '/people/{slug}',
'tokens' => [ 'tokens' => [
'id' => SLUG_PATTERN 'slug' => SLUG_PATTERN,
] ]
], ],
'default_user_info' => [ 'default_user_info' => [
@ -215,10 +226,6 @@ $routes = [
'file' => '[a-z0-9\-]+\.[a-z]{3,4}' 'file' => '[a-z0-9\-]+\.[a-z]{3,4}'
] ]
], ],
'cache_purge' => [
'path' => '/cache_purge',
'action' => 'clearCache',
],
'settings' => [ 'settings' => [
'path' => '/settings', 'path' => '/settings',
], ],

View File

@ -10,7 +10,7 @@
* @author Timothy J. Warren <tim@timshomepage.net> * @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2020 Timothy J. Warren * @copyright 2015 - 2020 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License * @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5 * @version 5.1
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/ */
@ -19,20 +19,24 @@ namespace Aviat\AnimeClient;
use Aura\Html\HelperLocatorFactory; use Aura\Html\HelperLocatorFactory;
use Aura\Router\RouterContainer; use Aura\Router\RouterContainer;
use Aura\Session\SessionFactory; use Aura\Session\SessionFactory;
use Aviat\AnimeClient\API\{ use Aviat\AnimeClient\API\{Anilist, Kitsu};
Anilist, use Aviat\AnimeClient\Component;
Kitsu,
Kitsu\KitsuRequestBuilder
};
use Aviat\AnimeClient\Model; use Aviat\AnimeClient\Model;
use Aviat\Banker\Teller; use Aviat\Banker\Teller;
use Aviat\Ion\Config; use Aviat\Ion\Config;
use Aviat\Ion\Di\Container; use Aviat\Ion\Di\Container;
use Aviat\Ion\Di\ContainerInterface; use Aviat\Ion\Di\ContainerInterface;
use Psr\SimpleCache\CacheInterface; use Laminas\Diactoros\ServerRequestFactory;
use Laminas\Diactoros\{Response, ServerRequestFactory}; use Monolog\Formatter\JsonFormatter;
use Monolog\Handler\RotatingFileHandler; use Monolog\Handler\RotatingFileHandler;
use Monolog\Logger; use Monolog\Logger;
use Psr\SimpleCache\CacheInterface;
if ( ! defined('APP_DIR'))
{
define('APP_DIR', __DIR__);
define('TEMPLATE_DIR', APP_DIR . '/templates');
}
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Setup DI container // Setup DI container
@ -45,17 +49,18 @@ return static function (array $configArray = []): Container {
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
$appLogger = new Logger('animeclient'); $appLogger = new Logger('animeclient');
$appLogger->pushHandler(new RotatingFileHandler(__DIR__ . '/logs/app.log', Logger::NOTICE)); $appLogger->pushHandler(new RotatingFileHandler(__DIR__ . '/logs/app.log', 2, Logger::WARNING));
$anilistRequestLogger = new Logger('anilist-request');
$anilistRequestLogger->pushHandler(new RotatingFileHandler(__DIR__ . '/logs/anilist_request.log', Logger::NOTICE));
$kitsuRequestLogger = new Logger('kitsu-request');
$kitsuRequestLogger->pushHandler(new RotatingFileHandler(__DIR__ . '/logs/kitsu_request.log', Logger::NOTICE));
$container->setLogger($appLogger); $container->setLogger($appLogger);
$container->setLogger($anilistRequestLogger, 'anilist-request');
$container->setLogger($kitsuRequestLogger, 'kitsu-request'); foreach (['anilist-request', 'kitsu-request', 'kitsu-graphql'] as $channel)
{
$logger = new Logger($channel);
$handler = new RotatingFileHandler(__DIR__ . "/logs/{$channel}.log", 2, Logger::WARNING);
$handler->setFormatter(new JsonFormatter());
$logger->pushHandler($handler);
$container->setLogger($logger, $channel);
}
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// Injected Objects // Injected Objects
@ -74,29 +79,52 @@ return static function (array $configArray = []): Container {
// Create Aura Router Object // Create Aura Router Object
$container->set('aura-router', fn() => new RouterContainer); $container->set('aura-router', fn() => new RouterContainer);
// Create Html helper Object // Create Html helpers
$container->set('html-helper', static function(ContainerInterface $container) { $container->set('html-helper', static function(ContainerInterface $container) {
$htmlHelper = (new HelperLocatorFactory)->newInstance(); $htmlHelper = (new HelperLocatorFactory)->newInstance();
$htmlHelper->set('menu', static function() use ($container) { $helpers = [
$menuHelper = new Helper\Menu(); 'menu' => Helper\Menu::class,
$menuHelper->setContainer($container); 'field' => Helper\Form::class,
return $menuHelper; 'picture' => Helper\Picture::class,
}); ];
$htmlHelper->set('field', static function() use ($container) {
$formHelper = new Helper\Form(); foreach ($helpers as $name => $class)
$formHelper->setContainer($container); {
return $formHelper; $htmlHelper->set($name, static function() use ($class, $container) {
}); $helper = new $class;
$htmlHelper->set('picture', static function() use ($container) { $helper->setContainer($container);
$pictureHelper = new Helper\Picture(); return $helper;
$pictureHelper->setContainer($container); });
return $pictureHelper; }
});
return $htmlHelper; return $htmlHelper;
}); });
// Create Request/Response Objects // Create Component helpers
$container->set('component-helper', static function (ContainerInterface $container) {
$helper = (new HelperLocatorFactory)->newInstance();
$components = [
'animeCover' => Component\AnimeCover::class,
'mangaCover' => Component\MangaCover::class,
'character' => Component\Character::class,
'media' => Component\Media::class,
'tabs' => Component\Tabs::class,
'verticalTabs' => Component\VerticalTabs::class,
];
foreach ($components as $name => $componentClass)
{
$helper->set($name, static function () use ($container, $componentClass) {
$helper = new $componentClass;
$helper->setContainer($container);
return $helper;
});
}
return $helper;
});
// Create Request Object
$container->set('request', fn () => ServerRequestFactory::fromGlobals( $container->set('request', fn () => ServerRequestFactory::fromGlobals(
$_SERVER, $_SERVER,
$_GET, $_GET,
@ -104,7 +132,6 @@ return static function (array $configArray = []): Container {
$_COOKIE, $_COOKIE,
$_FILES $_FILES
)); ));
$container->set('response', fn () => new Response);
// Create session Object // Create session Object
$container->set('session', fn () => (new SessionFactory())->newInstance($_COOKIE)); $container->set('session', fn () => (new SessionFactory())->newInstance($_COOKIE));
@ -114,7 +141,7 @@ return static function (array $configArray = []): Container {
// Models // Models
$container->set('kitsu-model', static function(ContainerInterface $container): Kitsu\Model { $container->set('kitsu-model', static function(ContainerInterface $container): Kitsu\Model {
$requestBuilder = new KitsuRequestBuilder($container); $requestBuilder = new Kitsu\RequestBuilder($container);
$requestBuilder->setLogger($container->getLogger('kitsu-request')); $requestBuilder->setLogger($container->getLogger('kitsu-request'));
$listItem = new Kitsu\ListItem(); $listItem = new Kitsu\ListItem();
@ -130,7 +157,7 @@ return static function (array $configArray = []): Container {
return $model; return $model;
}); });
$container->set('anilist-model', static function(ContainerInterface $container): Anilist\Model { $container->set('anilist-model', static function(ContainerInterface $container): Anilist\Model {
$requestBuilder = new Anilist\AnilistRequestBuilder(); $requestBuilder = new Anilist\RequestBuilder($container);
$requestBuilder->setLogger($container->getLogger('anilist-request')); $requestBuilder->setLogger($container->getLogger('anilist-request'));
$listItem = new Anilist\ListItem(); $listItem = new Anilist\ListItem();

View File

@ -1,7 +1,7 @@
<article <article
class="media" class="media"
data-kitsu-id="<?= $item['id'] ?>" data-kitsu-id="<?= $item['id'] ?>"
data-mal-id="<?= $item['mal_id'] ?>" data-mal-id="<?= $item['mal_id'] ?>"
> >
<?php if ($auth->isAuthenticated()): ?> <?php if ($auth->isAuthenticated()): ?>
<button title="Increment episode count" class="plus-one" hidden>+1 Episode</button> <button title="Increment episode count" class="plus-one" hidden>+1 Episode</button>
@ -31,13 +31,13 @@
<?php if ($item['rewatched'] > 0): ?> <?php if ($item['rewatched'] > 0): ?>
<div class="row"> <div class="row">
<?php if ($item['rewatched'] == 1): ?> <?php if ($item['rewatched'] == 1): ?>
<div>Rewatched once</div> <div>Rewatched once</div>
<?php elseif ($item['rewatched'] == 2): ?> <?php elseif ($item['rewatched'] == 2): ?>
<div>Rewatched twice</div> <div>Rewatched twice</div>
<?php elseif ($item['rewatched'] == 3): ?> <?php elseif ($item['rewatched'] == 3): ?>
<div>Rewatched thrice</div> <div>Rewatched thrice</div>
<?php else: ?> <?php else: ?>
<div>Rewatched <?= $item['rewatched'] ?> times</div> <div>Rewatched <?= $item['rewatched'] ?> times</div>
<?php endif ?> <?php endif ?>
</div> </div>
<?php endif ?> <?php endif ?>

View File

@ -0,0 +1,6 @@
<article class="<?= $className ?>">
<div class="name">
<a href="<?= $link ?>"><?= $name ?></a>
</div>
<a href="<?= $link ?>"><?= $picture ?></a>
</article>

View File

@ -0,0 +1,74 @@
<article class="media" data-kitsu-id="<?= $item['id'] ?>" data-mal-id="<?= $item['mal_id'] ?>">
<?php if ($auth->isAuthenticated()): ?>
<div class="edit-buttons" hidden>
<button class="plus-one-chapter">+1 Chapter</button>
<?php /* <button class="plus-one-volume">+1 Volume</button> */ ?>
</div>
<?php endif ?>
<?= $helper->picture("images/manga/{$item['manga']['id']}.webp") ?>
<div class="name">
<a href="<?= $url->generate('manga.details', ['id' => $item['manga']['slug']]) ?>">
<?= $escape->html($item['manga']['title']) ?>
<?php foreach($item['manga']['titles'] as $title): ?>
<br /><small><?= $title ?></small>
<?php endforeach ?>
</a>
</div>
<div class="table">
<?php if ($auth->isAuthenticated()): ?>
<div class="row">
<span class="edit">
<a class="bracketed"
title="Edit information about this manga"
href="<?= $url->generate('edit', [
'controller' => 'manga',
'id' => $item['id'],
'status' => $name
]) ?>">
Edit
</a>
</span>
</div>
<?php endif ?>
<div class="row">
<div><?= $item['manga']['type'] ?></div>
<div class="user-rating">Rating: <?= $item['user_rating'] ?> / 10</div>
</div>
<?php if ($item['rereading']): ?>
<div class="row">
<?php foreach(['rereading'] as $attr): ?>
<?php if($item[$attr]): ?>
<span class="item-<?= $attr ?>"><?= ucfirst($attr) ?></span>
<?php endif ?>
<?php endforeach ?>
</div>
<?php endif ?>
<?php if ($item['reread'] > 0): ?>
<div class="row">
<?php if ($item['reread'] == 1): ?>
<div>Reread once</div>
<?php elseif ($item['reread'] == 2): ?>
<div>Reread twice</div>
<?php elseif ($item['reread'] == 3): ?>
<div>Reread thrice</div>
<?php else: ?>
<div>Reread <?= $item['reread'] ?> times</div>
<?php endif ?>
</div>
<?php endif ?>
<div class="row">
<div class="chapter_completion">
Chapters: <span class="chapters_read"><?= $item['chapters']['read'] ?></span> /
<span class="chapter_count"><?= $item['chapters']['total'] ?></span>
</div>
<?php /* </div>
<div class="row"> */ ?>
<div class="volume_completion">
Volumes: <span class="volume_count"><?= $item['volumes']['total'] ?></span>
</div>
</div>
</div>
</article>

12
app/templates/media.php Normal file
View File

@ -0,0 +1,12 @@
<article class="<?= $className ?>">
<a href="<?= $link ?>"><?= $picture ?></a>
<div class="name">
<a href="<?= $link ?>">
<?= array_shift($titles) ?>
<?php foreach ($titles as $title): ?>
<br />
<small><?= $title ?></small>
<?php endforeach ?>
</a>
</div>
</article>

View File

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

32
app/templates/tabs.php Normal file
View File

@ -0,0 +1,32 @@
<div class="tabs">
<?php $i = 0; foreach ($data as $tabName => $tabData): ?>
<?php if ( ! empty($tabData)): ?>
<?php $id = "{$name}-{$i}"; ?>
<input
role='tab'
aria-controls="_<?= $id ?>"
type="radio"
name="<?= $name ?>"
id="<?= $id ?>"
<?= ($i === 0) ? 'checked="checked"' : '' ?>
/>
<label for="<?= $id ?>"><?= ucfirst($tabName) ?></label>
<?php if ($hasSectionWrapper): ?>
<div class="content full-height">
<?php endif ?>
<section
id="_<?= $id ?>"
role="tabpanel"
class="<?= $className ?>"
>
<?= $callback($tabData, $tabName) ?>
</section>
<?php if ($hasSectionWrapper): ?>
</div>
<?php endif ?>
<?php endif ?>
<?php $i++; endforeach ?>
</div>

View File

@ -0,0 +1,25 @@
<div class="vertical-tabs">
<?php $i = 0; ?>
<?php foreach ($data as $tabName => $tabData): ?>
<?php $id = "{$name}-{$i}" ?>
<div class="tab">
<input
type="radio"
role='tab'
aria-controls="_<?= $id ?>"
name="<?= $name ?>"
id="<?= $id ?>"
<?= $i === 0 ? 'checked="checked"' : '' ?>
/>
<label for="<?= $id ?>"><?= $tabName ?></label>
<section
id='_<?= $id ?>'
role="tabpanel"
class="<?= $className ?>"
>
<?= $callback($tabData, $tabName) ?>
</section>
</div>
<?php $i++; ?>
<?php endforeach ?>
</div>

View File

@ -20,7 +20,7 @@
<section class="media-wrap"> <section class="media-wrap">
<?php foreach($items as $item): ?> <?php foreach($items as $item): ?>
<?php if ($item['private'] && ! $auth->isAuthenticated()) continue; ?> <?php if ($item['private'] && ! $auth->isAuthenticated()) continue; ?>
<?php include __DIR__ . '/cover-item.php' ?> <?= $component->animeCover($item) ?>
<?php endforeach ?> <?php endforeach ?>
</section> </section>
</section> </section>

View File

@ -1,6 +1,11 @@
<?php use function Aviat\AnimeClient\getLocalImg; ?> <?php
use Aviat\AnimeClient\Kitsu;
use function Aviat\AnimeClient\getLocalImg;
?>
<main class="details fixed"> <main class="details fixed">
<section class="flex"> <section class="flex" unselectable>
<aside class="info"> <aside class="info">
<?= $helper->picture("images/anime/{$data['id']}-original.webp") ?> <?= $helper->picture("images/anime/{$data['id']}-original.webp") ?>
@ -11,20 +16,33 @@
<td class="align-right">Airing Status</td> <td class="align-right">Airing Status</td>
<td><?= $data['status'] ?></td> <td><?= $data['status'] ?></td>
</tr> </tr>
<tr> <tr>
<td>Show Type</td> <td>Show Type</td>
<td><?= $data['show_type'] ?></td> <td><?= (strlen($data['show_type']) > 3) ? ucfirst(strtolower($data['show_type'])) : $data['show_type'] ?></td>
</tr> </tr>
<tr>
<td>Episode Count</td> <?php if ($data['episode_count'] !== 1): ?>
<td><?= $data['episode_count'] ?? '-' ?></td>
</tr>
<?php if ( ! empty($data['episode_length'])): ?>
<tr> <tr>
<td>Episode Length</td> <td>Episode Count</td>
<td><?= $data['episode_length'] ?> minutes</td> <td><?= $data['episode_count'] ?? '-' ?></td>
</tr> </tr>
<?php endif ?> <?php endif ?>
<?php if (( ! empty($data['episode_length'])) && $data['episode_count'] !== 1): ?>
<tr>
<td>Episode Length</td>
<td><?= Kitsu::friendlyTime($data['episode_length']) ?></td>
</tr>
<?php endif ?>
<?php if (isset($data['total_length'], $data['episode_count']) && $data['total_length'] > 0): ?>
<tr>
<td>Total Length</td>
<td><?= Kitsu::friendlyTime($data['total_length']) ?></td>
</tr>
<?php endif ?>
<?php if ( ! empty($data['age_rating'])): ?> <?php if ( ! empty($data['age_rating'])): ?>
<tr> <tr>
<td>Age Rating</td> <td>Age Rating</td>
@ -32,6 +50,18 @@
</td> </td>
</tr> </tr>
<?php endif ?> <?php endif ?>
<?php if (count($data['links']) > 0): ?>
<tr>
<td>External Links</td>
<td>
<?php foreach ($data['links'] as $urlName => $externalUrl): ?>
<a rel='external' href="<?= $externalUrl ?>"><?= $urlName ?></a><br />
<?php endforeach ?>
</td>
</tr>
<?php endif ?>
<tr> <tr>
<td>Genres</td> <td>Genres</td>
<td> <td>
@ -40,11 +70,13 @@
</tr> </tr>
</table> </table>
<br /> <br />
</aside> </aside>
<article class="text"> <article class="text">
<h2 class="toph"><a rel="external" href="<?= $data['url'] ?>"><?= $data['title'] ?></a></h2> <h2 class="toph"><?= $data['title'] ?></h2>
<?php foreach ($data['titles_more'] as $title): ?> <?php foreach ($data['titles_more'] as $title): ?>
<h3><?= $title ?></h3> <h3><?= $title ?></h3>
<?php endforeach ?> <?php endforeach ?>
@ -99,81 +131,69 @@
<?php endif ?> <?php endif ?>
<?php if ( ! empty($data['trailer_id'])): ?> <?php if ( ! empty($data['trailer_id'])): ?>
<div class="responsive-iframe"> <div class="responsive-iframe">
<h4>Trailer</h4> <h4>Trailer</h4>
<iframe <iframe
width="560" width="560"
height="315" height="315"
src="https://www.youtube.com/embed/<?= $data['trailer_id'] ?>" role='img'
frameborder="0" src="https://www.youtube.com/embed/<?= $data['trailer_id'] ?>"
allow="autoplay; encrypted-media" allow="autoplay; encrypted-media"
allowfullscreen allowfullscreen
></iframe> tabindex='0'
title="<?= $data['title'] ?> trailer video"
></iframe>
</div> </div>
<?php endif ?> <?php endif ?>
</article> </article>
</section> </section>
<?php if (count($data['characters']) > 0): ?> <?php if (count($data['characters']) > 0): ?>
<section> <section>
<h2>Characters</h2> <h2>Characters</h2>
<div class="tabs"> <?= $component->tabs('character-types', $data['characters'], static function ($characterList, $role)
<?php $i = 0 ?> use ($component, $url, $helper) {
<?php foreach ($data['characters'] as $role => $list): ?> $rendered = [];
<input foreach ($characterList as $id => $character):
type="radio" name="character-types" if (empty($character['image']['original']))
id="character-types-<?= $i ?>" <?= ($i === 0) ? 'checked' : '' ?> /> {
<label for="character-types-<?= $i ?>"><?= ucfirst($role) ?></label> continue;
<section class="content media-wrap flex flex-wrap flex-justify-start"> }
<?php foreach ($list as $id => $char): ?> $rendered[] = $component->character(
<?php if ( ! empty($char['image']['original'])): ?> $character['name'],
<article class="<?= $role === 'supporting' ? 'small-' : '' ?>character"> $url->generate('character', ['slug' => $character['slug']]),
<?php $link = $url->generate('character', ['slug' => $char['slug']]) ?> $helper->picture("images/characters/{$id}.webp"),
<div class="name"> (strtolower($role) !== 'main') ? 'small-character' : 'character'
<?= $helper->a($link, $char['name']); ?> );
</div> endforeach;
<a href="<?= $link ?>">
<?= $helper->picture("images/characters/{$id}.webp") ?> return implode('', array_map('mb_trim', $rendered));
</a> }) ?>
</article> </section>
<?php endif ?>
<?php endforeach ?>
</section>
<?php $i++; ?>
<?php endforeach ?>
</div>
</section>
<?php endif ?> <?php endif ?>
<?php if (count($data['staff']) > 0): ?> <?php if (count($data['staff']) > 0): ?>
<section> <section>
<h2>Staff</h2> <h2>Staff</h2>
<div class="vertical-tabs"> <?= $component->verticalTabs('staff-role', $data['staff'], static function ($staffList)
<?php $i = 0; ?> use ($component, $url, $helper) {
<?php foreach ($data['staff'] as $role => $people): ?> $rendered = [];
<div class="tab"> foreach ($staffList as $id => $person):
<input type="radio" name="staff-roles" id="staff-role<?= $i ?>" <?= $i === 0 ? 'checked' : '' ?> /> if (empty($person['image']['original']))
<label for="staff-role<?= $i ?>"><?= $role ?></label> {
<section class='content media-wrap flex flex-wrap flex-justify-start'> continue;
<?php foreach ($people as $pid => $person): ?> }
<article class='character small-person'> $rendered[] = $component->character(
<?php $link = $url->generate('person', ['id' => $person['id']]) ?> $person['name'],
<div class="name"> $url->generate('person', ['slug' => $person['slug']]),
<a href="<?= $link ?>"> $helper->picture(getLocalImg($person['image']['original'] ?? NULL)),
<?= $person['name'] ?> 'character small-person',
</a> );
</div> endforeach;
<a href="<?= $link ?>">
<?= $helper->picture(getLocalImg($person['image']['original'] ?? NULL)) ?> return implode('', array_map('mb_trim', $rendered));
</a> }) ?>
</article> </section>
<?php endforeach ?>
</section>
</div>
<?php $i++; ?>
<?php endforeach ?>
</div>
</section>
<?php endif ?> <?php endif ?>
</main> </main>

View File

@ -32,7 +32,7 @@
<td> <td>
<select name="watching_status" id="watching_status"> <select name="watching_status" id="watching_status">
<?php foreach($statuses as $status_key => $status_title): ?> <?php foreach($statuses as $status_key => $status_title): ?>
<option <?php if($item['watching_status'] === $status_key): ?>selected="selected"<?php endif ?> <option <?php if(strtolower($item['watching_status']) === $status_key): ?>selected="selected"<?php endif ?>
value="<?= $status_key ?>"><?= $status_title ?></option> value="<?= $status_key ?>"><?= $status_title ?></option>
<?php endforeach ?> <?php endforeach ?>
</select> </select>

View File

@ -1,4 +1,4 @@
<?php use function Aviat\AnimeClient\col_not_empty; ?> <?php use function Aviat\AnimeClient\colNotEmpty; ?>
<main class="media-list"> <main class="media-list">
<?php if ($auth->isAuthenticated()): ?> <?php if ($auth->isAuthenticated()): ?>
<a class="bracketed" href="<?= $url->generate('anime.add.get') ?>">Add Item</a> <a class="bracketed" href="<?= $url->generate('anime.add.get') ?>">Add Item</a>
@ -15,7 +15,7 @@
<h3>There's nothing here!</h3> <h3>There's nothing here!</h3>
<?php else: ?> <?php else: ?>
<?php <?php
$hasNotes = col_not_empty($items, 'notes'); $hasNotes = colNotEmpty($items, 'notes');
?> ?>
<table class='media-wrap'> <table class='media-wrap'>
<thead> <thead>
@ -31,7 +31,6 @@
<th>Rated</th> <th>Rated</th>
<th>Attributes</th> <th>Attributes</th>
<?php if($hasNotes): ?><th>Notes</th><?php endif ?> <?php if($hasNotes): ?><th>Notes</th><?php endif ?>
<th>Genres</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -103,10 +102,6 @@
</ul> </ul>
</td> </td>
<?php if ($hasNotes): ?><td><p><?= $escape->html($item['notes']) ?></p></td><?php endif ?> <?php if ($hasNotes): ?><td><p><?= $escape->html($item['notes']) ?></p></td><?php endif ?>
<td class="align-left">
<?php sort($item['anime']->genres) ?>
<?= implode(', ', $item['anime']->genres) ?>
</td>
</tr> </tr>
<?php endforeach ?> <?php endforeach ?>
</tbody> </tbody>

View File

@ -1,7 +1,7 @@
<?php <?php
use function Aviat\AnimeClient\getLocalImg; use function Aviat\AnimeClient\getLocalImg;
use Aviat\AnimeClient\API\Kitsu; use Aviat\AnimeClient\Kitsu;
?> ?>
<main class="character-page details fixed"> <main class="character-page details fixed">
@ -33,63 +33,20 @@ use Aviat\AnimeClient\API\Kitsu;
<?php if ( ! (empty($data['media']['anime']) || empty($data['media']['manga']))): ?> <?php if ( ! (empty($data['media']['anime']) || empty($data['media']['manga']))): ?>
<h3>Media</h3> <h3>Media</h3>
<div class="tabs">
<?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"> <?= $component->tabs('character-media', $data['media'], static function ($media, $mediaType) use ($url, $component, $helper) {
<?php foreach ($data['media']['anime'] as $id => $anime): ?> $rendered = [];
<article class="media"> foreach ($media as $id => $item)
<?php {
$link = $url->generate('anime.details', ['id' => $anime['attributes']['slug']]); $rendered[] = $component->media(
$titles = Kitsu::filterTitles($anime['attributes']); array_merge([$item['title']], $item['titles']),
?> $url->generate("{$mediaType}.details", ['id' => $item['slug']]),
<a href="<?= $link ?>"> $helper->picture("images/{$mediaType}/{$item['id']}.webp")
<?= $helper->picture("images/anime/{$id}.webp") ?> );
</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>
<?php endif ?>
<?php if ( ! empty($data['media']['manga'])): ?> return implode('', array_map('mb_trim', $rendered));
<input type="radio" id="media-manga" name="media-tabs" /> }, 'media-wrap content') ?>
<label for="media-manga">Manga</label>
<section class="media-wrap content">
<?php foreach ($data['media']['manga'] as $id => $manga): ?>
<article class="media">
<?php
$link = $url->generate('manga.details', ['id' => $manga['attributes']['slug']]);
$titles = Kitsu::filterTitles($manga['attributes']);
?>
<a href="<?= $link ?>">
<?= $helper->picture("images/manga/{$id}.webp") ?>
</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>
<?php endif ?>
</div>
<?php endif ?> <?php endif ?>
<section> <section>
@ -158,66 +115,47 @@ use Aviat\AnimeClient\API\Kitsu;
<?php if ( ! empty($vas)): ?> <?php if ( ! empty($vas)): ?>
<h4>Voice Actors</h4> <h4>Voice Actors</h4>
<div class="tabs"> <?= $component->tabs('character-vas', $vas, static function ($casting) use ($url, $component, $helper) {
<?php $i = 0; ?> $castings = [];
foreach ($casting as $id => $c):
$person = $component->character(
$c['person']['name'],
$url->generate('person', ['slug' => $c['person']['slug']]),
$helper->picture(getLocalImg($c['person']['image']))
);
$medias = array_map(fn ($series) => $component->media(
array_merge([$series['title']], $series['titles']),
$url->generate('anime.details', ['id' => $series['slug']]),
$helper->picture(getLocalImg($series['posterImage'], TRUE))
), $c['series']);
$media = implode('', array_map('mb_trim', $medias));
<?php foreach ($vas as $language => $casting): ?> $castings[] = <<<HTML
<input <?= $i === 0 ? 'checked="checked"' : '' ?> type="radio" id="character-va<?= $i ?>" <tr>
name="character-vas" <td>{$person}</td>
/> <td width="75%">
<label for="character-va<?= $i ?>"><?= $language ?></label> <section class="align-left media-wrap-flex">
<section class="content"> {$media}
<table class="borderless max-table"> </section>
<tr> </td>
<th>Cast Member</th> </tr>
<th>Series</th> HTML;
</tr> endforeach;
<?php foreach ($casting as $cid => $c): ?>
<tr> $languages = implode('', array_map('mb_trim', $castings));
<td>
<article class="character"> return <<<HTML
<?php <table class="borderless max-table">
$link = $url->generate('person', ['id' => $c['person']['id']]); <thead>
?> <tr>
<a href="<?= $link ?>"> <th>Cast Member</th>
<?= $helper->picture(getLocalImg($c['person']['image'])) ?> <th>Series</th>
<div class="name"> </tr>
<?= $c['person']['name'] ?> </thead>
</div> <tbody>{$languages}</tbody>
</a> </table>
</article> HTML;
</td> }, 'content') ?>
<td width="75%">
<section class="align-left media-wrap-flex">
<?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>
</section>
<?php $i++ ?>
<?php endforeach ?>
</div>
<?php endif ?> <?php endif ?>
<?php endif ?> <?php endif ?>
</section> </section>

View File

@ -19,7 +19,7 @@
<tr> <tr>
<td class="align-right"><label for="media_id">Media</label></td> <td class="align-right"><label for="media_id">Media</label></td>
<td class='align-left'> <td class='align-left'>
<?php include '_media-select-list.php' ?> <?php include 'media-select-list.php' ?>
</td> </td>
</tr> </tr>
<tr> <tr>

View File

@ -1,3 +1,4 @@
<?php use function Aviat\AnimeClient\renderTemplate; ?>
<main class="media-list"> <main class="media-list">
<?php if ($auth->isAuthenticated()): ?> <?php if ($auth->isAuthenticated()): ?>
<a class="bracketed" href="<?= $url->generate($collection_type . '.collection.add.get') ?>">Add Item</a> <a class="bracketed" href="<?= $url->generate($collection_type . '.collection.add.get') ?>">Add Item</a>
@ -8,30 +9,20 @@
<br /> <br />
<label>Filter: <input type='text' class='media-filter' /></label> <label>Filter: <input type='text' class='media-filter' /></label>
<br /> <br />
<div class="tabs"> <?= $component->tabs('collection-tab', $sections, static function ($items) use ($auth, $collection_type, $helper, $url, $component) {
<?php $i = 0; ?> $rendered = [];
<?php foreach ($sections as $name => $items): ?> foreach ($items as $item)
<input type="radio" id="collection-tab-<?= $i ?>" name="collection-tabs" /> {
<label for="collection-tab-<?= $i ?>"><h2><?= $name ?></h2></label> $rendered[] = renderTemplate(__DIR__ . '/cover-item.php', [
<div class="content full-height"> 'auth' => $auth,
<section class="media-wrap"> 'collection_type' => $collection_type,
<?php foreach ($items as $item): ?> 'helper' => $helper,
<?php include __DIR__ . '/cover-item.php'; ?> 'item' => $item,
<?php endforeach ?> 'url' => $url,
</section> ]);
</div> }
<?php $i++; ?>
<?php endforeach ?> return implode('', array_map('mb_trim', $rendered));
<!-- All Tab --> }, 'media-wrap', true) ?>
<input type='radio' checked='checked' id='collection-tab-<?= $i ?>' name='collection-tabs' />
<label for='collection-tab-<?= $i ?>'><h2>All</h2></label>
<div class='content full-height'>
<section class="media-wrap">
<?php foreach ($all as $item): ?>
<?php include __DIR__ . '/cover-item.php'; ?>
<?php endforeach ?>
</section>
</div>
</div>
<?php endif ?> <?php endif ?>
</main> </main>

View File

@ -1,3 +1,4 @@
<?php use function Aviat\AnimeClient\renderTemplate ?>
<?php if ($auth->isAuthenticated()): ?> <?php if ($auth->isAuthenticated()): ?>
<main> <main>
<h2>Edit Anime Collection Item</h2> <h2>Edit Anime Collection Item</h2>
@ -24,7 +25,7 @@
<tr> <tr>
<td class="align-right"><label for="media_id">Media</label></td> <td class="align-right"><label for="media_id">Media</label></td>
<td class="align-left"> <td class="align-left">
<?php include '_media-select-list.php' ?> <?php include 'media-select-list.php' ?>
</td> </td>
</tr> </tr>
<tr> <tr>

View File

@ -1,44 +0,0 @@
<input type='radio' checked='checked' id='collection-tab-<?= $i ?>' name='collection-tabs' />
<label for='collection-tab-<?= $i ?>'><h2>All</h2></label>
<div class="content full-height">
<table class="full-width media-wrap">
<thead>
<tr>
<?php if ($auth->isAuthenticated()): ?><td>&nbsp;</td><?php endif ?>
<th>Title</th>
<th>Media</th>
<th>Episode Count</th>
<th>Episode Length</th>
<th>Show Type</th>
<th>Age Rating</th>
<th>Notes</th>
<th>Genres</th>
</tr>
</thead>
<tbody>
<?php foreach ($all as $item): ?>
<?php $editLink = $url->generate($collection_type . '.collection.edit.get', ['id' => $item['hummingbird_id']]); ?>
<tr>
<?php if ($auth->isAuthenticated()): ?>
<td>
<a class="bracketed" href="<?= $editLink ?>">Edit</a>
</td>
<?php endif ?>
<td class="align-left">
<a href="<?= $url->generate('anime.details', ['id' => $item['slug']]) ?>">
<?= $item['title'] ?>
</a>
<?= ! empty($item['alternate_title']) ? ' <br /><small> ' . $item['alternate_title'] . '</small>' : '' ?>
</td>
<td><?= implode(', ', $item['media']) ?></td>
<td><?= ($item['episode_count'] > 1) ? $item['episode_count'] : '-' ?></td>
<td><?= $item['episode_length'] ?></td>
<td><?= $item['show_type'] ?></td>
<td><?= $item['age_rating'] ?></td>
<td class="align-left"><?= nl2br($item['notes'], TRUE) ?></td>
<td class="align-left"><?= implode(', ', $item['genres']) ?></td>
</tr>
<?php endforeach ?>
</tbody>
</table>
</div>

View File

@ -11,6 +11,9 @@
</a> </a>
<?= ! empty($item['alternate_title']) ? ' <br /><small> ' . $item['alternate_title'] . '</small>' : '' ?> <?= ! empty($item['alternate_title']) ? ' <br /><small> ' . $item['alternate_title'] . '</small>' : '' ?>
</td> </td>
<?php if ($hasMedia): ?>
<td><?= implode(', ', $item['media']) ?></td>
<?php endif ?>
<td><?= ($item['episode_count'] > 1) ? $item['episode_count'] : '-' ?></td> <td><?= ($item['episode_count'] > 1) ? $item['episode_count'] : '-' ?></td>
<td><?= $item['episode_length'] ?></td> <td><?= $item['episode_length'] ?></td>
<td><?= $item['show_type'] ?></td> <td><?= $item['show_type'] ?></td>

View File

@ -1,4 +1,4 @@
<?php use function Aviat\AnimeClient\col_not_empty; ?> <?php use function Aviat\AnimeClient\{colNotEmpty, renderTemplate}; ?>
<main> <main>
<?php if ($auth->isAuthenticated()): ?> <?php if ($auth->isAuthenticated()): ?>
<a class="bracketed" href="<?= $url->generate($collection_type . '.collection.add.get') ?>">Add Item</a> <a class="bracketed" href="<?= $url->generate($collection_type . '.collection.add.get') ?>">Add Item</a>
@ -9,38 +9,48 @@
<br /> <br />
<label>Filter: <input type='text' class='media-filter' /></label> <label>Filter: <input type='text' class='media-filter' /></label>
<br /> <br />
<?php $i = 0; ?> <?= $component->tabs('collection-tab', $sections, static function ($items, $section) use ($auth, $helper, $url, $collection_type) {
<div class="tabs"> $hasNotes = colNotEmpty($items, 'notes');
<?php foreach ($sections as $name => $items): ?> $hasMedia = $section === 'All';
<?php $hasNotes = col_not_empty($items, 'notes') ?> $firstTh = ($auth->isAuthenticated()) ? '<td>&nbsp;</td>' : '';
<input type="radio" id="collection-tab-<?= $i ?>" name="collection-tabs" /> $mediaTh = ($hasMedia) ? '<th>Media</th>' : '';
<label for="collection-tab-<?= $i ?>"><h2><?= $name ?></h2></label> $noteTh = ($hasNotes) ? '<th>Notes</th>' : '';
<div class="content full-height">
<table class="full-width media-wrap"> $rendered = [];
<thead> foreach ($items as $item)
<tr> {
<?php if ($auth->isAuthenticated()): ?><td>&nbsp;</td><?php endif ?> $rendered[] = renderTemplate(__DIR__ . '/list-item.php', [
<th>Title</th> 'auth' => $auth,
<th>Episode Count</th> 'collection_type' => $collection_type,
<th>Episode Length</th> 'hasMedia' => $hasMedia,
<th>Show Type</th> 'hasNotes' => $hasNotes,
<th>Age Rating</th> 'helper' => $helper,
<?php if ($hasNotes): ?><th>Notes</th><?php endif ?> 'item' => $item,
<th>Genres</th> 'url' => $url,
</tr> ]);
</thead> }
<tbody> $rows = implode('', array_map('mb_trim', $rendered));
<?php foreach ($items as $item): ?>
<?php include 'list-item.php' ?> return <<<HTML
<?php endforeach ?> <table class="full-width media-wrap">
</tbody> <thead>
</table> <tr>
</div> {$firstTh}
<?php $i++ ?> <th>Title</th>
<?php endforeach ?> {$mediaTh}
<!-- All --> <th>Episode Count</th>
<?php include 'list-all.php' ?> <th>Episode Length</th>
</div> <th>Show Type</th>
<th>Age Rating</th>
{$noteTh}
<th>Genres</th>
</tr>
</thead>
<tbody>{$rows}</tbody>
</table>
HTML;
}) ?>
<?php endif ?> <?php endif ?>
</main> </main>
<script defer="defer" src="<?= $urlGenerator->assetUrl('js/tables.min.js') ?>"></script> <script defer="defer" src="<?= $urlGenerator->assetUrl('js/tables.min.js') ?>"></script>

View File

@ -39,5 +39,4 @@
} }
} }
?> ?>
</header> </header>

View File

@ -19,80 +19,7 @@
<h2><?= $escape->html($name) ?></h2> <h2><?= $escape->html($name) ?></h2>
<section class="media-wrap"> <section class="media-wrap">
<?php foreach($items as $item): ?> <?php foreach($items as $item): ?>
<article class="media" data-kitsu-id="<?= $item['id'] ?>" data-mal-id="<?= $item['mal_id'] ?>"> <?= $component->mangaCover($item, $name) ?>
<?php if ($auth->isAuthenticated()): ?>
<div class="edit-buttons" hidden>
<button class="plus-one-chapter">+1 Chapter</button>
<?php /* <button class="plus-one-volume">+1 Volume</button> */ ?>
</div>
<?php endif ?>
<?= $helper->picture("images/manga/{$item['manga']['id']}.webp") ?>
<div class="name">
<a href="<?= $url->generate('manga.details', ['id' => $item['manga']['slug']]) ?>">
<?= $escape->html($item['manga']['title']) ?>
<?php foreach($item['manga']['titles'] as $title): ?>
<br /><small><?= $title ?></small>
<?php endforeach ?>
</a>
</div>
<div class="table">
<?php if ($auth->isAuthenticated()): ?>
<div class="row">
<span class="edit">
<a class="bracketed"
title="Edit information about this manga"
href="<?= $url->generate('edit', [
'controller' => 'manga',
'id' => $item['id'],
'status' => $name
]) ?>">
Edit
</a>
</span>
</div>
<?php endif ?>
<div class="row">
<div><?= $item['manga']['type'] ?></div>
<div class="user-rating">Rating: <?= $item['user_rating'] ?> / 10</div>
</div>
<?php if ($item['rereading']): ?>
<div class="row">
<?php foreach(['rereading'] as $attr): ?>
<?php if($item[$attr]): ?>
<span class="item-<?= $attr ?>"><?= ucfirst($attr) ?></span>
<?php endif ?>
<?php endforeach ?>
</div>
<?php endif ?>
<?php if ($item['reread'] > 0): ?>
<div class="row">
<?php if ($item['reread'] == 1): ?>
<div>Reread once</div>
<?php elseif ($item['reread'] == 2): ?>
<div>Reread twice</div>
<?php elseif ($item['reread'] == 3): ?>
<div>Reread thrice</div>
<?php else: ?>
<div>Reread <?= $item['reread'] ?> times</div>
<?php endif ?>
</div>
<?php endif ?>
<div class="row">
<div class="chapter_completion">
Chapters: <span class="chapters_read"><?= $item['chapters']['read'] ?></span> /
<span class="chapter_count"><?= $item['chapters']['total'] ?></span>
</div>
<?php /* </div>
<div class="row"> */ ?>
<div class="volume_completion">
Volumes: <span class="volume_count"><?= $item['volumes']['total'] ?></span>
</div>
</div>
</div>
</article>
<?php endforeach ?> <?php endforeach ?>
</section> </section>
</section> </section>

View File

@ -7,17 +7,45 @@
<table class="media-details"> <table class="media-details">
<tr> <tr>
<td>Manga Type</td> <td class="align-right">Publishing Status</td>
<td><?= ucfirst($data['manga_type']) ?></td> <td><?= $data['status'] ?></td>
</tr> </tr>
<tr>
<td>Manga Type</td>
<td><?= ucfirst(strtolower($data['manga_type'])) ?></td>
</tr>
<?php if ( ! empty($data['volume_count'])): ?>
<tr> <tr>
<td>Volume Count</td> <td>Volume Count</td>
<td><?= $data['volume_count'] ?></td> <td><?= $data['volume_count'] ?></td>
</tr> </tr>
<?php endif ?>
<?php if ( ! empty($data['chapter_count'])): ?>
<tr> <tr>
<td>Chapter Count</td> <td>Chapter Count</td>
<td><?= $data['chapter_count'] ?></td> <td><?= $data['chapter_count'] ?></td>
</tr> </tr>
<?php endif ?>
<?php if ( ! empty($data['age_rating'])): ?>
<tr>
<td>Age Rating</td>
<td><abbr title="<?= $data['age_rating_guide'] ?>"><?= $data['age_rating'] ?></abbr>
</td>
</tr>
<?php endif ?>
<?php if (count($data['links']) > 0): ?>
<tr>
<td>External Links</td>
<td>
<?php foreach ($data['links'] as $urlName => $externalUrl): ?>
<a rel='external' href="<?= $externalUrl ?>"><?= $urlName ?></a><br />
<?php endforeach ?>
</td>
</tr>
<?php endif ?>
<tr> <tr>
<td>Genres</td> <td>Genres</td>
<td> <td>
@ -29,8 +57,8 @@
<br /> <br />
</aside> </aside>
<article class="text"> <article class="text">
<h2 class="toph"><a rel="external" href="<?= $data['url'] ?>"><?= $data['title'] ?></a></h2> <h2 class="toph"><?= $data['title'] ?></h2>
<?php foreach ($data['titles'] as $title): ?> <?php foreach ($data['titles_more'] as $title): ?>
<h3><?= $title ?></h3> <h3><?= $title ?></h3>
<?php endforeach ?> <?php endforeach ?>
@ -44,61 +72,34 @@
<?php if (count($data['characters']) > 0): ?> <?php if (count($data['characters']) > 0): ?>
<h2>Characters</h2> <h2>Characters</h2>
<div class="tabs"> <?= $component->tabs('manga-characters', $data['characters'], static function($list, $role) use ($component, $helper, $url) {
<?php $i = 0 ?> $rendered = [];
<?php foreach ($data['characters'] as $role => $list): ?> foreach ($list as $id => $char)
<input {
type="radio" name="character-role-tabs" $rendered[] = $component->character(
id="character-tabs<?= $i ?>" <?= $i === 0 ? 'checked' : '' ?> /> $char['name'],
<label for="character-tabs<?= $i ?>"><?= ucfirst($role) ?></label> $url->generate('character', ['slug' => $char['slug']]),
<section class="content media-wrap flex flex-wrap flex-justify-start"> $helper->picture("images/characters/{$id}.webp"),
<?php foreach ($list as $id => $char): ?> ($role !== 'main') ? 'small-character' : 'character'
<?php if ( ! empty($char['image']['original'])): ?> );
<article class="<?= $role === 'supporting' ? 'small-' : '' ?>character"> }
<?php $link = $url->generate('character', ['slug' => $char['slug']]) ?>
<div class="name"> return implode('', array_map('mb_trim', $rendered));
<?= $helper->a($link, $char['name']); ?> }) ?>
</div>
<a href="<?= $link ?>">
<?= $helper->picture("images/characters/{$id}.webp") ?>
</a>
</article>
<?php endif ?>
<?php endforeach ?>
</section>
<?php $i++ ?>
<?php endforeach ?>
</div>
<?php endif ?> <?php endif ?>
<?php if (count($data['staff']) > 0): ?> <?php if (count($data['staff']) > 0): ?>
<h2>Staff</h2> <h2>Staff</h2>
<div class="vertical-tabs"> <?= $component->verticalTabs('manga-staff', $data['staff'],
<?php $i = 0 ?> fn($people) => implode('', array_map(
<?php foreach ($data['staff'] as $role => $people): ?> fn ($person) => $component->character(
<div class="tab"> $person['name'],
<input $url->generate('person', ['slug' => $person['slug']]),
type="radio" name="staff-roles" id="staff-role<?= $i ?>" <?= $i === 0 ? 'checked' : '' ?> /> $helper->picture("images/people/{$person['id']}.webp")
<label for="staff-role<?= $i ?>"><?= $role ?></label> ),
<section class='content media-wrap flex flex-wrap flex-justify-start'> $people
<?php foreach ($people as $pid => $person): ?> ))
<article class='character person'> ) ?>
<?php $link = $url->generate('person', ['id' => $pid]) ?>
<div class="name">
<a href="<?= $link ?>">
<?= $person['name'] ?>
</a>
</div>
<a href="<?= $link ?>">
<?= $helper->picture("images/people/{$pid}.webp") ?>
</a>
</article>
<?php endforeach ?>
</section>
</div>
<?php $i++ ?>
<?php endforeach ?>
</div>
<?php endif ?> <?php endif ?>
</main> </main>

View File

@ -25,7 +25,6 @@
<th># of Volumes</th> <th># of Volumes</th>
<th>Attributes</th> <th>Attributes</th>
<th>Type</th> <th>Type</th>
<th>Genres</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -70,9 +69,6 @@
</ul> </ul>
</td> </td>
<td><?= $item['manga']['type'] ?></td> <td><?= $item['manga']['type'] ?></td>
<td class="align-left">
<?= implode(', ', $item['manga']['genres']) ?>
</td>
</tr> </tr>
<?php endforeach ?> <?php endforeach ?>
</tbody> </tbody>

View File

@ -1,67 +0,0 @@
<?php
use function Aviat\AnimeClient\getLocalImg;
use Aviat\AnimeClient\API\Kitsu;
?>
<h3>Voice Acting Roles</h3>
<div class="tabs">
<?php $i = 0; ?>
<?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">
<table class="borderless max-table">
<tr>
<th>Character</th>
<th>Series</th>
</tr>
<?php foreach ($characterList as $cid => $character): ?>
<tr>
<td>
<article class="character">
<?php
$link = $url->generate('character', ['slug' => $character['character']['slug']]);
?>
<a href="<?= $link ?>">
<?php $imgPath = ($character['character']['image'] === NULL)
? 'images/characters/empty.png'
: getLocalImg($character['character']['image']['original']);
echo $helper->picture($imgPath);
?>
<div class="name">
<?= $character['character']['canonicalName'] ?>
</div>
</a>
</article>
</td>
<td>
<section class="align-left media-wrap">
<?php foreach ($character['media'] as $sid => $series): ?>
<article class="media">
<?php
$link = $url->generate('anime.details', ['id' => $series['slug']]);
$titles = Kitsu::filterTitles($series);
?>
<a href="<?= $link ?>">
<?= $helper->picture("images/anime/{$sid}.webp") ?>
</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>
</section>
<?php $i++ ?>
<?php endforeach ?>
</div>

View File

@ -1,7 +1,5 @@
<?php <?php
use function Aviat\AnimeClient\getLocalImg;
use Aviat\AnimeClient\API\Kitsu;
?> ?>
<main class="details fixed"> <main class="details fixed">
<section class="flex flex-no-wrap"> <section class="flex flex-no-wrap">
@ -10,12 +8,21 @@ use Aviat\AnimeClient\API\Kitsu;
</div> </div>
<div> <div>
<h2 class="toph"><?= $data['name'] ?></h2> <h2 class="toph"><?= $data['name'] ?></h2>
<?php foreach ($data['names'] as $name): ?>
<h3><?= $name ?></h3>
<?php endforeach ?>
<br />
<hr />
<div class="description">
<p><?= str_replace("\n", '</p><p>', $data['description']) ?></p>
</div>
</div> </div>
</section> </section>
<?php if ( ! empty($data['staff'])): ?> <?php if ( ! empty($data['staff'])): ?>
<section> <section>
<h3>Castings</h3> <h3>Castings</h3>
<div class="vertical-tabs"> <div class="vertical-tabs">
<?php $i = 0 ?> <?php $i = 0 ?>
<?php foreach ($data['staff'] as $role => $entries): ?> <?php foreach ($data['staff'] as $role => $entries): ?>
@ -24,31 +31,17 @@ use Aviat\AnimeClient\API\Kitsu;
type="radio" name="staff-roles" id="staff-role<?= $i ?>" <?= $i === 0 ? 'checked' : '' ?> /> type="radio" name="staff-roles" id="staff-role<?= $i ?>" <?= $i === 0 ? 'checked' : '' ?> />
<label for="staff-role<?= $i ?>"><?= $role ?></label> <label for="staff-role<?= $i ?>"><?= $role ?></label>
<?php foreach ($entries as $type => $casting): ?> <?php foreach ($entries as $type => $casting): ?>
<?php if ($type === 'characters') continue; ?> <?php if (isset($entries['manga'], $entries['anime'])): ?>
<?php if ( ! (empty($entries['manga']) || empty($entries['anime']))): ?>
<h4><?= ucfirst($type) ?></h4> <h4><?= ucfirst($type) ?></h4>
<?php endif ?> <?php endif ?>
<section class="content"> <section class="content media-wrap flex flex-wrap flex-justify-start">
<?php foreach ($casting as $sid => $series): ?> <?php foreach ($casting as $sid => $series): ?>
<article class="media"> <?php $mediaType = in_array($type, ['anime', 'manga'], TRUE) ? $type : 'anime'; ?>
<?php <?= $component->media(
$mediaType = in_array($type, ['anime', 'manga'], TRUE) ? $type : 'anime'; $series['titles'],
$link = $url->generate("{$mediaType}.details", ['id' => $series['slug']]); $url->generate("{$mediaType}.details", ['id' => $series['slug']]),
$titles = Kitsu::filterTitles($series); $helper->picture("images/{$type}/{$sid}.webp")
?> ) ?>
<a href="<?= $link ?>">
<?= $helper->picture("images/{$type}/{$sid}.webp") ?>
</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; ?> <?php endforeach; ?>
</section> </section>
<?php endforeach ?> <?php endforeach ?>
@ -59,9 +52,53 @@ use Aviat\AnimeClient\API\Kitsu;
</section> </section>
<?php endif ?> <?php endif ?>
<?php if ( ! (empty($data['characters']['main']) || empty($data['characters']['supporting']))): ?> <?php if ( ! empty($data['characters'])): ?>
<section> <section>
<?php include 'character-mapping.php' ?> <h3>Voice Acting Roles</h3>
<?= $component->tabs('voice-acting-roles', $data['characters'], static function ($characterList) use ($component, $helper, $url) {
$voiceRoles = [];
foreach ($characterList as $cid => $item):
$character = $component->character(
$item['character']['canonicalName'],
$url->generate('character', ['slug' => $item['character']['slug']]),
$helper->picture(getLocalImg($item['character']['image']['original'] ?? null))
);
$medias = [];
foreach ($item['media'] as $sid => $series)
{
$medias[] = $component->media(
$series['titles'],
$url->generate('anime.details', ['id' => $series['slug']]),
$helper->picture("images/anime/{$sid}.webp")
);
}
$media = implode('', array_map('mb_trim', $medias));
$voiceRoles[] = <<<HTML
<tr>
<td>{$character}</td>
<td>
<section class="align-left media-wrap">{$media}</section>
</td>
</tr>
HTML;
endforeach;
$roles = implode('', array_map('mb_trim', $voiceRoles));
return <<<HTML
<table class="borderless max-table">
<thead>
<tr>
<th>Character</th>
<th>Series</th>
</tr>
</thead>
<tbody>{$roles}</tbody>
</table>
HTML;
}) ?>
</section> </section>
<?php endif ?> <?php endif ?>
</main> </main>

View File

@ -28,9 +28,7 @@ $nestedPrefix = 'config';
/> />
<label for="settings-tab<?= $i ?>"><h3><?= $sectionMapping[$section] ?></h3></label> <label for="settings-tab<?= $i ?>"><h3><?= $sectionMapping[$section] ?></h3></label>
<section class="content"> <section class="content">
<?php require __DIR__ . '/_form.php' ?>
<?php if ($section === 'anilist'): ?> <?php if ($section === 'anilist'): ?>
<hr />
<?php $auth = $anilistModel->checkAuth(); ?> <?php $auth = $anilistModel->checkAuth(); ?>
<?php if (array_key_exists('errors', $auth)): ?> <?php if (array_key_exists('errors', $auth)): ?>
<p class="static-message error">Not Authorized.</p> <p class="static-message error">Not Authorized.</p>
@ -43,11 +41,15 @@ $nestedPrefix = 'config';
<p class="static-message info"> <p class="static-message info">
Linked to Anilist. Your access token will expire around <?= date('F j, Y, g:i a T', $expires) ?> Linked to Anilist. Your access token will expire around <?= date('F j, Y, g:i a T', $expires) ?>
</p> </p>
<?php require __DIR__ . '/_form.php' ?>
<?= $helper->a( <?= $helper->a(
$url->generate('anilist-redirect'), $url->generate('anilist-redirect'),
'Update Access Token' 'Update Access Token',
) ?> ['class' => 'bracketed user-btn']
) ?>
<?php endif ?> <?php endif ?>
<?php else: ?>
<?php require __DIR__ . '/_form.php' ?>
<?php endif ?> <?php endif ?>
</section> </section>
<?php $i++; ?> <?php $i++; ?>

View File

@ -1,5 +1,5 @@
<?php <?php
use Aviat\AnimeClient\API\Kitsu; use Aviat\AnimeClient\Kitsu;
?> ?>
<main class="user-page details"> <main class="user-page details">
<h2 class="toph"> <h2 class="toph">
@ -36,7 +36,7 @@ use Aviat\AnimeClient\API\Kitsu;
$character = $data['waifu']['character']; $character = $data['waifu']['character'];
echo $helper->a( echo $helper->a(
$url->generate('character', ['slug' => $character['slug']]), $url->generate('character', ['slug' => $character['slug']]),
$character['canonicalName'] $character['names']['canonical']
); );
?> ?>
</td> </td>
@ -57,79 +57,43 @@ use Aviat\AnimeClient\API\Kitsu;
<article> <article>
<?php if ( ! empty($data['favorites'])): ?> <?php if ( ! empty($data['favorites'])): ?>
<h3>Favorites</h3> <h3>Favorites</h3>
<div class="tabs"> <?= $component->tabs('user-favorites', $data['favorites'], static function ($items, $type) use ($component, $helper, $url) {
<?php $i = 0 ?> $rendered = [];
<?php if ( ! empty($data['favorites']['characters'])): ?> if ($type === 'character')
<input type="radio" name="user-favorites" id="user-fav-chars" <?= $i === 0 ? 'checked' : '' ?> /> {
<label for="user-fav-chars">Characters</label> uasort($items, fn ($a, $b) => $a['names']['canonical'] <=> $b['names']['canonical']);
<section class="content full-width media-wrap"> }
<?php foreach($data['favorites']['characters'] as $id => $char): ?> else
<?php if ( ! empty($char['image']['original'])): ?> {
<article class="character"> uasort($items, fn ($a, $b) => $a['titles']['canonical'] <=> $b['titles']['canonical']);
<?php $link = $url->generate('character', ['slug' => $char['slug']]) ?> }
<div class="name"><?= $helper->a($link, $char['canonicalName']); ?></div>
<a href="<?= $link ?>"> foreach ($items as $id => $item)
<?= $helper->picture("images/characters/{$char['id']}.webp") ?> {
</a> if ($type === 'character')
</article> {
<?php endif ?> $rendered[] = $component->character(
<?php endforeach ?> $item['names']['canonical'],
</section> $url->generate('character', ['slug' => $item['slug']]),
<?php $i++; ?> $helper->picture("images/characters/{$item['id']}.webp")
<?php endif ?> );
<?php if ( ! empty($data['favorites']['anime'])): ?> }
<input type="radio" name="user-favorites" id="user-fav-anime" <?= $i === 0 ? 'checked' : '' ?> /> else
<label for="user-fav-anime">Anime</label> {
<section class="content full-width media-wrap"> $rendered[] = $component->media(
<?php foreach($data['favorites']['anime'] as $anime): ?> array_merge(
<article class="media"> [$item['titles']['canonical']],
<?php Kitsu::getFilteredTitles($item['titles']),
$link = $url->generate('anime.details', ['id' => $anime['slug']]); ),
$titles = Kitsu::filterTitles($anime); $url->generate("{$type}.details", ['id' => $item['slug']]),
?> $helper->picture("images/{$type}/{$item['id']}.webp"),
<a href="<?= $link ?>"> );
<?= $helper->picture("images/anime/{$anime['id']}.webp") ?> }
</a> }
<div class="name">
<a href="<?= $link ?>"> return implode('', array_map('mb_trim', $rendered));
<?= array_shift($titles) ?>
<?php foreach ($titles as $title): ?> }, 'content full-width media-wrap') ?>
<br /><small><?= $title ?></small>
<?php endforeach ?>
</a>
</div>
</article>
<?php endforeach ?>
</section>
<?php $i++; ?>
<?php endif ?>
<?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($data['favorites']['manga'] as $manga): ?>
<article class="media">
<?php
$link = $url->generate('manga.details', ['id' => $manga['slug']]);
$titles = Kitsu::filterTitles($manga);
?>
<a href="<?= $link ?>">
<?= $helper->picture("images/manga/{$manga['id']}.webp") ?>
</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>
<?php $i++; ?>
<?php endif ?>
</div>
<?php endif ?> <?php endif ?>
</article> </article>
</section> </section>

View File

@ -9,7 +9,7 @@
* @author Timothy J. Warren <tim@timshomepage.net> * @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2020 Timothy J. Warren * @copyright 2015 - 2020 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License * @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5 * @version 5.1
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/ */

View File

@ -34,7 +34,8 @@
} }
}, },
"require": { "require": {
"amphp/http-client": "^4.2.2", "amphp/amp": "^2.5.0",
"amphp/http-client": "^4.5.0",
"aura/html": "^2.5.0", "aura/html": "^2.5.0",
"aura/router": "^3.1.0", "aura/router": "^3.1.0",
"aura/session": "^2.1.0", "aura/session": "^2.1.0",
@ -43,6 +44,7 @@
"danielstjules/stringy": "^3.1.0", "danielstjules/stringy": "^3.1.0",
"ext-dom": "*", "ext-dom": "*",
"ext-iconv": "*", "ext-iconv": "*",
"ext-intl": "*",
"ext-json": "*", "ext-json": "*",
"ext-gd": "*", "ext-gd": "*",
"ext-pdo": "*", "ext-pdo": "*",
@ -65,7 +67,7 @@
"phpstan/phpstan": "^0.12.19", "phpstan/phpstan": "^0.12.19",
"phpunit/phpunit": "^8.5.2", "phpunit/phpunit": "^8.5.2",
"roave/security-advisories": "dev-master", "roave/security-advisories": "dev-master",
"robmorgan/phinx": "^0.10.6", "robmorgan/phinx": "^0.12.4",
"sebastian/phpcpd": "^4.1.0", "sebastian/phpcpd": "^4.1.0",
"spatie/phpunit-snapshot-assertions": "^4.1.0", "spatie/phpunit-snapshot-assertions": "^4.1.0",
"squizlabs/php_codesniffer": "^3.5.4", "squizlabs/php_codesniffer": "^3.5.4",

View File

@ -9,6 +9,9 @@ use ConsoleKit\Console;
$_SERVER['HTTP_HOST'] = 'localhost'; $_SERVER['HTTP_HOST'] = 'localhost';
define('APP_DIR', __DIR__ . '/app');
define('TEMPLATE_DIR', APP_DIR . '/templates');
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Start console script // Start console script
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------

View File

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

View File

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

View File

@ -94,6 +94,7 @@ a:hover, a:active {
iframe { iframe {
display: block; display: block;
margin: 0 auto; margin: 0 auto;
border: 0;
} }
/* ----------------------------------------------------------------------------- /* -----------------------------------------------------------------------------

View File

@ -261,7 +261,7 @@ function ajaxSerialize(data) {
* *
* @param {string} url - the url to request * @param {string} url - the url to request
* @param {Object} config - the configuration object * @param {Object} config - the configuration object
* @return {void} * @return {XMLHttpRequest}
*/ */
AnimeClient.ajax = (url, config) => { AnimeClient.ajax = (url, config) => {
// Set some sane defaults // Set some sane defaults
@ -322,6 +322,8 @@ AnimeClient.ajax = (url, config) => {
} else { } else {
request.send(config.data); request.send(config.data);
} }
return request
}; };
/** /**
@ -330,6 +332,7 @@ AnimeClient.ajax = (url, config) => {
* @param {string} url * @param {string} url
* @param {object|function} data * @param {object|function} data
* @param {function} [callback] * @param {function} [callback]
* @return {XMLHttpRequest}
*/ */
AnimeClient.get = (url, data, callback = null) => { AnimeClient.get = (url, data, callback = null) => {
if (callback === null) { if (callback === null) {

View File

@ -6,25 +6,31 @@ const search = (query) => {
_.show('.cssload-loader'); _.show('.cssload-loader');
// Do the api search // Do the api search
_.get(_.url('/anime-collection/search'), { query }, (searchResults, status) => { return _.get(_.url('/anime-collection/search'), { query }, (searchResults, status) => {
searchResults = JSON.parse(searchResults); searchResults = JSON.parse(searchResults);
// Hide the loader // Hide the loader
_.hide('.cssload-loader'); _.hide('.cssload-loader');
// Show the results // Show the results
_.$('#series-list')[ 0 ].innerHTML = renderAnimeSearchResults(searchResults.data); _.$('#series-list')[ 0 ].innerHTML = renderAnimeSearchResults(searchResults);
}); });
}; };
if (_.hasElement('.anime #search')) { if (_.hasElement('.anime #search')) {
let prevRequest = null;
_.on('#search', 'input', _.throttle(250, (e) => { _.on('#search', 'input', _.throttle(250, (e) => {
const query = encodeURIComponent(e.target.value); const query = encodeURIComponent(e.target.value);
if (query === '') { if (query === '') {
return; return;
} }
search(query); if (prevRequest !== null) {
prevRequest.abort();
}
prevRequest = search(query);
})); }));
} }
@ -47,12 +53,12 @@ _.on('body.anime.list', 'click', '.plus-one', (e) => {
// If the episode count is 0, and incremented, // If the episode count is 0, and incremented,
// change status to currently watching // change status to currently watching
if (isNaN(watchedCount) || watchedCount === 0) { if (isNaN(watchedCount) || watchedCount === 0) {
data.data.status = 'current'; data.data.status = 'CURRENT';
} }
// If you increment at the last episode, mark as completed // If you increment at the last episode, mark as completed
if ((!isNaN(watchedCount)) && (watchedCount + 1) === totalCount) { if ((!isNaN(watchedCount)) && (watchedCount + 1) === totalCount) {
data.data.status = 'completed'; data.data.status = 'COMPLETED';
} }
_.show('#loading-shadow'); _.show('#loading-shadow');
@ -72,7 +78,7 @@ _.on('body.anime.list', 'click', '.plus-one', (e) => {
return; return;
} }
if (resData.data.attributes.status === 'completed') { if (resData.data.libraryEntry.update.libraryEntry.status === 'COMPLETED') {
_.hide(parentSel); _.hide(parentSel);
} }

View File

@ -1,4 +1,5 @@
import './anon.js'; import './anon.js';
import './session-check.js';
import './anime.js'; import './anime.js';
import './manga.js'; import './manga.js';

View File

@ -3,21 +3,27 @@ import { renderMangaSearchResults } from './template-helpers.js'
const search = (query) => { const search = (query) => {
_.show('.cssload-loader'); _.show('.cssload-loader');
_.get(_.url('/manga/search'), { query }, (searchResults, status) => { return _.get(_.url('/manga/search'), { query }, (searchResults, status) => {
searchResults = JSON.parse(searchResults); searchResults = JSON.parse(searchResults);
_.hide('.cssload-loader'); _.hide('.cssload-loader');
_.$('#series-list')[ 0 ].innerHTML = renderMangaSearchResults(searchResults.data); _.$('#series-list')[ 0 ].innerHTML = renderMangaSearchResults(searchResults);
}); });
}; };
if (_.hasElement('.manga #search')) { if (_.hasElement('.manga #search')) {
let prevRequest = null
_.on('#search', 'input', _.throttle(250, (e) => { _.on('#search', 'input', _.throttle(250, (e) => {
let query = encodeURIComponent(e.target.value); let query = encodeURIComponent(e.target.value);
if (query === '') { if (query === '') {
return; return;
} }
search(query); if (prevRequest !== null) {
prevRequest.abort();
}
prevRequest = search(query);
})); }));
} }
@ -48,12 +54,12 @@ _.on('.manga.list', 'click', '.edit-buttons button', (e) => {
// If the episode count is 0, and incremented, // If the episode count is 0, and incremented,
// change status to currently reading // change status to currently reading
if (isNaN(completed) || completed === 0) { if (isNaN(completed) || completed === 0) {
data.data.status = 'current'; data.data.status = 'CURRENT';
} }
// If you increment at the last chapter, mark as completed // If you increment at the last chapter, mark as completed
if ((!isNaN(completed)) && (completed + 1) === total) { if ((!isNaN(completed)) && (completed + 1) === total) {
data.data.status = 'completed'; data.data.status = 'COMPLETED';
} }
// Update the total count // Update the total count
@ -67,7 +73,7 @@ _.on('.manga.list', 'click', '.edit-buttons button', (e) => {
type: 'POST', type: 'POST',
mimeType: 'application/json', mimeType: 'application/json',
success: () => { success: () => {
if (data.data.status === 'completed') { if (String(data.data.status).toUpperCase() === 'COMPLETED') {
_.hide(parentSel); _.hide(parentSel);
} }

View File

@ -0,0 +1,41 @@
import _ from './anime-client.js';
(() => {
// Var is intentional
var hidden = null;
var visibilityChange = null;
if (typeof document.hidden !== "undefined") {
hidden = "hidden";
visibilityChange = "visibilitychange";
} else if (typeof document.msHidden !== "undefined") {
hidden = "msHidden";
visibilityChange = "msvisibilitychange";
} else if (typeof document.webkitHidden !== "undefined") {
hidden = "webkitHidden";
visibilityChange = "webkitvisibilitychange";
}
function handleVisibilityChange() {
// Check the user's session to see if they are currently logged-in
// when the page becomes visible
if ( ! document[hidden]) {
_.get('/heartbeat', (beat) => {
const status = JSON.parse(beat)
// If the session is expired, immediately reload so that
// you can't attempt to do an action that requires authentication
if (status.hasAuth !== true) {
document.removeEventListener(visibilityChange, handleVisibilityChange, false);
location.reload();
}
});
}
}
if (hidden === null) {
console.info('Page visibility API not supported, JS session check will not work');
} else {
document.addEventListener(visibilityChange, handleVisibilityChange, false);
}
})();

View File

@ -10,20 +10,19 @@ _.on('main', 'change', '.big-check', (e) => {
export function renderAnimeSearchResults (data) { export function renderAnimeSearchResults (data) {
const results = []; const results = [];
data.forEach(x => { data.forEach(item => {
const item = x.attributes;
const titles = item.titles.join('<br />'); const titles = item.titles.join('<br />');
results.push(` results.push(`
<article class="media search"> <article class="media search">
<div class="name"> <div class="name">
<input type="radio" class="mal-check" id="mal_${item.slug}" name="mal_id" value="${x.mal_id}" /> <input type="radio" class="mal-check" id="mal_${item.slug}" name="mal_id" value="${item.mal_id}" />
<input type="radio" class="big-check" id="${item.slug}" name="id" value="${x.id}" /> <input type="radio" class="big-check" id="${item.slug}" name="id" value="${item.id}" />
<label for="${item.slug}"> <label for="${item.slug}">
<picture width="220"> <picture width="220">
<source srcset="/public/images/anime/${x.id}.webp" type="image/webp" /> <source srcset="/public/images/anime/${item.id}.webp" type="image/webp" />
<source srcset="/public/images/anime/${x.id}.jpg" type="image/jpeg" /> <source srcset="/public/images/anime/${item.id}.jpg" type="image/jpeg" />
<img src="/public/images/anime/${x.id}.jpg" alt="" width="220" /> <img src="/public/images/anime/${item.id}.jpg" alt="" width="220" />
</picture> </picture>
<span class="name"> <span class="name">
${item.canonicalTitle}<br /> ${item.canonicalTitle}<br />
@ -48,20 +47,19 @@ export function renderAnimeSearchResults (data) {
export function renderMangaSearchResults (data) { export function renderMangaSearchResults (data) {
const results = []; const results = [];
data.forEach(x => { data.forEach(item => {
const item = x.attributes;
const titles = item.titles.join('<br />'); const titles = item.titles.join('<br />');
results.push(` results.push(`
<article class="media search"> <article class="media search">
<div class="name"> <div class="name">
<input type="radio" id="mal_${item.slug}" name="mal_id" value="${x.mal_id}" /> <input type="radio" id="mal_${item.slug}" name="mal_id" value="${item.mal_id}" />
<input type="radio" class="big-check" id="${item.slug}" name="id" value="${x.id}" /> <input type="radio" class="big-check" id="${item.slug}" name="id" value="${item.id}" />
<label for="${item.slug}"> <label for="${item.slug}">
<picture width="220"> <picture width="220">
<source srcset="/public/images/manga/${x.id}.webp" type="image/webp" /> <source srcset="/public/images/manga/${item.id}.webp" type="image/webp" />
<source srcset="/public/images/manga/${x.id}.jpg" type="image/jpeg" /> <source srcset="/public/images/manga/${item.id}.jpg" type="image/jpeg" />
<img src="/public/images/manga/${x.id}.jpg" alt="" width="220" /> <img src="/public/images/manga/${item.id}.jpg" alt="" width="220" />
</picture> </picture>
<span class="name"> <span class="name">
${item.canonicalTitle}<br /> ${item.canonicalTitle}<br />

View File

@ -10,7 +10,7 @@
* @author Timothy J. Warren <tim@timshomepage.net> * @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2020 Timothy J. Warren * @copyright 2015 - 2020 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License * @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5 * @version 5.1
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/ */

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -260,7 +260,7 @@ function ajaxSerialize(data) {
* *
* @param {string} url - the url to request * @param {string} url - the url to request
* @param {Object} config - the configuration object * @param {Object} config - the configuration object
* @return {void} * @return {XMLHttpRequest}
*/ */
AnimeClient.ajax = (url, config) => { AnimeClient.ajax = (url, config) => {
// Set some sane defaults // Set some sane defaults
@ -321,6 +321,8 @@ AnimeClient.ajax = (url, config) => {
} else { } else {
request.send(config.data); request.send(config.data);
} }
return request
}; };
/** /**
@ -329,6 +331,7 @@ AnimeClient.ajax = (url, config) => {
* @param {string} url * @param {string} url
* @param {object|function} data * @param {object|function} data
* @param {function} [callback] * @param {function} [callback]
* @return {XMLHttpRequest}
*/ */
AnimeClient.get = (url, data, callback = null) => { AnimeClient.get = (url, data, callback = null) => {
if (callback === null) { if (callback === null) {

View File

@ -260,7 +260,7 @@ function ajaxSerialize(data) {
* *
* @param {string} url - the url to request * @param {string} url - the url to request
* @param {Object} config - the configuration object * @param {Object} config - the configuration object
* @return {void} * @return {XMLHttpRequest}
*/ */
AnimeClient.ajax = (url, config) => { AnimeClient.ajax = (url, config) => {
// Set some sane defaults // Set some sane defaults
@ -321,6 +321,8 @@ AnimeClient.ajax = (url, config) => {
} else { } else {
request.send(config.data); request.send(config.data);
} }
return request
}; };
/** /**
@ -329,6 +331,7 @@ AnimeClient.ajax = (url, config) => {
* @param {string} url * @param {string} url
* @param {object|function} data * @param {object|function} data
* @param {function} [callback] * @param {function} [callback]
* @return {XMLHttpRequest}
*/ */
AnimeClient.get = (url, data, callback = null) => { AnimeClient.get = (url, data, callback = null) => {
if (callback === null) { if (callback === null) {
@ -459,6 +462,46 @@ if ('serviceWorker' in navigator) {
}); });
} }
(() => {
// Var is intentional
var hidden = null;
var visibilityChange = null;
if (typeof document.hidden !== "undefined") {
hidden = "hidden";
visibilityChange = "visibilitychange";
} else if (typeof document.msHidden !== "undefined") {
hidden = "msHidden";
visibilityChange = "msvisibilitychange";
} else if (typeof document.webkitHidden !== "undefined") {
hidden = "webkitHidden";
visibilityChange = "webkitvisibilitychange";
}
function handleVisibilityChange() {
// Check the user's session to see if they are currently logged-in
// when the page becomes visible
if ( ! document[hidden]) {
AnimeClient.get('/heartbeat', (beat) => {
const status = JSON.parse(beat);
// If the session is expired, immediately reload so that
// you can't attempt to do an action that requires authentication
if (status.hasAuth !== true) {
document.removeEventListener(visibilityChange, handleVisibilityChange, false);
location.reload();
}
});
}
}
if (hidden === null) {
console.info('Page visibility API not supported, JS session check will not work');
} else {
document.addEventListener(visibilityChange, handleVisibilityChange, false);
}
})();
// Click on hidden MAL checkbox so // Click on hidden MAL checkbox so
// that MAL id is passed // that MAL id is passed
AnimeClient.on('main', 'change', '.big-check', (e) => { AnimeClient.on('main', 'change', '.big-check', (e) => {
@ -469,20 +512,19 @@ AnimeClient.on('main', 'change', '.big-check', (e) => {
function renderAnimeSearchResults (data) { function renderAnimeSearchResults (data) {
const results = []; const results = [];
data.forEach(x => { data.forEach(item => {
const item = x.attributes;
const titles = item.titles.join('<br />'); const titles = item.titles.join('<br />');
results.push(` results.push(`
<article class="media search"> <article class="media search">
<div class="name"> <div class="name">
<input type="radio" class="mal-check" id="mal_${item.slug}" name="mal_id" value="${x.mal_id}" /> <input type="radio" class="mal-check" id="mal_${item.slug}" name="mal_id" value="${item.mal_id}" />
<input type="radio" class="big-check" id="${item.slug}" name="id" value="${x.id}" /> <input type="radio" class="big-check" id="${item.slug}" name="id" value="${item.id}" />
<label for="${item.slug}"> <label for="${item.slug}">
<picture width="220"> <picture width="220">
<source srcset="/public/images/anime/${x.id}.webp" type="image/webp" /> <source srcset="/public/images/anime/${item.id}.webp" type="image/webp" />
<source srcset="/public/images/anime/${x.id}.jpg" type="image/jpeg" /> <source srcset="/public/images/anime/${item.id}.jpg" type="image/jpeg" />
<img src="/public/images/anime/${x.id}.jpg" alt="" width="220" /> <img src="/public/images/anime/${item.id}.jpg" alt="" width="220" />
</picture> </picture>
<span class="name"> <span class="name">
${item.canonicalTitle}<br /> ${item.canonicalTitle}<br />
@ -507,20 +549,19 @@ function renderAnimeSearchResults (data) {
function renderMangaSearchResults (data) { function renderMangaSearchResults (data) {
const results = []; const results = [];
data.forEach(x => { data.forEach(item => {
const item = x.attributes;
const titles = item.titles.join('<br />'); const titles = item.titles.join('<br />');
results.push(` results.push(`
<article class="media search"> <article class="media search">
<div class="name"> <div class="name">
<input type="radio" id="mal_${item.slug}" name="mal_id" value="${x.mal_id}" /> <input type="radio" id="mal_${item.slug}" name="mal_id" value="${item.mal_id}" />
<input type="radio" class="big-check" id="${item.slug}" name="id" value="${x.id}" /> <input type="radio" class="big-check" id="${item.slug}" name="id" value="${item.id}" />
<label for="${item.slug}"> <label for="${item.slug}">
<picture width="220"> <picture width="220">
<source srcset="/public/images/manga/${x.id}.webp" type="image/webp" /> <source srcset="/public/images/manga/${item.id}.webp" type="image/webp" />
<source srcset="/public/images/manga/${x.id}.jpg" type="image/jpeg" /> <source srcset="/public/images/manga/${item.id}.jpg" type="image/jpeg" />
<img src="/public/images/manga/${x.id}.jpg" alt="" width="220" /> <img src="/public/images/manga/${item.id}.jpg" alt="" width="220" />
</picture> </picture>
<span class="name"> <span class="name">
${item.canonicalTitle}<br /> ${item.canonicalTitle}<br />
@ -547,25 +588,31 @@ const search = (query) => {
AnimeClient.show('.cssload-loader'); AnimeClient.show('.cssload-loader');
// Do the api search // Do the api search
AnimeClient.get(AnimeClient.url('/anime-collection/search'), { query }, (searchResults, status) => { return AnimeClient.get(AnimeClient.url('/anime-collection/search'), { query }, (searchResults, status) => {
searchResults = JSON.parse(searchResults); searchResults = JSON.parse(searchResults);
// Hide the loader // Hide the loader
AnimeClient.hide('.cssload-loader'); AnimeClient.hide('.cssload-loader');
// Show the results // Show the results
AnimeClient.$('#series-list')[ 0 ].innerHTML = renderAnimeSearchResults(searchResults.data); AnimeClient.$('#series-list')[ 0 ].innerHTML = renderAnimeSearchResults(searchResults);
}); });
}; };
if (AnimeClient.hasElement('.anime #search')) { if (AnimeClient.hasElement('.anime #search')) {
let prevRequest = null;
AnimeClient.on('#search', 'input', AnimeClient.throttle(250, (e) => { AnimeClient.on('#search', 'input', AnimeClient.throttle(250, (e) => {
const query = encodeURIComponent(e.target.value); const query = encodeURIComponent(e.target.value);
if (query === '') { if (query === '') {
return; return;
} }
search(query); if (prevRequest !== null) {
prevRequest.abort();
}
prevRequest = search(query);
})); }));
} }
@ -588,12 +635,12 @@ AnimeClient.on('body.anime.list', 'click', '.plus-one', (e) => {
// If the episode count is 0, and incremented, // If the episode count is 0, and incremented,
// change status to currently watching // change status to currently watching
if (isNaN(watchedCount) || watchedCount === 0) { if (isNaN(watchedCount) || watchedCount === 0) {
data.data.status = 'current'; data.data.status = 'CURRENT';
} }
// If you increment at the last episode, mark as completed // If you increment at the last episode, mark as completed
if ((!isNaN(watchedCount)) && (watchedCount + 1) === totalCount) { if ((!isNaN(watchedCount)) && (watchedCount + 1) === totalCount) {
data.data.status = 'completed'; data.data.status = 'COMPLETED';
} }
AnimeClient.show('#loading-shadow'); AnimeClient.show('#loading-shadow');
@ -613,7 +660,7 @@ AnimeClient.on('body.anime.list', 'click', '.plus-one', (e) => {
return; return;
} }
if (resData.data.attributes.status === 'completed') { if (resData.data.libraryEntry.update.libraryEntry.status === 'COMPLETED') {
AnimeClient.hide(parentSel); AnimeClient.hide(parentSel);
} }
@ -633,21 +680,27 @@ AnimeClient.on('body.anime.list', 'click', '.plus-one', (e) => {
const search$1 = (query) => { const search$1 = (query) => {
AnimeClient.show('.cssload-loader'); AnimeClient.show('.cssload-loader');
AnimeClient.get(AnimeClient.url('/manga/search'), { query }, (searchResults, status) => { return AnimeClient.get(AnimeClient.url('/manga/search'), { query }, (searchResults, status) => {
searchResults = JSON.parse(searchResults); searchResults = JSON.parse(searchResults);
AnimeClient.hide('.cssload-loader'); AnimeClient.hide('.cssload-loader');
AnimeClient.$('#series-list')[ 0 ].innerHTML = renderMangaSearchResults(searchResults.data); AnimeClient.$('#series-list')[ 0 ].innerHTML = renderMangaSearchResults(searchResults);
}); });
}; };
if (AnimeClient.hasElement('.manga #search')) { if (AnimeClient.hasElement('.manga #search')) {
let prevRequest = null;
AnimeClient.on('#search', 'input', AnimeClient.throttle(250, (e) => { AnimeClient.on('#search', 'input', AnimeClient.throttle(250, (e) => {
let query = encodeURIComponent(e.target.value); let query = encodeURIComponent(e.target.value);
if (query === '') { if (query === '') {
return; return;
} }
search$1(query); if (prevRequest !== null) {
prevRequest.abort();
}
prevRequest = search$1(query);
})); }));
} }
@ -678,12 +731,12 @@ AnimeClient.on('.manga.list', 'click', '.edit-buttons button', (e) => {
// If the episode count is 0, and incremented, // If the episode count is 0, and incremented,
// change status to currently reading // change status to currently reading
if (isNaN(completed) || completed === 0) { if (isNaN(completed) || completed === 0) {
data.data.status = 'current'; data.data.status = 'CURRENT';
} }
// If you increment at the last chapter, mark as completed // If you increment at the last chapter, mark as completed
if ((!isNaN(completed)) && (completed + 1) === total) { if ((!isNaN(completed)) && (completed + 1) === total) {
data.data.status = 'completed'; data.data.status = 'COMPLETED';
} }
// Update the total count // Update the total count
@ -697,7 +750,7 @@ AnimeClient.on('.manga.list', 'click', '.edit-buttons button', (e) => {
type: 'POST', type: 'POST',
mimeType: 'application/json', mimeType: 'application/json',
success: () => { success: () => {
if (data.data.status === 'completed') { if (String(data.data.status).toUpperCase() === 'COMPLETED') {
AnimeClient.hide(parentSel); AnimeClient.hide(parentSel);
} }

View File

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 13 13"><path fill="#7cac3f" d="M7 0a7 7 0 0 0-7 7 7 7 0 0 0 7 6 7 7 0 0 0 6-6 7 7 0 0 0-6-7zM4 4l3 1 3 2-3 1-3 1V7z"/></svg> <svg height="2500" viewBox="2.167 .438 251.038 259.969" width="2500" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path d="m221.503 210.324c-105.235 50.083-170.545 8.18-212.352-17.271-2.587-1.604-6.984.375-3.169 4.757 13.928 16.888 59.573 57.593 119.153 57.593 59.621 0 95.09-32.532 99.527-38.207 4.407-5.627 1.294-8.731-3.16-6.872zm29.555-16.322c-2.826-3.68-17.184-4.366-26.22-3.256-9.05 1.078-22.634 6.609-21.453 9.93.606 1.244 1.843.686 8.06.127 6.234-.622 23.698-2.826 27.337 1.931 3.656 4.79-5.57 27.608-7.255 31.288-1.628 3.68.622 4.629 3.68 2.178 3.016-2.45 8.476-8.795 12.14-17.774 3.639-9.028 5.858-21.622 3.71-24.424z" fill="#00A8E1" fill-rule="nonzero"/><path d="m150.744 108.13c0 13.141.332 24.1-6.31 35.77-5.361 9.489-13.853 15.324-23.341 15.324-12.952 0-20.495-9.868-20.495-24.432 0-28.75 25.76-33.968 50.146-33.968zm34.015 82.216c-2.23 1.992-5.456 2.135-7.97.806-11.196-9.298-13.189-13.615-19.356-22.487-18.502 18.882-31.596 24.527-55.601 24.527-28.37 0-50.478-17.506-50.478-52.565 0-27.373 14.85-46.018 35.96-55.126 18.313-8.066 43.884-9.489 63.43-11.718v-4.365c0-8.018.616-17.506-4.08-24.432-4.128-6.215-12.003-8.777-18.93-8.777-12.856 0-24.337 6.594-27.136 20.257-.57 3.037-2.799 6.026-5.835 6.168l-32.735-3.51c-2.751-.618-5.787-2.847-5.028-7.07 7.543-39.66 43.36-51.616 75.43-51.616 16.415 0 37.858 4.365 50.81 16.795 16.415 15.323 14.849 35.77 14.849 58.02v52.565c0 15.798 6.547 22.724 12.714 31.264 2.182 3.036 2.657 6.69-.095 8.966-6.879 5.74-19.119 16.415-25.855 22.393l-.095-.095" fill="#000"/><path d="m221.503 210.324c-105.235 50.083-170.545 8.18-212.352-17.271-2.587-1.604-6.984.375-3.169 4.757 13.928 16.888 59.573 57.593 119.153 57.593 59.621 0 95.09-32.532 99.527-38.207 4.407-5.627 1.294-8.731-3.16-6.872zm29.555-16.322c-2.826-3.68-17.184-4.366-26.22-3.256-9.05 1.078-22.634 6.609-21.453 9.93.606 1.244 1.843.686 8.06.127 6.234-.622 23.698-2.826 27.337 1.931 3.656 4.79-5.57 27.608-7.255 31.288-1.628 3.68.622 4.629 3.68 2.178 3.016-2.45 8.476-8.795 12.14-17.774 3.639-9.028 5.858-21.622 3.71-24.424z" fill="#00A8E1" fill-rule="nonzero"/><path d="m150.744 108.13c0 13.141.332 24.1-6.31 35.77-5.361 9.489-13.853 15.324-23.341 15.324-12.952 0-20.495-9.868-20.495-24.432 0-28.75 25.76-33.968 50.146-33.968zm34.015 82.216c-2.23 1.992-5.456 2.135-7.97.806-11.196-9.298-13.189-13.615-19.356-22.487-18.502 18.882-31.596 24.527-55.601 24.527-28.37 0-50.478-17.506-50.478-52.565 0-27.373 14.85-46.018 35.96-55.126 18.313-8.066 43.884-9.489 63.43-11.718v-4.365c0-8.018.616-17.506-4.08-24.432-4.128-6.215-12.003-8.777-18.93-8.777-12.856 0-24.337 6.594-27.136 20.257-.57 3.037-2.799 6.026-5.835 6.168l-32.735-3.51c-2.751-.618-5.787-2.847-5.028-7.07 7.543-39.66 43.36-51.616 75.43-51.616 16.415 0 37.858 4.365 50.81 16.795 16.415 15.323 14.849 35.77 14.849 58.02v52.565c0 15.798 6.547 22.724 12.714 31.264 2.182 3.036 2.657 6.69-.095 8.966-6.879 5.74-19.119 16.415-25.855 22.393l-.095-.095" fill="#000"/></g></svg>

Before

Width:  |  Height:  |  Size: 177 B

After

Width:  |  Height:  |  Size: 2.9 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.6 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path fill="#1b1a26" d="M26 21L16 27 6 21 6 12 26 12 26 21z" class="hide-inactive"/><g><path fill="#fd0" d="m2.63,8.19l0,15.62l13.37,7.81l13.37,-7.81l0,-15.62l-13.37,-7.81l-13.37,7.81zm22.11,12.72l-8.74,5.09l-8.73,-5.1l0,-6.46l8.73,5.1l8.73,-5.1l0.01,6.47z"/></g></svg>

After

Width:  |  Height:  |  Size: 329 B

View File

@ -6,7 +6,7 @@ undefined)return current.closest(parentSelector);while(current!==document.docume
element){listener.call(element,e);e.stopPropagation()}})})}AnimeClient.on=function(sel,event,target,listener){if(listener===undefined){listener=target;AnimeClient.$(sel).forEach(function(el){addEvent(el,event,listener)})}else AnimeClient.$(sel).forEach(function(el){delegateEvent(el,target,event,listener)})};function ajaxSerialize(data){var pairs=[];Object.keys(data).forEach(function(name){var value=data[name].toString();name=encodeURIComponent(name);value=encodeURIComponent(value);pairs.push(name+ element){listener.call(element,e);e.stopPropagation()}})})}AnimeClient.on=function(sel,event,target,listener){if(listener===undefined){listener=target;AnimeClient.$(sel).forEach(function(el){addEvent(el,event,listener)})}else AnimeClient.$(sel).forEach(function(el){delegateEvent(el,target,event,listener)})};function ajaxSerialize(data){var pairs=[];Object.keys(data).forEach(function(name){var value=data[name].toString();name=encodeURIComponent(name);value=encodeURIComponent(value);pairs.push(name+
"="+value)});return pairs.join("&")}AnimeClient.ajax=function(url,config){var defaultConfig={data:{},type:"GET",dataType:"",success:AnimeClient.noop,mimeType:"application/x-www-form-urlencoded",error:AnimeClient.noop};config=Object.assign({},defaultConfig,config);var request=new XMLHttpRequest;var method=String(config.type).toUpperCase();if(method==="GET")url+=url.match(/\?/)?ajaxSerialize(config.data):"?"+ajaxSerialize(config.data);request.open(method,url);request.onreadystatechange=function(){if(request.readyState=== "="+value)});return pairs.join("&")}AnimeClient.ajax=function(url,config){var defaultConfig={data:{},type:"GET",dataType:"",success:AnimeClient.noop,mimeType:"application/x-www-form-urlencoded",error:AnimeClient.noop};config=Object.assign({},defaultConfig,config);var request=new XMLHttpRequest;var method=String(config.type).toUpperCase();if(method==="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);if(method=== 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);if(method===
"GET")request.send(null);else request.send(config.data)};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",hide);AnimeClient.on("form.js-delete","submit",confirmDelete);AnimeClient.on(".js-clear-cache","click",clearAPICache);AnimeClient.on(".vertical-tabs input","change",scrollToSection);AnimeClient.on(".media-filter", "GET")request.send(null);else request.send(config.data);return request};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",hide);AnimeClient.on("form.js-delete","submit",confirmDelete);AnimeClient.on(".js-clear-cache","click",clearAPICache);AnimeClient.on(".vertical-tabs input","change",scrollToSection);AnimeClient.on(".media-filter",
"input",filterMedia);function hide(event){AnimeClient.hide(event.target)}function confirmDelete(event){var proceed=confirm("Are you ABSOLUTELY SURE you want to delete this item?");if(proceed===false){event.preventDefault();event.stopPropagation()}}function clearAPICache(){AnimeClient.get("/cache_purge",function(){AnimeClient.showMessage("success","Successfully purged api cache")})}function scrollToSection(event){var el=event.currentTarget.parentElement;var rect=el.getBoundingClientRect();var top= "input",filterMedia);function hide(event){AnimeClient.hide(event.target)}function confirmDelete(event){var proceed=confirm("Are you ABSOLUTELY SURE you want to delete this item?");if(proceed===false){event.preventDefault();event.stopPropagation()}}function clearAPICache(){AnimeClient.get("/cache_purge",function(){AnimeClient.showMessage("success","Successfully purged api cache")})}function scrollToSection(event){var el=event.currentTarget.parentElement;var rect=el.getBoundingClientRect();var top=
rect.top+window.pageYOffset;window.scrollTo({top:top,behavior:"smooth"})}function filterMedia(event){var rawFilter=event.target.value;var filter=new RegExp(rawFilter,"i");if(rawFilter!==""){AnimeClient.$("article.media").forEach(function(article){var titleLink=AnimeClient.$(".name a",article)[0];var title=String(titleLink.textContent).trim();if(!filter.test(title))AnimeClient.hide(article);else AnimeClient.show(article)});AnimeClient.$("table.media-wrap tbody tr").forEach(function(tr){var titleCell= rect.top+window.pageYOffset;window.scrollTo({top:top,behavior:"smooth"})}function filterMedia(event){var rawFilter=event.target.value;var filter=new RegExp(rawFilter,"i");if(rawFilter!==""){AnimeClient.$("article.media").forEach(function(article){var titleLink=AnimeClient.$(".name a",article)[0];var title=String(titleLink.textContent).trim();if(!filter.test(title))AnimeClient.hide(article);else AnimeClient.show(article)});AnimeClient.$("table.media-wrap tbody tr").forEach(function(tr){var titleCell=
AnimeClient.$("td.align-left",tr)[0];var titleLink=AnimeClient.$("a",titleCell)[0];var linkTitle=String(titleLink.textContent).trim();var textTitle=String(titleCell.textContent).trim();if(!(filter.test(linkTitle)||filter.test(textTitle)))AnimeClient.hide(tr);else AnimeClient.show(tr)})}else{AnimeClient.show("article.media");AnimeClient.show("table.media-wrap tbody tr")}}if("serviceWorker"in navigator)navigator.serviceWorker.register("/sw.js").then(function(reg){console.log("Service worker registered", AnimeClient.$("td.align-left",tr)[0];var titleLink=AnimeClient.$("a",titleCell)[0];var linkTitle=String(titleLink.textContent).trim();var textTitle=String(titleCell.textContent).trim();if(!(filter.test(linkTitle)||filter.test(textTitle)))AnimeClient.hide(tr);else AnimeClient.show(tr)})}else{AnimeClient.show("article.media");AnimeClient.show("table.media-wrap tbody tr")}}if("serviceWorker"in navigator)navigator.serviceWorker.register("/sw.js").then(function(reg){console.log("Service worker registered",

File diff suppressed because one or more lines are too long

View File

@ -6,21 +6,22 @@ undefined)return current.closest(parentSelector);while(current!==document.docume
element){listener.call(element,e);e.stopPropagation()}})})}AnimeClient.on=function(sel,event,target,listener){if(listener===undefined){listener=target;AnimeClient.$(sel).forEach(function(el){addEvent(el,event,listener)})}else AnimeClient.$(sel).forEach(function(el){delegateEvent(el,target,event,listener)})};function ajaxSerialize(data){var pairs=[];Object.keys(data).forEach(function(name){var value=data[name].toString();name=encodeURIComponent(name);value=encodeURIComponent(value);pairs.push(name+ element){listener.call(element,e);e.stopPropagation()}})})}AnimeClient.on=function(sel,event,target,listener){if(listener===undefined){listener=target;AnimeClient.$(sel).forEach(function(el){addEvent(el,event,listener)})}else AnimeClient.$(sel).forEach(function(el){delegateEvent(el,target,event,listener)})};function ajaxSerialize(data){var pairs=[];Object.keys(data).forEach(function(name){var value=data[name].toString();name=encodeURIComponent(name);value=encodeURIComponent(value);pairs.push(name+
"="+value)});return pairs.join("&")}AnimeClient.ajax=function(url,config){var defaultConfig={data:{},type:"GET",dataType:"",success:AnimeClient.noop,mimeType:"application/x-www-form-urlencoded",error:AnimeClient.noop};config=Object.assign({},defaultConfig,config);var request=new XMLHttpRequest;var method=String(config.type).toUpperCase();if(method==="GET")url+=url.match(/\?/)?ajaxSerialize(config.data):"?"+ajaxSerialize(config.data);request.open(method,url);request.onreadystatechange=function(){if(request.readyState=== "="+value)});return pairs.join("&")}AnimeClient.ajax=function(url,config){var defaultConfig={data:{},type:"GET",dataType:"",success:AnimeClient.noop,mimeType:"application/x-www-form-urlencoded",error:AnimeClient.noop};config=Object.assign({},defaultConfig,config);var request=new XMLHttpRequest;var method=String(config.type).toUpperCase();if(method==="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);if(method=== 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);if(method===
"GET")request.send(null);else request.send(config.data)};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",hide);AnimeClient.on("form.js-delete","submit",confirmDelete);AnimeClient.on(".js-clear-cache","click",clearAPICache);AnimeClient.on(".vertical-tabs input","change",scrollToSection);AnimeClient.on(".media-filter", "GET")request.send(null);else request.send(config.data);return request};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",hide);AnimeClient.on("form.js-delete","submit",confirmDelete);AnimeClient.on(".js-clear-cache","click",clearAPICache);AnimeClient.on(".vertical-tabs input","change",scrollToSection);AnimeClient.on(".media-filter",
"input",filterMedia);function hide(event){AnimeClient.hide(event.target)}function confirmDelete(event){var proceed=confirm("Are you ABSOLUTELY SURE you want to delete this item?");if(proceed===false){event.preventDefault();event.stopPropagation()}}function clearAPICache(){AnimeClient.get("/cache_purge",function(){AnimeClient.showMessage("success","Successfully purged api cache")})}function scrollToSection(event){var el=event.currentTarget.parentElement;var rect=el.getBoundingClientRect();var top= "input",filterMedia);function hide(event){AnimeClient.hide(event.target)}function confirmDelete(event){var proceed=confirm("Are you ABSOLUTELY SURE you want to delete this item?");if(proceed===false){event.preventDefault();event.stopPropagation()}}function clearAPICache(){AnimeClient.get("/cache_purge",function(){AnimeClient.showMessage("success","Successfully purged api cache")})}function scrollToSection(event){var el=event.currentTarget.parentElement;var rect=el.getBoundingClientRect();var top=
rect.top+window.pageYOffset;window.scrollTo({top:top,behavior:"smooth"})}function filterMedia(event){var rawFilter=event.target.value;var filter=new RegExp(rawFilter,"i");if(rawFilter!==""){AnimeClient.$("article.media").forEach(function(article){var titleLink=AnimeClient.$(".name a",article)[0];var title=String(titleLink.textContent).trim();if(!filter.test(title))AnimeClient.hide(article);else AnimeClient.show(article)});AnimeClient.$("table.media-wrap tbody tr").forEach(function(tr){var titleCell= rect.top+window.pageYOffset;window.scrollTo({top:top,behavior:"smooth"})}function filterMedia(event){var rawFilter=event.target.value;var filter=new RegExp(rawFilter,"i");if(rawFilter!==""){AnimeClient.$("article.media").forEach(function(article){var titleLink=AnimeClient.$(".name a",article)[0];var title=String(titleLink.textContent).trim();if(!filter.test(title))AnimeClient.hide(article);else AnimeClient.show(article)});AnimeClient.$("table.media-wrap tbody tr").forEach(function(tr){var titleCell=
AnimeClient.$("td.align-left",tr)[0];var titleLink=AnimeClient.$("a",titleCell)[0];var linkTitle=String(titleLink.textContent).trim();var textTitle=String(titleCell.textContent).trim();if(!(filter.test(linkTitle)||filter.test(textTitle)))AnimeClient.hide(tr);else AnimeClient.show(tr)})}else{AnimeClient.show("article.media");AnimeClient.show("table.media-wrap tbody tr")}}if("serviceWorker"in navigator)navigator.serviceWorker.register("/sw.js").then(function(reg){console.log("Service worker registered", AnimeClient.$("td.align-left",tr)[0];var titleLink=AnimeClient.$("a",titleCell)[0];var linkTitle=String(titleLink.textContent).trim();var textTitle=String(titleCell.textContent).trim();if(!(filter.test(linkTitle)||filter.test(textTitle)))AnimeClient.hide(tr);else AnimeClient.show(tr)})}else{AnimeClient.show("article.media");AnimeClient.show("table.media-wrap tbody tr")}}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.join("<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_'+ reg.scope)})["catch"](function(error){console.error("Failed to register service worker",error)});(function(){var hidden=null;var visibilityChange=null;if(typeof document.hidden!=="undefined"){hidden="hidden";visibilityChange="visibilitychange"}else if(typeof document.msHidden!=="undefined"){hidden="msHidden";visibilityChange="msvisibilitychange"}else if(typeof document.webkitHidden!=="undefined"){hidden="webkitHidden";visibilityChange="webkitvisibilitychange"}function handleVisibilityChange(){if(!document[hidden])AnimeClient.get("/heartbeat",
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<span class="name">\n\t\t\t\t\t\t\t'+ function(beat){var status=JSON.parse(beat);if(status.hasAuth!==true){document.removeEventListener(visibilityChange,handleVisibilityChange,false);location.reload()}})}if(hidden===null)console.info("Page visibility API not supported, JS session check will not work");else document.addEventListener(visibilityChange,handleVisibilityChange,false)})();AnimeClient.on("main","change",".big-check",function(e){var id=e.target.id;document.getElementById("mal_"+id).checked=true});function renderAnimeSearchResults(data){var results=
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; [];data.forEach(function(item){var titles=item.titles.join("<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="'+item.mal_id+'" />\n\t\t\t\t\t<input type="radio" class="big-check" id="'+item.slug+'" name="id" value="'+item.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/'+item.id+'.webp" type="image/webp" />\n\t\t\t\t\t\t\t<source srcset="/public/images/anime/'+
var titles=item.titles.join("<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/'+ item.id+'.jpg" type="image/jpeg" />\n\t\t\t\t\t\t\t<img src="/public/images/anime/'+item.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="/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')});
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("")}function renderMangaSearchResults(data){var results=[];data.forEach(function(item){var titles=item.titles.join("<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="'+item.mal_id+'" />\n\t\t\t\t\t<input type="radio" class="big-check" id="'+item.slug+'" name="id" value="'+item.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/'+
return results.join("")}var search=function(query){AnimeClient.show(".cssload-loader");AnimeClient.get(AnimeClient.url("/anime-collection/search"),{query:query},function(searchResults,status){searchResults=JSON.parse(searchResults);AnimeClient.hide(".cssload-loader");AnimeClient.$("#series-list")[0].innerHTML=renderAnimeSearchResults(searchResults.data)})};if(AnimeClient.hasElement(".anime #search"))AnimeClient.on("#search","input",AnimeClient.throttle(250,function(e){var query=encodeURIComponent(e.target.value); item.id+'.webp" type="image/webp" />\n\t\t\t\t\t\t\t<source srcset="/public/images/manga/'+item.id+'.jpg" type="image/jpeg" />\n\t\t\t\t\t\t\t<img src="/public/images/manga/'+item.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/'+
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}}; 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.show(".cssload-loader");return AnimeClient.get(AnimeClient.url("/anime-collection/search"),{query:query},function(searchResults,status){searchResults=JSON.parse(searchResults);AnimeClient.hide(".cssload-loader");AnimeClient.$("#series-list")[0].innerHTML=renderAnimeSearchResults(searchResults)})};if(AnimeClient.hasElement(".anime #search")){var prevRequest=
if(isNaN(watchedCount)||watchedCount===0)data.data.status="current";if(!isNaN(watchedCount)&&watchedCount+1===totalCount)data.data.status="completed";AnimeClient.show("#loading-shadow");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("#loading-shadow");AnimeClient.showMessage("error","Failed to update "+title+". ");AnimeClient.scrollToTop();return}if(resData.data.attributes.status=== null;AnimeClient.on("#search","input",AnimeClient.throttle(250,function(e){var query=encodeURIComponent(e.target.value);if(query==="")return;if(prevRequest!==null)prevRequest.abort();prevRequest=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,
"completed")AnimeClient.hide(parentSel);AnimeClient.hide("#loading-shadow");AnimeClient.showMessage("success","Successfully updated "+title);AnimeClient.$(".completed_number",parentSel)[0].textContent=++watchedCount;AnimeClient.scrollToTop()},error:function(){AnimeClient.hide("#loading-shadow");AnimeClient.showMessage("error","Failed to update "+title+". ");AnimeClient.scrollToTop()}})});var search$1=function(query){AnimeClient.show(".cssload-loader");AnimeClient.get(AnimeClient.url("/manga/search"), 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("#loading-shadow");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("#loading-shadow");
{query:query},function(searchResults,status){searchResults=JSON.parse(searchResults);AnimeClient.hide(".cssload-loader");AnimeClient.$("#series-list")[0].innerHTML=renderMangaSearchResults(searchResults.data)})};if(AnimeClient.hasElement(".manga #search"))AnimeClient.on("#search","input",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; AnimeClient.showMessage("error","Failed to update "+title+". ");AnimeClient.scrollToTop();return}if(resData.data.libraryEntry.update.libraryEntry.status==="COMPLETED")AnimeClient.hide(parentSel);AnimeClient.hide("#loading-shadow");AnimeClient.showMessage("success","Successfully updated "+title);AnimeClient.$(".completed_number",parentSel)[0].textContent=++watchedCount;AnimeClient.scrollToTop()},error:function(){AnimeClient.hide("#loading-shadow");AnimeClient.showMessage("error","Failed to update "+
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)|| title+". ");AnimeClient.scrollToTop()}})});var search$1=function(query){AnimeClient.show(".cssload-loader");return AnimeClient.get(AnimeClient.url("/manga/search"),{query:query},function(searchResults,status){searchResults=JSON.parse(searchResults);AnimeClient.hide(".cssload-loader");AnimeClient.$("#series-list")[0].innerHTML=renderMangaSearchResults(searchResults)})};if(AnimeClient.hasElement(".manga #search")){var prevRequest$1=null;AnimeClient.on("#search","input",AnimeClient.throttle(250,function(e){var query=
completed===0)data.data.status="current";if(!isNaN(completed)&&completed+1===total)data.data.status="completed";data.data.progress=++completed;AnimeClient.show("#loading-shadow");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("#loading-shadow");AnimeClient.$("."+type+"s_read",parentSel)[0].textContent=completed;AnimeClient.showMessage("success", encodeURIComponent(e.target.value);if(query==="")return;if(prevRequest$1!==null)prevRequest$1.abort();prevRequest$1=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",
"Successfully updated "+mangaName);AnimeClient.scrollToTop()},error:function(){AnimeClient.hide("#loading-shadow");AnimeClient.showMessage("error","Failed to update "+mangaName);AnimeClient.scrollToTop()}})})})() 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("#loading-shadow");AnimeClient.ajax(AnimeClient.url("/manga/increment"),{data:data,dataType:"json",
type:"POST",mimeType:"application/json",success:function(){if(String(data.data.status).toUpperCase()==="COMPLETED")AnimeClient.hide(parentSel);AnimeClient.hide("#loading-shadow");AnimeClient.$("."+type+"s_read",parentSel)[0].textContent=completed;AnimeClient.showMessage("success","Successfully updated "+mangaName);AnimeClient.scrollToTop()},error:function(){AnimeClient.hide("#loading-shadow");AnimeClient.showMessage("error","Failed to update "+mangaName);AnimeClient.scrollToTop()}})})})()
//# sourceMappingURL=scripts.min.js.map //# sourceMappingURL=scripts.min.js.map

File diff suppressed because one or more lines are too long

View File

@ -10,18 +10,13 @@
* @author Timothy J. Warren <tim@timshomepage.net> * @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2020 Timothy J. Warren * @copyright 2015 - 2020 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License * @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5 * @version 5.1
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/ */
namespace Aviat\AnimeClient\Enum; namespace Aviat\AnimeClient;
use Aviat\Ion\Enum as BaseEnum; class API {
/**
* Types of lists
*/
final class APISource extends BaseEnum {
public const KITSU = 'kitsu';
public const ANILIST = 'anilist'; public const ANILIST = 'anilist';
public const KITSU = 'kitsu';
} }

View File

@ -10,7 +10,7 @@
* @author Timothy J. Warren <tim@timshomepage.net> * @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2020 Timothy J. Warren * @copyright 2015 - 2020 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License * @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5 * @version 5.1
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/ */
@ -33,6 +33,12 @@ use Psr\Log\LoggerAwareTrait;
abstract class APIRequestBuilder { abstract class APIRequestBuilder {
use LoggerAwareTrait; use LoggerAwareTrait;
/**
* Where to look for GraphQL request files
* @var string
*/
protected string $filePath = __DIR__;
/** /**
* Url prefix for making url requests * Url prefix for making url requests
* @var string * @var string
@ -294,6 +300,74 @@ abstract class APIRequestBuilder {
return $this; return $this;
} }
/**
* Create a GraphQL query and return the Request object
*
* @param string $name
* @param array $variables
* @return Request
*/
public function queryRequest(string $name, array $variables = []): Request
{
$file = "{$this->filePath}/Queries/{$name}.graphql";
if ( ! file_exists($file))
{
throw new LogicException('GraphQL query file does not exist.');
}
$query = file_get_contents($file);
$body = [
'query' => $query
];
if ( ! empty($variables))
{
$body['variables'] = [];
foreach($variables as $key => $val)
{
$body['variables'][$key] = $val;
}
}
return $this->setUpRequest('POST', $this->baseUrl, [
'body' => $body,
]);
}
/**
* Create a GraphQL mutation request, and return the Request object
*
* @param string $name
* @param array $variables
* @return Request
* @throws Throwable
*/
public function mutateRequest (string $name, array $variables = []): Request
{
$file = "{$this->filePath}/Mutations/{$name}.graphql";
if ( ! file_exists($file))
{
throw new LogicException('GraphQL mutation file does not exist.');
}
$query = file_get_contents($file);
$body = [
'query' => $query
];
if (!empty($variables)) {
$body['variables'] = [];
foreach ($variables as $key => $val)
{
$body['variables'][$key] = $val;
}
}
return $this->setUpRequest('POST', $this->baseUrl, [
'body' => $body,
]);
}
/** /**
* Create the full request url * Create the full request url
* *

View File

@ -10,7 +10,7 @@
* @author Timothy J. Warren <tim@timshomepage.net> * @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2020 Timothy J. Warren * @copyright 2015 - 2020 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License * @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5 * @version 5.1
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/ */
@ -32,6 +32,14 @@ abstract class AbstractListItem {
*/ */
abstract public function create(array $data): Request; abstract public function create(array $data): Request;
/**
* Create a full list item for syncing
*
* @param array $data
* @return Request
*/
abstract public function createFull(array $data): Request;
/** /**
* Retrieve a list item * Retrieve a list item
* *

View File

@ -10,7 +10,7 @@
* @author Timothy J. Warren <tim@timshomepage.net> * @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2020 Timothy J. Warren * @copyright 2015 - 2020 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License * @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5 * @version 5.1
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/ */

View File

@ -0,0 +1,15 @@
{
"name": "Anilist Schema",
"schemaPath": "schema.graphql",
"extensions": {
"endpoints": {
"Anilist": {
"url": "https://graphql.anilist.co",
"headers": {
"user-agent": "JS GraphQL"
},
"introspect": true
}
}
}
}

View File

@ -1,47 +0,0 @@
<?php declare(strict_types=1);
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu to manage anime and manga watch lists
*
* PHP version 7.4
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2020 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\API\Anilist;
use const Aviat\AnimeClient\USER_AGENT;
use Aviat\AnimeClient\API\APIRequestBuilder;
final class AnilistRequestBuilder extends APIRequestBuilder {
/**
* The base url for api requests
* @var string $base_url
*/
protected string $baseUrl = 'https://graphql.anilist.co';
/**
* Valid HTTP request methods
* @var array
*/
protected array $validMethods = ['POST'];
/**
* HTTP headers to send with every request
*
* @var array
*/
protected array $defaultHeaders = [
'User-Agent' => USER_AGENT,
'Accept' => 'application/json',
'Content-Type' => 'application/json',
];
}

View File

@ -1,159 +0,0 @@
query ($id: Int) {
Media(type: ANIME, idMal:$id) {
id
idMal
isAdult
season
title {
romaji
english
native
userPreferred
}
description(asHtml: true)
duration
format
status
chapters
volumes
genres
synonyms
countryOfOrigin
source
startDate {
year
month
day
}
endDate {
year
month
day
}
trailer {
id
site
}
coverImage {
large
medium
}
bannerImage
tags {
id
name
description
category
isGeneralSpoiler
isMediaSpoiler
isAdult
}
characters {
edges {
role
voiceActors {
id
name {
first
last
native
}
language
image {
large
medium
}
description(asHtml: true)
siteUrl
}
node {
id
name {
first
last
native
}
image {
large
medium
}
description
siteUrl
}
}
pageInfo {
total
perPage
currentPage
lastPage
hasNextPage
}
}
staff {
edges {
role
node {
id
name {
first
last
native
}
language
image {
large
medium
}
description(asHtml: true)
siteUrl
}
}
pageInfo {
total
perPage
currentPage
lastPage
hasNextPage
}
}
studios {
edges {
isMain
node {
name
siteUrl
}
}
pageInfo {
total
perPage
currentPage
lastPage
hasNextPage
}
}
externalLinks {
id
url
site
}
mediaListEntry {
id
userId
status
score
progress
progressVolumes
repeat
private
notes
}
streamingEpisodes {
title
thumbnail
url
site
}
siteUrl
}
}

View File

@ -1,9 +0,0 @@
query ($id: Int, $type: MediaType) {
Media (idMal: $id, type: $type) {
mediaListEntry {
id
userId
mediaId
}
}
}

View File

@ -1,101 +0,0 @@
query ($id: Int){
Media(type: MANGA, id: $id) {
id
idMal
isAdult
season
title {
romaji
english
native
userPreferred
}
description(asHtml:true)
duration
format
status
chapters
volumes
genres
synonyms
countryOfOrigin
source
startDate {
year
month
day
}
endDate {
year
month
day
}
trailer {
id
site
}
coverImage {
large
medium
}
bannerImage
tags {
id
name
description
category
isGeneralSpoiler
isMediaSpoiler
isAdult
}
characters {
edges {
id
}
nodes {
id
name {
first
last
native
}
image {
large
medium
}
description
siteUrl
}
pageInfo {
total
perPage
currentPage
lastPage
hasNextPage
}
}
externalLinks {
id
url
site
}
mediaListEntry {
id
userId
status
score
progress
progressVolumes
repeat
private
notes
}
streamingEpisodes {
title
thumbnail
url
site
}
siteUrl
}
}

View File

@ -1,5 +0,0 @@
query ($id: Int) {
Media (type: ANIME, malId: $id) {
id
}
}

View File

@ -1,56 +0,0 @@
query ($name: String) {
MediaListCollection(userName: $name, type: ANIME) {
lists {
entries {
id
mediaId
score
progress
repeat
private
notes
status
media {
id
idMal
title {
romaji
english
native
userPreferred
}
type
format
status
episodes
season
genres
synonyms
countryOfOrigin
source
trailer {
id
}
coverImage {
large
medium
}
bannerImage
tags {
id
}
externalLinks {
id
}
mediaListEntry {
id
}
}
user {
id
}
}
}
}
}

View File

@ -1,56 +0,0 @@
query ($name: String) {
MediaListCollection(userName: $name, type: MANGA) {
lists {
entries {
id
mediaId
score
progress
progressVolumes
repeat
private
notes
status
media {
id
idMal
title {
romaji
english
native
userPreferred
}
type
format
status
chapters
volumes
genres
synonyms
countryOfOrigin
source
trailer {
id
}
coverImage {
large
medium
}
bannerImage
tags {
id
}
externalLinks {
id
}
mediaListEntry {
id
}
}
user {
id
}
}
}
}
}

View File

@ -10,7 +10,7 @@
* @author Timothy J. Warren <tim@timshomepage.net> * @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2020 Timothy J. Warren * @copyright 2015 - 2020 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License * @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5 * @version 5.1
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/ */
@ -27,7 +27,7 @@ use Aviat\AnimeClient\Types\FormItemData;
* CRUD operations for MAL list items * CRUD operations for MAL list items
*/ */
final class ListItem extends AbstractListItem { final class ListItem extends AbstractListItem {
use AnilistTrait; use RequestBuilderTrait;
/** /**
* Create a minimal list item * Create a minimal list item
@ -38,7 +38,7 @@ final class ListItem extends AbstractListItem {
public function create(array $data): Request public function create(array $data): Request
{ {
$checkedData = Types\MediaListEntry::check($data); $checkedData = Types\MediaListEntry::check($data);
return $this->mutateRequest('CreateMediaListEntry', $checkedData); return $this->requestBuilder->mutateRequest('CreateMediaListEntry', $checkedData);
} }
/** /**
@ -50,7 +50,7 @@ final class ListItem extends AbstractListItem {
public function createFull(array $data): Request public function createFull(array $data): Request
{ {
$checkedData = Types\MediaListEntry::check($data); $checkedData = Types\MediaListEntry::check($data);
return $this->mutateRequest('CreateFullMediaListEntry', $checkedData); return $this->requestBuilder->mutateRequest('CreateFullMediaListEntry', $checkedData);
} }
/** /**
@ -62,7 +62,7 @@ final class ListItem extends AbstractListItem {
*/ */
public function delete(string $id, string $type = 'anime'): Request public function delete(string $id, string $type = 'anime'): Request
{ {
return $this->mutateRequest('DeleteMediaListEntry', ['id' => $id]); return $this->requestBuilder->mutateRequest('DeleteMediaListEntry', ['id' => $id]);
} }
/** /**
@ -73,7 +73,7 @@ final class ListItem extends AbstractListItem {
*/ */
public function get(string $id): array public function get(string $id): array
{ {
return $this->runQuery('MediaListItem', ['id' => $id]); return $this->requestBuilder->runQuery('MediaListItem', ['id' => $id]);
} }
/** /**
@ -90,7 +90,7 @@ final class ListItem extends AbstractListItem {
'progress' => $data->progress, 'progress' => $data->progress,
]); ]);
return $this->mutateRequest('IncrementMediaListEntry', $checkedData); return $this->requestBuilder->mutateRequest('IncrementMediaListEntry', $checkedData);
} }
/** /**
@ -120,6 +120,6 @@ final class ListItem extends AbstractListItem {
'notes' => $notes, 'notes' => $notes,
]); ]);
return $this->mutateRequest('UpdateMediaListEntry', $updateData); return $this->requestBuilder->mutateRequest('UpdateMediaListEntry', $updateData);
} }
} }

View File

@ -10,7 +10,7 @@
* @author Timothy J. Warren <tim@timshomepage.net> * @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2020 Timothy J. Warren * @copyright 2015 - 2020 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License * @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5 * @version 5.1
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/ */

View File

@ -10,7 +10,7 @@
* @author Timothy J. Warren <tim@timshomepage.net> * @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2020 Timothy J. Warren * @copyright 2015 - 2020 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License * @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5 * @version 5.1
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/ */
@ -35,7 +35,7 @@ use Throwable;
*/ */
final class Model final class Model
{ {
use AnilistTrait; use RequestBuilderTrait;
/** /**
* @var ListItem * @var ListItem
*/ */
@ -77,7 +77,7 @@ final class Model
]) ])
->getFullRequest(); ->getFullRequest();
$response = $this->getResponseFromRequest($request); $response = $this->requestBuilder->getResponseFromRequest($request);
return Json::decode(wait($response->getBody()->buffer())); return Json::decode(wait($response->getBody()->buffer()));
} }
@ -89,7 +89,7 @@ final class Model
*/ */
public function checkAuth(): array public function checkAuth(): array
{ {
return $this->runQuery('CheckLogin'); return $this->requestBuilder->runQuery('CheckLogin');
} }
/** /**
@ -110,7 +110,7 @@ final class Model
throw new InvalidArgumentException('Anilist username is not defined in config'); throw new InvalidArgumentException('Anilist username is not defined in config');
} }
return $this->runQuery('SyncUserList', [ return $this->requestBuilder->runQuery('SyncUserList', [
'name' => $anilistUser, 'name' => $anilistUser,
'type' => $type, 'type' => $type,
]); ]);
@ -275,17 +275,25 @@ final class Model
* Get the Anilist list item id from the 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 * this way is more accurate than getting the list item id
* directly from the MAL id * directly from the MAL id
*
* @param string $mediaId
* @return string|null
*/ */
private function getListIdFromMediaId(string $mediaId): string private function getListIdFromMediaId(string $mediaId): ?string
{ {
$config = $this->container->get('config'); $config = $this->container->get('config');
$anilistUser = $config->get(['anilist', 'username']); $anilistUser = $config->get(['anilist', 'username']);
$info = $this->runQuery('ListItemIdByMediaId', [ $info = $this->requestBuilder->runQuery('ListItemIdByMediaId', [
'id' => $mediaId, 'id' => $mediaId,
'userName' => $anilistUser, 'userName' => $anilistUser,
]); ]);
if ( ! empty($info['errors']))
{
return NULL;
}
return (string)$info['data']['MediaList']['id']; return (string)$info['data']['MediaList']['id'];
} }
@ -303,7 +311,7 @@ final class Model
return NULL; return NULL;
} }
$info = $this->runQuery('MediaIdByMalId', [ $info = $this->requestBuilder->runQuery('MediaIdByMalId', [
'id' => $malId, 'id' => $malId,
'type' => mb_strtoupper($type), 'type' => mb_strtoupper($type),
]); ]);

View File

@ -10,42 +10,43 @@
* @author Timothy J. Warren <tim@timshomepage.net> * @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2020 Timothy J. Warren * @copyright 2015 - 2020 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License * @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5 * @version 5.1
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/ */
namespace Aviat\AnimeClient\API\Anilist; namespace Aviat\AnimeClient\API\Anilist;
use const Aviat\AnimeClient\USER_AGENT;
use function Amp\Promise\wait;
use function Aviat\AnimeClient\getResponse;
use Amp\Http\Client\Request; use Amp\Http\Client\Request;
use Amp\Http\Client\Response; use Amp\Http\Client\Response;
use Aviat\AnimeClient\API\Anilist; use Aviat\AnimeClient\API\Anilist;
use Aviat\Ion\Json;
use Aviat\Ion\Di\ContainerAware; use Aviat\Ion\Di\ContainerAware;
use Aviat\Ion\Di\ContainerInterface;
use Aviat\Ion\Json;
use Aviat\Ion\JsonException;
use function Amp\Promise\wait;
use function Aviat\AnimeClient\getResponse;
use const Aviat\AnimeClient\USER_AGENT;
use Aviat\AnimeClient\API\APIRequestBuilder;
use LogicException; use LogicException;
use Throwable;
trait AnilistTrait { final class RequestBuilder extends APIRequestBuilder {
use ContainerAware; use ContainerAware;
/**
* The request builder for the Anilist API
* @var AnilistRequestBuilder
*/
protected AnilistRequestBuilder $requestBuilder;
/** /**
* The base url for api requests * The base url for api requests
* @var string $base_url * @var string $base_url
*/ */
protected string $baseUrl = Anilist::BASE_URL; protected string $baseUrl = Anilist::BASE_URL;
/**
* Valid HTTP request methods
* @var array
*/
protected array $validMethods = ['POST'];
/** /**
* HTTP headers to send with every request * HTTP headers to send with every request
* *
@ -53,21 +54,14 @@ trait AnilistTrait {
*/ */
protected array $defaultHeaders = [ protected array $defaultHeaders = [
'Accept' => 'application/json', 'Accept' => 'application/json',
'Accept-Encoding' => 'gzip', // 'Accept-Encoding' => 'gzip',
'Content-type' => 'application/json', 'Content-type' => 'application/json',
'User-Agent' => USER_AGENT, 'User-Agent' => USER_AGENT,
]; ];
/** public function __construct(ContainerInterface $container)
* Set the request builder object
*
* @param AnilistRequestBuilder $requestBuilder
* @return self
*/
public function setRequestBuilder($requestBuilder): self
{ {
$this->requestBuilder = $requestBuilder; $this->setContainer($container);
return $this;
} }
/** /**
@ -82,7 +76,7 @@ trait AnilistTrait {
$config = $this->getContainer()->get('config'); $config = $this->getContainer()->get('config');
$anilistConfig = $config->get('anilist'); $anilistConfig = $config->get('anilist');
$request = $this->requestBuilder->newRequest('POST', $url); $request = $this->newRequest('POST', $url);
// You can only authenticate the request if you // You can only authenticate the request if you
// actually have an access_token saved // actually have an access_token saved
@ -123,13 +117,12 @@ trait AnilistTrait {
*/ */
public function runQuery(string $name, array $variables = []): array public function runQuery(string $name, array $variables = []): array
{ {
$file = realpath(__DIR__ . "/GraphQL/Queries/{$name}.graphql"); $file = realpath(__DIR__ . "/Queries/{$name}.graphql");
if ( ! file_exists($file)) if ( ! file_exists($file))
{ {
throw new LogicException('GraphQL query file does not exist.'); throw new LogicException('GraphQL query file does not exist.');
} }
// $query = str_replace(["\t", "\n"], ' ', file_get_contents($file));
$query = file_get_contents($file); $query = file_get_contents($file);
$body = [ $body = [
'query' => $query 'query' => $query
@ -157,13 +150,12 @@ trait AnilistTrait {
*/ */
public function mutateRequest (string $name, array $variables = []): Request public function mutateRequest (string $name, array $variables = []): Request
{ {
$file = realpath(__DIR__ . "/GraphQL/Mutations/{$name}.graphql"); $file = realpath(__DIR__ . "/Mutations/{$name}.graphql");
if (!file_exists($file)) if (!file_exists($file))
{ {
throw new LogicException('GraphQL mutation file does not exist.'); throw new LogicException('GraphQL mutation file does not exist.');
} }
// $query = str_replace(["\t", "\n"], ' ', file_get_contents($file));
$query = file_get_contents($file); $query = file_get_contents($file);
$body = [ $body = [
@ -207,11 +199,7 @@ trait AnilistTrait {
*/ */
private function getResponse(string $url, array $options = []): Response private function getResponse(string $url, array $options = []): Response
{ {
$logger = NULL; $logger = $this->container->getLogger('anilist-request');
if ($this->getContainer())
{
$logger = $this->container->getLogger('anilist-request');
}
$request = $this->setUpRequest($url, $options); $request = $this->setUpRequest($url, $options);
$response = getResponse($request); $response = getResponse($request);
@ -232,13 +220,9 @@ trait AnilistTrait {
* @return Response * @return Response
* @throws Throwable * @throws Throwable
*/ */
private function getResponseFromRequest(Request $request): Response public function getResponseFromRequest(Request $request): Response
{ {
$logger = NULL; $logger = $this->container->getLogger('anilist-request');
if ($this->getContainer())
{
$logger = $this->container->getLogger('anilist-request');
}
$response = getResponse($request); $response = getResponse($request);
@ -265,29 +249,30 @@ trait AnilistTrait {
$response = $this->getResponse(Anilist::BASE_URL, $options); $response = $this->getResponse(Anilist::BASE_URL, $options);
$validResponseCodes = [200, 201]; $validResponseCodes = [200, 201];
$logger = NULL; $logger = $this->container->getLogger('anilist-request');
if ($this->getContainer()) $logger->debug('Anilist response', [
{ 'status' => $response->getStatus(),
$logger = $this->container->getLogger('anilist-request'); 'reason' => $response->getReason(),
$logger->debug('Anilist response', [ 'body' => $response->getBody(),
'status' => $response->getStatus(), 'headers' => $response->getHeaders(),
'reason' => $response->getReason(), //'requestHeaders' => $request->getHeaders(),
'body' => $response->getBody(), ]);
'headers' => $response->getHeaders(),
//'requestHeaders' => $request->getHeaders(),
]);
}
if ( ! \in_array($response->getStatus(), $validResponseCodes, TRUE)) if ( ! \in_array($response->getStatus(), $validResponseCodes, TRUE))
{ {
if ($logger !== NULL) $logger->warning('Non 200 response for POST api call', (array)$response->getBody());
{
$logger->warning('Non 200 response for POST api call', (array)$response->getBody());
}
} }
// dump(wait($response->getBody()->buffer())); $rawBody = wait($response->getBody()->buffer());
try
return Json::decode(wait($response->getBody()->buffer())); {
return Json::decode($rawBody);
}
catch (JsonException $e)
{
dump($e);
dump($rawBody);
die();
}
} }
} }

View File

@ -0,0 +1,41 @@
<?php declare(strict_types=1);
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu to manage anime and manga watch lists
*
* PHP version 7.4
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2020 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.1
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\API\Anilist;
use Aviat\Ion\Di\ContainerAware;
trait RequestBuilderTrait {
use ContainerAware;
/**
* The request builder for the Anilist API
* @var RequestBuilder
*/
protected RequestBuilder $requestBuilder;
/**
* Set the request builder object
*
* @param RequestBuilder $requestBuilder
* @return self
*/
public function setRequestBuilder($requestBuilder): self
{
$this->requestBuilder = $requestBuilder;
return $this;
}
}

View File

@ -10,7 +10,7 @@
* @author Timothy J. Warren <tim@timshomepage.net> * @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2020 Timothy J. Warren * @copyright 2015 - 2020 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License * @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5 * @version 5.1
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/ */

View File

@ -10,7 +10,7 @@
* @author Timothy J. Warren <tim@timshomepage.net> * @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2020 Timothy J. Warren * @copyright 2015 - 2020 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License * @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5 * @version 5.1
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/ */

View File

@ -10,7 +10,7 @@
* @author Timothy J. Warren <tim@timshomepage.net> * @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2020 Timothy J. Warren * @copyright 2015 - 2020 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License * @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5 * @version 5.1
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/ */
@ -20,37 +20,37 @@ use Aviat\AnimeClient\Types\AbstractType;
class MediaListEntry extends AbstractType { class MediaListEntry extends AbstractType {
/** /**
* @var int * @var int|string
*/ */
public $id; public $id;
/** /**
* @var string * @var string|null
*/ */
public $notes; public ?string $notes;
/** /**
* @var bool * @var bool
*/ */
public $private; public ?bool $private;
/** /**
* @var int * @var int
*/ */
public $progress; public int $progress;
/** /**
* @var int * @var int
*/ */
public $repeat; public ?int $repeat;
/** /**
* @var string * @var string
*/ */
public $status; public string $status;
/** /**
* @var int * @var int
*/ */
public $score; public ?int $score;
} }

File diff suppressed because it is too large Load Diff

View File

@ -10,7 +10,7 @@
* @author Timothy J. Warren <tim@timshomepage.net> * @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2020 Timothy J. Warren * @copyright 2015 - 2020 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License * @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5 * @version 5.1
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/ */
@ -57,7 +57,7 @@ trait CacheTrait {
* *
* @param string $key * @param string $key
* @param callable $primer * @param callable $primer
* @param array $primeArgs * @param array|null $primeArgs
* @return mixed|null * @return mixed|null
* @throws InvalidArgumentException * @throws InvalidArgumentException
*/ */
@ -78,22 +78,4 @@ trait CacheTrait {
return $value; return $value;
} }
/**
* Generate a hash as a cache key from the current method call
*
* @param mixed $object
* @param string $method
* @param array $args
* @return string
*/
public function getHashForMethodCall($object, string $method, array $args = []): string
{
$keyObj = [
'class' => get_class($object),
'method' => $method,
'args' => $args,
];
return sha1(json_encode($keyObj));
}
} }

View File

@ -10,7 +10,7 @@
* @author Timothy J. Warren <tim@timshomepage.net> * @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2020 Timothy J. Warren * @copyright 2015 - 2020 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License * @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5 * @version 5.1
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/ */

View File

@ -10,7 +10,7 @@
* @author Timothy J. Warren <tim@timshomepage.net> * @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2020 Timothy J. Warren * @copyright 2015 - 2020 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License * @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5 * @version 5.1
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/ */

View File

@ -10,7 +10,7 @@
* @author Timothy J. Warren <tim@timshomepage.net> * @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2020 Timothy J. Warren * @copyright 2015 - 2020 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License * @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5 * @version 5.1
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/ */

View File

@ -10,7 +10,7 @@
* @author Timothy J. Warren <tim@timshomepage.net> * @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2020 Timothy J. Warren * @copyright 2015 - 2020 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License * @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5 * @version 5.1
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/ */

View File

@ -10,7 +10,7 @@
* @author Timothy J. Warren <tim@timshomepage.net> * @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2020 Timothy J. Warren * @copyright 2015 - 2020 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License * @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5 * @version 5.1
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/ */

View File

@ -10,7 +10,7 @@
* @author Timothy J. Warren <tim@timshomepage.net> * @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2020 Timothy J. Warren * @copyright 2015 - 2020 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License * @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5 * @version 5.1
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/ */

View File

@ -10,7 +10,7 @@
* @author Timothy J. Warren <tim@timshomepage.net> * @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2020 Timothy J. Warren * @copyright 2015 - 2020 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License * @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5 * @version 5.1
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/ */

View File

@ -10,7 +10,7 @@
* @author Timothy J. Warren <tim@timshomepage.net> * @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2020 Timothy J. Warren * @copyright 2015 - 2020 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License * @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5 * @version 5.1
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/ */

View File

@ -10,7 +10,7 @@
* @author Timothy J. Warren <tim@timshomepage.net> * @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2020 Timothy J. Warren * @copyright 2015 - 2020 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License * @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5 * @version 5.1
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/ */

View File

@ -1,354 +0,0 @@
<?php declare(strict_types=1);
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu to manage anime and manga watch lists
*
* PHP version 7.4
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2020 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\API;
use function in_array;
/**
* Class encapsulating Json API data structure for a request or response
*/
final class JsonAPI {
/*
* Basic structure is generally like so:
* [
* 'id' => '12016665',
* 'type' => 'libraryEntries',
* 'links' => [
* 'self' => 'https://kitsu.io/api/edge/library-entries/13016665'
* ],
* 'attributes' => [
*
* ]
* ]
*/
/**
* Inline all included data
*
* @param array $data - The raw JsonAPI response data
* @return array
*/
public static function organizeData(array $data): array
{
// relationships that have singular data
$singular = [
'waifu'
];
// Reorganize included data
$included = array_key_exists('included', $data)
? static::organizeIncluded($data['included'])
: [];
// Inline organized data
foreach($data['data'] as $i => &$item)
{
if ( ! is_array($item))
{
continue;
}
if (array_key_exists('relationships', $item))
{
foreach($item['relationships'] as $relType => $props)
{
if (array_keys($props) === ['links'])
{
unset($item['relationships'][$relType]);
if (empty($item['relationships']))
{
unset($item['relationships']);
}
continue;
}
if (array_key_exists('links', $props))
{
unset($item['relationships'][$relType]['links']);
}
if (array_key_exists('data', $props))
{
if (empty($props['data']))
{
unset($item['relationships'][$relType]['data']);
if (empty($item['relationships'][$relType]))
{
unset($item['relationships'][$relType]);
}
continue;
}
// Single data item
if (array_key_exists('id', $props['data']))
{
$idKey = $props['data']['id'];
$dataType = $props['data']['type'];
$relationship =& $item['relationships'][$relType];
unset($relationship['data']);
if (in_array($relType, $singular, TRUE))
{
$relationship = $included[$dataType][$idKey];
continue;
}
if ($relType === $dataType)
{
$relationship[$idKey] = $included[$dataType][$idKey];
continue;
}
$relationship[$dataType][$idKey] = $included[$dataType][$idKey];
}
// Multiple data items
else
{
foreach($props['data'] as $j => $datum)
{
$idKey = $props['data'][$j]['id'];
$dataType = $props['data'][$j]['type'];
$relationship =& $item['relationships'][$relType];
if ($relType === $dataType)
{
$relationship[$idKey] = $included[$dataType][$idKey];
continue;
}
$relationship[$dataType][$idKey][$j] = $included[$dataType][$idKey];
}
unset($item['relationships'][$relType]['data']);
}
}
}
}
}
unset($item);
$data['data']['included'] = $included;
return $data['data'];
}
/**
* Restructure included data to make it simpler to inline
*
* @param array $included
* @return array
*/
public static function organizeIncluded(array $included): array
{
$organized = [];
// First pass, create [ type => items[] ] structure
foreach($included as &$item)
{
$type = $item['type'];
$id = $item['id'];
$organized[$type] = $organized[$type] ?? [];
$newItem = [];
foreach(['attributes', 'relationships'] as $key)
{
if (array_key_exists($key, $item))
{
// Remove 'links' type relationships
if ($key === 'relationships')
{
foreach($item['relationships'] as $relType => $props)
{
if (array_keys($props) === ['links'])
{
unset($item['relationships'][$relType]);
if (empty($item['relationships']))
{
continue 2;
}
}
}
}
$newItem[$key] = $item[$key];
}
}
$organized[$type][$id] = $newItem;
}
unset($item);
// Second pass, go through and fill missing relationships in the first pass
foreach($organized as $type => $items)
{
foreach($items as $id => $item)
{
if (array_key_exists('relationships', $item) && is_array($item['relationships']))
{
foreach($item['relationships'] as $relType => $props)
{
if (array_key_exists('data', $props) && is_array($props['data']) && array_key_exists('id', $props['data']))
{
$idKey = $props['data']['id'];
$dataType = $props['data']['type'];
$relationship =& $organized[$type][$id]['relationships'][$relType];
unset($relationship['links'], $relationship['data']);
if ($relType === $dataType)
{
$relationship[$idKey] = $included[$dataType][$idKey];
continue;
}
if ( ! array_key_exists($dataType, $organized))
{
$organized[$dataType] = [];
}
if (array_key_exists($idKey, $organized[$dataType]))
{
$relationship[$dataType][$idKey] = $organized[$dataType][$idKey];
}
}
}
}
}
}
return $organized;
}
/**
* Take organized includes and inline them, where applicable
*
* @param array $included
* @param string $key The key of the include to inline the other included values into
* @return array
*/
public static function inlineIncludedRelationships(array $included, string $key): array
{
$inlined = [
$key => []
];
foreach ($included[$key] as $itemId => $item)
{
// Duplicate the item for the output
$inlined[$key][$itemId] = $item;
foreach($item['relationships'] as $type => $ids)
{
$inlined[$key][$itemId]['relationships'][$type] = [];
if ( ! array_key_exists($type, $included)) continue;
if (array_key_exists('data', $ids ))
{
$ids = array_column($ids['data'], 'id');
}
foreach($ids as $id)
{
$inlined[$key][$itemId]['relationships'][$type][$id] = $included[$type][$id];
}
}
}
return $inlined;
}
/**
* Reorganizes 'included' data to be keyed by
* type => [
* id => data/attributes,
* ]
*
* @param array $includes
* @return array
*/
public static function organizeIncludes(array $includes): array
{
$organized = [];
$types = array_unique(array_column($includes, 'type'));
sort($types);
foreach ($types as $type)
{
$organized[$type] = [];
}
foreach ($includes as $item)
{
$type = $item['type'];
$id = $item['id'];
if (array_key_exists('attributes', $item))
{
$organized[$type][$id] = $item['attributes'];
}
if (array_key_exists('relationships', $item))
{
$organized[$type][$id]['relationships'] = static::organizeRelationships($item['relationships']);
}
}
return $organized;
}
/**
* Reorganize relationship mappings to make them simpler to use
*
* Remove verbose structure, and just map:
* type => [ idArray ]
*
* @param array $relationships
* @return array
*/
public static function organizeRelationships(array $relationships): array
{
$organized = $relationships;
foreach($relationships as $key => $data)
{
$organized[$key] = $organized[$key] ?? [];
if ( ! array_key_exists('data', $data))
{
continue;
}
foreach ($data['data'] as $item)
{
if (is_array($item) && array_key_exists('id', $item))
{
$organized[$key][] = $item['id'];
}
}
}
return $organized;
}
}

View File

@ -1,270 +0,0 @@
<?php declare(strict_types=1);
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu to manage anime and manga watch lists
*
* PHP version 7.4
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2020 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\API;
use Aviat\AnimeClient\API\Kitsu\Enum\AnimeAiringStatus;
use DateTimeImmutable;
/**
* Data massaging helpers for the Kitsu API
*/
final class Kitsu {
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';
public const ANIME_HISTORY_LIST_CACHE_KEY = 'kitsu-anime-history-list';
public const MANGA_HISTORY_LIST_CACHE_KEY = 'kitsu-manga-history-list';
/**
* Determine whether an anime is airing, finished airing, or has not yet aired
*
* @param string $startDate
* @param string $endDate
* @return string
*/
public static function getAiringStatus(string $startDate = NULL, string $endDate = NULL): string
{
$startAirDate = new DateTimeImmutable($startDate ?? 'tomorrow');
$endAirDate = new DateTimeImmutable($endDate ?? 'next year');
$now = new DateTimeImmutable();
$isDoneAiring = $now > $endAirDate;
$isCurrentlyAiring = ($now > $startAirDate) && ! $isDoneAiring;
if ($isCurrentlyAiring)
{
return AnimeAiringStatus::AIRING;
}
if ($isDoneAiring)
{
return AnimeAiringStatus::FINISHED_AIRING;
}
return AnimeAiringStatus::NOT_YET_AIRED;
}
/**
* Reorganize streaming links
*
* @param array $included
* @return array
*/
public static function parseStreamingLinks(array $included): array
{
if (
( ! array_key_exists('streamingLinks', $included)) ||
count($included['streamingLinks']) === 0
)
{
return [];
}
$links = [];
foreach ($included['streamingLinks'] as $streamingLink)
{
$url = $streamingLink['url'];
// 'Fix' links that start with the hostname,
// rather than a protocol
if (strpos($url, '//') === FALSE)
{
$url = '//' . $url;
}
$host = parse_url($url, \PHP_URL_HOST);
$links[] = [
'meta' => static::getServiceMetaData($host),
'link' => $streamingLink['url'],
'subs' => $streamingLink['subs'],
'dubs' => $streamingLink['dubs']
];
}
usort($links, fn ($a, $b) => $a['meta']['name'] <=> $b['meta']['name']);
return $links;
}
/**
* Reorganize streaming links for the current list item
*
* @param array $included
* @param string $animeId
* @return array
*/
public static function parseListItemStreamingLinks(array $included, string $animeId): array
{
// Anime lists have a different structure to search through
if (array_key_exists('anime', $included) && ! array_key_exists('streamingLinks', $included))
{
$links = [];
$anime = $included['anime'][$animeId];
if (count($anime['relationships']['streamingLinks']) > 0)
{
return static::parseStreamingLinks($anime['relationships']);
}
return $links;
}
return [];
}
/**
* Get the list of titles
*
* @param array $data
* @return array
*/
public static function getTitles(array $data): array
{
$raw = array_unique([
$data['canonicalTitle'],
...array_values($data['titles']),
...array_values($data['abbreviatedTitles'] ?? []),
]);
return array_diff($raw,[$data['canonicalTitle']]);
}
/**
* Filter out duplicate and very similar names from
*
* @param array $data The 'attributes' section of the api data response
* @return array List of alternate titles
*/
public static function filterTitles(array $data): array
{
// The 'canonical' title is always returned
$valid = [$data['canonicalTitle']];
if (array_key_exists('titles', $data) && is_array($data['titles']))
{
foreach($data['titles'] as $alternateTitle)
{
if (self::titleIsUnique($alternateTitle, $valid))
{
$valid[] = $alternateTitle;
}
}
}
return $valid;
}
/**
* Get the name and logo for the streaming service of the current link
*
* @param string $hostname
* @return array
*/
protected static function getServiceMetaData(string $hostname = NULL): array
{
$hostname = str_replace('www.', '', $hostname);
$serviceMap = [
'amazon.com' => [
'name' => 'Amazon Prime',
'link' => TRUE,
'image' => 'streaming-logos/amazon.svg',
],
'crunchyroll.com' => [
'name' => 'Crunchyroll',
'link' => TRUE,
'image' => 'streaming-logos/crunchyroll.svg',
],
'daisuki.net' => [
'name' => 'Daisuki',
'link' => TRUE,
'image' => 'streaming-logos/daisuki.svg'
],
'funimation.com' => [
'name' => 'Funimation',
'link' => TRUE,
'image' => 'streaming-logos/funimation.svg',
],
'hidive.com' => [
'name' => 'Hidive',
'link' => TRUE,
'image' => 'streaming-logos/hidive.svg',
],
'hulu.com' => [
'name' => 'Hulu',
'link' => TRUE,
'image' => 'streaming-logos/hulu.svg',
],
'tubitv.com' => [
'name' => 'TubiTV',
'link' => TRUE,
'image' => 'streaming-logos/tubitv.svg',
],
'viewster.com' => [
'name' => 'Viewster',
'link' => TRUE,
'image' => 'streaming-logos/viewster.svg'
],
];
if (array_key_exists($hostname, $serviceMap))
{
return $serviceMap[$hostname];
}
// Default to Netflix, because the API links are broken,
// and there's no other real identifier for Netflix
return [
'name' => 'Netflix',
'link' => FALSE,
'image' => 'streaming-logos/netflix.svg',
];
}
/**
* Determine if an alternate title is unique enough to list
*
* @param string $title
* @param array $existingTitles
* @return bool
*/
private static function titleIsUnique(string $title = NULL, array $existingTitles = []): bool
{
if (empty($title))
{
return FALSE;
}
foreach($existingTitles as $existing)
{
$isSubset = mb_substr_count($existing, $title) > 0;
$diff = levenshtein(mb_strtolower($existing), mb_strtolower($title));
if ($diff <= 4 || $isSubset || mb_strlen($title) > 45 || mb_strlen($existing) > 50)
{
return FALSE;
}
}
return TRUE;
}
}

View File

@ -0,0 +1,15 @@
{
"name": "Kitsu Schema",
"schemaPath": "schema.graphql",
"extensions": {
"endpoints": {
"Kitsu": {
"url": "https://kitsu.io/api/graphql",
"headers": {
"user-agent": "JS GraphQL"
},
"introspect": true
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More