Version 5.1 - All the GraphQL #32
8
.gitignore
vendored
8
.gitignore
vendored
@ -13,7 +13,7 @@ composer.lock
|
||||
*.sqlite
|
||||
*.db
|
||||
*.sqlite3
|
||||
docs/*
|
||||
apidocs/**
|
||||
tests/test_data/sessions/*
|
||||
cache.properties
|
||||
build/**
|
||||
@ -25,4 +25,8 @@ app/config/*.toml
|
||||
phinx.yml
|
||||
.idea/
|
||||
Caddyfile
|
||||
build/humbuglog.txt
|
||||
build/humbuglog.txt
|
||||
public/images/anime/**
|
||||
public/images/avatars/**
|
||||
public/images/manga/**
|
||||
public/images/characters/**
|
@ -45,6 +45,10 @@ Update your anime/manga list on Kitsu.io and MyAnimeList.net
|
||||
3. Configure settings in `app/config/config.toml` to your liking
|
||||
4. Create the following directories if they don't exist, and make sure they are world writable
|
||||
* public/js/cache
|
||||
* public/images/avatars
|
||||
* public/images/anime
|
||||
* public/images/characters
|
||||
* public/images/manga
|
||||
5. Make sure the `console` script is executable
|
||||
|
||||
### Using MAL API
|
||||
|
@ -18,46 +18,6 @@
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| CSS Folder
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The folder where css files exist, in relation to the document root
|
||||
|
|
||||
*/
|
||||
'css_root' => 'css/',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Path from
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Path fragment to rewrite in css files
|
||||
|
|
||||
*/
|
||||
'path_from' => '',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Path to
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The path fragment replacement for the css files
|
||||
|
|
||||
*/
|
||||
'path_to' => '',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| CSS Groups file
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The file where the css groups are configured
|
||||
|
|
||||
*/
|
||||
'css_groups_file' => __DIR__ . '/minify_css_groups.php',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| JS Folder
|
||||
@ -70,13 +30,40 @@ return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| JS Groups file
|
||||
| JS Groups
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The file where the javascript groups are configured
|
||||
| Config array for javascript files to concatenate and minify
|
||||
|
|
||||
*/
|
||||
'js_groups_file' => __DIR__ . '/minify_js_groups.php',
|
||||
|
||||
'groups' => [
|
||||
'base' => [
|
||||
'base/classList.js',
|
||||
'base/AnimeClient.js',
|
||||
],
|
||||
'event' => [
|
||||
'base/events.js',
|
||||
],
|
||||
'table' => [
|
||||
'base/sort_tables.js',
|
||||
],
|
||||
'table_edit' => [
|
||||
'base/sort_tables.js',
|
||||
'anime_edit.js',
|
||||
'manga_edit.js',
|
||||
],
|
||||
'edit' => [
|
||||
'anime_edit.js',
|
||||
'manga_edit.js',
|
||||
],
|
||||
'anime_collection' => [
|
||||
'lib/mustache.js',
|
||||
'anime_collection.js',
|
||||
],
|
||||
'manga_collection' => [
|
||||
'lib/mustache.js',
|
||||
'manga_collection.js',
|
||||
],
|
||||
]
|
||||
];
|
||||
// End of minify_config.php
|
@ -1,40 +0,0 @@
|
||||
<?php declare(strict_types=1);
|
||||
/**
|
||||
* Hummingbird Anime List Client
|
||||
*
|
||||
* An API client for Kitsu and MyAnimeList to manage anime and manga watch lists
|
||||
*
|
||||
* PHP version 7
|
||||
*
|
||||
* @package HummingbirdAnimeClient
|
||||
* @author Timothy J. Warren <tim@timshomepage.net>
|
||||
* @copyright 2015 - 2017 Timothy J. Warren
|
||||
* @license http://www.opensource.org/licenses/mit-license.html MIT License
|
||||
* @version 4.0
|
||||
* @link https://github.com/timw4mail/HummingBirdAnimeClient
|
||||
*/
|
||||
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* This is the config array for css files to concatenate and minify
|
||||
*/
|
||||
return [
|
||||
/*-----
|
||||
Css
|
||||
-----*/
|
||||
|
||||
/*
|
||||
For each group create an array like so
|
||||
|
||||
'my_group' => array(
|
||||
'path/to/css/file1.css',
|
||||
'path/to/css/file2.css'
|
||||
),
|
||||
*/
|
||||
'base' => [
|
||||
'base.css'
|
||||
]
|
||||
];
|
||||
// End of css_groups.php
|
@ -1,53 +0,0 @@
|
||||
<?php declare(strict_types=1);
|
||||
/**
|
||||
* Hummingbird Anime List Client
|
||||
*
|
||||
* An API client for Kitsu and MyAnimeList to manage anime and manga watch lists
|
||||
*
|
||||
* PHP version 7
|
||||
*
|
||||
* @package HummingbirdAnimeClient
|
||||
* @author Timothy J. Warren <tim@timshomepage.net>
|
||||
* @copyright 2015 - 2017 Timothy J. Warren
|
||||
* @license http://www.opensource.org/licenses/mit-license.html MIT License
|
||||
* @version 4.0
|
||||
* @link https://github.com/timw4mail/HummingBirdAnimeClient
|
||||
*/
|
||||
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* This is the config array for javascript files to concatenate and minify
|
||||
*/
|
||||
return [
|
||||
'base' => [
|
||||
'base/classList.js',
|
||||
'base/AnimeClient.js',
|
||||
],
|
||||
'event' => [
|
||||
'base/events.js',
|
||||
],
|
||||
'table' => [
|
||||
'base/sort_tables.js',
|
||||
],
|
||||
'table_edit' => [
|
||||
'base/sort_tables.js',
|
||||
'anime_edit.js',
|
||||
'manga_edit.js',
|
||||
],
|
||||
'edit' => [
|
||||
'anime_edit.js',
|
||||
'manga_edit.js',
|
||||
],
|
||||
'anime_collection' => [
|
||||
'lib/mustache.js',
|
||||
'anime_collection.js',
|
||||
],
|
||||
'manga_collection' => [
|
||||
'lib/mustache.js',
|
||||
'manga_collection.js',
|
||||
],
|
||||
];
|
||||
|
||||
// End of js_groups.php
|
@ -147,6 +147,16 @@ return [
|
||||
// ---------------------------------------------------------------------
|
||||
// Default / Shared routes
|
||||
// ---------------------------------------------------------------------
|
||||
'image_proxy' => [
|
||||
'path' => '/public/images/{type}/{file}',
|
||||
'action' => 'images',
|
||||
'controller' => DEFAULT_CONTROLLER,
|
||||
'verb' => 'get',
|
||||
'tokens' => [
|
||||
'type' => '[a-z0-9\-]+',
|
||||
'file' => '[a-z0-9\-]+\.[a-z]{3}'
|
||||
]
|
||||
],
|
||||
'cache_purge' => [
|
||||
'path' => '/cache_purge',
|
||||
'action' => 'clearCache',
|
||||
|
@ -15,7 +15,7 @@
|
||||
<?php if ($auth->isAuthenticated()): ?>
|
||||
<button title="Increment episode count" class="plus_one" hidden>+1 Episode</button>
|
||||
<?php endif ?>
|
||||
<img src="<?= $item['anime']['image'] ?>" alt="" />
|
||||
<img src="<?= $urlGenerator->assetUrl("images/anime/{$item['anime']['id']}.jpg") ?>" alt="" />
|
||||
<div class="name">
|
||||
<a href="<?= $url->generate('anime.details', ['id' => $item['anime']['slug']]); ?>">
|
||||
<?= array_shift($item['anime']['titles']) ?>
|
||||
@ -28,17 +28,17 @@
|
||||
<?php if ($auth->isAuthenticated()): ?>
|
||||
<div class="row">
|
||||
<span class="edit">
|
||||
<a class="bracketed" title="Edit information about this anime" href="<?=
|
||||
<a class="bracketed" title="Edit information about this anime" href="<?=
|
||||
$url->generate('edit', [
|
||||
'controller' => 'anime',
|
||||
'id' => $item['id'],
|
||||
'controller' => 'anime',
|
||||
'id' => $item['id'],
|
||||
'status' => $item['watching_status']
|
||||
]);
|
||||
?>">Edit</a>
|
||||
</span>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
|
||||
|
||||
<?php if ($item['private'] || $item['rewatching']): ?>
|
||||
<div class="row">
|
||||
<?php foreach(['private', 'rewatching'] as $attr): ?>
|
||||
@ -48,13 +48,13 @@
|
||||
<?php endforeach ?>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
|
||||
|
||||
<?php if ($item['rewatched'] > 0): ?>
|
||||
<div class="row">
|
||||
<div>Rewatched <?= $item['rewatched'] ?> time(s)</div>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
|
||||
|
||||
<?php if (count($item['anime']['streaming_links']) > 0): ?>
|
||||
<div class="row">
|
||||
<?php foreach($item['anime']['streaming_links'] as $link): ?>
|
||||
@ -70,7 +70,7 @@
|
||||
<?php endforeach ?>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
|
||||
|
||||
<div class="row">
|
||||
<div class="user_rating">Rating: <?= $item['user_rating'] ?> / 10</div>
|
||||
<div class="completion">Episodes:
|
||||
|
@ -1,7 +1,7 @@
|
||||
<main class="details fixed">
|
||||
<section class="flex flex-no-wrap">
|
||||
<div>
|
||||
<img class="cover" width="402" height="284" src="<?= $data['cover_image'] ?>" alt="" />
|
||||
<img class="cover" width="402" height="284" src="<?= $urlGenerator->assetUrl("images/anime/{$data['id']}.jpg") ?>" alt="" />
|
||||
<br />
|
||||
<br />
|
||||
<table class="media_details">
|
||||
@ -79,8 +79,8 @@
|
||||
|
||||
<?php if (count($characters) > 0): ?>
|
||||
<h2>Characters</h2>
|
||||
<section class="media-wrap">
|
||||
<?php foreach($characters as $char): ?>
|
||||
<section class="align_left media-wrap">
|
||||
<?php foreach($characters as $id => $char): ?>
|
||||
<?php if ( ! empty($char['image']['original'])): ?>
|
||||
<article class="character">
|
||||
<?php $link = $url->generate('character', ['slug' => $char['slug']]) ?>
|
||||
@ -88,7 +88,7 @@
|
||||
<?= $helper->a($link, $char['name']); ?>
|
||||
</div>
|
||||
<a href="<?= $link ?>">
|
||||
<?= $helper->img($char['image']['original'], [
|
||||
<?= $helper->img($urlGenerator->assetUrl("images/characters/{$id}.jpg"), [
|
||||
'width' => '225'
|
||||
]) ?>
|
||||
</a>
|
||||
|
@ -13,7 +13,7 @@
|
||||
</th>
|
||||
<th>
|
||||
<article class="media">
|
||||
<?= $helper->img($item['anime']['image']); ?>
|
||||
<?= $helper->img($urlGenerator->assetUrl('images/anime', "{$item['anime']['id']}.jpg")) ?>
|
||||
</article>
|
||||
</th>
|
||||
</tr>
|
||||
|
@ -1,12 +1,126 @@
|
||||
<main class="details fixed">
|
||||
<?php use Aviat\AnimeClient\API\Kitsu; ?>
|
||||
<main class="details">
|
||||
<section class="flex flex-no-wrap">
|
||||
<div>
|
||||
<img class="cover" width="284" src="<?= $data['image']['original'] ?>" alt="" />
|
||||
<img class="cover" width="284" src="<?= $urlGenerator->assetUrl("images/characters/{$data[0]['id']}.jpg") ?>" alt="" />
|
||||
</div>
|
||||
<div>
|
||||
<h2><?= $data['name'] ?></h2>
|
||||
<h2><?= $data[0]['attributes']['name'] ?></h2>
|
||||
|
||||
<p><?= $data['description'] ?></p>
|
||||
<p class="description"><?= $data[0]['attributes']['description'] ?></p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<?php if (array_key_exists('anime', $data['included']) || array_key_exists('manga', $data['included'])): ?>
|
||||
<h3>Media</h3>
|
||||
<section class="flex flex-no-wrap">
|
||||
<?php if (array_key_exists('anime', $data['included'])): ?>
|
||||
<div>
|
||||
<h4>Anime</h4>
|
||||
<section class="align_left media-wrap">
|
||||
<?php foreach($data['included']['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 ?>">
|
||||
<img src="<?= $urlGenerator->assetUrl("images/anime/{$id}.jpg") ?>" width="220" alt="" />
|
||||
</a>
|
||||
<div class="name">
|
||||
<a href="<?= $link ?>">
|
||||
<?= array_shift($titles) ?>
|
||||
<?php foreach ($titles as $title): ?>
|
||||
<br /><small><?= $title ?></small>
|
||||
<?php endforeach ?>
|
||||
</a>
|
||||
</div>
|
||||
</article>
|
||||
<?php endforeach ?>
|
||||
</section>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
<?php if (array_key_exists('manga', $data['included'])): ?>
|
||||
<div>
|
||||
<h4>Manga</h4>
|
||||
<section class="align_left media-wrap">
|
||||
|
||||
<?php foreach($data['included']['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 ?>">
|
||||
<img src="<?= $urlGenerator->assetUrl("images/manga/{$id}.jpg") ?>" width="220" alt="" />
|
||||
</a>
|
||||
<div class="name">
|
||||
<a href="<?= $link ?>">
|
||||
<?= array_shift($titles) ?>
|
||||
<?php foreach ($titles as $title): ?>
|
||||
<br /><small><?= $title ?></small>
|
||||
<?php endforeach ?>
|
||||
</a>
|
||||
</div>
|
||||
</article>
|
||||
<?php endforeach ?>
|
||||
|
||||
</section>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
</section>
|
||||
<?php endif ?>
|
||||
|
||||
<section>
|
||||
<?php if ($castCount > 0): ?>
|
||||
<h3>Castings</h3>
|
||||
<?php foreach($castings as $role => $entries): ?>
|
||||
<h4><?= $role ?></h4>
|
||||
<?php foreach($entries as $language => $casting): ?>
|
||||
<h5><?= $language ?></h5>
|
||||
<table class="min-table">
|
||||
<tr>
|
||||
<th>Cast Member</th>
|
||||
<th>Series</th>
|
||||
</tr>
|
||||
<?php foreach($casting as $c):?>
|
||||
<tr>
|
||||
<td style="width:229px">
|
||||
<article class="character">
|
||||
<img src="<?= $c['person']['image'] ?>" alt="" />
|
||||
<div class="name">
|
||||
<?= $c['person']['name'] ?>
|
||||
</div>
|
||||
</article>
|
||||
</td>
|
||||
<td>
|
||||
<section class="align_left media-wrap">
|
||||
<?php foreach($c['series'] as $series): ?>
|
||||
<article class="media">
|
||||
<?php
|
||||
$link = $url->generate('anime.details', ['id' => $series['attributes']['slug']]);
|
||||
$titles = Kitsu::filterTitles($series['attributes']);
|
||||
?>
|
||||
<a href="<?= $link ?>">
|
||||
<img src="<?= $series['attributes']['posterImage']['small'] ?>" width="220" alt="" />
|
||||
</a>
|
||||
<div class="name">
|
||||
<a href="<?= $link ?>">
|
||||
<?= array_shift($titles) ?>
|
||||
<?php foreach ($titles as $title): ?>
|
||||
<br /><small><?= $title ?></small>
|
||||
<?php endforeach ?>
|
||||
</a>
|
||||
</div>
|
||||
</article>
|
||||
<?php endforeach ?>
|
||||
</section>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</table>
|
||||
<?php endforeach ?>
|
||||
<?php endforeach ?>
|
||||
<?php endif ?>
|
||||
</section>
|
||||
</main>
|
@ -11,7 +11,7 @@
|
||||
<section class="media-wrap">
|
||||
<?php foreach($items as $item): ?>
|
||||
<article class="media" id="a-<?= $item['hummingbird_id'] ?>">
|
||||
<img src="https://media.kitsu.io/anime/poster_images/<?= $item['hummingbird_id'] ?>/small.jpg"
|
||||
<img src="<?= $urlGenerator->assetUrl("images/anime/{$item['hummingbird_id']}.jpg") ?>"
|
||||
alt="<?= $item['title'] ?> cover image" />
|
||||
<div class="name">
|
||||
<a href="<?= $url->generate('anime.details', ['id' => $item['slug']]) ?>">
|
||||
|
@ -6,7 +6,7 @@
|
||||
<meta http-equiv="cache-control" content="no-store" />
|
||||
<meta http-equiv="Content-Security-Policy" content="script-src 'self'" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1, user-scalable=0" />
|
||||
<link rel="stylesheet" href="<?= $urlGenerator->assetUrl('css.php/g/base/debug') ?>" />
|
||||
<link rel="stylesheet" href="<?= $urlGenerator->assetUrl('css/app.min.css') ?>" />
|
||||
<link rel="icon" href="<?= $urlGenerator->assetUrl('images/icons/favicon.ico') ?>" />
|
||||
<link rel="apple-touch-icon" sizes="57x57" href="<?= $urlGenerator->assetUrl('images/icons/apple-icon-57x57.png') ?>">
|
||||
<link rel="apple-touch-icon" sizes="60x60" href="<?= $urlGenerator->assetUrl('images/icons/apple-icon-60x60.png') ?>">
|
||||
@ -21,17 +21,19 @@
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="<?= $urlGenerator->assetUrl('images/icons/favicon-32x32.png') ?>">
|
||||
<link rel="icon" type="image/png" sizes="96x96" href="<?= $urlGenerator->assetUrl('images/icons/favicon-96x96.png') ?>">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="<?= $urlGenerator->assetUrl('images/icons/favicon-16x16.png') ?>">
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
<script defer="defer" src="<?= $urlGenerator->assetUrl('js.php/g/base') ?>"></script>
|
||||
</head>
|
||||
<body class="<?= $escape->attr($url_type) ?> list">
|
||||
<header>
|
||||
<?php include 'main-menu.php' ?>
|
||||
<?php if(isset($message) && is_array($message)):
|
||||
<?php
|
||||
include 'main-menu.php';
|
||||
if(isset($message) && is_array($message))
|
||||
{
|
||||
foreach($message as $m)
|
||||
{
|
||||
extract($m);
|
||||
include 'message.php';
|
||||
include 'message.php';
|
||||
}
|
||||
endif ?>
|
||||
}
|
||||
?>
|
||||
</header>
|
@ -1,37 +1,58 @@
|
||||
<?php declare(strict_types=1); namespace Aviat\AnimeClient; ?>
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
namespace Aviat\AnimeClient;
|
||||
|
||||
$whose = $config->get('whose_list') . "'s ";
|
||||
$lastSegment = $urlGenerator->lastSegment();
|
||||
$extraSegment = $lastSegment === 'list' ? '/list' : '';
|
||||
|
||||
?>
|
||||
<h1 class="flex flex-align-end flex-wrap">
|
||||
<span class="flex-no-wrap grow-1">
|
||||
<?php if(strpos($route_path, 'collection') === FALSE): ?>
|
||||
<a href="<?= $escape->attr($urlGenerator->defaultUrl($url_type)) ?>">
|
||||
<?= $config->get('whose_list') ?>'s <?= ucfirst($url_type) ?> List
|
||||
</a>
|
||||
<?= $helper->a(
|
||||
$urlGenerator->defaultUrl($url_type),
|
||||
$whose . ucfirst($url_type) . ' List'
|
||||
) ?>
|
||||
<?php if($config->get("show_{$url_type}_collection")): ?>
|
||||
[<a href="<?= $url->generate('collection.view') ?>"><?= ucfirst($url_type) ?> Collection</a>]
|
||||
[<?= $helper->a(
|
||||
$url->generate('collection.view') . $extraSegment,
|
||||
ucfirst($url_type) . ' Collection'
|
||||
) ?>]
|
||||
<?php endif ?>
|
||||
[<a href="<?= $urlGenerator->defaultUrl($other_type) ?>"><?= ucfirst($other_type) ?> List</a>]
|
||||
[<?= $helper->a(
|
||||
$urlGenerator->defaultUrl($other_type) . $extraSegment,
|
||||
ucfirst($other_type) . ' List'
|
||||
) ?>]
|
||||
<?php else: ?>
|
||||
<a href="<?= $url->generate('collection.view') ?>">
|
||||
<?= $config->get('whose_list') ?>'s <?= ucfirst($url_type) ?> Collection
|
||||
</a>
|
||||
[<a href="<?= $urlGenerator->defaultUrl('anime') ?>">Anime List</a>]
|
||||
[<a href="<?= $urlGenerator->defaultUrl('manga') ?>">Manga List</a>]
|
||||
<?= $whose . ucfirst($url_type) . ' Collection' ?>
|
||||
[<?= $helper->a($urlGenerator->defaultUrl('anime') . $extraSegment, 'Anime List') ?>]
|
||||
[<?= $helper->a($urlGenerator->defaultUrl('manga') . $extraSegment, 'Manga List') ?>]
|
||||
<?php endif ?>
|
||||
</span>
|
||||
<span class="flex-no-wrap small-font">
|
||||
[<?= $helper->a($url->generate('user_info'), 'About '. $config->get('whose_list')) ?>]
|
||||
</span>
|
||||
|
||||
<span class="flex-no-wrap small-font">[<?= $helper->a(
|
||||
$url->generate('user_info'),
|
||||
'About '. $config->get('whose_list')
|
||||
) ?>]</span>
|
||||
|
||||
<?php if ($auth->isAuthenticated()): ?>
|
||||
<span class="flex-no-wrap"> </span>
|
||||
<span class="flex-no-wrap small-font">
|
||||
<button type="button" class="js-clear-cache user-btn">Clear API Cache</button>
|
||||
</span>
|
||||
<button type="button" class="js-clear-cache user-btn">Clear API Cache</button>
|
||||
</span>
|
||||
<span class="flex-no-wrap"> </span>
|
||||
<?php endif ?>
|
||||
|
||||
<span class="flex-no-wrap small-font">
|
||||
<?php if ($auth->isAuthenticated()): ?>
|
||||
<a class="bracketed" href="<?= $url->generate('logout') ?>">Logout</a>
|
||||
<?= $helper->a(
|
||||
$url->generate('logout'),
|
||||
'Logout',
|
||||
['class' => 'bracketed']
|
||||
) ?>
|
||||
<?php else: ?>
|
||||
[<a href="<?= $url->generate('login'); ?>"><?= $config->get('whose_list') ?>'s Login</a>]
|
||||
[<?= $helper->a($url->generate('login'), "{$whose} Login") ?>]
|
||||
<?php endif ?>
|
||||
</span>
|
||||
</h1>
|
||||
@ -40,8 +61,8 @@
|
||||
<?= $helper->menu($menu_name) ?>
|
||||
<br />
|
||||
<ul>
|
||||
<li class="<?= Util::isNotSelected('list', $urlGenerator->lastSegment()) ?>"><a href="<?= $urlGenerator->url($route_path) ?>">Cover View</a></li>
|
||||
<li class="<?= Util::isSelected('list', $urlGenerator->lastSegment()) ?>"><a href="<?= $urlGenerator->url("{$route_path}/list") ?>">List View</a></li>
|
||||
<li class="<?= Util::isNotSelected('list', $lastSegment) ?>"><a href="<?= $urlGenerator->url($route_path) ?>">Cover View</a></li>
|
||||
<li class="<?= Util::isSelected('list', $lastSegment) ?>"><a href="<?= $urlGenerator->url("{$route_path}/list") ?>">List View</a></li>
|
||||
</ul>
|
||||
<?php endif ?>
|
||||
</nav>
|
||||
|
@ -17,7 +17,7 @@
|
||||
<?php /* <button class="plus_one_volume">+1 Volume</button> */ ?>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
<img src="<?= $escape->attr($item['manga']['image']) ?>" />
|
||||
<img src="<?= $urlGenerator->assetUrl('images/manga', "{$item['manga']['id']}.jpg") ?>" />
|
||||
<div class="name">
|
||||
<a href="<?= $url->generate('manga.details', ['id' => $item['manga']['slug']]) ?>">
|
||||
<?= $escape->html(array_shift($item['manga']['titles'])) ?>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<main class="details fixed">
|
||||
<section class="flex flex-no-wrap">
|
||||
<div>
|
||||
<img class="cover" src="<?= $data['cover_image'] ?>" alt="<?= $data['title'] ?> cover image" />
|
||||
<img class="cover" src="<?= $urlGenerator->assetUrl('images/manga', "{$data['id']}.jpg") ?>" alt="<?= $data['title'] ?> cover image" />
|
||||
<br />
|
||||
<br />
|
||||
<table>
|
||||
@ -39,7 +39,7 @@
|
||||
<?php if (count($characters) > 0): ?>
|
||||
<h2>Characters</h2>
|
||||
<section class="media-wrap">
|
||||
<?php foreach($characters as $char): ?>
|
||||
<?php foreach($characters as $id => $char): ?>
|
||||
<?php if ( ! empty($char['image']['original'])): ?>
|
||||
<article class="character">
|
||||
<?php $link = $url->generate('character', ['slug' => $char['slug']]) ?>
|
||||
@ -47,7 +47,7 @@
|
||||
<?= $helper->a($link, $char['name']); ?>
|
||||
</div>
|
||||
<a href="<?= $link ?>">
|
||||
<?= $helper->img($char['image']['original'], [
|
||||
<?= $helper->img($urlGenerator->assetUrl('images/characters', "{$id}.jpg"), [
|
||||
'width' => '225'
|
||||
]) ?>
|
||||
</a>
|
||||
|
@ -15,7 +15,7 @@
|
||||
</th>
|
||||
<th>
|
||||
<article class="media">
|
||||
<?= $helper->img($item['manga']['image']); ?>
|
||||
<?= $helper->img($urlGenerator->assetUrl('images/manga', "{$item['manga']['id']}.jpg")); ?>
|
||||
</article>
|
||||
</th>
|
||||
</tr>
|
||||
|
@ -9,7 +9,12 @@
|
||||
<?= $attributes['name'] ?>
|
||||
</a>
|
||||
</h2>
|
||||
<img src="<?= $attributes['avatar']['original'] ?>" alt="" />
|
||||
<?php
|
||||
$file = basename(parse_url($attributes['avatar']['original'], \PHP_URL_PATH));
|
||||
$parts = explode('.', $file);
|
||||
$ext = end($parts);
|
||||
?>
|
||||
<img src="<?= $urlGenerator->assetUrl('images/avatars', "{$data['id']}.{$ext}") ?>" alt="" />
|
||||
</center>
|
||||
<br />
|
||||
<br />
|
||||
@ -65,13 +70,13 @@
|
||||
<?php if ( ! empty($favorites['characters'])): ?>
|
||||
<h4>Favorite Characters</h4>
|
||||
<section class="media-wrap">
|
||||
<?php foreach($favorites['characters'] as $char): ?>
|
||||
<?php foreach($favorites['characters'] as $id => $char): ?>
|
||||
<?php if ( ! empty($char['image']['original'])): ?>
|
||||
<article class="small_character">
|
||||
<?php $link = $url->generate('character', ['slug' => $char['slug']]) ?>
|
||||
<div class="name"><?= $helper->a($link, $char['name']); ?></div>
|
||||
<a href="<?= $link ?>">
|
||||
<?= $helper->img($char['image']['original']) ?>
|
||||
<?= $helper->img($urlGenerator->assetUrl('images/characters', "{$char['id']}.jpg")) ?>
|
||||
</a>
|
||||
</article>
|
||||
<?php endif ?>
|
||||
@ -88,7 +93,7 @@
|
||||
$titles = Kitsu::filterTitles($anime);
|
||||
?>
|
||||
<a href="<?= $link ?>">
|
||||
<img src="<?= $anime['posterImage']['small'] ?>" width="220" alt="" />
|
||||
<img src="<?= $urlGenerator->assetUrl('images/anime', "{$anime['id']}.jpg") ?>" width="220" alt="" />
|
||||
</a>
|
||||
<div class="name">
|
||||
<a href="<?= $link ?>">
|
||||
@ -112,7 +117,7 @@
|
||||
$titles = Kitsu::filterTitles($manga);
|
||||
?>
|
||||
<a href="<?= $link ?>">
|
||||
<img src="<?= $manga['posterImage']['small'] ?>" width="220" alt="" />
|
||||
<img src="<?= $urlGenerator->assetUrl('images/manga', "{$manga['id']}.jpg") ?>" width="220" alt="" />
|
||||
</a>
|
||||
<div class="name">
|
||||
<a href="<?= $link ?>">
|
||||
|
@ -21,7 +21,7 @@
|
||||
"aura/router": "^3.0",
|
||||
"aura/session": "^2.0",
|
||||
"aviat/banker": "^1.0.0",
|
||||
"aviat/ion": "^2.0.0",
|
||||
"aviat/ion": "^2.1.0",
|
||||
"monolog/monolog": "^1.0",
|
||||
"psr/http-message": "~1.0",
|
||||
"psr/log": "~1.0",
|
||||
@ -37,12 +37,13 @@
|
||||
"phploc/phploc": "^3.0",
|
||||
"phpmd/phpmd": "^2.4",
|
||||
"phpunit/phpunit": "^6.0",
|
||||
"robmorgan/phinx": "~0.6.4",
|
||||
"robmorgan/phinx": "^0.8.0",
|
||||
"consolidation/robo": "~1.0",
|
||||
"henrikbjorn/lurker": "^1.1.0",
|
||||
"symfony/var-dumper": "^3.2",
|
||||
"squizlabs/php_codesniffer": "^3.0.0@beta",
|
||||
"phpstan/phpstan": "^0.6.4"
|
||||
"phpstan/phpstan": "^0.6.4",
|
||||
"spatie/phpunit-snapshot-assertions": "^0.4.1"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "vendor/bin/robo build",
|
||||
|
4
console
4
console
@ -18,9 +18,9 @@ unset($APP_DIR);
|
||||
unset($SRC_DIR);
|
||||
unset($CONF_DIR);
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
// -----------------------------------------------------------------------------
|
||||
// Start console script
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
// -----------------------------------------------------------------------------
|
||||
$console = new \ConsoleKit\Console([
|
||||
'cache-prime' => Command\CachePrime::class,
|
||||
'cache-clear' => Command\CacheClear::class,
|
||||
|
@ -1,12 +0,0 @@
|
||||
{
|
||||
"source": {
|
||||
"directories": [
|
||||
"src"
|
||||
]
|
||||
},
|
||||
"timeout": 10,
|
||||
"logs": {
|
||||
"text": "build\/humbuglog.txt",
|
||||
"json": "build\/humbug.json"
|
||||
}
|
||||
}
|
@ -56,7 +56,7 @@
|
||||
</collector>
|
||||
|
||||
<!-- Configuration of generation process -->
|
||||
<generator output="docs">
|
||||
<generator>
|
||||
<!-- @output - (Base-)Directory to store output data in -->
|
||||
|
||||
<!-- A generation process consists of one or more build tasks and of (optional) enrich sources -->
|
||||
@ -117,10 +117,10 @@
|
||||
<!-- An engine and thus build node can have additional configuration child nodes, please check the documentation for the engine to find out more -->
|
||||
|
||||
<!-- default engine "html" -->
|
||||
<build engine="html" output="html" />
|
||||
<!-- <template dir="${phpDox.home}/templates/html" /> -
|
||||
<build engine="html" output="apidocs">
|
||||
<!-- <template dir="${phpDox.home}/templates/html" /> -->
|
||||
<file extension="html" />
|
||||
</build> -->
|
||||
</build>
|
||||
|
||||
</generator>
|
||||
</project>
|
||||
|
29
public/css.js
Normal file
29
public/css.js
Normal file
@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Script for optimizing css
|
||||
*/
|
||||
const fs = require('fs');
|
||||
const postcss = require('postcss');
|
||||
const atImport = require('postcss-import');
|
||||
const cssNext = require('postcss-cssnext');
|
||||
const cssNano = require('cssnano');
|
||||
|
||||
const css = fs.readFileSync('css/base.css', 'utf8');
|
||||
|
||||
postcss()
|
||||
.use(atImport())
|
||||
.use(cssNext())
|
||||
.use(cssNano({
|
||||
autoprefixer: false,
|
||||
colormin: false,
|
||||
minifyFontValues: false,
|
||||
options: {
|
||||
sourcemap: false
|
||||
}
|
||||
}))
|
||||
.process(css, {
|
||||
from: 'css/base.css',
|
||||
to: 'css/app.min.css'
|
||||
}).then(result => {
|
||||
fs.writeFileSync('css/app.min.css', result.css);
|
||||
fs.writeFileSync('css/app.min.css.map', result.map);
|
||||
});
|
180
public/css.php
180
public/css.php
@ -1,180 +0,0 @@
|
||||
<?php declare(strict_types=1);
|
||||
/**
|
||||
* Hummingbird Anime List Client
|
||||
*
|
||||
* An API client for Kitsu and MyAnimeList to manage anime and manga watch lists
|
||||
*
|
||||
* PHP version 7
|
||||
*
|
||||
* @package HummingbirdAnimeClient
|
||||
* @author Timothy J. Warren <tim@timshomepage.net>
|
||||
* @copyright 2015 - 2017 Timothy J. Warren
|
||||
* @license http://www.opensource.org/licenses/mit-license.html MIT License
|
||||
* @version 4.0
|
||||
* @link https://github.com/timw4mail/HummingBirdAnimeClient
|
||||
*/
|
||||
|
||||
namespace Aviat\EasyMin;
|
||||
|
||||
require_once('./min.php');
|
||||
|
||||
/**
|
||||
* Simple CSS Minifier
|
||||
*/
|
||||
class CSSMin extends BaseMin {
|
||||
|
||||
protected $cssRoot;
|
||||
protected $pathFrom;
|
||||
protected $pathTo;
|
||||
protected $group;
|
||||
protected $lastModified;
|
||||
protected $requestedTime;
|
||||
|
||||
public function __construct(array $config, array $groups)
|
||||
{
|
||||
$group = $_GET['g'];
|
||||
$this->cssRoot = $config['css_root'];
|
||||
$this->pathFrom = $config['path_from'];
|
||||
$this->pathTo = $config['path_to'];
|
||||
$this->group = $groups[$group];
|
||||
$this->lastModified = $this->getLastModified();
|
||||
|
||||
$this->send();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the CSS
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function send()
|
||||
{
|
||||
if($this->lastModified >= $this->getIfModified() && $this->isNotDebug())
|
||||
{
|
||||
throw new FileNotChangedException();
|
||||
}
|
||||
|
||||
$css = ( ! array_key_exists('debug', $_GET))
|
||||
? $this->compress($this->getCss())
|
||||
: $this->getCss();
|
||||
|
||||
$this->output($css);
|
||||
}
|
||||
|
||||
/**
|
||||
* Function for compressing the CSS as tightly as possible
|
||||
*
|
||||
* @param string $buffer
|
||||
* @return string
|
||||
*/
|
||||
protected function compress($buffer)
|
||||
{
|
||||
//Remove CSS comments
|
||||
$buffer = preg_replace('!/\*[^*]*\*+([^/][^*]*\*+)*/!', '', $buffer);
|
||||
|
||||
//Remove tabs, spaces, newlines, etc.
|
||||
$buffer = preg_replace('`\s+`', ' ', $buffer);
|
||||
$replace = [
|
||||
' )' => ')',
|
||||
') ' => ')',
|
||||
' }' => '}',
|
||||
'} ' => '}',
|
||||
' {' => '{',
|
||||
'{ ' => '{',
|
||||
', ' => ',',
|
||||
': ' => ':',
|
||||
'; ' => ';',
|
||||
];
|
||||
|
||||
//Eradicate every last space!
|
||||
$buffer = trim(strtr($buffer, $replace));
|
||||
$buffer = str_replace('{ ', '{', $buffer);
|
||||
$buffer = str_replace('} ', '}', $buffer);
|
||||
|
||||
return $buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the most recent file modification date
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
protected function getLastModified()
|
||||
{
|
||||
$modified = [];
|
||||
|
||||
// Get all the css files, and concatenate them together
|
||||
if(isset($this->group))
|
||||
{
|
||||
foreach($this->group as $file)
|
||||
{
|
||||
$newFile = realpath("{$this->cssRoot}{$file}");
|
||||
$modified[] = filemtime($newFile);
|
||||
}
|
||||
}
|
||||
|
||||
//Add this page for last modified check
|
||||
$modified[] = filemtime(__FILE__);
|
||||
|
||||
//Get the latest modified date
|
||||
rsort($modified);
|
||||
|
||||
return array_shift($modified);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the css to display
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getCss()
|
||||
{
|
||||
$css = '';
|
||||
|
||||
foreach($this->group as $file)
|
||||
{
|
||||
$newFile = realpath("{$this->cssRoot}{$file}");
|
||||
$css .= file_get_contents($newFile);
|
||||
}
|
||||
|
||||
// Correct paths that have changed due to concatenation
|
||||
// based on rules in the config file
|
||||
$css = str_replace($this->pathFrom, $this->pathTo, $css);
|
||||
|
||||
return $css;
|
||||
}
|
||||
|
||||
/**
|
||||
* Output the CSS
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function output($css)
|
||||
{
|
||||
$this->sendFinalOutput($css, 'text/css', $this->lastModified);
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// ! Start Minifying
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
//Get config files
|
||||
$config = require('../app/appConf/minify_config.php');
|
||||
$groups = require($config['css_groups_file']);
|
||||
|
||||
if ( ! array_key_exists($_GET['g'], $groups))
|
||||
{
|
||||
throw new InvalidArgumentException('You must specify a css group that exists');
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
new CSSMin($config, $groups);
|
||||
}
|
||||
catch (FileNotChangedException $e)
|
||||
{
|
||||
BaseMin::send304();
|
||||
}
|
||||
|
||||
//End of css.php
|
1
public/css/app.min.css
vendored
Normal file
1
public/css/app.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
public/css/app.min.css.map
Normal file
1
public/css/app.min.css.map
Normal file
@ -0,0 +1 @@
|
||||
undefined
|
File diff suppressed because it is too large
Load Diff
@ -1,643 +0,0 @@
|
||||
@import "./marx.myth.css";
|
||||
|
||||
:root {
|
||||
--link-shadow: 1px 1px 1px #000;
|
||||
--shadow: 1px 2px 1px rgba(0, 0, 0, 0.85);
|
||||
--title-overlay: rgba(0, 0, 0, 0.45);
|
||||
--title-overlay-fallback: #000;
|
||||
--text-color: #ffffff;
|
||||
--normal-padding: 0.25em 0.125em;
|
||||
--link-hover-color: #7d12db;
|
||||
--edit-link-hover-color: #db7d12;
|
||||
--edit-link-color: #12db18;
|
||||
--radius: 5px;
|
||||
}
|
||||
|
||||
template, [hidden="hidden"], .media[hidden] {display:none}
|
||||
|
||||
body {margin: 0.5em;}
|
||||
|
||||
button {
|
||||
background:rgba(255,255,255,0.65);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
table {
|
||||
min-width:85%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
td {
|
||||
padding:1em;
|
||||
padding:1rem;
|
||||
}
|
||||
|
||||
thead td, thead th {
|
||||
padding:0.5em;
|
||||
padding:0.5rem;
|
||||
}
|
||||
|
||||
input[type=number] {
|
||||
width: 4em;
|
||||
}
|
||||
|
||||
tbody > tr:nth-child(odd) {
|
||||
background: #ddd;
|
||||
}
|
||||
|
||||
a:hover, a:active {
|
||||
color: var(--link-hover-color)
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------------------------------
|
||||
Utility classes
|
||||
------------------------------------------------------------------------------*/
|
||||
|
||||
.bracketed {
|
||||
color: var(--edit-link-color);
|
||||
}
|
||||
.bracketed, h1 a {
|
||||
text-shadow: var(--link-shadow);
|
||||
}
|
||||
.bracketed:before {content: '[\00a0'}
|
||||
.bracketed:after {content: '\00a0]'}
|
||||
.bracketed:hover, .bracketed:active {
|
||||
color: var(--edit-link-hover-color)
|
||||
}
|
||||
|
||||
.grow-1 {flex-grow: 1}
|
||||
.flex-wrap {flex-wrap: wrap}
|
||||
.flex-no-wrap {flex-wrap: nowrap}
|
||||
.flex-align-end {align-items: flex-end}
|
||||
.flex-align-space-around {align-content: space-around}
|
||||
.flex-justify-space-around {justify-content: space-around}
|
||||
.flex-self-center {align-self:center}
|
||||
.flex {display: flex}
|
||||
|
||||
.small-font {
|
||||
font-size:1.6rem;
|
||||
}
|
||||
|
||||
.justify {text-align:justify}
|
||||
.align_center {text-align:center}
|
||||
.align_left {text-align:left}
|
||||
.align_right {text-align:right}
|
||||
|
||||
.valign_top {vertical-align:top}
|
||||
|
||||
.no_border {border:none}
|
||||
|
||||
.media-wrap {
|
||||
text-align:center;
|
||||
margin:0 auto;
|
||||
}
|
||||
|
||||
.danger {
|
||||
background-color: #ff4136;
|
||||
border-color: #924949;
|
||||
color:#fff;
|
||||
}
|
||||
|
||||
.danger:hover, .danger:active {
|
||||
background-color: #924949;
|
||||
border-color: #ff4136;
|
||||
color:#fff;
|
||||
}
|
||||
|
||||
.user-btn {
|
||||
border-color: var(--edit-link-color);
|
||||
color: var(--edit-link-color);
|
||||
text-shadow: var(--link-shadow);
|
||||
padding:0 0.5em;
|
||||
padding:0 0.5rem;
|
||||
}
|
||||
.user-btn:hover, .user-btn:active {
|
||||
border-color: var(--edit-link-hover-color);
|
||||
background-color: var(--edit-link-hover-color);
|
||||
}
|
||||
|
||||
.full_width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------------------------------
|
||||
CSS loading icon
|
||||
------------------------------------------------------------------------------*/
|
||||
|
||||
.cssload-loader {
|
||||
position: relative;
|
||||
left: calc(50% - 31px);
|
||||
width: 62px;
|
||||
height: 62px;
|
||||
border-radius: 50%;
|
||||
perspective: 780px;
|
||||
}
|
||||
|
||||
.cssload-inner {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.cssload-inner.cssload-one {
|
||||
left: 0%;
|
||||
top: 0%;
|
||||
animation: cssload-rotate-one 1.15s linear infinite;
|
||||
border-bottom: 3px solid rgb(0,0,0);
|
||||
}
|
||||
|
||||
.cssload-inner.cssload-two {
|
||||
right: 0%;
|
||||
top: 0%;
|
||||
animation: cssload-rotate-two 1.15s linear infinite;
|
||||
border-right: 3px solid rgb(0,0,0);
|
||||
}
|
||||
|
||||
.cssload-inner.cssload-three {
|
||||
right: 0%;
|
||||
bottom: 0%;
|
||||
animation: cssload-rotate-three 1.15s linear infinite;
|
||||
border-top: 3px solid rgb(0,0,0);
|
||||
}
|
||||
|
||||
@keyframes cssload-rotate-one {
|
||||
0% {
|
||||
transform: rotateX(35deg) rotateY(-45deg) rotateZ(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotateX(35deg) rotateY(-45deg) rotateZ(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes cssload-rotate-two {
|
||||
0% {
|
||||
transform: rotateX(50deg) rotateY(10deg) rotateZ(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotateX(50deg) rotateY(10deg) rotateZ(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes cssload-rotate-three {
|
||||
0% {
|
||||
transform: rotateX(35deg) rotateY(55deg) rotateZ(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotateX(35deg) rotateY(55deg) rotateZ(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------------------------------
|
||||
Table sorting and form styles
|
||||
------------------------------------------------------------------------------*/
|
||||
.sorting,
|
||||
.sorting_asc,
|
||||
.sorting_desc {
|
||||
vertical-align:text-bottom;
|
||||
}
|
||||
.sorting::before {
|
||||
content: " ↕\00a0";
|
||||
}
|
||||
.sorting_asc::before {
|
||||
content: " ↑\00a0";
|
||||
}
|
||||
.sorting_desc::before {
|
||||
content: " ↓\00a0";
|
||||
}
|
||||
|
||||
.form { width:100%; }
|
||||
|
||||
.form thead th, .form thead tr {
|
||||
background: inherit;
|
||||
border:0;
|
||||
}
|
||||
|
||||
.form tr > td:nth-child(odd) {
|
||||
text-align:right;
|
||||
min-width:25px;
|
||||
max-width:30%;
|
||||
}
|
||||
.form tr > td:nth-child(even) {
|
||||
text-align:left;
|
||||
width:70%;
|
||||
}
|
||||
|
||||
.invisible tbody > tr:nth-child(odd) {
|
||||
background: inherit;
|
||||
}
|
||||
.invisible tr, .invisible td, .invisible th {
|
||||
border:0;
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------------------------------
|
||||
Message boxes
|
||||
------------------------------------------------------------------------------*/
|
||||
|
||||
.message{
|
||||
position:relative;
|
||||
margin:0.5em auto;
|
||||
padding:0.5em;
|
||||
width:95%;
|
||||
}
|
||||
|
||||
.message .close{
|
||||
width:1em;
|
||||
height:1em;
|
||||
position:absolute;
|
||||
right:0.5em;
|
||||
top:0.5em;
|
||||
text-align:center;
|
||||
vertical-align:middle;
|
||||
line-height:1em;
|
||||
}
|
||||
|
||||
.message:hover .close:after {
|
||||
content: '☒';
|
||||
}
|
||||
|
||||
.message:hover {
|
||||
cursor:pointer;
|
||||
}
|
||||
|
||||
.message .icon{
|
||||
left:0.5em;
|
||||
top:0.5em;
|
||||
margin-right:1em;
|
||||
}
|
||||
|
||||
.message.error{
|
||||
border:1px solid #924949;
|
||||
background: #f3e6e6;
|
||||
}
|
||||
|
||||
.message.error .icon::after {
|
||||
content: '✘';
|
||||
}
|
||||
|
||||
.message.success{
|
||||
border:1px solid #1f8454;
|
||||
background: #70dda9;
|
||||
}
|
||||
.message.success .icon::after {
|
||||
content: '✔'
|
||||
}
|
||||
|
||||
.message.info{
|
||||
border:1px solid #bfbe3a;
|
||||
background: #FFFFCC;
|
||||
}
|
||||
|
||||
.message.info .icon::after {
|
||||
content: '⚠';
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------------------------------
|
||||
Base list styles
|
||||
------------------------------------------------------------------------------*/
|
||||
|
||||
.media, .character, .small_character {
|
||||
position:relative;
|
||||
vertical-align:top;
|
||||
display:inline-block;
|
||||
text-align:center;
|
||||
width:220px;
|
||||
height:311px;
|
||||
margin: var(--normal-padding);
|
||||
}
|
||||
|
||||
.media > img,
|
||||
.character > img,
|
||||
.small_character > img {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.media .edit_buttons > button {
|
||||
margin:0.5em auto;
|
||||
}
|
||||
|
||||
.name,
|
||||
.media_metadata > div,
|
||||
.medium_metadata > div,
|
||||
.row {
|
||||
text-shadow: var(--shadow);
|
||||
background: var(--title-overlay-fallback);
|
||||
background: var(--title-overlay);
|
||||
color: var(--text-color);
|
||||
padding: var(--normal-padding);
|
||||
text-align:right;
|
||||
}
|
||||
|
||||
.media_type, .age_rating {
|
||||
text-align:left;
|
||||
}
|
||||
|
||||
.media > .media_metadata {
|
||||
position:absolute;
|
||||
bottom:0;
|
||||
right:0;
|
||||
}
|
||||
|
||||
.media > .medium_metadata {
|
||||
position:absolute;
|
||||
bottom: 0;
|
||||
left:0;
|
||||
}
|
||||
|
||||
.media > .name {
|
||||
position:absolute;
|
||||
top: 0;
|
||||
}
|
||||
.small_character:hover > .name,
|
||||
.character:hover > .name,
|
||||
.media:hover > .name,
|
||||
.media:hover > .media_metadata > div,
|
||||
.media:hover > .medium_metadata > div,
|
||||
.media:hover > .table .row
|
||||
{
|
||||
transition: .25s ease;
|
||||
background:rgba(0,0,0,0.75);
|
||||
}
|
||||
|
||||
.media:hover > button[hidden],
|
||||
.media:hover > .edit_buttons[hidden]
|
||||
{
|
||||
transition: .25s ease;
|
||||
display:block;
|
||||
}
|
||||
|
||||
.small_character > .name a,
|
||||
.small_character > .name a small,
|
||||
.character > .name a,
|
||||
.character > .name a small,
|
||||
.media > .name a,
|
||||
.media > .name a small
|
||||
{
|
||||
background:none;
|
||||
color:#fff;
|
||||
text-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* -----------------------------------------------------------------------------
|
||||
Anime-list-specific styles
|
||||
------------------------------------------------------------------------------*/
|
||||
.anime .name, .manga .name {
|
||||
text-align:center;
|
||||
width:100%;
|
||||
padding:0.5em 0.25em;
|
||||
}
|
||||
|
||||
.anime .media_type,
|
||||
.anime .airing_status,
|
||||
.anime .user_rating,
|
||||
.anime .completion,
|
||||
.anime .age_rating,
|
||||
.anime .edit,
|
||||
.anime .delete {
|
||||
background: none;
|
||||
text-align:center;
|
||||
}
|
||||
|
||||
|
||||
.anime .table, .manga .table {
|
||||
position:absolute;
|
||||
bottom:0;
|
||||
left:0;
|
||||
width:100%;
|
||||
}
|
||||
|
||||
.anime .row, .manga .row {
|
||||
width:100%;
|
||||
background: var(--title-overlay-fallback);
|
||||
background: var(--title-overlay);
|
||||
display: flex;
|
||||
align-content: space-around;
|
||||
justify-content: space-around;
|
||||
text-align:center;
|
||||
padding:0 inherit;
|
||||
}
|
||||
|
||||
.anime .row > span, .manga .row > span {
|
||||
text-align:left;
|
||||
}
|
||||
|
||||
.anime .row > div, .manga .row > div {
|
||||
font-size:0.8em;
|
||||
display:flex-item;
|
||||
align-self:center;
|
||||
text-align:center;
|
||||
vertical-align:middle;
|
||||
}
|
||||
|
||||
.anime .media > button.plus_one {
|
||||
position:absolute;
|
||||
top: 138px;
|
||||
top: calc(50% - 21.5px);
|
||||
left: 44px;
|
||||
left: calc(50% - 66.5px);
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------------------------------
|
||||
Manga-list-specific styles
|
||||
------------------------------------------------------------------------------*/
|
||||
.manga .row {
|
||||
padding:1px;
|
||||
}
|
||||
|
||||
.manga .media {
|
||||
border:1px solid #ddd;
|
||||
height:310px;
|
||||
margin:0.25em;
|
||||
}
|
||||
|
||||
.manga .media > .edit_buttons {
|
||||
position:absolute;
|
||||
top: 86px;
|
||||
top: calc(50% - 58.5px);
|
||||
left: 43.5px;
|
||||
left: calc(50% - 66.5px);
|
||||
}
|
||||
|
||||
|
||||
/* -----------------------------------------------------------------------------
|
||||
Search page styles
|
||||
------------------------------------------------------------------------------*/
|
||||
.media.search > .name {
|
||||
background-color:#555;
|
||||
background-color: rgba(000,000,000,0.35);
|
||||
background-size: cover;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.big-check {
|
||||
display:none;
|
||||
}
|
||||
|
||||
.big-check:checked + label {
|
||||
transition: .25s ease;
|
||||
background:rgba(0,0,0,0.75);
|
||||
}
|
||||
|
||||
.big-check:checked + label:after {
|
||||
content: '✓';
|
||||
font-size: 15em;
|
||||
font-size: 15rem;
|
||||
text-align:center;
|
||||
color: greenyellow;
|
||||
position:absolute;
|
||||
top:147px;
|
||||
left:0;
|
||||
height:100%;
|
||||
width:100%;
|
||||
}
|
||||
|
||||
#series_list article.media {
|
||||
position:relative;
|
||||
}
|
||||
#series_list .name, #series_list .name label {
|
||||
position:absolute;
|
||||
display:block;
|
||||
top:0;
|
||||
left:0;
|
||||
height:100%;
|
||||
width:100%;
|
||||
vertical-align:middle;
|
||||
line-height: 1.25em;
|
||||
}
|
||||
#series_list .name small {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
Details page styles
|
||||
-----------------------------------------------------------------------------*/
|
||||
.details {
|
||||
margin: 1.5rem auto 0 auto;
|
||||
padding:1rem;
|
||||
font-size:inherit;
|
||||
}
|
||||
|
||||
.details.fixed {
|
||||
max-width:93rem;
|
||||
}
|
||||
|
||||
.details .cover {
|
||||
display: block;
|
||||
width: 284px;
|
||||
/* height: 402px; */
|
||||
}
|
||||
|
||||
.details h2 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.details .flex > div {
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
.details .media_details {
|
||||
max-width:300px;
|
||||
}
|
||||
.details .media_details td {
|
||||
padding:0 1.5rem;
|
||||
}
|
||||
|
||||
.details p {
|
||||
text-align:justify;
|
||||
}
|
||||
|
||||
.details .media_details td:nth-child(odd) {
|
||||
width:1%;
|
||||
white-space:nowrap;
|
||||
text-align:right;
|
||||
}
|
||||
.details .media_details td:nth-child(even) {
|
||||
text-align:left;
|
||||
}
|
||||
|
||||
.character,
|
||||
.small_character {
|
||||
background: rgba(0,0,0,0.5);
|
||||
width: 225px;
|
||||
height: 350px;
|
||||
vertical-align: middle;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.small_character a {
|
||||
display:inline-block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.small_character .name,
|
||||
.character .name {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.small_character img,
|
||||
.character img {
|
||||
position: relative;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
z-index: 5;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
User page styles
|
||||
-----------------------------------------------------------------------------*/
|
||||
.small_character {
|
||||
width: 160px;
|
||||
height: 250px;
|
||||
}
|
||||
|
||||
.user-page .media-wrap {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.media a {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
Viewport-based styles
|
||||
-----------------------------------------------------------------------------*/
|
||||
|
||||
@media screen and (max-width: 40em) {
|
||||
nav a {
|
||||
line-height:4em;
|
||||
line-height:4rem;
|
||||
}
|
||||
|
||||
.media {
|
||||
margin:2px 0;
|
||||
}
|
||||
|
||||
main {
|
||||
padding:0 0,5em 0.5em;
|
||||
padding:0 0.5rem 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
Images / Logos
|
||||
-----------------------------------------------------------------------------*/
|
||||
.streaming-logo {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
vertical-align:middle;
|
||||
}
|
||||
|
||||
.cover_streaming_link .streaming-logo {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
:root
|
||||
{
|
||||
--default-font-list:'Open Sans', 'Nimbus Sans L', 'Helvetica Neue', Helvetica, 'Lucida Grande', sans-serif;
|
||||
:root {
|
||||
--default-font-list: system-ui,sans-serif;
|
||||
--monospace-font-list:'Anonymous Pro','Fira Code',Menlo,Monaco,Consolas,'Courier New',monospace;
|
||||
--serif-font-list:Georgia,Times,'Times New Roman',serif;
|
||||
-ms-text-size-adjust:100%;
|
||||
-webkit-text-size-adjust:100%;
|
||||
box-sizing:border-box;
|
||||
@ -9,48 +10,41 @@
|
||||
line-height:1.4;
|
||||
overflow-y:scroll;
|
||||
text-size-adjust:100%;
|
||||
scroll-behavior: smooth;
|
||||
scroll-behavior:smooth;
|
||||
}
|
||||
|
||||
audio:not([controls])
|
||||
{
|
||||
audio:not([controls]) {
|
||||
display:none;
|
||||
}
|
||||
|
||||
details
|
||||
{
|
||||
details {
|
||||
display:block;
|
||||
}
|
||||
|
||||
input[type=search]
|
||||
{
|
||||
input[type=search] {
|
||||
-webkit-appearance:textfield;
|
||||
}
|
||||
|
||||
input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration
|
||||
{
|
||||
input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration {
|
||||
-webkit-appearance:none;
|
||||
}
|
||||
|
||||
main
|
||||
{
|
||||
main {
|
||||
display:block;
|
||||
margin:0 auto;
|
||||
padding:0 1.6em 1.6em;
|
||||
padding:0 1.6rem 1.6rem;
|
||||
}
|
||||
|
||||
summary
|
||||
{
|
||||
summary {
|
||||
display:block;
|
||||
}
|
||||
|
||||
pre
|
||||
{
|
||||
pre {
|
||||
background:#efefef;
|
||||
color:#444;
|
||||
display:block;
|
||||
font-family:Menlo, Monaco, Consolas, 'Courier New', monospace;
|
||||
font-family:var(--monospace-font-list);
|
||||
font-size:1.4em;
|
||||
font-size:1.4rem;
|
||||
margin:1.6em 0;
|
||||
@ -62,29 +56,24 @@ pre
|
||||
word-wrap:break-word;
|
||||
}
|
||||
|
||||
progress
|
||||
{
|
||||
progress {
|
||||
display:inline-block;
|
||||
}
|
||||
|
||||
small
|
||||
{
|
||||
small {
|
||||
color:#777;
|
||||
font-size:75%;
|
||||
}
|
||||
|
||||
big
|
||||
{
|
||||
big {
|
||||
font-size:125%;
|
||||
}
|
||||
|
||||
template
|
||||
{
|
||||
template {
|
||||
display:none;
|
||||
}
|
||||
|
||||
textarea
|
||||
{
|
||||
textarea {
|
||||
border:.1rem solid #ccc;
|
||||
border-radius:0;
|
||||
display:block;
|
||||
@ -95,55 +84,47 @@ textarea
|
||||
vertical-align:middle;
|
||||
}
|
||||
|
||||
[hidden]
|
||||
{
|
||||
[hidden] {
|
||||
display:none;
|
||||
}
|
||||
|
||||
[unselectable]
|
||||
{
|
||||
[unselectable] {
|
||||
-moz-user-select:none;
|
||||
-ms-user-select:none;
|
||||
-webkit-user-select:none;
|
||||
user-select:none;
|
||||
}
|
||||
|
||||
*,::before,::after
|
||||
{
|
||||
*,::before,::after {
|
||||
border-style:solid;
|
||||
border-width:0;
|
||||
box-sizing:inherit;
|
||||
}
|
||||
|
||||
*
|
||||
{
|
||||
* {
|
||||
font-size:inherit;
|
||||
line-height:inherit;
|
||||
margin:0;
|
||||
padding:0;
|
||||
}
|
||||
|
||||
::before,::after
|
||||
{
|
||||
::before,::after {
|
||||
text-decoration:inherit;
|
||||
vertical-align:inherit;
|
||||
}
|
||||
|
||||
a
|
||||
{
|
||||
a {
|
||||
-webkit-transition:.25s ease;
|
||||
color:#1271db;
|
||||
text-decoration:none;
|
||||
transition:.25s ease;
|
||||
}
|
||||
|
||||
audio,canvas,iframe,img,svg,video
|
||||
{
|
||||
audio,canvas,iframe,img,svg,video {
|
||||
vertical-align:middle;
|
||||
}
|
||||
|
||||
button,input,select,textarea
|
||||
{
|
||||
button,input,select,textarea {
|
||||
border:.1rem solid #ccc;
|
||||
color:inherit;
|
||||
font-family:inherit;
|
||||
@ -152,37 +133,31 @@ button,input,select,textarea
|
||||
min-height:1.4em;
|
||||
}
|
||||
|
||||
code,kbd,pre,samp
|
||||
{
|
||||
font-family:Menlo, Monaco, Consolas, 'Courier New', monospace, monospace;
|
||||
code,kbd,pre,samp {
|
||||
font-family:var(--monospace-font-list);
|
||||
}
|
||||
|
||||
table
|
||||
{
|
||||
table {
|
||||
border-collapse:collapse;
|
||||
border-spacing:0;
|
||||
margin-bottom:1.6rem;
|
||||
}
|
||||
|
||||
::-moz-selection
|
||||
{
|
||||
::-moz-selection {
|
||||
background-color:#b3d4fc;
|
||||
text-shadow:none;
|
||||
}
|
||||
|
||||
::selection
|
||||
{
|
||||
::selection {
|
||||
background-color:#b3d4fc;
|
||||
text-shadow:none;
|
||||
}
|
||||
|
||||
button::-moz-focus-inner
|
||||
{
|
||||
button::-moz-focus-inner {
|
||||
border:0;
|
||||
}
|
||||
|
||||
body
|
||||
{
|
||||
body {
|
||||
color:#444;
|
||||
font-family:var(--default-font-list);
|
||||
font-size:1.6rem;
|
||||
@ -191,20 +166,17 @@ body
|
||||
padding:0;
|
||||
}
|
||||
|
||||
p
|
||||
{
|
||||
p {
|
||||
margin:0 0 1.6rem;
|
||||
}
|
||||
|
||||
h1,h2,h3,h4,h5,h6
|
||||
{
|
||||
font-family:Lato, var(--default-font-list);
|
||||
h1,h2,h3,h4,h5,h6 {
|
||||
font-family:var(--default-font-list);
|
||||
margin:2em 0 1.6em;
|
||||
margin:2rem 0 1.6rem;
|
||||
}
|
||||
|
||||
h1
|
||||
{
|
||||
h1 {
|
||||
border-bottom:.1rem solid rgba(0,0,0,0.2);
|
||||
font-size:3.6em;
|
||||
font-size:3.6rem;
|
||||
@ -212,16 +184,14 @@ h1
|
||||
font-weight:500;
|
||||
}
|
||||
|
||||
h2
|
||||
{
|
||||
h2 {
|
||||
font-size:3em;
|
||||
font-size:3rem;
|
||||
font-style:normal;
|
||||
font-weight:500;
|
||||
}
|
||||
|
||||
h3
|
||||
{
|
||||
h3 {
|
||||
font-size:2.4em;
|
||||
font-size:2.4rem;
|
||||
font-style:normal;
|
||||
@ -229,8 +199,7 @@ h3
|
||||
margin:1.6rem 0 .4rem;
|
||||
}
|
||||
|
||||
h4
|
||||
{
|
||||
h4 {
|
||||
font-size:1.8em;
|
||||
font-size:1.8rem;
|
||||
font-style:normal;
|
||||
@ -238,8 +207,7 @@ h4
|
||||
margin:1.6rem 0 .4rem;
|
||||
}
|
||||
|
||||
h5
|
||||
{
|
||||
h5 {
|
||||
font-size:1.6em;
|
||||
font-size:1.6rem;
|
||||
font-style:normal;
|
||||
@ -247,8 +215,7 @@ h5
|
||||
margin:1.6rem 0 .4rem;
|
||||
}
|
||||
|
||||
h6
|
||||
{
|
||||
h6 {
|
||||
color:#777;
|
||||
font-size:1.4em;
|
||||
font-size:1.4rem;
|
||||
@ -257,66 +224,56 @@ h6
|
||||
margin:1.6rem 0 .4rem;
|
||||
}
|
||||
|
||||
code
|
||||
{
|
||||
code {
|
||||
background:#efefef;
|
||||
color:#444;
|
||||
font-family:Menlo, Monaco, Consolas, 'Courier New', monospace;
|
||||
font-family:var(--monospace-font-list);
|
||||
font-size:1.4rem;
|
||||
word-break:break-all;
|
||||
word-wrap:break-word;
|
||||
}
|
||||
|
||||
a:hover,a:focus
|
||||
{
|
||||
a:hover,a:focus {
|
||||
text-decoration:none;
|
||||
}
|
||||
|
||||
dl
|
||||
{
|
||||
dl {
|
||||
margin-bottom:1.6rem;
|
||||
}
|
||||
|
||||
dd
|
||||
{
|
||||
dd {
|
||||
margin-left:4rem;
|
||||
}
|
||||
|
||||
ul,ol
|
||||
{
|
||||
ul,ol {
|
||||
margin-bottom:.8rem;
|
||||
padding-left:2rem;
|
||||
}
|
||||
|
||||
blockquote
|
||||
{
|
||||
blockquote {
|
||||
border-left:.2rem solid #1271db;
|
||||
font-family:Georgia, Times, 'Times New Roman', serif;
|
||||
font-family:var(--serif-font-list);
|
||||
font-style:italic;
|
||||
margin:1.6rem 0;
|
||||
padding-left:1.6rem;
|
||||
}
|
||||
|
||||
figcaption
|
||||
{
|
||||
font-family:Georgia, Times, 'Times New Roman', serif;
|
||||
figcaption {
|
||||
font-family:var(--serif-font-list);
|
||||
}
|
||||
|
||||
html
|
||||
{
|
||||
html {
|
||||
font-size:62.5%;
|
||||
}
|
||||
|
||||
main,header,footer,article,section,aside,details,summary
|
||||
{
|
||||
main,header,footer,article,section,aside,details,summary {
|
||||
display:block;
|
||||
height:auto;
|
||||
margin:0 auto;
|
||||
width:100%;
|
||||
}
|
||||
|
||||
footer
|
||||
{
|
||||
footer {
|
||||
border-top:.1rem solid rgba(0,0,0,0.2);
|
||||
clear:both;
|
||||
display:inline-block;
|
||||
@ -326,23 +283,20 @@ footer
|
||||
text-align:center;
|
||||
}
|
||||
|
||||
hr
|
||||
{
|
||||
hr {
|
||||
border-top:.1rem solid rgba(0,0,0,0.2);
|
||||
display:block;
|
||||
margin-bottom:1.6rem;
|
||||
width:100%;
|
||||
}
|
||||
|
||||
img
|
||||
{
|
||||
img {
|
||||
height:auto;
|
||||
/* max-width:100%; */
|
||||
/* max-width:100%; */
|
||||
vertical-align:baseline;
|
||||
}
|
||||
|
||||
input[type=text],input[type=password],input[type=email],input[type=url],input[type=date],input[type=month],input[type=time],input[type=datetime],input[type=datetime-local],input[type=week],input[type=number],input[type=search],input[type=tel],input[type=color],select
|
||||
{
|
||||
input[type=text],input[type=password],input[type=email],input[type=url],input[type=date],input[type=month],input[type=time],input[type=datetime],input[type=datetime-local],input[type=week],input[type=number],input[type=search],input[type=tel],input[type=color],select {
|
||||
border:.1rem solid #ccc;
|
||||
border-radius:0;
|
||||
display:inline-block;
|
||||
@ -350,8 +304,7 @@ input[type=text],input[type=password],input[type=email],input[type=url],input[ty
|
||||
vertical-align:middle;
|
||||
}
|
||||
|
||||
input:not([type])
|
||||
{
|
||||
input:not([type]) {
|
||||
-webkit-appearance:none;
|
||||
background-clip:padding-box;
|
||||
background-color:#fff;
|
||||
@ -363,88 +316,73 @@ input:not([type])
|
||||
text-align:left;
|
||||
}
|
||||
|
||||
input[type=color]
|
||||
{
|
||||
input[type=color] {
|
||||
padding:.8rem 1.6rem;
|
||||
}
|
||||
|
||||
input[type=text]:focus,input[type=password]:focus,input[type=email]:focus,input[type=url]:focus,input[type=date]:focus,input[type=month]:focus,input[type=time]:focus,input[type=datetime]:focus,input[type=datetime-local]:focus,input[type=week]:focus,input[type=number]:focus,input[type=search]:focus,input[type=tel]:focus,input[type=color]:focus,select:focus,textarea:focus
|
||||
{
|
||||
input[type=text]:focus,input[type=password]:focus,input[type=email]:focus,input[type=url]:focus,input[type=date]:focus,input[type=month]:focus,input[type=time]:focus,input[type=datetime]:focus,input[type=datetime-local]:focus,input[type=week]:focus,input[type=number]:focus,input[type=search]:focus,input[type=tel]:focus,input[type=color]:focus,select:focus,textarea:focus {
|
||||
border-color:#b3d4fc;
|
||||
}
|
||||
|
||||
input:not([type]):focus
|
||||
{
|
||||
input:not([type]):focus {
|
||||
border-color:#b3d4fc;
|
||||
}
|
||||
|
||||
input[type=radio],input[type=checkbox]
|
||||
{
|
||||
input[type=radio],input[type=checkbox] {
|
||||
vertical-align:middle;
|
||||
}
|
||||
|
||||
input[type=file]:focus,input[type=radio]:focus,input[type=checkbox]:focus
|
||||
{
|
||||
input[type=file]:focus,input[type=radio]:focus,input[type=checkbox]:focus {
|
||||
outline:.1rem solid thin #444;
|
||||
}
|
||||
|
||||
input[type=text][disabled],input[type=password][disabled],input[type=email][disabled],input[type=url][disabled],input[type=date][disabled],input[type=month][disabled],input[type=time][disabled],input[type=datetime][disabled],input[type=datetime-local][disabled],input[type=week][disabled],input[type=number][disabled],input[type=search][disabled],input[type=tel][disabled],input[type=color][disabled],select[disabled],textarea[disabled]
|
||||
{
|
||||
input[type=text][disabled],input[type=password][disabled],input[type=email][disabled],input[type=url][disabled],input[type=date][disabled],input[type=month][disabled],input[type=time][disabled],input[type=datetime][disabled],input[type=datetime-local][disabled],input[type=week][disabled],input[type=number][disabled],input[type=search][disabled],input[type=tel][disabled],input[type=color][disabled],select[disabled],textarea[disabled] {
|
||||
background-color:#efefef;
|
||||
color:#777;
|
||||
cursor:not-allowed;
|
||||
}
|
||||
|
||||
input:not([type])[disabled]
|
||||
{
|
||||
input:not([type])[disabled] {
|
||||
background-color:#efefef;
|
||||
color:#777;
|
||||
cursor:not-allowed;
|
||||
}
|
||||
|
||||
input[readonly],select[readonly],textarea[readonly]
|
||||
{
|
||||
input[readonly],select[readonly],textarea[readonly] {
|
||||
background-color:#efefef;
|
||||
border-color:#ccc;
|
||||
color:#777;
|
||||
}
|
||||
|
||||
input:focus:invalid,textarea:focus:invalid,select:focus:invalid
|
||||
{
|
||||
input:focus:invalid,textarea:focus:invalid,select:focus:invalid {
|
||||
border-color:#e9322d;
|
||||
color:#b94a48;
|
||||
}
|
||||
|
||||
input[type=file]:focus:invalid:focus,input[type=radio]:focus:invalid:focus,input[type=checkbox]:focus:invalid:focus
|
||||
{
|
||||
input[type=file]:focus:invalid:focus,input[type=radio]:focus:invalid:focus,input[type=checkbox]:focus:invalid:focus {
|
||||
outline-color:#ff4136;
|
||||
}
|
||||
|
||||
select
|
||||
{
|
||||
select {
|
||||
background-color:#fff;
|
||||
border:.1rem solid #ccc;
|
||||
}
|
||||
|
||||
select[multiple]
|
||||
{
|
||||
select[multiple] {
|
||||
height:auto;
|
||||
}
|
||||
|
||||
label
|
||||
{
|
||||
label {
|
||||
line-height:2;
|
||||
}
|
||||
|
||||
fieldset
|
||||
{
|
||||
fieldset {
|
||||
border:0;
|
||||
margin:0;
|
||||
padding:.8rem 0;
|
||||
}
|
||||
|
||||
legend
|
||||
{
|
||||
legend {
|
||||
border-bottom:.1rem solid #ccc;
|
||||
color:#444;
|
||||
display:block;
|
||||
@ -453,8 +391,7 @@ legend
|
||||
width:100%;
|
||||
}
|
||||
|
||||
input[type=submit],button
|
||||
{
|
||||
input[type=submit],button {
|
||||
-moz-user-select:none;
|
||||
-ms-user-select:none;
|
||||
-webkit-transition:.25s ease;
|
||||
@ -476,62 +413,52 @@ input[type=submit],button
|
||||
vertical-align:baseline;
|
||||
}
|
||||
|
||||
input[type=submit] a,button a
|
||||
{
|
||||
input[type=submit] a,button a {
|
||||
color:#444;
|
||||
}
|
||||
|
||||
input[type=submit]::-moz-focus-inner,button::-moz-focus-inner
|
||||
{
|
||||
input[type=submit]::-moz-focus-inner,button::-moz-focus-inner {
|
||||
padding:0;
|
||||
}
|
||||
|
||||
input[type=submit]:hover,button:hover
|
||||
{
|
||||
input[type=submit]:hover,button:hover {
|
||||
background:#444;
|
||||
border-color:#444;
|
||||
color:#fff;
|
||||
}
|
||||
|
||||
input[type=submit]:hover a,button:hover a
|
||||
{
|
||||
input[type=submit]:hover a,button:hover a {
|
||||
color:#fff;
|
||||
}
|
||||
|
||||
input[type=submit]:active,button:active
|
||||
{
|
||||
input[type=submit]:active,button:active {
|
||||
background:#6a6a6a;
|
||||
border-color:#6a6a6a;
|
||||
color:#fff;
|
||||
}
|
||||
|
||||
input[type=submit]:active a,button:active a
|
||||
{
|
||||
input[type=submit]:active a,button:active a {
|
||||
color:#fff;
|
||||
}
|
||||
|
||||
input[type=submit]:disabled,button:disabled
|
||||
{
|
||||
input[type=submit]:disabled,button:disabled {
|
||||
box-shadow:none;
|
||||
cursor:not-allowed;
|
||||
opacity:.40;
|
||||
opacity:.4;
|
||||
}
|
||||
|
||||
nav ul
|
||||
{
|
||||
nav ul {
|
||||
list-style:none;
|
||||
margin:0;
|
||||
padding:0;
|
||||
text-align:center;
|
||||
}
|
||||
|
||||
nav ul li
|
||||
{
|
||||
nav ul li {
|
||||
display:inline;
|
||||
}
|
||||
|
||||
nav a
|
||||
{
|
||||
nav a {
|
||||
-webkit-transition:.25s ease;
|
||||
border-bottom:.2rem solid transparent;
|
||||
color:#444;
|
||||
@ -540,48 +467,40 @@ nav a
|
||||
transition:.25s ease;
|
||||
}
|
||||
|
||||
nav a:hover,nav li.selected a
|
||||
{
|
||||
nav a:hover,nav li.selected a {
|
||||
border-color:rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
nav a:active
|
||||
{
|
||||
nav a:active {
|
||||
border-color:rgba(0,0,0,0.56);
|
||||
}
|
||||
|
||||
caption
|
||||
{
|
||||
caption {
|
||||
padding:.8rem 0;
|
||||
}
|
||||
|
||||
thead th
|
||||
{
|
||||
thead th {
|
||||
background:#efefef;
|
||||
color:#444;
|
||||
}
|
||||
|
||||
tr
|
||||
{
|
||||
tr {
|
||||
background:#fff;
|
||||
margin-bottom:.8rem;
|
||||
}
|
||||
|
||||
th,td
|
||||
{
|
||||
th,td {
|
||||
border:.1rem solid #ccc;
|
||||
padding:.8rem 1.6rem;
|
||||
text-align:center;
|
||||
vertical-align:inherit;
|
||||
}
|
||||
|
||||
tfoot tr
|
||||
{
|
||||
tfoot tr {
|
||||
background:none;
|
||||
}
|
||||
|
||||
tfoot td
|
||||
{
|
||||
tfoot td {
|
||||
color:#efefef;
|
||||
font-size:.8rem;
|
||||
font-style:italic;
|
||||
@ -589,28 +508,24 @@ tfoot td
|
||||
}
|
||||
|
||||
@media screen {
|
||||
[hidden~=screen]
|
||||
{
|
||||
[hidden~=screen] {
|
||||
display:inherit;
|
||||
}
|
||||
|
||||
[hidden~=screen]:not(:active):not(:focus):not(:target)
|
||||
{
|
||||
[hidden~=screen]:not(:active):not(:focus):not(:target) {
|
||||
clip:rect(0000)!important;
|
||||
position:absolute!important;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and max-width 40rem {
|
||||
article,section,aside
|
||||
{
|
||||
article,section,aside {
|
||||
clear:both;
|
||||
display:block;
|
||||
max-width:100%;
|
||||
}
|
||||
|
||||
img
|
||||
{
|
||||
img {
|
||||
margin-right:1.6rem;
|
||||
}
|
||||
}
|
3
public/cssfilter.js
Normal file
3
public/cssfilter.js
Normal file
@ -0,0 +1,3 @@
|
||||
module.exports = function filter(filename) {
|
||||
return ! String(filename).includes('min');
|
||||
}
|
0
public/images/characters/.gitkeep
Normal file
0
public/images/characters/.gitkeep
Normal file
128
public/js.php
128
public/js.php
@ -22,29 +22,48 @@ use Aviat\Ion\Json;
|
||||
|
||||
// Include guzzle
|
||||
require_once('../vendor/autoload.php');
|
||||
require_once('./min.php');
|
||||
|
||||
//Creative rewriting of /g/groupname to ?g=groupname
|
||||
$pi = $_SERVER['PATH_INFO'];
|
||||
$pia = explode('/', $pi);
|
||||
|
||||
$piaLen = count($pia);
|
||||
$i = 1;
|
||||
|
||||
while($i < $piaLen)
|
||||
{
|
||||
$j = $i+1;
|
||||
$j = (isset($pia[$j])) ? $j : $i;
|
||||
|
||||
$_GET[$pia[$i]] = $pia[$j];
|
||||
|
||||
$i = $j + 1;
|
||||
};
|
||||
|
||||
class FileNotChangedException extends \Exception {}
|
||||
|
||||
/**
|
||||
* Simple Javascript minfier, using google closure compiler
|
||||
*/
|
||||
class JSMin extends BaseMin {
|
||||
class JSMin {
|
||||
|
||||
protected $jsRoot;
|
||||
protected $jsGroup;
|
||||
protected $jsGroupsFile;
|
||||
protected $configFile;
|
||||
protected $cacheFile;
|
||||
|
||||
protected $lastModified;
|
||||
protected $requestedTime;
|
||||
protected $cacheModified;
|
||||
|
||||
public function __construct(array $config, array $groups)
|
||||
public function __construct(array $config, string $configFile)
|
||||
{
|
||||
$group = $_GET['g'];
|
||||
$groups = $config['groups'];
|
||||
|
||||
$this->jsRoot = $config['js_root'];
|
||||
$this->jsGroup = $groups[$group];
|
||||
$this->jsGroupsFile = $config['js_groups_file'];
|
||||
$this->configFile = $configFile;
|
||||
$this->cacheFile = "{$this->jsRoot}cache/{$group}";
|
||||
$this->lastModified = $this->getLastModified();
|
||||
|
||||
@ -99,7 +118,7 @@ class JSMin extends BaseMin {
|
||||
protected function closureCall(array $options)
|
||||
{
|
||||
$formFields = http_build_query($options);
|
||||
|
||||
|
||||
$request = (new Request)
|
||||
->setMethod('POST')
|
||||
->setUri('http://closure-compiler.appspot.com/compile')
|
||||
@ -109,7 +128,7 @@ class JSMin extends BaseMin {
|
||||
'Content-type' => 'application/x-www-form-urlencoded'
|
||||
])
|
||||
->setBody($formFields);
|
||||
|
||||
|
||||
$response = wait((new Client)->request($request, [
|
||||
Client::OP_AUTO_ENCODING => false
|
||||
]));
|
||||
@ -128,7 +147,7 @@ class JSMin extends BaseMin {
|
||||
$errorRes = $this->closureCall($options);
|
||||
$errorJson = $errorRes->getBody();
|
||||
$errorObj = Json::decode($errorJson) ?: (object)[];
|
||||
|
||||
|
||||
|
||||
// Show error if exists
|
||||
if ( ! empty($errorObj->errors) || ! empty($errorObj->serverErrors))
|
||||
@ -178,7 +197,7 @@ class JSMin extends BaseMin {
|
||||
|
||||
//Add this page too, as well as the groups file
|
||||
$modified[] = filemtime(__FILE__);
|
||||
$modified[] = filemtime($this->jsGroupsFile);
|
||||
$modified[] = filemtime($this->configFile);
|
||||
|
||||
rsort($modified);
|
||||
$lastModified = $modified[0];
|
||||
@ -227,14 +246,97 @@ class JSMin extends BaseMin {
|
||||
{
|
||||
$this->sendFinalOutput($js, 'application/javascript', $this->lastModified);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get value of the if-modified-since header
|
||||
*
|
||||
* @return int - timestamp to compare for cache control
|
||||
*/
|
||||
protected function getIfModified()
|
||||
{
|
||||
return (array_key_exists('HTTP_IF_MODIFIED_SINCE', $_SERVER))
|
||||
? strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE'])
|
||||
: time();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get value of etag to compare to hash of output
|
||||
*
|
||||
* @return string - the etag to compare
|
||||
*/
|
||||
protected function getIfNoneMatch()
|
||||
{
|
||||
return (array_key_exists('HTTP_IF_NONE_MATCH', $_SERVER))
|
||||
? $_SERVER['HTTP_IF_NONE_MATCH']
|
||||
: '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether or not to send debug version
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
protected function isNotDebug()
|
||||
{
|
||||
return ! $this->isDebugCall();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether or not to send debug version
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
protected function isDebugCall()
|
||||
{
|
||||
return array_key_exists('debug', $_GET);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send actual output to browser
|
||||
*
|
||||
* @param string $content - the body of the response
|
||||
* @param string $mimeType - the content type
|
||||
* @param int $lastModified - the last modified date
|
||||
* @return void
|
||||
*/
|
||||
protected function sendFinalOutput($content, $mimeType, $lastModified)
|
||||
{
|
||||
//This GZIPs the CSS for transmission to the user
|
||||
//making file size smaller and transfer rate quicker
|
||||
ob_start("ob_gzhandler");
|
||||
|
||||
$expires = $lastModified + 691200;
|
||||
$lastModifiedDate = gmdate('D, d M Y H:i:s', $lastModified);
|
||||
$expiresDate = gmdate('D, d M Y H:i:s', $expires);
|
||||
|
||||
header("Content-Type: {$mimeType}; charset=utf8");
|
||||
header("Cache-control: public, max-age=691200, must-revalidate");
|
||||
header("Last-Modified: {$lastModifiedDate} GMT");
|
||||
header("Expires: {$expiresDate} GMT");
|
||||
|
||||
echo $content;
|
||||
|
||||
ob_end_flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a 304 Not Modified header
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function send304()
|
||||
{
|
||||
header("status: 304 Not Modified", true, 304);
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// ! Start Minifying
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
$config = require_once('../app/appConf/minify_config.php');
|
||||
$groups = require_once($config['js_groups_file']);
|
||||
$configFile = realpath(__DIR__ . '/../app/appConf/minify_config.php');
|
||||
$config = require_once($configFile);
|
||||
$groups = $config['groups'];
|
||||
$cacheDir = "{$config['js_root']}cache";
|
||||
|
||||
if ( ! is_dir($cacheDir))
|
||||
@ -249,11 +351,11 @@ if ( ! array_key_exists($_GET['g'], $groups))
|
||||
|
||||
try
|
||||
{
|
||||
new JSMin($config, $groups);
|
||||
new JSMin($config, $configFile);
|
||||
}
|
||||
catch (FileNotChangedException $e)
|
||||
{
|
||||
BaseMin::send304();
|
||||
JSMin::send304();
|
||||
}
|
||||
|
||||
//end of js.php
|
@ -8,7 +8,7 @@
|
||||
// Action to increment episode count
|
||||
_.on('body.anime.list', 'click', '.plus_one', (e) => {
|
||||
let parentSel = _.closestParent(e.target, 'article');
|
||||
let watchedCount = parseInt(_.$('.completed_number', parentSel)[0].textContent, 10);
|
||||
let watchedCount = parseInt(_.$('.completed_number', parentSel)[0].textContent, 10) || 0;
|
||||
let totalCount = parseInt(_.$('.total_number', parentSel)[0].textContent, 10);
|
||||
let title = _.$('.name a', parentSel)[0].textContent;
|
||||
|
||||
|
@ -9,7 +9,7 @@
|
||||
let thisSel = e.target;
|
||||
let parentSel = _.closestParent(e.target, 'article');
|
||||
let type = thisSel.classList.contains('plus_one_chapter') ? 'chapter' : 'volume';
|
||||
let completed = parseInt(_.$(`.${type}s_read`, parentSel)[0].textContent, 10);
|
||||
let completed = parseInt(_.$(`.${type}s_read`, parentSel)[0].textContent, 10) || 0;
|
||||
let total = parseInt(_.$(`.${type}_count`, parentSel)[0].textContent, 10);
|
||||
let mangaName = _.$('.name', parentSel)[0].textContent;
|
||||
|
||||
|
121
public/min.php
121
public/min.php
@ -1,121 +0,0 @@
|
||||
<?php declare(strict_types=1);
|
||||
/**
|
||||
* Hummingbird Anime List Client
|
||||
*
|
||||
* An API client for Kitsu and MyAnimeList to manage anime and manga watch lists
|
||||
*
|
||||
* PHP version 7
|
||||
*
|
||||
* @package HummingbirdAnimeClient
|
||||
* @author Timothy J. Warren <tim@timshomepage.net>
|
||||
* @copyright 2015 - 2017 Timothy J. Warren
|
||||
* @license http://www.opensource.org/licenses/mit-license.html MIT License
|
||||
* @version 4.0
|
||||
* @link https://github.com/timw4mail/HummingBirdAnimeClient
|
||||
*/
|
||||
|
||||
|
||||
namespace Aviat\EasyMin;
|
||||
|
||||
//Creative rewriting of /g/groupname to ?g=groupname
|
||||
$pi = $_SERVER['PATH_INFO'];
|
||||
$pia = explode('/', $pi);
|
||||
|
||||
$piaLen = count($pia);
|
||||
$i = 1;
|
||||
|
||||
while($i < $piaLen)
|
||||
{
|
||||
$j = $i+1;
|
||||
$j = (isset($pia[$j])) ? $j : $i;
|
||||
|
||||
$_GET[$pia[$i]] = $pia[$j];
|
||||
|
||||
$i = $j + 1;
|
||||
};
|
||||
|
||||
class FileNotChangedException extends \Exception {}
|
||||
class BaseMin {
|
||||
|
||||
/**
|
||||
* Get value of the if-modified-since header
|
||||
*
|
||||
* @return int - timestamp to compare for cache control
|
||||
*/
|
||||
protected function getIfModified()
|
||||
{
|
||||
return (array_key_exists('HTTP_IF_MODIFIED_SINCE', $_SERVER))
|
||||
? strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE'])
|
||||
: time();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get value of etag to compare to hash of output
|
||||
*
|
||||
* @return string - the etag to compare
|
||||
*/
|
||||
protected function getIfNoneMatch()
|
||||
{
|
||||
return (array_key_exists('HTTP_IF_NONE_MATCH', $_SERVER))
|
||||
? $_SERVER['HTTP_IF_NONE_MATCH']
|
||||
: '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether or not to send debug version
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
protected function isNotDebug()
|
||||
{
|
||||
return ! $this->isDebugCall();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether or not to send debug version
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
protected function isDebugCall()
|
||||
{
|
||||
return array_key_exists('debug', $_GET);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send actual output to browser
|
||||
*
|
||||
* @param string $content - the body of the response
|
||||
* @param string $mimeType - the content type
|
||||
* @param int $lastModified - the last modified date
|
||||
* @return void
|
||||
*/
|
||||
protected function sendFinalOutput($content, $mimeType, $lastModified)
|
||||
{
|
||||
//This GZIPs the CSS for transmission to the user
|
||||
//making file size smaller and transfer rate quicker
|
||||
ob_start("ob_gzhandler");
|
||||
|
||||
$expires = $lastModified + 691200;
|
||||
$lastModifiedDate = gmdate('D, d M Y H:i:s', $lastModified);
|
||||
$expiresDate = gmdate('D, d M Y H:i:s', $expires);
|
||||
|
||||
header("Content-Type: {$mimeType}; charset=utf8");
|
||||
header("Cache-control: public, max-age=691200, must-revalidate");
|
||||
header("Last-Modified: {$lastModifiedDate} GMT");
|
||||
header("Expires: {$expiresDate} GMT");
|
||||
|
||||
echo $content;
|
||||
|
||||
ob_end_flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a 304 Not Modified header
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function send304()
|
||||
{
|
||||
header("status: 304 Not Modified", true, 304);
|
||||
}
|
||||
}
|
@ -1,14 +1,13 @@
|
||||
{
|
||||
"scripts": {
|
||||
"build": "postcss -u postcss-import --autoprefixer.browsers \"> 5%\" -u postcss-cssnext -o css/base.css css/base.myth.css",
|
||||
"watch": "postcss -u postcss-import --autoprefixer.browsers \"> 5%\" -u postcss-cssnext -w -o css/base.css css/base.myth.css"
|
||||
"build": "node ./css.js",
|
||||
"watch": "watch 'npm run build' --filter=./cssfilter.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^6.6.1",
|
||||
"npm-run-all": "^4.0.0",
|
||||
"cssnano": "^3.10.0",
|
||||
"postcss-cachify": "^1.3.1",
|
||||
"postcss-cli": "^2.6.0",
|
||||
"postcss-cssnext": "^2.9.0",
|
||||
"postcss-import": "^9.0.0"
|
||||
"postcss-import": "^9.0.0",
|
||||
"watch": "^1.0.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,9 @@
|
||||
{{#data}}
|
||||
<article class="media search">
|
||||
<div class="name" style="background-image:url({{attributes.posterImage.small}})">
|
||||
<div class="name">
|
||||
<input type="radio" class="big-check" id="{{attributes.slug}}" name="id" value="{{id}}" />
|
||||
<label for="{{attributes.slug}}">
|
||||
<img src="/public/images/anime/{{id}}.jpg" alt="" width="220" />
|
||||
<span class="name">
|
||||
{{attributes.canonicalTitle}}
|
||||
<br />
|
||||
|
@ -1,8 +1,9 @@
|
||||
{{#data}}
|
||||
<article class="media search">
|
||||
<div class="name" style="background-image:url({{attributes.posterImage.small}})">
|
||||
<div class="name">
|
||||
<input type="radio" class="big-check" id="{{attributes.slug}}" name="id" value="{{id}}" />
|
||||
<label for="{{attributes.slug}}">
|
||||
<img src="/public/images/manga/{{id}}.jpg" alt="" width="220" />
|
||||
<span class="name">
|
||||
{{attributes.canonicalTitle}}
|
||||
<br />
|
||||
|
1836
public/yarn.lock
1836
public/yarn.lock
File diff suppressed because it is too large
Load Diff
@ -54,7 +54,9 @@ class JsonAPI {
|
||||
];
|
||||
|
||||
// Reorganize included data
|
||||
$included = static::organizeIncluded($data['included']);
|
||||
$included = (array_key_exists('included', $data))
|
||||
? static::organizeIncluded($data['included'])
|
||||
: [];
|
||||
|
||||
// Inline organized data
|
||||
foreach($data['data'] as $i => &$item)
|
||||
@ -125,23 +127,13 @@ class JsonAPI {
|
||||
$typeKey = $props['data'][$j]['type'];
|
||||
$relationship =& $item['relationships'][$relType];
|
||||
|
||||
unset($relationship['data'][$j]);
|
||||
|
||||
if (empty($relationship['data']))
|
||||
{
|
||||
unset($relationship['data']);
|
||||
}
|
||||
|
||||
if ($relType === $typeKey)
|
||||
{
|
||||
$relationship[$idKey] = $included[$typeKey][$idKey];
|
||||
continue;
|
||||
}
|
||||
|
||||
$relationship[$typeKey][$idKey] = array_merge(
|
||||
$included[$typeKey][$idKey],
|
||||
$relationship[$typeKey][$idKey] ?? []
|
||||
);
|
||||
$relationship[$typeKey][$idKey][$j] = $included[$typeKey][$idKey];
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -149,6 +141,8 @@ class JsonAPI {
|
||||
}
|
||||
}
|
||||
|
||||
$data['data']['included'] = $included;
|
||||
|
||||
return $data['data'];
|
||||
}
|
||||
|
||||
@ -202,11 +196,11 @@ class JsonAPI {
|
||||
{
|
||||
foreach($items as $id => $item)
|
||||
{
|
||||
if (array_key_exists('relationships', $item))
|
||||
if (array_key_exists('relationships', $item) && is_array($item['relationships']))
|
||||
{
|
||||
foreach($item['relationships'] as $relType => $props)
|
||||
{
|
||||
if (array_key_exists('data', $props))
|
||||
if (array_key_exists('data', $props) && is_array($props['data']) && array_key_exists('id', $props['data']))
|
||||
{
|
||||
if (array_key_exists($props['data']['id'], $organized[$props['data']['type']]))
|
||||
{
|
||||
|
@ -219,11 +219,11 @@ class Kitsu {
|
||||
|
||||
foreach($existingTitles as $existing)
|
||||
{
|
||||
$isSubset = stripos($existing, $title) !== FALSE;
|
||||
$isSubset = mb_substr_count($existing, $title) > 0;
|
||||
$diff = levenshtein($existing, $title);
|
||||
$onlydifferentCase = (mb_strtolower($existing) === mb_strtolower($title));
|
||||
|
||||
if ($diff < 3 OR $isSubset OR $onlydifferentCase)
|
||||
if ($diff <= 3 OR $isSubset OR $onlydifferentCase OR mb_strlen($title) > 55)
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
@ -22,7 +22,7 @@ use function Amp\wait;
|
||||
|
||||
use Amp\Artax\{Client, Request};
|
||||
use Aviat\AnimeClient\AnimeClient;
|
||||
use Aviat\AnimeClient\API\Kitsu as K;
|
||||
use Aviat\AnimeClient\API\{FailedResponseException, Kitsu as K};
|
||||
use Aviat\Ion\Json;
|
||||
|
||||
trait KitsuTrait {
|
||||
@ -142,8 +142,10 @@ trait KitsuTrait {
|
||||
{
|
||||
if ($logger)
|
||||
{
|
||||
$logger->warning('Non 200 response for api call', (array)$response->getBody());
|
||||
$logger->warning('Non 200 response for api call', (array)$response);
|
||||
}
|
||||
|
||||
throw new FailedResponseException('Failed to get the proper response from the API');
|
||||
}
|
||||
|
||||
return Json::decode($response->getBody(), TRUE);
|
||||
|
@ -47,7 +47,7 @@ class Model {
|
||||
use ContainerAware;
|
||||
use KitsuTrait;
|
||||
|
||||
const FULL_TRANSFORMED_LIST_CACHE_KEY = 'kitsu-full-organized-anime-list';
|
||||
const LIST_PAGE_SIZE = 100;
|
||||
|
||||
/**
|
||||
* Class to map anime list items
|
||||
@ -160,13 +160,16 @@ class Model {
|
||||
*/
|
||||
public function getCharacter(string $slug): array
|
||||
{
|
||||
// @todo catch non-existent characters and show 404
|
||||
$data = $this->getRequest('/characters', [
|
||||
'query' => [
|
||||
'filter' => [
|
||||
'name' => $slug
|
||||
'slug' => $slug,
|
||||
],
|
||||
// 'include' => 'primaryMedia,castings'
|
||||
'fields' => [
|
||||
'anime' => 'canonicalTitle,titles,slug,posterImage',
|
||||
'manga' => 'canonicalTitle,titles,slug,posterImage'
|
||||
],
|
||||
'include' => 'castings.person,castings.media'
|
||||
]
|
||||
]);
|
||||
|
||||
@ -235,9 +238,9 @@ class Model {
|
||||
*
|
||||
* @param string $malId
|
||||
* @param string $type "anime" or "manga"
|
||||
* @return string
|
||||
* @return string|NULL
|
||||
*/
|
||||
public function getKitsuIdFromMALId(string $malId, string $type="anime"): string
|
||||
public function getKitsuIdFromMALId(string $malId, string $type="anime")
|
||||
{
|
||||
$options = [
|
||||
'query' => [
|
||||
@ -254,6 +257,11 @@ class Model {
|
||||
|
||||
$raw = $this->getRequest('mappings', $options);
|
||||
|
||||
if ( ! array_key_exists('included', $raw))
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return $raw['included'][0]['id'];
|
||||
}
|
||||
|
||||
@ -277,7 +285,7 @@ class Model {
|
||||
}
|
||||
|
||||
$transformed = $this->animeTransformer->transform($baseData);
|
||||
$transformed['included'] = $baseData['included'];
|
||||
$transformed['included'] = JsonAPI::organizeIncluded($baseData['included']);
|
||||
return $transformed;
|
||||
}
|
||||
|
||||
@ -374,7 +382,7 @@ class Model {
|
||||
{
|
||||
$status = $options['filter']['status'] ?? '';
|
||||
$count = $this->getAnimeListCount($status);
|
||||
$size = 100;
|
||||
$size = static::LIST_PAGE_SIZE;
|
||||
$pages = ceil($count / $size);
|
||||
|
||||
$requester = new ParallelAPIRequest();
|
||||
@ -460,7 +468,7 @@ class Model {
|
||||
* @param array $options
|
||||
* @return Request
|
||||
*/
|
||||
public function getPagedAnimeList(int $limit = 100, int $offset = 0, array $options = [
|
||||
public function getPagedAnimeList(int $limit, int $offset = 0, array $options = [
|
||||
'include' => 'anime.mappings'
|
||||
]): Request
|
||||
{
|
||||
@ -618,7 +626,7 @@ class Model {
|
||||
{
|
||||
$status = $options['filter']['status'] ?? '';
|
||||
$count = $this->getMangaListCount($status);
|
||||
$size = 100;
|
||||
$size = static::LIST_PAGE_SIZE;
|
||||
$pages = ceil($count / $size);
|
||||
|
||||
$requester = new ParallelAPIRequest();
|
||||
@ -668,7 +676,7 @@ class Model {
|
||||
* @param array $options
|
||||
* @return Request
|
||||
*/
|
||||
public function getPagedMangaList(int $limit = 100, int $offset = 0, array $options = [
|
||||
public function getPagedMangaList(int $limit, int $offset = 0, array $options = [
|
||||
'include' => 'manga.mappings'
|
||||
]): Request
|
||||
{
|
||||
@ -821,6 +829,7 @@ class Model {
|
||||
}
|
||||
|
||||
$baseData = $data['data']['attributes'];
|
||||
$baseData['id'] = $data['id'];
|
||||
$baseData['included'] = $data['included'];
|
||||
return $baseData;
|
||||
}
|
||||
@ -856,6 +865,7 @@ class Model {
|
||||
}
|
||||
|
||||
$baseData = $data['data'][0]['attributes'];
|
||||
$baseData['id'] = $data['data'][0]['id'];
|
||||
$baseData['included'] = $data['included'];
|
||||
return $baseData;
|
||||
}
|
||||
|
@ -40,7 +40,9 @@ class AnimeListTransformer extends AbstractTransformer {
|
||||
$genres = array_column($anime['relationships']['genres'], 'name') ?? [];
|
||||
sort($genres);
|
||||
|
||||
$rating = (int) 2 * $item['attributes']['rating'];
|
||||
$rating = (int) $item['attributes']['rating'] !== 0
|
||||
? (int) 2 * $item['attributes']['rating']
|
||||
: '-';
|
||||
|
||||
$total_episodes = array_key_exists('episodeCount', $anime) && (int) $anime['episodeCount'] !== 0
|
||||
? (int) $anime['episodeCount']
|
||||
@ -68,7 +70,7 @@ class AnimeListTransformer extends AbstractTransformer {
|
||||
'id' => $item['id'],
|
||||
'mal_id' => $MALid,
|
||||
'episodes' => [
|
||||
'watched' => (int) $item['attributes']['progress'] !== '0'
|
||||
'watched' => (int) $item['attributes']['progress'] !== 0
|
||||
? (int) $item['attributes']['progress']
|
||||
: '-',
|
||||
'total' => $total_episodes,
|
||||
@ -80,6 +82,7 @@ class AnimeListTransformer extends AbstractTransformer {
|
||||
'ended' => $anime['endDate']
|
||||
],
|
||||
'anime' => [
|
||||
'id' => $animeId,
|
||||
'age_rating' => $anime['ageRating'],
|
||||
'title' => $anime['canonicalTitle'],
|
||||
'titles' => Kitsu::filterTitles($anime),
|
||||
@ -93,7 +96,7 @@ class AnimeListTransformer extends AbstractTransformer {
|
||||
'notes' => $item['attributes']['notes'],
|
||||
'rewatching' => (bool) $item['attributes']['reconsuming'],
|
||||
'rewatched' => (int) $item['attributes']['reconsumeCount'],
|
||||
'user_rating' => ($rating === 0) ? '-' : (int) $rating,
|
||||
'user_rating' => $rating,
|
||||
'private' => (bool) $item['attributes']['private'] ?? FALSE,
|
||||
];
|
||||
}
|
||||
@ -118,12 +121,16 @@ class AnimeListTransformer extends AbstractTransformer {
|
||||
'reconsuming' => $rewatching,
|
||||
'reconsumeCount' => $item['rewatched'],
|
||||
'notes' => $item['notes'],
|
||||
'progress' => $item['episodes_watched'],
|
||||
'private' => $privacy
|
||||
]
|
||||
];
|
||||
|
||||
if (is_numeric($item['user_rating']))
|
||||
if (is_numeric($item['episodes_watched']) && $item['episodes_watched'] > 0)
|
||||
{
|
||||
$untransformed['data']['progress'] = (int) $item['episodes_watched'];
|
||||
}
|
||||
|
||||
if (is_numeric($item['user_rating']) && $item['user_rating'] > 0)
|
||||
{
|
||||
$untransformed['data']['rating'] = $item['user_rating'] / 2;
|
||||
}
|
||||
|
@ -36,10 +36,11 @@ class AnimeTransformer extends AbstractTransformer {
|
||||
$item['included'] = JsonAPI::organizeIncludes($item['included']);
|
||||
$item['genres'] = array_column($item['included']['genres'], 'name') ?? [];
|
||||
sort($item['genres']);
|
||||
|
||||
|
||||
$titles = Kitsu::filterTitles($item);
|
||||
|
||||
return [
|
||||
'id' => $item['id'],
|
||||
'slug' => $item['slug'],
|
||||
'title' => $titles[0],
|
||||
'titles' => $titles,
|
||||
|
@ -42,18 +42,22 @@ class MangaListTransformer extends AbstractTransformer {
|
||||
$genres = array_column($manga['relationships']['genres'], 'name') ?? [];
|
||||
sort($genres);
|
||||
|
||||
$rating = (is_numeric($item['attributes']['rating']))
|
||||
? intval(2 * $item['attributes']['rating'])
|
||||
$rating = (int) $item['attributes']['rating'] !== 0
|
||||
? (int) 2 * $item['attributes']['rating']
|
||||
: '-';
|
||||
|
||||
$totalChapters = ($manga['chapterCount'] > 0)
|
||||
$totalChapters = ((int) $manga['chapterCount'] !== 0)
|
||||
? $manga['chapterCount']
|
||||
: '-';
|
||||
|
||||
$totalVolumes = ($manga['volumeCount'] > 0)
|
||||
$totalVolumes = ((int) $manga['volumeCount'] !== 0)
|
||||
? $manga['volumeCount']
|
||||
: '-';
|
||||
|
||||
$readChapters = ((int) $item['attributes']['progress'] !== 0)
|
||||
? $item['attributes']['progress']
|
||||
: '-';
|
||||
|
||||
$MALid = NULL;
|
||||
|
||||
if (array_key_exists('mappings', $manga['relationships']))
|
||||
@ -72,7 +76,7 @@ class MangaListTransformer extends AbstractTransformer {
|
||||
'id' => $item['id'],
|
||||
'mal_id' => $MALid,
|
||||
'chapters' => [
|
||||
'read' => $item['attributes']['progress'],
|
||||
'read' => $readChapters,
|
||||
'total' => $totalChapters
|
||||
],
|
||||
'volumes' => [
|
||||
@ -80,6 +84,7 @@ class MangaListTransformer extends AbstractTransformer {
|
||||
'total' => $totalVolumes
|
||||
],
|
||||
'manga' => [
|
||||
'id' => $mangaId,
|
||||
'titles' => Kitsu::filterTitles($manga),
|
||||
'alternate_title' => NULL,
|
||||
'slug' => $manga['slug'],
|
||||
@ -113,14 +118,18 @@ class MangaListTransformer extends AbstractTransformer {
|
||||
'mal_id' => $item['mal_id'],
|
||||
'data' => [
|
||||
'status' => $item['status'],
|
||||
'progress' => (int)$item['chapters_read'],
|
||||
'reconsuming' => $rereading,
|
||||
'reconsumeCount' => (int)$item['reread_count'],
|
||||
'notes' => $item['notes'],
|
||||
],
|
||||
];
|
||||
|
||||
if (is_numeric($item['new_rating']))
|
||||
if (is_numeric($item['chapters_read']) && $item['chapters_read'] > 0)
|
||||
{
|
||||
$map['data']['progress'] = (int)$item['chapters_read'];
|
||||
}
|
||||
|
||||
if (is_numeric($item['new_rating']) && $item['new_rating'] > 0)
|
||||
{
|
||||
$map['data']['rating'] = $item['new_rating'] / 2;
|
||||
}
|
||||
|
@ -33,7 +33,7 @@ class MangaTransformer extends AbstractTransformer {
|
||||
public function transform($item)
|
||||
{
|
||||
$genres = [];
|
||||
|
||||
|
||||
foreach($item['included'] as $included)
|
||||
{
|
||||
if ($included['type'] === 'genres')
|
||||
@ -41,10 +41,11 @@ class MangaTransformer extends AbstractTransformer {
|
||||
$genres[] = $included['attributes']['name'];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
sort($genres);
|
||||
|
||||
|
||||
return [
|
||||
'id' => $item['id'],
|
||||
'title' => $item['canonicalTitle'],
|
||||
'en_title' => $item['titles']['en'],
|
||||
'jp_title' => $item['titles']['en_jp'],
|
||||
|
@ -44,9 +44,7 @@ class AnimeListTransformer extends AbstractTransformer {
|
||||
{
|
||||
$map = [
|
||||
'id' => $item['mal_id'],
|
||||
'data' => [
|
||||
'episode' => $item['data']['progress']
|
||||
]
|
||||
'data' => []
|
||||
];
|
||||
|
||||
$data =& $item['data'];
|
||||
@ -55,6 +53,10 @@ class AnimeListTransformer extends AbstractTransformer {
|
||||
{
|
||||
switch($key)
|
||||
{
|
||||
case 'progress':
|
||||
$map['data']['episode'] = $value;
|
||||
break;
|
||||
|
||||
case 'notes':
|
||||
$map['data']['comments'] = $value;
|
||||
break;
|
||||
|
@ -44,9 +44,7 @@ class MangaListTransformer extends AbstractTransformer {
|
||||
{
|
||||
$map = [
|
||||
'id' => $item['mal_id'],
|
||||
'data' => [
|
||||
'chapter' => $item['data']['progress']
|
||||
]
|
||||
'data' => []
|
||||
];
|
||||
|
||||
$data =& $item['data'];
|
||||
@ -55,6 +53,10 @@ class MangaListTransformer extends AbstractTransformer {
|
||||
{
|
||||
switch($key)
|
||||
{
|
||||
case 'progress':
|
||||
$map['data']['chapter'] = $value;
|
||||
break;
|
||||
|
||||
case 'notes':
|
||||
$map['data']['comments'] = $value;
|
||||
break;
|
||||
|
@ -63,61 +63,48 @@ class SyncKitsuWithMal extends BaseCommand {
|
||||
$this->kitsuModel = $this->container->get('kitsu-model');
|
||||
$this->malModel = $this->container->get('mal-model');
|
||||
|
||||
$this->syncAnime();
|
||||
$this->syncManga();
|
||||
$this->sync('anime');
|
||||
$this->sync('manga');
|
||||
}
|
||||
|
||||
public function syncAnime()
|
||||
public function sync(string $type)
|
||||
{
|
||||
$malCount = count($this->malModel->getAnimeList());
|
||||
$kitsuCount = $this->kitsuModel->getAnimeListCount();
|
||||
$uType = ucfirst($type);
|
||||
$malCount = count($this->malModel->{"get{$uType}List"}());
|
||||
$kitsuCount = $this->kitsuModel->{"get{$uType}ListCount"}();
|
||||
|
||||
$this->echoBox("Number of MAL anime list items: {$malCount}");
|
||||
$this->echoBox("Number of Kitsu anime list items: {$kitsuCount}");
|
||||
$this->echoBox("Number of MAL {$type} list items: {$malCount}");
|
||||
$this->echoBox("Number of Kitsu {$type} list items: {$kitsuCount}");
|
||||
|
||||
$data = $this->diffAnimeLists();
|
||||
|
||||
$this->echoBox("Number of anime items that need to be added to MAL: " . count($data['addToMAL']));
|
||||
$data = $this->diffLists($type);
|
||||
|
||||
if ( ! empty($data['addToMAL']))
|
||||
{
|
||||
$this->echoBox("Adding missing anime list items to MAL");
|
||||
$this->createMALListItems($data['addToMAL'], 'anime');
|
||||
$count = count($data['addToMAL']);
|
||||
$this->echoBox("Adding {$count} missing {$type} list items to MAL");
|
||||
$this->createMALListItems($data['addToMAL'], $type);
|
||||
}
|
||||
|
||||
$this->echoBox('Number of anime items that need to be added to Kitsu: ' . count($data['addToKitsu']));
|
||||
|
||||
if ( ! empty($data['addToKitsu']))
|
||||
{
|
||||
$this->echoBox("Adding missing anime list items to Kitsu");
|
||||
$this->createKitsuListItems($data['addToKitsu'], 'anime');
|
||||
}
|
||||
}
|
||||
|
||||
public function syncManga()
|
||||
{
|
||||
$malCount = count($this->malModel->getMangaList());
|
||||
$kitsuCount = $this->kitsuModel->getMangaListCount();
|
||||
|
||||
$this->echoBox("Number of MAL manga list items: {$malCount}");
|
||||
$this->echoBox("Number of Kitsu manga list items: {$kitsuCount}");
|
||||
|
||||
$data = $this->diffMangaLists();
|
||||
|
||||
$this->echoBox("Number of manga items that need to be added to MAL: " . count($data['addToMAL']));
|
||||
|
||||
if ( ! empty($data['addToMAL']))
|
||||
{
|
||||
$this->echoBox("Adding missing manga list items to MAL");
|
||||
$this->createMALListItems($data['addToMAL'], 'manga');
|
||||
$count = count($data['addToKitsu']);
|
||||
$this->echoBox("Adding {$count} missing {$type} list items to Kitsu");
|
||||
$this->createKitsuListItems($data['addToKitsu'], $type);
|
||||
}
|
||||
|
||||
$this->echoBox('Number of manga items that need to be added to Kitsu: ' . count($data['addToKitsu']));
|
||||
|
||||
if ( ! empty($data['addToKitsu']))
|
||||
if ( ! empty($data['updateMAL']))
|
||||
{
|
||||
$this->echoBox("Adding missing manga list items to Kitsu");
|
||||
$this->createKitsuListItems($data['addToKitsu'], 'manga');
|
||||
$count = count($data['updateMAL']);
|
||||
$this->echoBox("Updating {$count} outdated MAL {$type} list items");
|
||||
$this->updateMALListItems($data['updateMAL'], $type);
|
||||
}
|
||||
|
||||
if ( ! empty($data['updateKitsu']))
|
||||
{
|
||||
print_r($data['updateKitsu']);
|
||||
$count = count($data['updateKitsu']);
|
||||
$this->echoBox("Updating {$count} outdated Kitsu {$type} list items");
|
||||
$this->updateKitsuListItems($data['updateKitsu'], $type);
|
||||
}
|
||||
}
|
||||
|
||||
@ -136,6 +123,19 @@ class SyncKitsuWithMal extends BaseCommand {
|
||||
return $output;
|
||||
}
|
||||
|
||||
public function formatMALList(string $type): array
|
||||
{
|
||||
if ($type === 'anime')
|
||||
{
|
||||
return $this->formatMALAnimeList();
|
||||
}
|
||||
|
||||
if ($type === 'manga')
|
||||
{
|
||||
return $this->formatMALMangaList();
|
||||
}
|
||||
}
|
||||
|
||||
public function formatMALAnimeList()
|
||||
{
|
||||
$orig = $this->malModel->getAnimeList();
|
||||
@ -149,10 +149,6 @@ class SyncKitsuWithMal extends BaseCommand {
|
||||
'status' => AnimeWatchingStatus::MAL_TO_KITSU[$item['my_status']],
|
||||
'progress' => $item['my_watched_episodes'],
|
||||
'reconsuming' => (bool) $item['my_rewatching'],
|
||||
'reconsumeCount' => array_key_exists('times_rewatched', $item)
|
||||
? $item['times_rewatched']
|
||||
: 0,
|
||||
// 'notes' => ,
|
||||
'rating' => $item['my_score'] / 2,
|
||||
'updatedAt' => (new \DateTime())
|
||||
->setTimestamp((int)$item['my_last_updated'])
|
||||
@ -179,10 +175,6 @@ class SyncKitsuWithMal extends BaseCommand {
|
||||
'progress' => $item['my_read_chapters'],
|
||||
'volumes' => $item['my_read_volumes'],
|
||||
'reconsuming' => (bool) $item['my_rereadingg'],
|
||||
/* 'reconsumeCount' => array_key_exists('times_rewatched', $item)
|
||||
? $item['times_rewatched']
|
||||
: 0, */
|
||||
// 'notes' => ,
|
||||
'rating' => $item['my_score'] / 2,
|
||||
'updatedAt' => (new \DateTime())
|
||||
->setTimestamp((int)$item['my_last_updated'])
|
||||
@ -194,18 +186,18 @@ class SyncKitsuWithMal extends BaseCommand {
|
||||
return $output;
|
||||
}
|
||||
|
||||
public function filterKitsuAnimeList()
|
||||
public function formatKitsuList(string $type = 'anime'): array
|
||||
{
|
||||
$data = $this->kitsuModel->getFullAnimeList();
|
||||
$data = $this->kitsuModel->{'getFull' . ucfirst($type) . 'List'}();
|
||||
$includes = JsonAPI::organizeIncludes($data['included']);
|
||||
$includes['mappings'] = $this->filterMappings($includes['mappings']);
|
||||
$includes['mappings'] = $this->filterMappings($includes['mappings'], $type);
|
||||
|
||||
$output = [];
|
||||
|
||||
foreach($data['data'] as $listItem)
|
||||
{
|
||||
$animeId = $listItem['relationships']['anime']['data']['id'];
|
||||
$potentialMappings = $includes['anime'][$animeId]['relationships']['mappings'];
|
||||
$id = $listItem['relationships'][$type]['data']['id'];
|
||||
$potentialMappings = $includes[$type][$id]['relationships']['mappings'];
|
||||
$malId = NULL;
|
||||
|
||||
foreach ($potentialMappings as $mappingId)
|
||||
@ -232,94 +224,14 @@ class SyncKitsuWithMal extends BaseCommand {
|
||||
return $output;
|
||||
}
|
||||
|
||||
public function filterKitsuMangaList()
|
||||
{
|
||||
$data = $this->kitsuModel->getFullMangaList();
|
||||
$includes = JsonAPI::organizeIncludes($data['included']);
|
||||
$includes['mappings'] = $this->filterMappings($includes['mappings'], 'manga');
|
||||
|
||||
$output = [];
|
||||
|
||||
foreach($data['data'] as $listItem)
|
||||
{
|
||||
$mangaId = $listItem['relationships']['manga']['data']['id'];
|
||||
$potentialMappings = $includes['manga'][$mangaId]['relationships']['mappings'];
|
||||
$malId = NULL;
|
||||
|
||||
foreach ($potentialMappings as $mappingId)
|
||||
{
|
||||
if (array_key_exists($mappingId, $includes['mappings']))
|
||||
{
|
||||
$malId = $includes['mappings'][$mappingId]['externalId'];
|
||||
}
|
||||
}
|
||||
|
||||
// Skip to the next item if there isn't a MAL ID
|
||||
if (is_null($malId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$output[$listItem['id']] = [
|
||||
'id' => $listItem['id'],
|
||||
'malId' => $malId,
|
||||
'data' => $listItem['attributes'],
|
||||
];
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
public function diffMangaLists()
|
||||
{
|
||||
$kitsuList = $this->filterKitsuMangaList();
|
||||
$malList = $this->formatMALMangaList();
|
||||
|
||||
$itemsToAddToMAL = [];
|
||||
$itemsToAddToKitsu = [];
|
||||
|
||||
$malIds = array_column($malList, 'id');
|
||||
$kitsuMalIds = array_column($kitsuList, 'malId');
|
||||
$missingMalIds = array_diff($malIds, $kitsuMalIds);
|
||||
|
||||
foreach($missingMalIds as $mid)
|
||||
{
|
||||
$itemsToAddToKitsu[] = array_merge($malList[$mid]['data'], [
|
||||
'id' => $this->kitsuModel->getKitsuIdFromMALId($mid, 'manga'),
|
||||
'type' => 'manga'
|
||||
]);
|
||||
}
|
||||
|
||||
foreach($kitsuList as $kitsuItem)
|
||||
{
|
||||
if (in_array($kitsuItem['malId'], $malIds))
|
||||
{
|
||||
// Eventually, compare the list entries, and determine which
|
||||
// needs to be updated
|
||||
continue;
|
||||
}
|
||||
|
||||
// Looks like this item only exists on Kitsu
|
||||
$itemsToAddToMAL[] = [
|
||||
'mal_id' => $kitsuItem['malId'],
|
||||
'data' => $kitsuItem['data']
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'addToMAL' => $itemsToAddToMAL,
|
||||
'addToKitsu' => $itemsToAddToKitsu
|
||||
];
|
||||
}
|
||||
|
||||
public function diffAnimeLists()
|
||||
public function diffLists(string $type = 'anime'): array
|
||||
{
|
||||
// Get libraryEntries with media.mappings from Kitsu
|
||||
// Organize mappings, and ignore entries without mappings
|
||||
$kitsuList = $this->filterKitsuAnimeList();
|
||||
$kitsuList = $this->formatKitsuList($type);
|
||||
|
||||
// Get MAL list data
|
||||
$malList = $this->formatMALAnimeList();
|
||||
$malList = $this->formatMALList($type);
|
||||
|
||||
$itemsToAddToMAL = [];
|
||||
$itemsToAddToKitsu = [];
|
||||
@ -334,8 +246,8 @@ class SyncKitsuWithMal extends BaseCommand {
|
||||
{
|
||||
// print_r($malList[$mid]);
|
||||
$itemsToAddToKitsu[] = array_merge($malList[$mid]['data'], [
|
||||
'id' => $this->kitsuModel->getKitsuIdFromMALId($mid),
|
||||
'type' => 'anime'
|
||||
'id' => $this->kitsuModel->getKitsuIdFromMALId($mid, $type),
|
||||
'type' => $type
|
||||
]);
|
||||
}
|
||||
|
||||
@ -343,8 +255,23 @@ class SyncKitsuWithMal extends BaseCommand {
|
||||
{
|
||||
if (in_array($kitsuItem['malId'], $malIds))
|
||||
{
|
||||
// Eventually, compare the list entries, and determine which
|
||||
// needs to be updated
|
||||
$item = $this->compareListItems($kitsuItem, $malList[$kitsuItem['malId']]);
|
||||
|
||||
if (is_null($item))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (in_array('kitsu', $item['updateType']))
|
||||
{
|
||||
$kitsuUpdateItems[] = $item['data'];
|
||||
}
|
||||
|
||||
if (in_array('mal', $item['updateType']))
|
||||
{
|
||||
$malUpdateItems[] = $item['data'];
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -356,15 +283,6 @@ class SyncKitsuWithMal extends BaseCommand {
|
||||
|
||||
}
|
||||
|
||||
// Compare each list entry
|
||||
// If a list item exists only on MAL, create it on Kitsu with the existing data from MAL
|
||||
// If a list item exists only on Kitsu, create it on MAL with the existing data from Kitsu
|
||||
// If an item already exists on both APIS:
|
||||
// Compare last updated dates, and use the later one
|
||||
// Otherwise, use rewatch count, then episode progress as critera for selecting the more up
|
||||
// to date entry
|
||||
// Based on the 'newer' entry, update the other api list item
|
||||
|
||||
return [
|
||||
'addToMAL' => $itemsToAddToMAL,
|
||||
'updateMAL' => $malUpdateItems,
|
||||
@ -373,6 +291,174 @@ class SyncKitsuWithMal extends BaseCommand {
|
||||
];
|
||||
}
|
||||
|
||||
public function compareListItems(array $kitsuItem, array $malItem)
|
||||
{
|
||||
$compareKeys = ['status', 'progress', 'rating', 'reconsuming'];
|
||||
$diff = [];
|
||||
$dateDiff = (new \DateTime($kitsuItem['data']['updatedAt'])) <=> (new \DateTime($malItem['data']['updatedAt']));
|
||||
|
||||
foreach($compareKeys as $key)
|
||||
{
|
||||
$diff[$key] = $kitsuItem['data'][$key] <=> $malItem['data'][$key];
|
||||
}
|
||||
|
||||
// No difference? Bail out early
|
||||
$diffValues = array_values($diff);
|
||||
$diffValues = array_unique($diffValues);
|
||||
if (count($diffValues) === 1 && $diffValues[0] === 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
$update = [
|
||||
'id' => $kitsuItem['id'],
|
||||
'mal_id' => $kitsuItem['malId'],
|
||||
'data' => []
|
||||
];
|
||||
$return = [
|
||||
'updateType' => []
|
||||
];
|
||||
|
||||
$sameStatus = $diff['status'] === 0;
|
||||
$sameProgress = $diff['progress'] === 0;
|
||||
$sameRating = $diff['rating'] === 0;
|
||||
|
||||
|
||||
// If status is the same, and progress count is different, use greater progress
|
||||
if ($sameStatus && ( ! $sameProgress))
|
||||
{
|
||||
if ($diff['progress'] === 1)
|
||||
{
|
||||
$update['data']['progress'] = $kitsuItem['data']['progress'];
|
||||
$return['updateType'][] = 'mal';
|
||||
}
|
||||
else if($diff['progress'] === -1)
|
||||
{
|
||||
$update['data']['progress'] = $malItem['data']['progress'];
|
||||
$return['updateType'][] = 'kitsu';
|
||||
}
|
||||
}
|
||||
|
||||
// If status and progress are different, it's a bit more complicated...
|
||||
// But, at least for now, assume newer record is correct
|
||||
if ( ! ($sameStatus || $sameProgress))
|
||||
{
|
||||
if ($dateDiff === 1)
|
||||
{
|
||||
$update['data']['status'] = $kitsuItem['data']['status'];
|
||||
|
||||
if ((int)$kitsuItem['data']['progress'] !== 0)
|
||||
{
|
||||
$update['data']['progress'] = $kitsuItem['data']['progress'];
|
||||
}
|
||||
|
||||
$return['updateType'][] = 'mal';
|
||||
}
|
||||
else if($dateDiff === -1)
|
||||
{
|
||||
$update['data']['status'] = $malItem['data']['status'];
|
||||
|
||||
if ((int)$malItem['data']['progress'] !== 0)
|
||||
{
|
||||
$update['data']['progress'] = $kitsuItem['data']['progress'];
|
||||
}
|
||||
|
||||
$return['updateType'][] = 'kitsu';
|
||||
}
|
||||
}
|
||||
|
||||
// If rating is different, use the rating from the item most recently updated
|
||||
if ( ! $sameRating)
|
||||
{
|
||||
if ($dateDiff === 1)
|
||||
{
|
||||
$update['data']['rating'] = $kitsuItem['data']['rating'];
|
||||
$return['updateType'][] = 'mal';
|
||||
}
|
||||
else if ($dateDiff === -1)
|
||||
{
|
||||
$update['data']['rating'] = $malItem['data']['rating'];
|
||||
$return['updateType'][] = 'kitsu';
|
||||
}
|
||||
}
|
||||
|
||||
// If status is different, use the status of the more recently updated item
|
||||
if ( ! $sameStatus)
|
||||
{
|
||||
if ($dateDiff === 1)
|
||||
{
|
||||
$update['data']['status'] = $kitsuItem['data']['status'];
|
||||
$return['updateType'][] = 'mal';
|
||||
}
|
||||
else if ($dateDiff === -1)
|
||||
{
|
||||
$update['data']['status'] = $malItem['data']['status'];
|
||||
$return['updateType'][] = 'kitsu';
|
||||
}
|
||||
}
|
||||
|
||||
$return['meta'] = [
|
||||
'kitsu' => $kitsuItem['data'],
|
||||
'mal' => $malItem['data'],
|
||||
'dateDiff' => $dateDiff,
|
||||
'diff' => $diff,
|
||||
];
|
||||
$return['data'] = $update;
|
||||
$return['updateType'] = array_unique($return['updateType']);
|
||||
return $return;
|
||||
}
|
||||
|
||||
public function updateKitsuListItems($itemsToUpdate, $type = 'anime')
|
||||
{
|
||||
$requester = new ParallelAPIRequest();
|
||||
foreach($itemsToUpdate as $item)
|
||||
{
|
||||
$requester->addRequest($this->kitsuModel->updateListItem($item));
|
||||
}
|
||||
|
||||
$responses = $requester->makeRequests();
|
||||
|
||||
foreach($responses as $key => $response)
|
||||
{
|
||||
$id = $itemsToUpdate[$key]['id'];
|
||||
if ($response->getStatus() === 200)
|
||||
{
|
||||
$this->echoBox("Successfully updated Kitsu {$type} list item with id: {$id}");
|
||||
}
|
||||
else
|
||||
{
|
||||
echo $response->getBody();
|
||||
$this->echoBox("Failed to update Kitsu {$type} list item with id: {$id}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function updateMALListItems($itemsToUpdate, $type = 'anime')
|
||||
{
|
||||
$transformer = new ALT();
|
||||
$requester = new ParallelAPIRequest();
|
||||
|
||||
foreach($itemsToUpdate as $item)
|
||||
{
|
||||
$requester->addRequest($this->malModel->updateListItem($item, $type));
|
||||
}
|
||||
|
||||
$responses = $requester->makeRequests();
|
||||
|
||||
foreach($responses as $key => $response)
|
||||
{
|
||||
$id = $itemsToUpdate[$key]['mal_id'];
|
||||
if ($response->getBody() === 'Updated')
|
||||
{
|
||||
$this->echoBox("Successfully updated MAL {$type} list item with id: {$id}");
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->echoBox("Failed to update MAL {$type} list item with id: {$id}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function createKitsuListItems($itemsToAdd, $type = 'anime')
|
||||
{
|
||||
$requester = new ParallelAPIRequest();
|
||||
|
@ -239,6 +239,13 @@ class Controller {
|
||||
*/
|
||||
protected function renderFullPage($view, string $template, array $data)
|
||||
{
|
||||
$csp = [
|
||||
"default-src 'self'",
|
||||
"object-src 'none'",
|
||||
"child-src 'none'",
|
||||
];
|
||||
|
||||
$view->addHeader('Content-Security-Policy', implode('; ', $csp));
|
||||
$view->appendOutput($this->loadPartial($view, 'header', $data));
|
||||
|
||||
if (array_key_exists('message', $data) && is_array($data['message']))
|
||||
|
@ -280,11 +280,11 @@ class Anime extends BaseController {
|
||||
);
|
||||
}
|
||||
|
||||
foreach($data['included'] as $included)
|
||||
if (array_key_exists('characters', $data['included']))
|
||||
{
|
||||
if ($included['type'] === 'characters')
|
||||
foreach($data['included']['characters'] as $id => $character)
|
||||
{
|
||||
$characters[$included['id']] = $included['attributes'];
|
||||
$characters[$id] = $character['attributes'];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -14,38 +14,154 @@
|
||||
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
|
||||
*/
|
||||
|
||||
namespace Aviat\AnimeClient\Controller;
|
||||
|
||||
use Aviat\AnimeClient\Controller as BaseController;
|
||||
|
||||
/**
|
||||
* Controller for character description pages
|
||||
*/
|
||||
class Character extends BaseController {
|
||||
|
||||
public function index(string $slug)
|
||||
{
|
||||
$model = $this->container->get('kitsu-model');
|
||||
|
||||
$data = $model->getCharacter($slug);
|
||||
|
||||
if (( ! array_key_exists('data', $data)) || empty($data['data']))
|
||||
{
|
||||
return $this->notFound(
|
||||
$this->formatTitle(
|
||||
'Characters',
|
||||
'Character not found'
|
||||
),
|
||||
'Character Not Found'
|
||||
);
|
||||
}
|
||||
|
||||
$this->outputHTML('character', [
|
||||
'title' => $this->formatTitle(
|
||||
'Characters',
|
||||
$data['data'][0]['attributes']['name']
|
||||
),
|
||||
'data' => $data['data'][0]['attributes']
|
||||
]);
|
||||
}
|
||||
namespace Aviat\AnimeClient\Controller;
|
||||
|
||||
use Aviat\AnimeClient\Controller as BaseController;
|
||||
use Aviat\AnimeClient\API\JsonAPI;
|
||||
use Aviat\Ion\ArrayWrapper;
|
||||
|
||||
/**
|
||||
* Controller for character description pages
|
||||
*/
|
||||
class Character extends BaseController {
|
||||
|
||||
use ArrayWrapper;
|
||||
|
||||
public function index(string $slug)
|
||||
{
|
||||
$model = $this->container->get('kitsu-model');
|
||||
|
||||
$rawData = $model->getCharacter($slug);
|
||||
|
||||
if (( ! array_key_exists('data', $rawData)) || empty($rawData['data']))
|
||||
{
|
||||
return $this->notFound(
|
||||
$this->formatTitle(
|
||||
'Characters',
|
||||
'Character not found'
|
||||
),
|
||||
'Character Not Found'
|
||||
);
|
||||
}
|
||||
|
||||
$data = JsonAPI::organizeData($rawData);
|
||||
|
||||
$viewData = [
|
||||
'title' => $this->formatTitle(
|
||||
'Characters',
|
||||
$data[0]['attributes']['name']
|
||||
),
|
||||
'data' => $data,
|
||||
'castCount' => 0,
|
||||
'castings' => []
|
||||
];
|
||||
|
||||
if (array_key_exists('included', $data) && array_key_exists('castings', $data['included']))
|
||||
{
|
||||
$viewData['castings'] = $this->organizeCast($data['included']['castings']);
|
||||
$viewData['castCount'] = $this->getCastCount($viewData['castings']);
|
||||
}
|
||||
|
||||
$this->outputHTML('character', $viewData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Organize VA => anime relationships
|
||||
*
|
||||
* @param array $cast
|
||||
* @return array
|
||||
*/
|
||||
private function dedupeCast(array $cast): array
|
||||
{
|
||||
$output = [];
|
||||
$people = [];
|
||||
|
||||
$i = 0;
|
||||
foreach ($cast as &$role)
|
||||
{
|
||||
if (empty($role['attributes']['role']))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
$person = current($role['relationships']['person']['people'])['attributes'];
|
||||
|
||||
if ( ! array_key_exists($person['name'], $people))
|
||||
{
|
||||
$people[$person['name']] = $i;
|
||||
$role['relationships']['media']['anime'] = [current($role['relationships']['media']['anime'])];
|
||||
$output[$i] = $role;
|
||||
|
||||
$i++;
|
||||
|
||||
continue;
|
||||
}
|
||||
else if(array_key_exists($person['name'], $people))
|
||||
{
|
||||
if (array_key_exists('anime', $role['relationships']['media']))
|
||||
{
|
||||
$key = $people[$person['name']];
|
||||
$output[$key]['relationships']['media']['anime'][] = current($role['relationships']['media']['anime']);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
private function getCastCount(array $cast): int
|
||||
{
|
||||
$count = 0;
|
||||
|
||||
foreach($cast as $role)
|
||||
{
|
||||
if (
|
||||
array_key_exists('attributes', $role) &&
|
||||
array_key_exists('role', $role['attributes']) &&
|
||||
( ! is_null($role['attributes']['role']))
|
||||
) {
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
private function organizeCast(array $cast): array
|
||||
{
|
||||
$cast = $this->dedupeCast($cast);
|
||||
$output = [];
|
||||
|
||||
foreach($cast as $id => $role)
|
||||
{
|
||||
if (empty($role['attributes']['role']))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$language = $role['attributes']['language'];
|
||||
$roleName = $role['attributes']['role'];
|
||||
$isVA = $role['attributes']['voiceActor'];
|
||||
|
||||
if ($isVA)
|
||||
{
|
||||
$person = current($role['relationships']['person']['people'])['attributes'];
|
||||
$name = $person['name'];
|
||||
$item = [
|
||||
'person' => $person,
|
||||
'series' => $role['relationships']['media']['anime']
|
||||
];
|
||||
|
||||
$output[$roleName][$language][] = $item;
|
||||
}
|
||||
else
|
||||
{
|
||||
$output[$roleName][] = $role['relationships']['person']['people'];
|
||||
}
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
}
|
@ -16,10 +16,16 @@
|
||||
|
||||
namespace Aviat\AnimeClient\Controller;
|
||||
|
||||
use function Amp\wait;
|
||||
|
||||
use Amp\Artax\Client;
|
||||
use Aviat\AnimeClient\Controller as BaseController;
|
||||
use Aviat\AnimeClient\API\JsonAPI;
|
||||
use Aviat\Ion\View\HtmlView;
|
||||
|
||||
/**
|
||||
* Controller for handling routes that don't fit elsewhere
|
||||
*/
|
||||
class Index extends BaseController {
|
||||
|
||||
/**
|
||||
@ -105,7 +111,7 @@ class Index extends BaseController {
|
||||
$data = $model->getUserData($username);
|
||||
$orgData = JsonAPI::organizeData($data);
|
||||
$this->outputHTML('me', [
|
||||
'title' => 'About' . $this->config->get('whose_list'),
|
||||
'title' => 'About ' . $this->config->get('whose_list'),
|
||||
'data' => $orgData[0],
|
||||
'attributes' => $orgData[0]['attributes'],
|
||||
'relationships' => $orgData[0]['relationships'],
|
||||
@ -113,11 +119,55 @@ class Index extends BaseController {
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get image covers from kitsu
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function images($type, $file)
|
||||
{
|
||||
$kitsuUrl = 'https://media.kitsu.io/';
|
||||
list($id, $ext) = explode('.', basename($file));
|
||||
switch ($type)
|
||||
{
|
||||
case 'anime':
|
||||
$kitsuUrl .= "anime/poster_images/{$id}/small.{$ext}";
|
||||
break;
|
||||
|
||||
case 'avatars':
|
||||
$kitsuUrl .= "users/avatars/{$id}/original.{$ext}";
|
||||
break;
|
||||
|
||||
case 'manga':
|
||||
$kitsuUrl .= "manga/poster_images/{$id}/small.{$ext}";
|
||||
break;
|
||||
|
||||
case 'characters':
|
||||
$kitsuUrl .= "characters/images/{$id}/original.{$ext}";
|
||||
break;
|
||||
|
||||
default:
|
||||
$this->notFound();
|
||||
return;
|
||||
}
|
||||
|
||||
$promise = (new Client)->request($kitsuUrl);
|
||||
$response = wait($promise);
|
||||
$data = (string) $response->getBody();
|
||||
|
||||
$baseSavePath = $this->config->get('img_cache_path');
|
||||
file_put_contents("{$baseSavePath}/{$type}/{$id}.{$ext}", $data);
|
||||
header('Content-type: ' . $response->getHeader('content-type')[0]);
|
||||
echo (string) $response->getBody();
|
||||
}
|
||||
|
||||
private function organizeFavorites(array $rawfavorites): array
|
||||
{
|
||||
// return $rawfavorites;
|
||||
$output = [];
|
||||
|
||||
unset($rawfavorites['data']);
|
||||
|
||||
foreach($rawfavorites as $item)
|
||||
{
|
||||
$rank = $item['attributes']['favRank'];
|
||||
@ -126,7 +176,7 @@ class Index extends BaseController {
|
||||
$output[$key] = $output[$key] ?? [];
|
||||
foreach ($fav as $id => $data)
|
||||
{
|
||||
$output[$key][$rank] = $data['attributes'];
|
||||
$output[$key][$rank] = array_merge(['id' => $id], $data['attributes']);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -26,6 +26,7 @@ use const Aviat\AnimeClient\{
|
||||
|
||||
use function Aviat\Ion\_dir;
|
||||
|
||||
use Aviat\AnimeClient\API\FailedResponseException;
|
||||
use Aviat\Ion\Di\ContainerInterface;
|
||||
use Aviat\Ion\Friend;
|
||||
|
||||
@ -256,13 +257,24 @@ class Dispatcher extends RoutingBase {
|
||||
{
|
||||
$logger = $this->container->getLogger('default');
|
||||
|
||||
$controller = new $controllerName($this->container);
|
||||
try
|
||||
{
|
||||
$controller = new $controllerName($this->container);
|
||||
|
||||
// Run the appropriate controller method
|
||||
$logger->debug('Dispatcher - controller arguments');
|
||||
$logger->debug(print_r($params, TRUE));
|
||||
// Run the appropriate controller method
|
||||
$logger->debug('Dispatcher - controller arguments', $params);
|
||||
|
||||
call_user_func_array([$controller, $method], $params);
|
||||
}
|
||||
catch (FailedResponseException $e)
|
||||
{
|
||||
$controllerName = DEFAULT_CONTROLLER;
|
||||
$controller = new $controllerName($this->container);
|
||||
$controller->errorPage(500,
|
||||
'API request timed out',
|
||||
'Failed to retrieve data from API (╯°□°)╯︵ ┻━┻');
|
||||
}
|
||||
|
||||
call_user_func_array([$controller, $method], $params);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -98,7 +98,7 @@ class MenuGenerator extends UrlGenerator {
|
||||
foreach ($menuConfig as $title => $path)
|
||||
{
|
||||
$has = $this->string($this->path())->contains($path);
|
||||
$selected = ($has && strlen($this->path()) >= strlen($path));
|
||||
$selected = ($has && mb_strlen($this->path()) >= mb_strlen($path));
|
||||
|
||||
$link = $this->helper->a($this->url($path), $title);
|
||||
|
||||
|
@ -22,33 +22,27 @@ use Aviat\Ion\Friend;
|
||||
use Aviat\Ion\Json;
|
||||
|
||||
class AnimeListTransformerTest extends AnimeClientTestCase {
|
||||
|
||||
protected $dir;
|
||||
protected $beforeTransform;
|
||||
protected $afterTransform;
|
||||
protected $transformer;
|
||||
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
parent::setUp();
|
||||
$this->dir = AnimeClientTestCase::TEST_DATA_DIR . '/Kitsu';
|
||||
|
||||
|
||||
$this->beforeTransform = Json::decodeFile("{$this->dir}/animeListItemBeforeTransform.json");
|
||||
$this->afterTransform = Json::decodeFile("{$this->dir}/animeListItemAfterTransform.json");
|
||||
|
||||
|
||||
$this->transformer = new AnimeListTransformer();
|
||||
}
|
||||
|
||||
|
||||
public function testTransform()
|
||||
{
|
||||
$expected = $this->afterTransform;
|
||||
$actual = $this->transformer->transform($this->beforeTransform);
|
||||
|
||||
// Json::encodeFile("{$this->dir}/animeListItemAfterTransform.json", $actual);
|
||||
|
||||
$this->assertEquals($expected, $actual);
|
||||
$this->assertMatchesSnapshot($actual);
|
||||
}
|
||||
|
||||
|
||||
public function dataUntransform()
|
||||
{
|
||||
return [[
|
||||
@ -60,19 +54,6 @@ class AnimeListTransformerTest extends AnimeClientTestCase {
|
||||
'rewatched' => 0,
|
||||
'notes' => 'Very formulaic.',
|
||||
'edit' => true
|
||||
],
|
||||
'expected' => [
|
||||
'id' => 14047981,
|
||||
'mal_id' => null,
|
||||
'data' => [
|
||||
'status' => 'current',
|
||||
'rating' => 4,
|
||||
'reconsuming' => false,
|
||||
'reconsumeCount' => 0,
|
||||
'notes' => 'Very formulaic.',
|
||||
'progress' => 38,
|
||||
'private' => false
|
||||
]
|
||||
]
|
||||
], [
|
||||
'input' => [
|
||||
@ -86,29 +67,29 @@ class AnimeListTransformerTest extends AnimeClientTestCase {
|
||||
'edit' => 'true',
|
||||
'private' => 'On',
|
||||
'rewatching' => 'On'
|
||||
],
|
||||
'expected' => [
|
||||
'id' => 14047981,
|
||||
'mal_id' => '12345',
|
||||
'data' => [
|
||||
'status' => 'current',
|
||||
'rating' => 4,
|
||||
'reconsuming' => true,
|
||||
'reconsumeCount' => 0,
|
||||
'notes' => 'Very formulaic.',
|
||||
'progress' => 38,
|
||||
'private' => true,
|
||||
]
|
||||
]
|
||||
], [
|
||||
'input' => [
|
||||
'id' => 14047983,
|
||||
'mal_id' => '12347',
|
||||
'watching_status' => 'current',
|
||||
'user_rating' => 0,
|
||||
'episodes_watched' => 12,
|
||||
'rewatched' => 0,
|
||||
'notes' => '',
|
||||
'edit' => 'true',
|
||||
'private' => 'On',
|
||||
'rewatching' => 'On'
|
||||
]
|
||||
]];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @dataProvider dataUntransform
|
||||
*/
|
||||
public function testUntransform($input, $expected)
|
||||
public function testUntransform($input)
|
||||
{
|
||||
$actual = $this->transformer->untransform($input);
|
||||
$this->assertEquals($expected, $actual);
|
||||
$this->assertMatchesSnapshot($actual);
|
||||
}
|
||||
}
|
@ -22,29 +22,27 @@ use Aviat\Ion\Friend;
|
||||
use Aviat\Ion\Json;
|
||||
|
||||
class AnimeTransformerTest extends AnimeClientTestCase {
|
||||
|
||||
|
||||
protected $dir;
|
||||
protected $beforeTransform;
|
||||
protected $afterTransform;
|
||||
protected $transformer;
|
||||
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
parent::setUp();
|
||||
$this->dir = AnimeClientTestCase::TEST_DATA_DIR . '/Kitsu';
|
||||
|
||||
|
||||
$this->beforeTransform = Json::decodeFile("{$this->dir}/animeBeforeTransform.json");
|
||||
$this->afterTransform = Json::decodeFile("{$this->dir}/animeAfterTransform.json");
|
||||
|
||||
|
||||
$this->transformer = new AnimeTransformer();
|
||||
}
|
||||
|
||||
|
||||
public function testTransform()
|
||||
{
|
||||
$expected = $this->afterTransform;
|
||||
$actual = $this->transformer->transform($this->beforeTransform);
|
||||
// Json::encodeFile("{$this->dir}/animeAfterTransform.json", $actual);
|
||||
|
||||
$this->assertEquals($expected, $actual);
|
||||
|
||||
$this->assertMatchesSnapshot($actual);
|
||||
}
|
||||
}
|
@ -47,19 +47,15 @@ class MangaListTransformerTest extends AnimeClientTestCase {
|
||||
}
|
||||
|
||||
$this->beforeTransform = $rawBefore['data'];
|
||||
$this->afterTransform = Json::decodeFile("{$this->dir}/mangaListAfterTransform.json");
|
||||
// $this->afterTransform = Json::decodeFile("{$this->dir}/mangaListAfterTransform.json");
|
||||
|
||||
$this->transformer = new MangaListTransformer();
|
||||
}
|
||||
|
||||
public function testTransform()
|
||||
{
|
||||
$expected = $this->afterTransform;
|
||||
$actual = $this->transformer->transformCollection($this->beforeTransform);
|
||||
|
||||
// Json::encodeFile("{$this->dir}/mangaListAfterTransform.json", $actual);
|
||||
|
||||
$this->assertEquals($expected, $actual);
|
||||
$this->assertMatchesSnapshot($actual);
|
||||
}
|
||||
|
||||
public function testUntransform()
|
||||
@ -69,6 +65,7 @@ class MangaListTransformerTest extends AnimeClientTestCase {
|
||||
'mal_id' => '26769',
|
||||
'chapters_read' => 67,
|
||||
'manga' => [
|
||||
'id' => '12345',
|
||||
'titles' => ["Bokura wa Minna Kawaisou"],
|
||||
'alternate_title' => NULL,
|
||||
'slug' => "bokura-wa-minna-kawaisou",
|
||||
|
@ -22,32 +22,29 @@ use Aviat\AnimeClient\Tests\AnimeClientTestCase;
|
||||
use Aviat\Ion\Json;
|
||||
|
||||
class MangaTransformerTest extends AnimeClientTestCase {
|
||||
|
||||
|
||||
protected $dir;
|
||||
protected $beforeTransform;
|
||||
protected $afterTransform;
|
||||
protected $transformer;
|
||||
|
||||
public function setUp()
|
||||
public function setUp()
|
||||
{
|
||||
parent::setUp();
|
||||
$this->dir = AnimeClientTestCase::TEST_DATA_DIR . '/Kitsu';
|
||||
|
||||
|
||||
$data = Json::decodeFile("{$this->dir}/mangaBeforeTransform.json");
|
||||
$baseData = $data['data'][0]['attributes'];
|
||||
$baseData['included'] = $data['included'];
|
||||
$baseData['id'] = $data['data'][0]['id'];
|
||||
$this->beforeTransform = $baseData;
|
||||
$this->afterTransform = Json::decodeFile("{$this->dir}/mangaAfterTransform.json");
|
||||
|
||||
|
||||
$this->transformer = new MangaTransformer();
|
||||
}
|
||||
|
||||
|
||||
public function testTransform()
|
||||
{
|
||||
$actual = $this->transformer->transform($this->beforeTransform);
|
||||
$expected = $this->afterTransform;
|
||||
//Json::encodeFile("{$this->dir}/mangaAfterTransform.json", $actual);
|
||||
|
||||
$this->assertEquals($expected, $actual);
|
||||
$this->assertMatchesSnapshot($actual);
|
||||
}
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
<?php return array (
|
||||
'id' => '15839442',
|
||||
'mal_id' => '33206',
|
||||
'episodes' =>
|
||||
array (
|
||||
'watched' => '-',
|
||||
'total' => '-',
|
||||
'length' => NULL,
|
||||
),
|
||||
'airing' =>
|
||||
array (
|
||||
'status' => 'Currently Airing',
|
||||
'started' => '2017-01-12',
|
||||
'ended' => NULL,
|
||||
),
|
||||
'anime' =>
|
||||
array (
|
||||
'id' => '12243',
|
||||
'age_rating' => NULL,
|
||||
'title' => 'Kobayashi-san Chi no Maid Dragon',
|
||||
'titles' =>
|
||||
array (
|
||||
0 => 'Kobayashi-san Chi no Maid Dragon',
|
||||
1 => 'Miss Kobayashi\'s Dragon Maid',
|
||||
2 => '小林さんちのメイドラゴン',
|
||||
),
|
||||
'slug' => 'kobayashi-san-chi-no-maid-dragon',
|
||||
'type' => 'TV',
|
||||
'image' => 'https://media.kitsu.io/anime/poster_images/12243/small.jpg?1481144116',
|
||||
'genres' =>
|
||||
array (
|
||||
0 => 'Comedy',
|
||||
1 => 'Fantasy',
|
||||
2 => 'Slice of Life',
|
||||
),
|
||||
'streaming_links' =>
|
||||
array (
|
||||
),
|
||||
),
|
||||
'watching_status' => 'current',
|
||||
'notes' => NULL,
|
||||
'rewatching' => false,
|
||||
'rewatched' => 0,
|
||||
'user_rating' => '-',
|
||||
'private' => false,
|
||||
);
|
@ -0,0 +1,14 @@
|
||||
<?php return array (
|
||||
'id' => 14047981,
|
||||
'mal_id' => NULL,
|
||||
'data' =>
|
||||
array (
|
||||
'status' => 'current',
|
||||
'reconsuming' => false,
|
||||
'reconsumeCount' => 0,
|
||||
'notes' => 'Very formulaic.',
|
||||
'progress' => 38,
|
||||
'private' => false,
|
||||
'rating' => 4,
|
||||
),
|
||||
);
|
@ -0,0 +1,14 @@
|
||||
<?php return array (
|
||||
'id' => 14047981,
|
||||
'mal_id' => '12345',
|
||||
'data' =>
|
||||
array (
|
||||
'status' => 'current',
|
||||
'reconsuming' => true,
|
||||
'reconsumeCount' => 0,
|
||||
'notes' => 'Very formulaic.',
|
||||
'progress' => 38,
|
||||
'private' => true,
|
||||
'rating' => 4,
|
||||
),
|
||||
);
|
@ -0,0 +1,13 @@
|
||||
<?php return array (
|
||||
'id' => 14047983,
|
||||
'mal_id' => '12347',
|
||||
'data' =>
|
||||
array (
|
||||
'status' => 'current',
|
||||
'reconsuming' => true,
|
||||
'reconsumeCount' => 0,
|
||||
'notes' => '',
|
||||
'progress' => 12,
|
||||
'private' => true,
|
||||
),
|
||||
);
|
@ -0,0 +1,104 @@
|
||||
<?php return array (
|
||||
'id' => 32344,
|
||||
'slug' => 'attack-on-titan',
|
||||
'title' => 'Attack on Titan',
|
||||
'titles' =>
|
||||
array (
|
||||
0 => 'Attack on Titan',
|
||||
1 => 'Shingeki no Kyojin',
|
||||
2 => '進撃の巨人',
|
||||
),
|
||||
'status' => 'Finished Airing',
|
||||
'cover_image' => 'https://media.kitsu.io/anime/poster_images/7442/small.jpg?1418580054',
|
||||
'show_type' => 'TV',
|
||||
'episode_count' => 25,
|
||||
'episode_length' => 24,
|
||||
'synopsis' => 'Several hundred years ago, humans were nearly exterminated by titans. Titans are typically several stories tall, seem to have no intelligence, devour human beings and, worst of all, seem to do it for the pleasure rather than as a food source. A small percentage of humanity survived by enclosing themselves in a city protected by extremely high walls, even taller than the biggest of titans. Flash forward to the present and the city has not seen a titan in over 100 years. Teenage boy Eren and his foster sister Mikasa witness something horrific as the city walls are destroyed by a colossal titan that appears out of thin air. As the smaller titans flood the city, the two kids watch in horror as their mother is eaten alive. Eren vows that he will murder every single titan and take revenge for all of mankind.
|
||||
|
||||
(Source: ANN)',
|
||||
'age_rating' => 'R',
|
||||
'age_rating_guide' => 'Violence, Profanity',
|
||||
'url' => 'https://kitsu.io/anime/attack-on-titan',
|
||||
'genres' =>
|
||||
array (
|
||||
0 => 'Action',
|
||||
1 => 'Drama',
|
||||
2 => 'Fantasy',
|
||||
3 => 'Super Power',
|
||||
),
|
||||
'streaming_links' =>
|
||||
array (
|
||||
0 =>
|
||||
array (
|
||||
'meta' =>
|
||||
array (
|
||||
'name' => 'Crunchyroll',
|
||||
'link' => true,
|
||||
'image' => 'streaming-logos/crunchyroll.svg',
|
||||
),
|
||||
'link' => 'http://www.crunchyroll.com/attack-on-titan',
|
||||
'subs' =>
|
||||
array (
|
||||
0 => 'en',
|
||||
),
|
||||
'dubs' =>
|
||||
array (
|
||||
0 => 'ja',
|
||||
),
|
||||
),
|
||||
1 =>
|
||||
array (
|
||||
'meta' =>
|
||||
array (
|
||||
'name' => 'Hulu',
|
||||
'link' => true,
|
||||
'image' => 'streaming-logos/hulu.svg',
|
||||
),
|
||||
'link' => 'http://www.hulu.com/attack-on-titan',
|
||||
'subs' =>
|
||||
array (
|
||||
0 => 'en',
|
||||
),
|
||||
'dubs' =>
|
||||
array (
|
||||
0 => 'ja',
|
||||
),
|
||||
),
|
||||
2 =>
|
||||
array (
|
||||
'meta' =>
|
||||
array (
|
||||
'name' => 'Funimation',
|
||||
'link' => true,
|
||||
'image' => 'streaming-logos/funimation.svg',
|
||||
),
|
||||
'link' => 'http://www.funimation.com/shows/attack-on-titan/videos/episodes',
|
||||
'subs' =>
|
||||
array (
|
||||
0 => 'en',
|
||||
),
|
||||
'dubs' =>
|
||||
array (
|
||||
0 => 'ja',
|
||||
),
|
||||
),
|
||||
3 =>
|
||||
array (
|
||||
'meta' =>
|
||||
array (
|
||||
'name' => 'Netflix',
|
||||
'link' => false,
|
||||
'image' => 'streaming-logos/netflix.svg',
|
||||
),
|
||||
'link' => 't',
|
||||
'subs' =>
|
||||
array (
|
||||
0 => 'en',
|
||||
),
|
||||
'dubs' =>
|
||||
array (
|
||||
0 => 'ja',
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
@ -0,0 +1,206 @@
|
||||
<?php return array (
|
||||
0 =>
|
||||
array (
|
||||
'id' => '15084773',
|
||||
'mal_id' => '26769',
|
||||
'chapters' =>
|
||||
array (
|
||||
'read' => 67,
|
||||
'total' => '-',
|
||||
),
|
||||
'volumes' =>
|
||||
array (
|
||||
'read' => '-',
|
||||
'total' => '-',
|
||||
),
|
||||
'manga' =>
|
||||
array (
|
||||
'id' => '20286',
|
||||
'titles' =>
|
||||
array (
|
||||
0 => 'Bokura wa Minna Kawaisou',
|
||||
),
|
||||
'alternate_title' => NULL,
|
||||
'slug' => 'bokura-wa-minna-kawaisou',
|
||||
'url' => 'https://kitsu.io/manga/bokura-wa-minna-kawaisou',
|
||||
'type' => 'manga',
|
||||
'image' => 'https://media.kitsu.io/manga/poster_images/20286/small.jpg?1434293999',
|
||||
'genres' =>
|
||||
array (
|
||||
0 => 'Comedy',
|
||||
1 => 'Romance',
|
||||
2 => 'School',
|
||||
3 => 'Slice of Life',
|
||||
4 => 'Thriller',
|
||||
),
|
||||
),
|
||||
'reading_status' => 'current',
|
||||
'notes' => '',
|
||||
'rereading' => false,
|
||||
'reread' => 0,
|
||||
'user_rating' => 9.0,
|
||||
),
|
||||
1 =>
|
||||
array (
|
||||
'id' => '15085607',
|
||||
'mal_id' => '16',
|
||||
'chapters' =>
|
||||
array (
|
||||
'read' => 17,
|
||||
'total' => 120,
|
||||
),
|
||||
'volumes' =>
|
||||
array (
|
||||
'read' => '-',
|
||||
'total' => 14,
|
||||
),
|
||||
'manga' =>
|
||||
array (
|
||||
'id' => '47',
|
||||
'titles' =>
|
||||
array (
|
||||
0 => 'Love Hina',
|
||||
),
|
||||
'alternate_title' => NULL,
|
||||
'slug' => 'love-hina',
|
||||
'url' => 'https://kitsu.io/manga/love-hina',
|
||||
'type' => 'manga',
|
||||
'image' => 'https://media.kitsu.io/manga/poster_images/47/small.jpg?1434249493',
|
||||
'genres' =>
|
||||
array (
|
||||
0 => 'Comedy',
|
||||
1 => 'Ecchi',
|
||||
2 => 'Harem',
|
||||
3 => 'Romance',
|
||||
4 => 'Sports',
|
||||
),
|
||||
),
|
||||
'reading_status' => 'current',
|
||||
'notes' => '',
|
||||
'rereading' => false,
|
||||
'reread' => 0,
|
||||
'user_rating' => 7.0,
|
||||
),
|
||||
2 =>
|
||||
array (
|
||||
'id' => '15084529',
|
||||
'mal_id' => '35003',
|
||||
'chapters' =>
|
||||
array (
|
||||
'read' => 16,
|
||||
'total' => '-',
|
||||
),
|
||||
'volumes' =>
|
||||
array (
|
||||
'read' => '-',
|
||||
'total' => '-',
|
||||
),
|
||||
'manga' =>
|
||||
array (
|
||||
'id' => '11777',
|
||||
'titles' =>
|
||||
array (
|
||||
0 => 'Yamada-kun to 7-nin no Majo',
|
||||
1 => 'Yamada-kun and the Seven Witches',
|
||||
),
|
||||
'alternate_title' => NULL,
|
||||
'slug' => 'yamada-kun-to-7-nin-no-majo',
|
||||
'url' => 'https://kitsu.io/manga/yamada-kun-to-7-nin-no-majo',
|
||||
'type' => 'manga',
|
||||
'image' => 'https://media.kitsu.io/manga/poster_images/11777/small.jpg?1438784325',
|
||||
'genres' =>
|
||||
array (
|
||||
0 => 'Comedy',
|
||||
1 => 'Ecchi',
|
||||
2 => 'Gender Bender',
|
||||
3 => 'Romance',
|
||||
4 => 'School',
|
||||
5 => 'Sports',
|
||||
6 => 'Supernatural',
|
||||
),
|
||||
),
|
||||
'reading_status' => 'current',
|
||||
'notes' => '',
|
||||
'rereading' => false,
|
||||
'reread' => 0,
|
||||
'user_rating' => 9.0,
|
||||
),
|
||||
3 =>
|
||||
array (
|
||||
'id' => '15312827',
|
||||
'mal_id' => '78523',
|
||||
'chapters' =>
|
||||
array (
|
||||
'read' => 68,
|
||||
'total' => '-',
|
||||
),
|
||||
'volumes' =>
|
||||
array (
|
||||
'read' => '-',
|
||||
'total' => '-',
|
||||
),
|
||||
'manga' =>
|
||||
array (
|
||||
'id' => '27175',
|
||||
'titles' =>
|
||||
array (
|
||||
0 => 'ReLIFE',
|
||||
),
|
||||
'alternate_title' => NULL,
|
||||
'slug' => 'relife',
|
||||
'url' => 'https://kitsu.io/manga/relife',
|
||||
'type' => 'manga',
|
||||
'image' => 'https://media.kitsu.io/manga/poster_images/27175/small.jpg?1464379411',
|
||||
'genres' =>
|
||||
array (
|
||||
0 => 'Romance',
|
||||
1 => 'School',
|
||||
2 => 'Slice of Life',
|
||||
),
|
||||
),
|
||||
'reading_status' => 'current',
|
||||
'notes' => '',
|
||||
'rereading' => false,
|
||||
'reread' => 0,
|
||||
'user_rating' => '-',
|
||||
),
|
||||
4 =>
|
||||
array (
|
||||
'id' => '15084769',
|
||||
'mal_id' => '60815',
|
||||
'chapters' =>
|
||||
array (
|
||||
'read' => 43,
|
||||
'total' => '-',
|
||||
),
|
||||
'volumes' =>
|
||||
array (
|
||||
'read' => '-',
|
||||
'total' => '-',
|
||||
),
|
||||
'manga' =>
|
||||
array (
|
||||
'id' => '25491',
|
||||
'titles' =>
|
||||
array (
|
||||
0 => 'Joshikausei',
|
||||
),
|
||||
'alternate_title' => NULL,
|
||||
'slug' => 'joshikausei',
|
||||
'url' => 'https://kitsu.io/manga/joshikausei',
|
||||
'type' => 'manga',
|
||||
'image' => 'https://media.kitsu.io/manga/poster_images/25491/small.jpg?1434305043',
|
||||
'genres' =>
|
||||
array (
|
||||
0 => 'Comedy',
|
||||
1 => 'School',
|
||||
2 => 'Slice of Life',
|
||||
),
|
||||
),
|
||||
'reading_status' => 'current',
|
||||
'notes' => '',
|
||||
'rereading' => false,
|
||||
'reread' => 0,
|
||||
'user_rating' => 8.0,
|
||||
),
|
||||
);
|
@ -0,0 +1,21 @@
|
||||
<?php return array (
|
||||
'id' => '20286',
|
||||
'title' => 'Bokura wa Minna Kawaisou',
|
||||
'en_title' => NULL,
|
||||
'jp_title' => 'Bokura wa Minna Kawaisou',
|
||||
'cover_image' => 'https://media.kitsu.io/manga/poster_images/20286/small.jpg?1434293999',
|
||||
'manga_type' => 'manga',
|
||||
'chapter_count' => '-',
|
||||
'volume_count' => '-',
|
||||
'synopsis' => 'Usa, a high-school student aspiring to begin a bachelor lifestyle, moves into a new apartment only to discover that he not only shares a room with a perverted roommate that has an obsession for underaged girls, but also that another girl, Ritsu, a love-at-first-sight, is living in the same building as well!
|
||||
(Source: Kirei Cake)',
|
||||
'url' => 'https://kitsu.io/manga/bokura-wa-minna-kawaisou',
|
||||
'genres' =>
|
||||
array (
|
||||
0 => 'Comedy',
|
||||
1 => 'Romance',
|
||||
2 => 'School',
|
||||
3 => 'Slice of Life',
|
||||
4 => 'Thriller',
|
||||
),
|
||||
);
|
@ -23,6 +23,7 @@ use function Aviat\Ion\_dir;
|
||||
use Aura\Web\WebFactory;
|
||||
use Aviat\Ion\Json;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Spatie\Snapshots\MatchesSnapshots;
|
||||
use Zend\Diactoros\{
|
||||
Response as HttpResponse,
|
||||
ServerRequestFactory
|
||||
@ -36,6 +37,9 @@ define('TEST_VIEW_DIR', __DIR__ . '/test_views');
|
||||
* Base class for TestCases
|
||||
*/
|
||||
class AnimeClientTestCase extends TestCase {
|
||||
use MatchesSnapshots;
|
||||
|
||||
|
||||
// Test directory constants
|
||||
const ROOT_DIR = ROOT_DIR;
|
||||
const SRC_DIR = SRC_DIR;
|
||||
|
@ -1,4 +1,5 @@
|
||||
{
|
||||
"id": 32344,
|
||||
"slug": "attack-on-titan",
|
||||
"synopsis": "Several hundred years ago, humans were nearly exterminated by titans. Titans are typically several stories tall, seem to have no intelligence, devour human beings and, worst of all, seem to do it for the pleasure rather than as a food source. A small percentage of humanity survived by enclosing themselves in a city protected by extremely high walls, even taller than the biggest of titans. Flash forward to the present and the city has not seen a titan in over 100 years. Teenage boy Eren and his foster sister Mikasa witness something horrific as the city walls are destroyed by a colossal titan that appears out of thin air. As the smaller titans flood the city, the two kids watch in horror as their mother is eaten alive. Eren vows that he will murder every single titan and take revenge for all of mankind.\n\n(Source: ANN)",
|
||||
"coverImageTopOffset": 263,
|
||||
|
Loading…
Reference in New Issue
Block a user