Merge pull request 'All in GraphQL' (#34) from develop into master
Reviewed-on: timw4mail/HummingBirdAnimeClient#34
This commit is contained in:
commit
45b0209d8a
@ -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
|
||||
|
@ -227,7 +227,7 @@ class RoboFile extends Tasks {
|
||||
{
|
||||
$this->lint();
|
||||
|
||||
$this->_run(['phpunit']);
|
||||
$this->_run(['vendor/bin/phpunit']);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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',
|
||||
],
|
||||
|
@ -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();
|
||||
|
6
app/templates/character.php
Normal file
6
app/templates/character.php
Normal file
@ -0,0 +1,6 @@
|
||||
<article class="<?= $className ?>">
|
||||
<div class="name">
|
||||
<a href="<?= $link ?>"><?= $name ?></a>
|
||||
</div>
|
||||
<a href="<?= $link ?>"><?= $picture ?></a>
|
||||
</article>
|
74
app/templates/manga-cover.php
Normal file
74
app/templates/manga-cover.php
Normal 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
12
app/templates/media.php
Normal 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>
|
5
app/templates/single-tab.php
Normal file
5
app/templates/single-tab.php
Normal 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
32
app/templates/tabs.php
Normal 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>
|
25
app/templates/vertical-tabs.php
Normal file
25
app/templates/vertical-tabs.php
Normal 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>
|
@ -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>
|
||||
|
@ -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>
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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> </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>
|
@ -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>
|
||||
|
@ -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> </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> </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>
|
@ -39,5 +39,4 @@
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
</header>
|
@ -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>
|
||||
|
@ -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>
|
@ -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>
|
||||
|
@ -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>
|
@ -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>
|
||||
|
@ -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++; ?>
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
*/
|
||||
|
||||
|
@ -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",
|
||||
|
3
console
3
console
@ -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
|
||||
// -----------------------------------------------------------------------------
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -94,6 +94,7 @@ a:hover, a:active {
|
||||
iframe {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------------------------------
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import './anon.js';
|
||||
|
||||
import './session-check.js';
|
||||
import './anime.js';
|
||||
import './manga.js';
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
41
frontEndSrc/js/session-check.js
Normal file
41
frontEndSrc/js/session-check.js
Normal 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);
|
||||
}
|
||||
})();
|
@ -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 />
|
||||
|
@ -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
|
||||
*/
|
||||
|
||||
|
2
public/css/auto.min.css
vendored
2
public/css/auto.min.css
vendored
File diff suppressed because one or more lines are too long
2
public/css/dark.min.css
vendored
2
public/css/dark.min.css
vendored
File diff suppressed because one or more lines are too long
2
public/css/light.min.css
vendored
2
public/css/light.min.css
vendored
File diff suppressed because one or more lines are too long
@ -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) {
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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 |
1
public/images/streaming-logos/animelab.svg
Normal file
1
public/images/streaming-logos/animelab.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 9.6 KiB |
1
public/images/streaming-logos/vrv.svg
Normal file
1
public/images/streaming-logos/vrv.svg
Normal 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 |
2
public/js/anon.min.js
vendored
2
public/js/anon.min.js
vendored
@ -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
29
public/js/scripts.min.js
vendored
29
public/js/scripts.min.js
vendored
@ -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
@ -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';
|
||||
}
|
@ -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
|
||||
*
|
||||
|
@ -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
|
||||
*
|
||||
|
@ -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
|
||||
*/
|
||||
|
||||
|
15
src/AnimeClient/API/Anilist/.graphqlconfig
Normal file
15
src/AnimeClient/API/Anilist/.graphqlconfig
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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',
|
||||
];
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
query ($id: Int, $type: MediaType) {
|
||||
Media (idMal: $id, type: $type) {
|
||||
mediaListEntry {
|
||||
id
|
||||
userId
|
||||
mediaId
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
query ($id: Int) {
|
||||
Media (type: ANIME, malId: $id) {
|
||||
id
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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
|
||||
*/
|
||||
|
||||
|
@ -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),
|
||||
]);
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
// dump(wait($response->getBody()->buffer()));
|
||||
|
||||
return Json::decode(wait($response->getBody()->buffer()));
|
||||
$rawBody = wait($response->getBody()->buffer());
|
||||
try
|
||||
{
|
||||
return Json::decode($rawBody);
|
||||
}
|
||||
catch (JsonException $e)
|
||||
{
|
||||
dump($e);
|
||||
dump($rawBody);
|
||||
die();
|
||||
}
|
||||
}
|
||||
}
|
41
src/AnimeClient/API/Anilist/RequestBuilderTrait.php
Normal file
41
src/AnimeClient/API/Anilist/RequestBuilderTrait.php
Normal 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;
|
||||
}
|
||||
}
|
@ -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
|
||||
*/
|
||||
|
||||
|
@ -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
|
||||
*/
|
||||
|
||||
|
@ -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;
|
||||
}
|
4427
src/AnimeClient/API/Anilist/schema.graphql
Normal file
4427
src/AnimeClient/API/Anilist/schema.graphql
Normal file
File diff suppressed because it is too large
Load Diff
@ -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));
|
||||
}
|
||||
}
|
@ -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
|
||||
*/
|
||||
|
||||
|
@ -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
|
||||
*/
|
||||
|
||||
|
@ -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
|
||||
*/
|
||||
|
||||
|
@ -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
|
||||
*/
|
||||
|
||||
|
@ -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
|
||||
*/
|
||||
|
||||
|
@ -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
|
||||
*/
|
||||
|
||||
|
@ -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
|
||||
*/
|
||||
|
||||
|
@ -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
|
||||
*/
|
||||
|
||||
|
@ -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
|
||||
*/
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
15
src/AnimeClient/API/Kitsu/.graphqlconfig
Normal file
15
src/AnimeClient/API/Kitsu/.graphqlconfig
Normal 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
Loading…
Reference in New Issue
Block a user