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
## 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
* Updated PHP requirement to 7.4
* Added anime watching history view

View File

@ -227,7 +227,7 @@ class RoboFile extends Tasks {
{
$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
// -------------------------------------------------------------------------
$routes = [
// ---------------------------------------------------------------------
// AJAX Routes
// ---------------------------------------------------------------------
'cache_purge' => [
'path' => '/cache_purge',
'action' => 'clearCache',
],
'heartbeat' => [
'path' => '/heartbeat',
'action' => 'heartbeat',
],
// ---------------------------------------------------------------------
// Anime List Routes
// ---------------------------------------------------------------------
@ -175,9 +186,9 @@ $routes = [
]
],
'person' => [
'path' => '/people/{id}',
'path' => '/people/{slug}',
'tokens' => [
'id' => SLUG_PATTERN
'slug' => SLUG_PATTERN,
]
],
'default_user_info' => [
@ -215,10 +226,6 @@ $routes = [
'file' => '[a-z0-9\-]+\.[a-z]{3,4}'
]
],
'cache_purge' => [
'path' => '/cache_purge',
'action' => 'clearCache',
],
'settings' => [
'path' => '/settings',
],

View File

@ -10,7 +10,7 @@
* @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
* @version 5.1
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
@ -19,20 +19,24 @@ namespace Aviat\AnimeClient;
use Aura\Html\HelperLocatorFactory;
use Aura\Router\RouterContainer;
use Aura\Session\SessionFactory;
use Aviat\AnimeClient\API\{
Anilist,
Kitsu,
Kitsu\KitsuRequestBuilder
};
use Aviat\AnimeClient\API\{Anilist, Kitsu};
use Aviat\AnimeClient\Component;
use Aviat\AnimeClient\Model;
use Aviat\Banker\Teller;
use Aviat\Ion\Config;
use Aviat\Ion\Di\Container;
use Aviat\Ion\Di\ContainerInterface;
use Psr\SimpleCache\CacheInterface;
use Laminas\Diactoros\{Response, ServerRequestFactory};
use Laminas\Diactoros\ServerRequestFactory;
use Monolog\Formatter\JsonFormatter;
use Monolog\Handler\RotatingFileHandler;
use Monolog\Logger;
use Psr\SimpleCache\CacheInterface;
if ( ! defined('APP_DIR'))
{
define('APP_DIR', __DIR__);
define('TEMPLATE_DIR', APP_DIR . '/templates');
}
// -----------------------------------------------------------------------------
// Setup DI container
@ -45,17 +49,18 @@ return static function (array $configArray = []): Container {
// -------------------------------------------------------------------------
$appLogger = new Logger('animeclient');
$appLogger->pushHandler(new RotatingFileHandler(__DIR__ . '/logs/app.log', Logger::NOTICE));
$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));
$appLogger->pushHandler(new RotatingFileHandler(__DIR__ . '/logs/app.log', 2, Logger::WARNING));
$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
@ -74,29 +79,52 @@ return static function (array $configArray = []): Container {
// Create Aura Router Object
$container->set('aura-router', fn() => new RouterContainer);
// Create Html helper Object
// Create Html helpers
$container->set('html-helper', static function(ContainerInterface $container) {
$htmlHelper = (new HelperLocatorFactory)->newInstance();
$htmlHelper->set('menu', static function() use ($container) {
$menuHelper = new Helper\Menu();
$menuHelper->setContainer($container);
return $menuHelper;
});
$htmlHelper->set('field', static function() use ($container) {
$formHelper = new Helper\Form();
$formHelper->setContainer($container);
return $formHelper;
});
$htmlHelper->set('picture', static function() use ($container) {
$pictureHelper = new Helper\Picture();
$pictureHelper->setContainer($container);
return $pictureHelper;
$helpers = [
'menu' => Helper\Menu::class,
'field' => Helper\Form::class,
'picture' => Helper\Picture::class,
];
foreach ($helpers as $name => $class)
{
$htmlHelper->set($name, static function() use ($class, $container) {
$helper = new $class;
$helper->setContainer($container);
return $helper;
});
}
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(
$_SERVER,
$_GET,
@ -104,7 +132,6 @@ return static function (array $configArray = []): Container {
$_COOKIE,
$_FILES
));
$container->set('response', fn () => new Response);
// Create session Object
$container->set('session', fn () => (new SessionFactory())->newInstance($_COOKIE));
@ -114,7 +141,7 @@ return static function (array $configArray = []): Container {
// Models
$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'));
$listItem = new Kitsu\ListItem();
@ -130,7 +157,7 @@ return static function (array $configArray = []): Container {
return $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'));
$listItem = new Anilist\ListItem();

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">
<?php foreach($items as $item): ?>
<?php if ($item['private'] && ! $auth->isAuthenticated()) continue; ?>
<?php include __DIR__ . '/cover-item.php' ?>
<?= $component->animeCover($item) ?>
<?php endforeach ?>
</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">
<section class="flex">
<section class="flex" unselectable>
<aside class="info">
<?= $helper->picture("images/anime/{$data['id']}-original.webp") ?>
@ -11,20 +16,33 @@
<td class="align-right">Airing Status</td>
<td><?= $data['status'] ?></td>
</tr>
<tr>
<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>
<?php if ($data['episode_count'] !== 1): ?>
<tr>
<td>Episode Count</td>
<td><?= $data['episode_count'] ?? '-' ?></td>
</tr>
<?php if ( ! empty($data['episode_length'])): ?>
<?php endif ?>
<?php if (( ! empty($data['episode_length'])) && $data['episode_count'] !== 1): ?>
<tr>
<td>Episode Length</td>
<td><?= $data['episode_length'] ?> minutes</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'])): ?>
<tr>
<td>Age Rating</td>
@ -32,6 +50,18 @@
</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>
<td>Genres</td>
<td>
@ -40,11 +70,13 @@
</tr>
</table>
<br />
</aside>
<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): ?>
<h3><?= $title ?></h3>
<?php endforeach ?>
@ -103,10 +135,12 @@
<iframe
width="560"
height="315"
role='img'
src="https://www.youtube.com/embed/<?= $data['trailer_id'] ?>"
frameborder="0"
allow="autoplay; encrypted-media"
allowfullscreen
tabindex='0'
title="<?= $data['title'] ?> trailer video"
></iframe>
</div>
<?php endif ?>
@ -117,31 +151,24 @@
<section>
<h2>Characters</h2>
<div class="tabs">
<?php $i = 0 ?>
<?php foreach ($data['characters'] as $role => $list): ?>
<input
type="radio" name="character-types"
id="character-types-<?= $i ?>" <?= ($i === 0) ? 'checked' : '' ?> />
<label for="character-types-<?= $i ?>"><?= ucfirst($role) ?></label>
<section class="content media-wrap flex flex-wrap flex-justify-start">
<?php foreach ($list as $id => $char): ?>
<?php if ( ! empty($char['image']['original'])): ?>
<article class="<?= $role === 'supporting' ? 'small-' : '' ?>character">
<?php $link = $url->generate('character', ['slug' => $char['slug']]) ?>
<div class="name">
<?= $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>
<?= $component->tabs('character-types', $data['characters'], static function ($characterList, $role)
use ($component, $url, $helper) {
$rendered = [];
foreach ($characterList as $id => $character):
if (empty($character['image']['original']))
{
continue;
}
$rendered[] = $component->character(
$character['name'],
$url->generate('character', ['slug' => $character['slug']]),
$helper->picture("images/characters/{$id}.webp"),
(strtolower($role) !== 'main') ? 'small-character' : 'character'
);
endforeach;
return implode('', array_map('mb_trim', $rendered));
}) ?>
</section>
<?php endif ?>
@ -149,31 +176,24 @@
<section>
<h2>Staff</h2>
<div class="vertical-tabs">
<?php $i = 0; ?>
<?php foreach ($data['staff'] as $role => $people): ?>
<div class="tab">
<input type="radio" name="staff-roles" id="staff-role<?= $i ?>" <?= $i === 0 ? 'checked' : '' ?> />
<label for="staff-role<?= $i ?>"><?= $role ?></label>
<section class='content media-wrap flex flex-wrap flex-justify-start'>
<?php foreach ($people as $pid => $person): ?>
<article class='character small-person'>
<?php $link = $url->generate('person', ['id' => $person['id']]) ?>
<div class="name">
<a href="<?= $link ?>">
<?= $person['name'] ?>
</a>
</div>
<a href="<?= $link ?>">
<?= $helper->picture(getLocalImg($person['image']['original'] ?? NULL)) ?>
</a>
</article>
<?php endforeach ?>
</section>
</div>
<?php $i++; ?>
<?php endforeach ?>
</div>
<?= $component->verticalTabs('staff-role', $data['staff'], static function ($staffList)
use ($component, $url, $helper) {
$rendered = [];
foreach ($staffList as $id => $person):
if (empty($person['image']['original']))
{
continue;
}
$rendered[] = $component->character(
$person['name'],
$url->generate('person', ['slug' => $person['slug']]),
$helper->picture(getLocalImg($person['image']['original'] ?? NULL)),
'character small-person',
);
endforeach;
return implode('', array_map('mb_trim', $rendered));
}) ?>
</section>
<?php endif ?>
</main>

View File

@ -32,7 +32,7 @@
<td>
<select name="watching_status" id="watching_status">
<?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>
<?php endforeach ?>
</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">
<?php if ($auth->isAuthenticated()): ?>
<a class="bracketed" href="<?= $url->generate('anime.add.get') ?>">Add Item</a>
@ -15,7 +15,7 @@
<h3>There's nothing here!</h3>
<?php else: ?>
<?php
$hasNotes = col_not_empty($items, 'notes');
$hasNotes = colNotEmpty($items, 'notes');
?>
<table class='media-wrap'>
<thead>
@ -31,7 +31,6 @@
<th>Rated</th>
<th>Attributes</th>
<?php if($hasNotes): ?><th>Notes</th><?php endif ?>
<th>Genres</th>
</tr>
</thead>
<tbody>
@ -103,10 +102,6 @@
</ul>
</td>
<?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>
<?php endforeach ?>
</tbody>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -19,80 +19,7 @@
<h2><?= $escape->html($name) ?></h2>
<section class="media-wrap">
<?php foreach($items as $item): ?>
<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>
<?= $component->mangaCover($item, $name) ?>
<?php endforeach ?>
</section>
</section>

View File

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

View File

@ -25,7 +25,6 @@
<th># of Volumes</th>
<th>Attributes</th>
<th>Type</th>
<th>Genres</th>
</tr>
</thead>
<tbody>
@ -70,9 +69,6 @@
</ul>
</td>
<td><?= $item['manga']['type'] ?></td>
<td class="align-left">
<?= implode(', ', $item['manga']['genres']) ?>
</td>
</tr>
<?php endforeach ?>
</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
use Aviat\AnimeClient\API\Kitsu;
use function Aviat\AnimeClient\getLocalImg;
?>
<main class="details fixed">
<section class="flex flex-no-wrap">
@ -10,12 +8,21 @@ use Aviat\AnimeClient\API\Kitsu;
</div>
<div>
<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>
</section>
<?php if ( ! empty($data['staff'])): ?>
<section>
<h3>Castings</h3>
<div class="vertical-tabs">
<?php $i = 0 ?>
<?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' : '' ?> />
<label for="staff-role<?= $i ?>"><?= $role ?></label>
<?php foreach ($entries as $type => $casting): ?>
<?php if ($type === 'characters') continue; ?>
<?php if ( ! (empty($entries['manga']) || empty($entries['anime']))): ?>
<?php if (isset($entries['manga'], $entries['anime'])): ?>
<h4><?= ucfirst($type) ?></h4>
<?php endif ?>
<section class="content">
<section class="content media-wrap flex flex-wrap flex-justify-start">
<?php foreach ($casting as $sid => $series): ?>
<article class="media">
<?php
$mediaType = in_array($type, ['anime', 'manga'], TRUE) ? $type : 'anime';
$link = $url->generate("{$mediaType}.details", ['id' => $series['slug']]);
$titles = Kitsu::filterTitles($series);
?>
<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 $mediaType = in_array($type, ['anime', 'manga'], TRUE) ? $type : 'anime'; ?>
<?= $component->media(
$series['titles'],
$url->generate("{$mediaType}.details", ['id' => $series['slug']]),
$helper->picture("images/{$type}/{$sid}.webp")
) ?>
<?php endforeach; ?>
</section>
<?php endforeach ?>
@ -59,9 +52,53 @@ use Aviat\AnimeClient\API\Kitsu;
</section>
<?php endif ?>
<?php if ( ! (empty($data['characters']['main']) || empty($data['characters']['supporting']))): ?>
<?php if ( ! empty($data['characters'])): ?>
<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>
<?php endif ?>
</main>

View File

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

View File

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

View File

@ -9,7 +9,7 @@
* @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
* @version 5.1
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/

View File

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

View File

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

View File

@ -163,7 +163,7 @@ CSS Tabs
/* text-align: center; */
}
.tabs .content {
.tabs .content, .single-tab {
display: none;
max-height: 950px;
border: 1px solid #e5e5e5;
@ -175,7 +175,14 @@ CSS Tabs
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;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,21 +3,27 @@ import { renderMangaSearchResults } from './template-helpers.js'
const search = (query) => {
_.show('.cssload-loader');
_.get(_.url('/manga/search'), { query }, (searchResults, status) => {
return _.get(_.url('/manga/search'), { query }, (searchResults, status) => {
searchResults = JSON.parse(searchResults);
_.hide('.cssload-loader');
_.$('#series-list')[ 0 ].innerHTML = renderMangaSearchResults(searchResults.data);
_.$('#series-list')[ 0 ].innerHTML = renderMangaSearchResults(searchResults);
});
};
if (_.hasElement('.manga #search')) {
let prevRequest = null
_.on('#search', 'input', _.throttle(250, (e) => {
let query = encodeURIComponent(e.target.value);
if (query === '') {
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,
// change status to currently reading
if (isNaN(completed) || completed === 0) {
data.data.status = 'current';
data.data.status = 'CURRENT';
}
// If you increment at the last chapter, mark as completed
if ((!isNaN(completed)) && (completed + 1) === total) {
data.data.status = 'completed';
data.data.status = 'COMPLETED';
}
// Update the total count
@ -67,7 +73,7 @@ _.on('.manga.list', 'click', '.edit-buttons button', (e) => {
type: 'POST',
mimeType: 'application/json',
success: () => {
if (data.data.status === 'completed') {
if (String(data.data.status).toUpperCase() === 'COMPLETED') {
_.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) {
const results = [];
data.forEach(x => {
const item = x.attributes;
data.forEach(item => {
const titles = item.titles.join('<br />');
results.push(`
<article class="media search">
<div class="name">
<input type="radio" class="mal-check" id="mal_${item.slug}" name="mal_id" value="${x.mal_id}" />
<input type="radio" class="big-check" id="${item.slug}" name="id" value="${x.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="${item.id}" />
<label for="${item.slug}">
<picture width="220">
<source srcset="/public/images/anime/${x.id}.webp" type="image/webp" />
<source srcset="/public/images/anime/${x.id}.jpg" type="image/jpeg" />
<img src="/public/images/anime/${x.id}.jpg" alt="" width="220" />
<source srcset="/public/images/anime/${item.id}.webp" type="image/webp" />
<source srcset="/public/images/anime/${item.id}.jpg" type="image/jpeg" />
<img src="/public/images/anime/${item.id}.jpg" alt="" width="220" />
</picture>
<span class="name">
${item.canonicalTitle}<br />
@ -48,20 +47,19 @@ export function renderAnimeSearchResults (data) {
export function renderMangaSearchResults (data) {
const results = [];
data.forEach(x => {
const item = x.attributes;
data.forEach(item => {
const titles = item.titles.join('<br />');
results.push(`
<article class="media search">
<div class="name">
<input type="radio" id="mal_${item.slug}" name="mal_id" value="${x.mal_id}" />
<input type="radio" class="big-check" id="${item.slug}" name="id" value="${x.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="${item.id}" />
<label for="${item.slug}">
<picture width="220">
<source srcset="/public/images/manga/${x.id}.webp" type="image/webp" />
<source srcset="/public/images/manga/${x.id}.jpg" type="image/jpeg" />
<img src="/public/images/manga/${x.id}.jpg" alt="" width="220" />
<source srcset="/public/images/manga/${item.id}.webp" type="image/webp" />
<source srcset="/public/images/manga/${item.id}.jpg" type="image/jpeg" />
<img src="/public/images/manga/${item.id}.jpg" alt="" width="220" />
</picture>
<span class="name">
${item.canonicalTitle}<br />

View File

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

View File

@ -260,7 +260,7 @@ function ajaxSerialize(data) {
*
* @param {string} url - the url to request
* @param {Object} config - the configuration object
* @return {void}
* @return {XMLHttpRequest}
*/
AnimeClient.ajax = (url, config) => {
// Set some sane defaults
@ -321,6 +321,8 @@ AnimeClient.ajax = (url, config) => {
} else {
request.send(config.data);
}
return request
};
/**
@ -329,6 +331,7 @@ AnimeClient.ajax = (url, config) => {
* @param {string} url
* @param {object|function} data
* @param {function} [callback]
* @return {XMLHttpRequest}
*/
AnimeClient.get = (url, data, 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
// that MAL id is passed
AnimeClient.on('main', 'change', '.big-check', (e) => {
@ -469,20 +512,19 @@ AnimeClient.on('main', 'change', '.big-check', (e) => {
function renderAnimeSearchResults (data) {
const results = [];
data.forEach(x => {
const item = x.attributes;
data.forEach(item => {
const titles = item.titles.join('<br />');
results.push(`
<article class="media search">
<div class="name">
<input type="radio" class="mal-check" id="mal_${item.slug}" name="mal_id" value="${x.mal_id}" />
<input type="radio" class="big-check" id="${item.slug}" name="id" value="${x.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="${item.id}" />
<label for="${item.slug}">
<picture width="220">
<source srcset="/public/images/anime/${x.id}.webp" type="image/webp" />
<source srcset="/public/images/anime/${x.id}.jpg" type="image/jpeg" />
<img src="/public/images/anime/${x.id}.jpg" alt="" width="220" />
<source srcset="/public/images/anime/${item.id}.webp" type="image/webp" />
<source srcset="/public/images/anime/${item.id}.jpg" type="image/jpeg" />
<img src="/public/images/anime/${item.id}.jpg" alt="" width="220" />
</picture>
<span class="name">
${item.canonicalTitle}<br />
@ -507,20 +549,19 @@ function renderAnimeSearchResults (data) {
function renderMangaSearchResults (data) {
const results = [];
data.forEach(x => {
const item = x.attributes;
data.forEach(item => {
const titles = item.titles.join('<br />');
results.push(`
<article class="media search">
<div class="name">
<input type="radio" id="mal_${item.slug}" name="mal_id" value="${x.mal_id}" />
<input type="radio" class="big-check" id="${item.slug}" name="id" value="${x.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="${item.id}" />
<label for="${item.slug}">
<picture width="220">
<source srcset="/public/images/manga/${x.id}.webp" type="image/webp" />
<source srcset="/public/images/manga/${x.id}.jpg" type="image/jpeg" />
<img src="/public/images/manga/${x.id}.jpg" alt="" width="220" />
<source srcset="/public/images/manga/${item.id}.webp" type="image/webp" />
<source srcset="/public/images/manga/${item.id}.jpg" type="image/jpeg" />
<img src="/public/images/manga/${item.id}.jpg" alt="" width="220" />
</picture>
<span class="name">
${item.canonicalTitle}<br />
@ -547,25 +588,31 @@ const search = (query) => {
AnimeClient.show('.cssload-loader');
// 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);
// Hide the loader
AnimeClient.hide('.cssload-loader');
// Show the results
AnimeClient.$('#series-list')[ 0 ].innerHTML = renderAnimeSearchResults(searchResults.data);
AnimeClient.$('#series-list')[ 0 ].innerHTML = renderAnimeSearchResults(searchResults);
});
};
if (AnimeClient.hasElement('.anime #search')) {
let prevRequest = null;
AnimeClient.on('#search', 'input', AnimeClient.throttle(250, (e) => {
const query = encodeURIComponent(e.target.value);
if (query === '') {
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,
// change status to currently watching
if (isNaN(watchedCount) || watchedCount === 0) {
data.data.status = 'current';
data.data.status = 'CURRENT';
}
// If you increment at the last episode, mark as completed
if ((!isNaN(watchedCount)) && (watchedCount + 1) === totalCount) {
data.data.status = 'completed';
data.data.status = 'COMPLETED';
}
AnimeClient.show('#loading-shadow');
@ -613,7 +660,7 @@ AnimeClient.on('body.anime.list', 'click', '.plus-one', (e) => {
return;
}
if (resData.data.attributes.status === 'completed') {
if (resData.data.libraryEntry.update.libraryEntry.status === 'COMPLETED') {
AnimeClient.hide(parentSel);
}
@ -633,21 +680,27 @@ AnimeClient.on('body.anime.list', 'click', '.plus-one', (e) => {
const search$1 = (query) => {
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);
AnimeClient.hide('.cssload-loader');
AnimeClient.$('#series-list')[ 0 ].innerHTML = renderMangaSearchResults(searchResults.data);
AnimeClient.$('#series-list')[ 0 ].innerHTML = renderMangaSearchResults(searchResults);
});
};
if (AnimeClient.hasElement('.manga #search')) {
let prevRequest = null;
AnimeClient.on('#search', 'input', AnimeClient.throttle(250, (e) => {
let query = encodeURIComponent(e.target.value);
if (query === '') {
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,
// change status to currently reading
if (isNaN(completed) || completed === 0) {
data.data.status = 'current';
data.data.status = 'CURRENT';
}
// If you increment at the last chapter, mark as completed
if ((!isNaN(completed)) && (completed + 1) === total) {
data.data.status = 'completed';
data.data.status = 'COMPLETED';
}
// Update the total count
@ -697,7 +750,7 @@ AnimeClient.on('.manga.list', 'click', '.edit-buttons button', (e) => {
type: 'POST',
mimeType: 'application/json',
success: () => {
if (data.data.status === 'completed') {
if (String(data.data.status).toUpperCase() === 'COMPLETED') {
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+
"="+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===
"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=
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",

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+
"="+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===
"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=
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",
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_'+
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'+
item.canonicalTitle+"<br />\n\t\t\t\t\t\t\t<small>"+titles+'</small>\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</label>\n\t\t\t\t</div>\n\t\t\t\t<div class="table">\n\t\t\t\t\t<div class="row">\n\t\t\t\t\t\t<span class="edit">\n\t\t\t\t\t\t\t<a class="bracketed" href="/anime/details/'+item.slug+'">Info Page</a>\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</article>\n\t\t')});return results.join("")}function renderMangaSearchResults(data){var results=[];data.forEach(function(x){var item=x.attributes;
var titles=item.titles.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/'+
x.id+'.jpg" type="image/jpeg" />\n\t\t\t\t\t\t\t<img src="/public/images/manga/'+x.id+'.jpg" alt="" width="220" />\n\t\t\t\t\t\t</picture>\n\t\t\t\t\t\t<span class="name">\n\t\t\t\t\t\t\t'+item.canonicalTitle+"<br />\n\t\t\t\t\t\t\t<small>"+titles+'</small>\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</label>\n\t\t\t\t</div>\n\t\t\t\t<div class="table">\n\t\t\t\t\t<div class="row">\n\t\t\t\t\t\t<span class="edit">\n\t\t\t\t\t\t\t<a class="bracketed" href="/manga/details/'+item.slug+'">Info Page</a>\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</article>\n\t\t')});
return results.join("")}var search=function(query){AnimeClient.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);
if(query==="")return;search(query)}));AnimeClient.on("body.anime.list","click",".plus-one",function(e){var parentSel=AnimeClient.closestParent(e.target,"article");var watchedCount=parseInt(AnimeClient.$(".completed_number",parentSel)[0].textContent,10)||0;var totalCount=parseInt(AnimeClient.$(".total_number",parentSel)[0].textContent,10);var title=AnimeClient.$(".name a",parentSel)[0].textContent;var data={id:parentSel.dataset.kitsuId,mal_id:parentSel.dataset.malId,data:{progress:watchedCount+1}};
if(isNaN(watchedCount)||watchedCount===0)data.data.status="current";if(!isNaN(watchedCount)&&watchedCount+1===totalCount)data.data.status="completed";AnimeClient.show("#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===
"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"),
{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;
var parentSel=AnimeClient.closestParent(e.target,"article");var type=thisSel.classList.contains("plus-one-chapter")?"chapter":"volume";var completed=parseInt(AnimeClient.$("."+type+"s_read",parentSel)[0].textContent,10)||0;var total=parseInt(AnimeClient.$("."+type+"_count",parentSel)[0].textContent,10);var mangaName=AnimeClient.$(".name",parentSel)[0].textContent;if(isNaN(completed))completed=0;var data={id:parentSel.dataset.kitsuId,mal_id:parentSel.dataset.malId,data:{progress:completed}};if(isNaN(completed)||
completed===0)data.data.status="current";if(!isNaN(completed)&&completed+1===total)data.data.status="completed";data.data.progress=++completed;AnimeClient.show("#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",
"Successfully updated "+mangaName);AnimeClient.scrollToTop()},error:function(){AnimeClient.hide("#loading-shadow");AnimeClient.showMessage("error","Failed to update "+mangaName);AnimeClient.scrollToTop()}})})})()
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",
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=
[];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/'+
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')});
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/'+
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/'+
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=
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,
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");
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 "+
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=
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",
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

File diff suppressed because one or more lines are too long

View File

@ -10,18 +10,13 @@
* @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
* @version 5.1
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Enum;
namespace Aviat\AnimeClient;
use Aviat\Ion\Enum as BaseEnum;
/**
* Types of lists
*/
final class APISource extends BaseEnum {
public const KITSU = 'kitsu';
class API {
public const ANILIST = 'anilist';
public const KITSU = 'kitsu';
}

View File

@ -10,7 +10,7 @@
* @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
* @version 5.1
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
@ -33,6 +33,12 @@ use Psr\Log\LoggerAwareTrait;
abstract class APIRequestBuilder {
use LoggerAwareTrait;
/**
* Where to look for GraphQL request files
* @var string
*/
protected string $filePath = __DIR__;
/**
* Url prefix for making url requests
* @var string
@ -294,6 +300,74 @@ abstract class APIRequestBuilder {
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
*

View File

@ -10,7 +10,7 @@
* @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
* @version 5.1
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
@ -32,6 +32,14 @@ abstract class AbstractListItem {
*/
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
*

View File

@ -10,7 +10,7 @@
* @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
* @version 5.1
* @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>
* @copyright 2015 - 2020 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5
* @version 5.1
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
@ -27,7 +27,7 @@ use Aviat\AnimeClient\Types\FormItemData;
* CRUD operations for MAL list items
*/
final class ListItem extends AbstractListItem {
use AnilistTrait;
use RequestBuilderTrait;
/**
* Create a minimal list item
@ -38,7 +38,7 @@ final class ListItem extends AbstractListItem {
public function create(array $data): Request
{
$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
{
$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
{
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
{
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,
]);
return $this->mutateRequest('IncrementMediaListEntry', $checkedData);
return $this->requestBuilder->mutateRequest('IncrementMediaListEntry', $checkedData);
}
/**
@ -120,6 +120,6 @@ final class ListItem extends AbstractListItem {
'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>
* @copyright 2015 - 2020 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5
* @version 5.1
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/

View File

@ -10,7 +10,7 @@
* @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
* @version 5.1
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
@ -35,7 +35,7 @@ use Throwable;
*/
final class Model
{
use AnilistTrait;
use RequestBuilderTrait;
/**
* @var ListItem
*/
@ -77,7 +77,7 @@ final class Model
])
->getFullRequest();
$response = $this->getResponseFromRequest($request);
$response = $this->requestBuilder->getResponseFromRequest($request);
return Json::decode(wait($response->getBody()->buffer()));
}
@ -89,7 +89,7 @@ final class Model
*/
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');
}
return $this->runQuery('SyncUserList', [
return $this->requestBuilder->runQuery('SyncUserList', [
'name' => $anilistUser,
'type' => $type,
]);
@ -275,17 +275,25 @@ final class Model
* Get the Anilist list item id from the media id from its MAL id
* this way is more accurate than getting the list item id
* directly from the MAL id
*
* @param string $mediaId
* @return string|null
*/
private function getListIdFromMediaId(string $mediaId): string
private function getListIdFromMediaId(string $mediaId): ?string
{
$config = $this->container->get('config');
$anilistUser = $config->get(['anilist', 'username']);
$info = $this->runQuery('ListItemIdByMediaId', [
$info = $this->requestBuilder->runQuery('ListItemIdByMediaId', [
'id' => $mediaId,
'userName' => $anilistUser,
]);
if ( ! empty($info['errors']))
{
return NULL;
}
return (string)$info['data']['MediaList']['id'];
}
@ -303,7 +311,7 @@ final class Model
return NULL;
}
$info = $this->runQuery('MediaIdByMalId', [
$info = $this->requestBuilder->runQuery('MediaIdByMalId', [
'id' => $malId,
'type' => mb_strtoupper($type),
]);

View File

@ -10,42 +10,43 @@
* @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
* @version 5.1
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
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\Response;
use Aviat\AnimeClient\API\Anilist;
use Aviat\Ion\Json;
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 Throwable;
trait AnilistTrait {
final class RequestBuilder extends APIRequestBuilder {
use ContainerAware;
/**
* The request builder for the Anilist API
* @var AnilistRequestBuilder
*/
protected AnilistRequestBuilder $requestBuilder;
/**
* The base url for api requests
* @var string $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
*
@ -53,21 +54,14 @@ trait AnilistTrait {
*/
protected array $defaultHeaders = [
'Accept' => 'application/json',
'Accept-Encoding' => 'gzip',
// 'Accept-Encoding' => 'gzip',
'Content-type' => 'application/json',
'User-Agent' => USER_AGENT,
];
/**
* Set the request builder object
*
* @param AnilistRequestBuilder $requestBuilder
* @return self
*/
public function setRequestBuilder($requestBuilder): self
public function __construct(ContainerInterface $container)
{
$this->requestBuilder = $requestBuilder;
return $this;
$this->setContainer($container);
}
/**
@ -82,7 +76,7 @@ trait AnilistTrait {
$config = $this->getContainer()->get('config');
$anilistConfig = $config->get('anilist');
$request = $this->requestBuilder->newRequest('POST', $url);
$request = $this->newRequest('POST', $url);
// You can only authenticate the request if you
// actually have an access_token saved
@ -123,13 +117,12 @@ trait AnilistTrait {
*/
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))
{
throw new LogicException('GraphQL query file does not exist.');
}
// $query = str_replace(["\t", "\n"], ' ', file_get_contents($file));
$query = file_get_contents($file);
$body = [
'query' => $query
@ -157,13 +150,12 @@ trait AnilistTrait {
*/
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))
{
throw new LogicException('GraphQL mutation file does not exist.');
}
// $query = str_replace(["\t", "\n"], ' ', file_get_contents($file));
$query = file_get_contents($file);
$body = [
@ -206,12 +198,8 @@ trait AnilistTrait {
* @throws Throwable
*/
private function getResponse(string $url, array $options = []): Response
{
$logger = NULL;
if ($this->getContainer())
{
$logger = $this->container->getLogger('anilist-request');
}
$request = $this->setUpRequest($url, $options);
$response = getResponse($request);
@ -232,13 +220,9 @@ trait AnilistTrait {
* @return Response
* @throws Throwable
*/
private function getResponseFromRequest(Request $request): Response
{
$logger = NULL;
if ($this->getContainer())
public function getResponseFromRequest(Request $request): Response
{
$logger = $this->container->getLogger('anilist-request');
}
$response = getResponse($request);
@ -265,9 +249,6 @@ trait AnilistTrait {
$response = $this->getResponse(Anilist::BASE_URL, $options);
$validResponseCodes = [200, 201];
$logger = NULL;
if ($this->getContainer())
{
$logger = $this->container->getLogger('anilist-request');
$logger->debug('Anilist response', [
'status' => $response->getStatus(),
@ -276,18 +257,22 @@ trait AnilistTrait {
'headers' => $response->getHeaders(),
//'requestHeaders' => $request->getHeaders(),
]);
}
if ( ! \in_array($response->getStatus(), $validResponseCodes, TRUE))
{
if ($logger !== NULL)
{
$logger->warning('Non 200 response for POST api call', (array)$response->getBody());
}
$rawBody = wait($response->getBody()->buffer());
try
{
return Json::decode($rawBody);
}
catch (JsonException $e)
{
dump($e);
dump($rawBody);
die();
}
// dump(wait($response->getBody()->buffer()));
return Json::decode(wait($response->getBody()->buffer()));
}
}

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>
* @copyright 2015 - 2020 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5
* @version 5.1
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/

View File

@ -10,7 +10,7 @@
* @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
* @version 5.1
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/

View File

@ -10,7 +10,7 @@
* @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
* @version 5.1
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
@ -20,37 +20,37 @@ use Aviat\AnimeClient\Types\AbstractType;
class MediaListEntry extends AbstractType {
/**
* @var int
* @var int|string
*/
public $id;
/**
* @var string
* @var string|null
*/
public $notes;
public ?string $notes;
/**
* @var bool
*/
public $private;
public ?bool $private;
/**
* @var int
*/
public $progress;
public int $progress;
/**
* @var int
*/
public $repeat;
public ?int $repeat;
/**
* @var string
*/
public $status;
public string $status;
/**
* @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>
* @copyright 2015 - 2020 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5
* @version 5.1
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
@ -57,7 +57,7 @@ trait CacheTrait {
*
* @param string $key
* @param callable $primer
* @param array $primeArgs
* @param array|null $primeArgs
* @return mixed|null
* @throws InvalidArgumentException
*/
@ -78,22 +78,4 @@ trait CacheTrait {
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>
* @copyright 2015 - 2020 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5
* @version 5.1
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/

View File

@ -10,7 +10,7 @@
* @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
* @version 5.1
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/

View File

@ -10,7 +10,7 @@
* @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
* @version 5.1
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/

View File

@ -10,7 +10,7 @@
* @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
* @version 5.1
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/

View File

@ -10,7 +10,7 @@
* @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
* @version 5.1
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/

View File

@ -10,7 +10,7 @@
* @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
* @version 5.1
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/

View File

@ -10,7 +10,7 @@
* @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
* @version 5.1
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/

View File

@ -10,7 +10,7 @@
* @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
* @version 5.1
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/

View File

@ -10,7 +10,7 @@
* @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
* @version 5.1
* @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