Merge branch 'develop' into 'master'

Merge develop into master

See merge request !18
This commit is contained in:
Timothy Warren 2017-04-24 09:28:40 -04:00
commit 428a77b93d
69 changed files with 2060 additions and 3904 deletions

8
.gitignore vendored
View File

@ -13,7 +13,7 @@ composer.lock
*.sqlite *.sqlite
*.db *.db
*.sqlite3 *.sqlite3
docs/* apidocs/**
tests/test_data/sessions/* tests/test_data/sessions/*
cache.properties cache.properties
build/** build/**
@ -25,4 +25,8 @@ app/config/*.toml
phinx.yml phinx.yml
.idea/ .idea/
Caddyfile Caddyfile
build/humbuglog.txt build/humbuglog.txt
public/images/anime/**
public/images/avatars/**
public/images/manga/**
public/images/characters/**

View File

@ -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 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 4. Create the following directories if they don't exist, and make sure they are world writable
* public/js/cache * public/js/cache
* public/images/avatars
* public/images/anime
* public/images/characters
* public/images/manga
5. Make sure the `console` script is executable 5. Make sure the `console` script is executable
### Using MAL API ### Using MAL API

View File

@ -18,46 +18,6 @@
return [ 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 | 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 // End of minify_config.php

View File

@ -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

View File

@ -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

View File

@ -147,6 +147,16 @@ return [
// --------------------------------------------------------------------- // ---------------------------------------------------------------------
// Default / Shared routes // 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' => [ 'cache_purge' => [
'path' => '/cache_purge', 'path' => '/cache_purge',
'action' => 'clearCache', 'action' => 'clearCache',

View File

@ -15,7 +15,7 @@
<?php if ($auth->isAuthenticated()): ?> <?php if ($auth->isAuthenticated()): ?>
<button title="Increment episode count" class="plus_one" hidden>+1 Episode</button> <button title="Increment episode count" class="plus_one" hidden>+1 Episode</button>
<?php endif ?> <?php endif ?>
<img src="<?= $item['anime']['image'] ?>" alt="" /> <img src="<?= $urlGenerator->assetUrl("images/anime/{$item['anime']['id']}.jpg") ?>" alt="" />
<div class="name"> <div class="name">
<a href="<?= $url->generate('anime.details', ['id' => $item['anime']['slug']]); ?>"> <a href="<?= $url->generate('anime.details', ['id' => $item['anime']['slug']]); ?>">
<?= array_shift($item['anime']['titles']) ?> <?= array_shift($item['anime']['titles']) ?>
@ -28,17 +28,17 @@
<?php if ($auth->isAuthenticated()): ?> <?php if ($auth->isAuthenticated()): ?>
<div class="row"> <div class="row">
<span class="edit"> <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', [ $url->generate('edit', [
'controller' => 'anime', 'controller' => 'anime',
'id' => $item['id'], 'id' => $item['id'],
'status' => $item['watching_status'] 'status' => $item['watching_status']
]); ]);
?>">Edit</a> ?>">Edit</a>
</span> </span>
</div> </div>
<?php endif ?> <?php endif ?>
<?php if ($item['private'] || $item['rewatching']): ?> <?php if ($item['private'] || $item['rewatching']): ?>
<div class="row"> <div class="row">
<?php foreach(['private', 'rewatching'] as $attr): ?> <?php foreach(['private', 'rewatching'] as $attr): ?>
@ -48,13 +48,13 @@
<?php endforeach ?> <?php endforeach ?>
</div> </div>
<?php endif ?> <?php endif ?>
<?php if ($item['rewatched'] > 0): ?> <?php if ($item['rewatched'] > 0): ?>
<div class="row"> <div class="row">
<div>Rewatched <?= $item['rewatched'] ?> time(s)</div> <div>Rewatched <?= $item['rewatched'] ?> time(s)</div>
</div> </div>
<?php endif ?> <?php endif ?>
<?php if (count($item['anime']['streaming_links']) > 0): ?> <?php if (count($item['anime']['streaming_links']) > 0): ?>
<div class="row"> <div class="row">
<?php foreach($item['anime']['streaming_links'] as $link): ?> <?php foreach($item['anime']['streaming_links'] as $link): ?>
@ -70,7 +70,7 @@
<?php endforeach ?> <?php endforeach ?>
</div> </div>
<?php endif ?> <?php endif ?>
<div class="row"> <div class="row">
<div class="user_rating">Rating: <?= $item['user_rating'] ?> / 10</div> <div class="user_rating">Rating: <?= $item['user_rating'] ?> / 10</div>
<div class="completion">Episodes: <div class="completion">Episodes:

View File

@ -1,7 +1,7 @@
<main class="details fixed"> <main class="details fixed">
<section class="flex flex-no-wrap"> <section class="flex flex-no-wrap">
<div> <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 />
<br /> <br />
<table class="media_details"> <table class="media_details">
@ -79,8 +79,8 @@
<?php if (count($characters) > 0): ?> <?php if (count($characters) > 0): ?>
<h2>Characters</h2> <h2>Characters</h2>
<section class="media-wrap"> <section class="align_left media-wrap">
<?php foreach($characters as $char): ?> <?php foreach($characters as $id => $char): ?>
<?php if ( ! empty($char['image']['original'])): ?> <?php if ( ! empty($char['image']['original'])): ?>
<article class="character"> <article class="character">
<?php $link = $url->generate('character', ['slug' => $char['slug']]) ?> <?php $link = $url->generate('character', ['slug' => $char['slug']]) ?>
@ -88,7 +88,7 @@
<?= $helper->a($link, $char['name']); ?> <?= $helper->a($link, $char['name']); ?>
</div> </div>
<a href="<?= $link ?>"> <a href="<?= $link ?>">
<?= $helper->img($char['image']['original'], [ <?= $helper->img($urlGenerator->assetUrl("images/characters/{$id}.jpg"), [
'width' => '225' 'width' => '225'
]) ?> ]) ?>
</a> </a>

View File

@ -13,7 +13,7 @@
</th> </th>
<th> <th>
<article class="media"> <article class="media">
<?= $helper->img($item['anime']['image']); ?> <?= $helper->img($urlGenerator->assetUrl('images/anime', "{$item['anime']['id']}.jpg")) ?>
</article> </article>
</th> </th>
</tr> </tr>

View File

@ -1,12 +1,126 @@
<main class="details fixed"> <?php use Aviat\AnimeClient\API\Kitsu; ?>
<main class="details">
<section class="flex flex-no-wrap"> <section class="flex flex-no-wrap">
<div> <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>
<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> </div>
</section> </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> </main>

View File

@ -11,7 +11,7 @@
<section class="media-wrap"> <section class="media-wrap">
<?php foreach($items as $item): ?> <?php foreach($items as $item): ?>
<article class="media" id="a-<?= $item['hummingbird_id'] ?>"> <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" /> alt="<?= $item['title'] ?> cover image" />
<div class="name"> <div class="name">
<a href="<?= $url->generate('anime.details', ['id' => $item['slug']]) ?>"> <a href="<?= $url->generate('anime.details', ['id' => $item['slug']]) ?>">

View File

@ -6,7 +6,7 @@
<meta http-equiv="cache-control" content="no-store" /> <meta http-equiv="cache-control" content="no-store" />
<meta http-equiv="Content-Security-Policy" content="script-src 'self'" /> <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" /> <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="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="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') ?>"> <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="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="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="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> <script defer="defer" src="<?= $urlGenerator->assetUrl('js.php/g/base') ?>"></script>
</head> </head>
<body class="<?= $escape->attr($url_type) ?> list"> <body class="<?= $escape->attr($url_type) ?> list">
<header> <header>
<?php include 'main-menu.php' ?> <?php
<?php if(isset($message) && is_array($message)): include 'main-menu.php';
if(isset($message) && is_array($message))
{
foreach($message as $m) foreach($message as $m)
{ {
extract($m); extract($m);
include 'message.php'; include 'message.php';
} }
endif ?> }
?>
</header> </header>

View File

@ -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"> <h1 class="flex flex-align-end flex-wrap">
<span class="flex-no-wrap grow-1"> <span class="flex-no-wrap grow-1">
<?php if(strpos($route_path, 'collection') === FALSE): ?> <?php if(strpos($route_path, 'collection') === FALSE): ?>
<a href="<?= $escape->attr($urlGenerator->defaultUrl($url_type)) ?>"> <?= $helper->a(
<?= $config->get('whose_list') ?>'s <?= ucfirst($url_type) ?> List $urlGenerator->defaultUrl($url_type),
</a> $whose . ucfirst($url_type) . ' List'
) ?>
<?php if($config->get("show_{$url_type}_collection")): ?> <?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 ?> <?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: ?> <?php else: ?>
<a href="<?= $url->generate('collection.view') ?>"> <?= $whose . ucfirst($url_type) . ' Collection' ?>
<?= $config->get('whose_list') ?>'s <?= ucfirst($url_type) ?> Collection [<?= $helper->a($urlGenerator->defaultUrl('anime') . $extraSegment, 'Anime List') ?>]
</a> [<?= $helper->a($urlGenerator->defaultUrl('manga') . $extraSegment, 'Manga List') ?>]
[<a href="<?= $urlGenerator->defaultUrl('anime') ?>">Anime List</a>]
[<a href="<?= $urlGenerator->defaultUrl('manga') ?>">Manga List</a>]
<?php endif ?> <?php endif ?>
</span> </span>
<span class="flex-no-wrap small-font">
[<?= $helper->a($url->generate('user_info'), 'About '. $config->get('whose_list')) ?>] <span class="flex-no-wrap small-font">[<?= $helper->a(
</span> $url->generate('user_info'),
'About '. $config->get('whose_list')
) ?>]</span>
<?php if ($auth->isAuthenticated()): ?> <?php if ($auth->isAuthenticated()): ?>
<span class="flex-no-wrap">&nbsp;</span> <span class="flex-no-wrap">&nbsp;</span>
<span class="flex-no-wrap small-font"> <span class="flex-no-wrap small-font">
<button type="button" class="js-clear-cache user-btn">Clear API Cache</button> <button type="button" class="js-clear-cache user-btn">Clear API Cache</button>
</span> </span>
<span class="flex-no-wrap">&nbsp;</span> <span class="flex-no-wrap">&nbsp;</span>
<?php endif ?> <?php endif ?>
<span class="flex-no-wrap small-font"> <span class="flex-no-wrap small-font">
<?php if ($auth->isAuthenticated()): ?> <?php if ($auth->isAuthenticated()): ?>
<a class="bracketed" href="<?= $url->generate('logout') ?>">Logout</a> <?= $helper->a(
$url->generate('logout'),
'Logout',
['class' => 'bracketed']
) ?>
<?php else: ?> <?php else: ?>
[<a href="<?= $url->generate('login'); ?>"><?= $config->get('whose_list') ?>'s Login</a>] [<?= $helper->a($url->generate('login'), "{$whose} Login") ?>]
<?php endif ?> <?php endif ?>
</span> </span>
</h1> </h1>
@ -40,8 +61,8 @@
<?= $helper->menu($menu_name) ?> <?= $helper->menu($menu_name) ?>
<br /> <br />
<ul> <ul>
<li class="<?= Util::isNotSelected('list', $urlGenerator->lastSegment()) ?>"><a href="<?= $urlGenerator->url($route_path) ?>">Cover View</a></li> <li class="<?= Util::isNotSelected('list', $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::isSelected('list', $lastSegment) ?>"><a href="<?= $urlGenerator->url("{$route_path}/list") ?>">List View</a></li>
</ul> </ul>
<?php endif ?> <?php endif ?>
</nav> </nav>

View File

@ -17,7 +17,7 @@
<?php /* <button class="plus_one_volume">+1 Volume</button> */ ?> <?php /* <button class="plus_one_volume">+1 Volume</button> */ ?>
</div> </div>
<?php endif ?> <?php endif ?>
<img src="<?= $escape->attr($item['manga']['image']) ?>" /> <img src="<?= $urlGenerator->assetUrl('images/manga', "{$item['manga']['id']}.jpg") ?>" />
<div class="name"> <div class="name">
<a href="<?= $url->generate('manga.details', ['id' => $item['manga']['slug']]) ?>"> <a href="<?= $url->generate('manga.details', ['id' => $item['manga']['slug']]) ?>">
<?= $escape->html(array_shift($item['manga']['titles'])) ?> <?= $escape->html(array_shift($item['manga']['titles'])) ?>

View File

@ -1,7 +1,7 @@
<main class="details fixed"> <main class="details fixed">
<section class="flex flex-no-wrap"> <section class="flex flex-no-wrap">
<div> <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 />
<br /> <br />
<table> <table>
@ -39,7 +39,7 @@
<?php if (count($characters) > 0): ?> <?php if (count($characters) > 0): ?>
<h2>Characters</h2> <h2>Characters</h2>
<section class="media-wrap"> <section class="media-wrap">
<?php foreach($characters as $char): ?> <?php foreach($characters as $id => $char): ?>
<?php if ( ! empty($char['image']['original'])): ?> <?php if ( ! empty($char['image']['original'])): ?>
<article class="character"> <article class="character">
<?php $link = $url->generate('character', ['slug' => $char['slug']]) ?> <?php $link = $url->generate('character', ['slug' => $char['slug']]) ?>
@ -47,7 +47,7 @@
<?= $helper->a($link, $char['name']); ?> <?= $helper->a($link, $char['name']); ?>
</div> </div>
<a href="<?= $link ?>"> <a href="<?= $link ?>">
<?= $helper->img($char['image']['original'], [ <?= $helper->img($urlGenerator->assetUrl('images/characters', "{$id}.jpg"), [
'width' => '225' 'width' => '225'
]) ?> ]) ?>
</a> </a>

View File

@ -15,7 +15,7 @@
</th> </th>
<th> <th>
<article class="media"> <article class="media">
<?= $helper->img($item['manga']['image']); ?> <?= $helper->img($urlGenerator->assetUrl('images/manga', "{$item['manga']['id']}.jpg")); ?>
</article> </article>
</th> </th>
</tr> </tr>

View File

@ -9,7 +9,12 @@
<?= $attributes['name'] ?> <?= $attributes['name'] ?>
</a> </a>
</h2> </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> </center>
<br /> <br />
<br /> <br />
@ -65,13 +70,13 @@
<?php if ( ! empty($favorites['characters'])): ?> <?php if ( ! empty($favorites['characters'])): ?>
<h4>Favorite Characters</h4> <h4>Favorite Characters</h4>
<section class="media-wrap"> <section class="media-wrap">
<?php foreach($favorites['characters'] as $char): ?> <?php foreach($favorites['characters'] as $id => $char): ?>
<?php if ( ! empty($char['image']['original'])): ?> <?php if ( ! empty($char['image']['original'])): ?>
<article class="small_character"> <article class="small_character">
<?php $link = $url->generate('character', ['slug' => $char['slug']]) ?> <?php $link = $url->generate('character', ['slug' => $char['slug']]) ?>
<div class="name"><?= $helper->a($link, $char['name']); ?></div> <div class="name"><?= $helper->a($link, $char['name']); ?></div>
<a href="<?= $link ?>"> <a href="<?= $link ?>">
<?= $helper->img($char['image']['original']) ?> <?= $helper->img($urlGenerator->assetUrl('images/characters', "{$char['id']}.jpg")) ?>
</a> </a>
</article> </article>
<?php endif ?> <?php endif ?>
@ -88,7 +93,7 @@
$titles = Kitsu::filterTitles($anime); $titles = Kitsu::filterTitles($anime);
?> ?>
<a href="<?= $link ?>"> <a href="<?= $link ?>">
<img src="<?= $anime['posterImage']['small'] ?>" width="220" alt="" /> <img src="<?= $urlGenerator->assetUrl('images/anime', "{$anime['id']}.jpg") ?>" width="220" alt="" />
</a> </a>
<div class="name"> <div class="name">
<a href="<?= $link ?>"> <a href="<?= $link ?>">
@ -112,7 +117,7 @@
$titles = Kitsu::filterTitles($manga); $titles = Kitsu::filterTitles($manga);
?> ?>
<a href="<?= $link ?>"> <a href="<?= $link ?>">
<img src="<?= $manga['posterImage']['small'] ?>" width="220" alt="" /> <img src="<?= $urlGenerator->assetUrl('images/manga', "{$manga['id']}.jpg") ?>" width="220" alt="" />
</a> </a>
<div class="name"> <div class="name">
<a href="<?= $link ?>"> <a href="<?= $link ?>">

View File

@ -21,7 +21,7 @@
"aura/router": "^3.0", "aura/router": "^3.0",
"aura/session": "^2.0", "aura/session": "^2.0",
"aviat/banker": "^1.0.0", "aviat/banker": "^1.0.0",
"aviat/ion": "^2.0.0", "aviat/ion": "^2.1.0",
"monolog/monolog": "^1.0", "monolog/monolog": "^1.0",
"psr/http-message": "~1.0", "psr/http-message": "~1.0",
"psr/log": "~1.0", "psr/log": "~1.0",
@ -37,12 +37,13 @@
"phploc/phploc": "^3.0", "phploc/phploc": "^3.0",
"phpmd/phpmd": "^2.4", "phpmd/phpmd": "^2.4",
"phpunit/phpunit": "^6.0", "phpunit/phpunit": "^6.0",
"robmorgan/phinx": "~0.6.4", "robmorgan/phinx": "^0.8.0",
"consolidation/robo": "~1.0", "consolidation/robo": "~1.0",
"henrikbjorn/lurker": "^1.1.0", "henrikbjorn/lurker": "^1.1.0",
"symfony/var-dumper": "^3.2", "symfony/var-dumper": "^3.2",
"squizlabs/php_codesniffer": "^3.0.0@beta", "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": { "scripts": {
"build": "vendor/bin/robo build", "build": "vendor/bin/robo build",

View File

@ -18,9 +18,9 @@ unset($APP_DIR);
unset($SRC_DIR); unset($SRC_DIR);
unset($CONF_DIR); unset($CONF_DIR);
// --------------------------------------------------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Start console script // Start console script
// --------------------------------------------------------------------------------------------------------------------- // -----------------------------------------------------------------------------
$console = new \ConsoleKit\Console([ $console = new \ConsoleKit\Console([
'cache-prime' => Command\CachePrime::class, 'cache-prime' => Command\CachePrime::class,
'cache-clear' => Command\CacheClear::class, 'cache-clear' => Command\CacheClear::class,

View File

@ -1,12 +0,0 @@
{
"source": {
"directories": [
"src"
]
},
"timeout": 10,
"logs": {
"text": "build\/humbuglog.txt",
"json": "build\/humbug.json"
}
}

View File

@ -56,7 +56,7 @@
</collector> </collector>
<!-- Configuration of generation process --> <!-- Configuration of generation process -->
<generator output="docs"> <generator>
<!-- @output - (Base-)Directory to store output data in --> <!-- @output - (Base-)Directory to store output data in -->
<!-- A generation process consists of one or more build tasks and of (optional) enrich sources --> <!-- 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 --> <!-- 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" --> <!-- default engine "html" -->
<build engine="html" output="html" /> <build engine="html" output="apidocs">
<!-- <template dir="${phpDox.home}/templates/html" /> - <!-- <template dir="${phpDox.home}/templates/html" /> -->
<file extension="html" /> <file extension="html" />
</build> --> </build>
</generator> </generator>
</project> </project>

29
public/css.js Normal file
View 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);
});

View File

@ -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

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
undefined

File diff suppressed because it is too large Load Diff

View File

@ -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;
}

View File

@ -1,6 +1,7 @@
:root :root {
{ --default-font-list: system-ui,sans-serif;
--default-font-list:'Open Sans', 'Nimbus Sans L', 'Helvetica Neue', Helvetica, 'Lucida Grande', 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%; -ms-text-size-adjust:100%;
-webkit-text-size-adjust:100%; -webkit-text-size-adjust:100%;
box-sizing:border-box; box-sizing:border-box;
@ -9,48 +10,41 @@
line-height:1.4; line-height:1.4;
overflow-y:scroll; overflow-y:scroll;
text-size-adjust:100%; text-size-adjust:100%;
scroll-behavior: smooth; scroll-behavior:smooth;
} }
audio:not([controls]) audio:not([controls]) {
{
display:none; display:none;
} }
details details {
{
display:block; display:block;
} }
input[type=search] input[type=search] {
{
-webkit-appearance:textfield; -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; -webkit-appearance:none;
} }
main main {
{
display:block; display:block;
margin:0 auto; margin:0 auto;
padding:0 1.6em 1.6em; padding:0 1.6em 1.6em;
padding:0 1.6rem 1.6rem; padding:0 1.6rem 1.6rem;
} }
summary summary {
{
display:block; display:block;
} }
pre pre {
{
background:#efefef; background:#efefef;
color:#444; color:#444;
display:block; display:block;
font-family:Menlo, Monaco, Consolas, 'Courier New', monospace; font-family:var(--monospace-font-list);
font-size:1.4em; font-size:1.4em;
font-size:1.4rem; font-size:1.4rem;
margin:1.6em 0; margin:1.6em 0;
@ -62,29 +56,24 @@ pre
word-wrap:break-word; word-wrap:break-word;
} }
progress progress {
{
display:inline-block; display:inline-block;
} }
small small {
{
color:#777; color:#777;
font-size:75%; font-size:75%;
} }
big big {
{
font-size:125%; font-size:125%;
} }
template template {
{
display:none; display:none;
} }
textarea textarea {
{
border:.1rem solid #ccc; border:.1rem solid #ccc;
border-radius:0; border-radius:0;
display:block; display:block;
@ -95,55 +84,47 @@ textarea
vertical-align:middle; vertical-align:middle;
} }
[hidden] [hidden] {
{
display:none; display:none;
} }
[unselectable] [unselectable] {
{
-moz-user-select:none; -moz-user-select:none;
-ms-user-select:none; -ms-user-select:none;
-webkit-user-select:none; -webkit-user-select:none;
user-select:none; user-select:none;
} }
*,::before,::after *,::before,::after {
{
border-style:solid; border-style:solid;
border-width:0; border-width:0;
box-sizing:inherit; box-sizing:inherit;
} }
* * {
{
font-size:inherit; font-size:inherit;
line-height:inherit; line-height:inherit;
margin:0; margin:0;
padding:0; padding:0;
} }
::before,::after ::before,::after {
{
text-decoration:inherit; text-decoration:inherit;
vertical-align:inherit; vertical-align:inherit;
} }
a a {
{
-webkit-transition:.25s ease; -webkit-transition:.25s ease;
color:#1271db; color:#1271db;
text-decoration:none; text-decoration:none;
transition:.25s ease; transition:.25s ease;
} }
audio,canvas,iframe,img,svg,video audio,canvas,iframe,img,svg,video {
{
vertical-align:middle; vertical-align:middle;
} }
button,input,select,textarea button,input,select,textarea {
{
border:.1rem solid #ccc; border:.1rem solid #ccc;
color:inherit; color:inherit;
font-family:inherit; font-family:inherit;
@ -152,37 +133,31 @@ button,input,select,textarea
min-height:1.4em; min-height:1.4em;
} }
code,kbd,pre,samp code,kbd,pre,samp {
{ font-family:var(--monospace-font-list);
font-family:Menlo, Monaco, Consolas, 'Courier New', monospace, monospace;
} }
table table {
{
border-collapse:collapse; border-collapse:collapse;
border-spacing:0; border-spacing:0;
margin-bottom:1.6rem; margin-bottom:1.6rem;
} }
::-moz-selection ::-moz-selection {
{
background-color:#b3d4fc; background-color:#b3d4fc;
text-shadow:none; text-shadow:none;
} }
::selection ::selection {
{
background-color:#b3d4fc; background-color:#b3d4fc;
text-shadow:none; text-shadow:none;
} }
button::-moz-focus-inner button::-moz-focus-inner {
{
border:0; border:0;
} }
body body {
{
color:#444; color:#444;
font-family:var(--default-font-list); font-family:var(--default-font-list);
font-size:1.6rem; font-size:1.6rem;
@ -191,20 +166,17 @@ body
padding:0; padding:0;
} }
p p {
{
margin:0 0 1.6rem; margin:0 0 1.6rem;
} }
h1,h2,h3,h4,h5,h6 h1,h2,h3,h4,h5,h6 {
{ font-family:var(--default-font-list);
font-family:Lato, var(--default-font-list);
margin:2em 0 1.6em; margin:2em 0 1.6em;
margin:2rem 0 1.6rem; margin:2rem 0 1.6rem;
} }
h1 h1 {
{
border-bottom:.1rem solid rgba(0,0,0,0.2); border-bottom:.1rem solid rgba(0,0,0,0.2);
font-size:3.6em; font-size:3.6em;
font-size:3.6rem; font-size:3.6rem;
@ -212,16 +184,14 @@ h1
font-weight:500; font-weight:500;
} }
h2 h2 {
{
font-size:3em; font-size:3em;
font-size:3rem; font-size:3rem;
font-style:normal; font-style:normal;
font-weight:500; font-weight:500;
} }
h3 h3 {
{
font-size:2.4em; font-size:2.4em;
font-size:2.4rem; font-size:2.4rem;
font-style:normal; font-style:normal;
@ -229,8 +199,7 @@ h3
margin:1.6rem 0 .4rem; margin:1.6rem 0 .4rem;
} }
h4 h4 {
{
font-size:1.8em; font-size:1.8em;
font-size:1.8rem; font-size:1.8rem;
font-style:normal; font-style:normal;
@ -238,8 +207,7 @@ h4
margin:1.6rem 0 .4rem; margin:1.6rem 0 .4rem;
} }
h5 h5 {
{
font-size:1.6em; font-size:1.6em;
font-size:1.6rem; font-size:1.6rem;
font-style:normal; font-style:normal;
@ -247,8 +215,7 @@ h5
margin:1.6rem 0 .4rem; margin:1.6rem 0 .4rem;
} }
h6 h6 {
{
color:#777; color:#777;
font-size:1.4em; font-size:1.4em;
font-size:1.4rem; font-size:1.4rem;
@ -257,66 +224,56 @@ h6
margin:1.6rem 0 .4rem; margin:1.6rem 0 .4rem;
} }
code code {
{
background:#efefef; background:#efefef;
color:#444; color:#444;
font-family:Menlo, Monaco, Consolas, 'Courier New', monospace; font-family:var(--monospace-font-list);
font-size:1.4rem; font-size:1.4rem;
word-break:break-all; word-break:break-all;
word-wrap:break-word; word-wrap:break-word;
} }
a:hover,a:focus a:hover,a:focus {
{
text-decoration:none; text-decoration:none;
} }
dl dl {
{
margin-bottom:1.6rem; margin-bottom:1.6rem;
} }
dd dd {
{
margin-left:4rem; margin-left:4rem;
} }
ul,ol ul,ol {
{
margin-bottom:.8rem; margin-bottom:.8rem;
padding-left:2rem; padding-left:2rem;
} }
blockquote blockquote {
{
border-left:.2rem solid #1271db; border-left:.2rem solid #1271db;
font-family:Georgia, Times, 'Times New Roman', serif; font-family:var(--serif-font-list);
font-style:italic; font-style:italic;
margin:1.6rem 0; margin:1.6rem 0;
padding-left:1.6rem; padding-left:1.6rem;
} }
figcaption figcaption {
{ font-family:var(--serif-font-list);
font-family:Georgia, Times, 'Times New Roman', serif;
} }
html html {
{
font-size:62.5%; font-size:62.5%;
} }
main,header,footer,article,section,aside,details,summary main,header,footer,article,section,aside,details,summary {
{
display:block; display:block;
height:auto; height:auto;
margin:0 auto; margin:0 auto;
width:100%; width:100%;
} }
footer footer {
{
border-top:.1rem solid rgba(0,0,0,0.2); border-top:.1rem solid rgba(0,0,0,0.2);
clear:both; clear:both;
display:inline-block; display:inline-block;
@ -326,23 +283,20 @@ footer
text-align:center; text-align:center;
} }
hr hr {
{
border-top:.1rem solid rgba(0,0,0,0.2); border-top:.1rem solid rgba(0,0,0,0.2);
display:block; display:block;
margin-bottom:1.6rem; margin-bottom:1.6rem;
width:100%; width:100%;
} }
img img {
{
height:auto; height:auto;
/* max-width:100%; */ /* max-width:100%; */
vertical-align:baseline; 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:.1rem solid #ccc;
border-radius:0; border-radius:0;
display:inline-block; display:inline-block;
@ -350,8 +304,7 @@ input[type=text],input[type=password],input[type=email],input[type=url],input[ty
vertical-align:middle; vertical-align:middle;
} }
input:not([type]) input:not([type]) {
{
-webkit-appearance:none; -webkit-appearance:none;
background-clip:padding-box; background-clip:padding-box;
background-color:#fff; background-color:#fff;
@ -363,88 +316,73 @@ input:not([type])
text-align:left; text-align:left;
} }
input[type=color] input[type=color] {
{
padding:.8rem 1.6rem; 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; border-color:#b3d4fc;
} }
input:not([type]):focus input:not([type]):focus {
{
border-color:#b3d4fc; border-color:#b3d4fc;
} }
input[type=radio],input[type=checkbox] input[type=radio],input[type=checkbox] {
{
vertical-align:middle; 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; 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; background-color:#efefef;
color:#777; color:#777;
cursor:not-allowed; cursor:not-allowed;
} }
input:not([type])[disabled] input:not([type])[disabled] {
{
background-color:#efefef; background-color:#efefef;
color:#777; color:#777;
cursor:not-allowed; cursor:not-allowed;
} }
input[readonly],select[readonly],textarea[readonly] input[readonly],select[readonly],textarea[readonly] {
{
background-color:#efefef; background-color:#efefef;
border-color:#ccc; border-color:#ccc;
color:#777; color:#777;
} }
input:focus:invalid,textarea:focus:invalid,select:focus:invalid input:focus:invalid,textarea:focus:invalid,select:focus:invalid {
{
border-color:#e9322d; border-color:#e9322d;
color:#b94a48; 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; outline-color:#ff4136;
} }
select select {
{
background-color:#fff; background-color:#fff;
border:.1rem solid #ccc; border:.1rem solid #ccc;
} }
select[multiple] select[multiple] {
{
height:auto; height:auto;
} }
label label {
{
line-height:2; line-height:2;
} }
fieldset fieldset {
{
border:0; border:0;
margin:0; margin:0;
padding:.8rem 0; padding:.8rem 0;
} }
legend legend {
{
border-bottom:.1rem solid #ccc; border-bottom:.1rem solid #ccc;
color:#444; color:#444;
display:block; display:block;
@ -453,8 +391,7 @@ legend
width:100%; width:100%;
} }
input[type=submit],button input[type=submit],button {
{
-moz-user-select:none; -moz-user-select:none;
-ms-user-select:none; -ms-user-select:none;
-webkit-transition:.25s ease; -webkit-transition:.25s ease;
@ -476,62 +413,52 @@ input[type=submit],button
vertical-align:baseline; vertical-align:baseline;
} }
input[type=submit] a,button a input[type=submit] a,button a {
{
color:#444; color:#444;
} }
input[type=submit]::-moz-focus-inner,button::-moz-focus-inner input[type=submit]::-moz-focus-inner,button::-moz-focus-inner {
{
padding:0; padding:0;
} }
input[type=submit]:hover,button:hover input[type=submit]:hover,button:hover {
{
background:#444; background:#444;
border-color:#444; border-color:#444;
color:#fff; color:#fff;
} }
input[type=submit]:hover a,button:hover a input[type=submit]:hover a,button:hover a {
{
color:#fff; color:#fff;
} }
input[type=submit]:active,button:active input[type=submit]:active,button:active {
{
background:#6a6a6a; background:#6a6a6a;
border-color:#6a6a6a; border-color:#6a6a6a;
color:#fff; color:#fff;
} }
input[type=submit]:active a,button:active a input[type=submit]:active a,button:active a {
{
color:#fff; color:#fff;
} }
input[type=submit]:disabled,button:disabled input[type=submit]:disabled,button:disabled {
{
box-shadow:none; box-shadow:none;
cursor:not-allowed; cursor:not-allowed;
opacity:.40; opacity:.4;
} }
nav ul nav ul {
{
list-style:none; list-style:none;
margin:0; margin:0;
padding:0; padding:0;
text-align:center; text-align:center;
} }
nav ul li nav ul li {
{
display:inline; display:inline;
} }
nav a nav a {
{
-webkit-transition:.25s ease; -webkit-transition:.25s ease;
border-bottom:.2rem solid transparent; border-bottom:.2rem solid transparent;
color:#444; color:#444;
@ -540,48 +467,40 @@ nav a
transition:.25s ease; 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); border-color:rgba(0,0,0,0.2);
} }
nav a:active nav a:active {
{
border-color:rgba(0,0,0,0.56); border-color:rgba(0,0,0,0.56);
} }
caption caption {
{
padding:.8rem 0; padding:.8rem 0;
} }
thead th thead th {
{
background:#efefef; background:#efefef;
color:#444; color:#444;
} }
tr tr {
{
background:#fff; background:#fff;
margin-bottom:.8rem; margin-bottom:.8rem;
} }
th,td th,td {
{
border:.1rem solid #ccc; border:.1rem solid #ccc;
padding:.8rem 1.6rem; padding:.8rem 1.6rem;
text-align:center; text-align:center;
vertical-align:inherit; vertical-align:inherit;
} }
tfoot tr tfoot tr {
{
background:none; background:none;
} }
tfoot td tfoot td {
{
color:#efefef; color:#efefef;
font-size:.8rem; font-size:.8rem;
font-style:italic; font-style:italic;
@ -589,28 +508,24 @@ tfoot td
} }
@media screen { @media screen {
[hidden~=screen] [hidden~=screen] {
{
display:inherit; display:inherit;
} }
[hidden~=screen]:not(:active):not(:focus):not(:target) [hidden~=screen]:not(:active):not(:focus):not(:target) {
{
clip:rect(0000)!important; clip:rect(0000)!important;
position:absolute!important; position:absolute!important;
} }
} }
@media screen and max-width 40rem { @media screen and max-width 40rem {
article,section,aside article,section,aside {
{
clear:both; clear:both;
display:block; display:block;
max-width:100%; max-width:100%;
} }
img img {
{
margin-right:1.6rem; margin-right:1.6rem;
} }
} }

3
public/cssfilter.js Normal file
View File

@ -0,0 +1,3 @@
module.exports = function filter(filename) {
return ! String(filename).includes('min');
}

View File

View File

@ -22,29 +22,48 @@ use Aviat\Ion\Json;
// Include guzzle // Include guzzle
require_once('../vendor/autoload.php'); 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 * Simple Javascript minfier, using google closure compiler
*/ */
class JSMin extends BaseMin { class JSMin {
protected $jsRoot; protected $jsRoot;
protected $jsGroup; protected $jsGroup;
protected $jsGroupsFile; protected $configFile;
protected $cacheFile; protected $cacheFile;
protected $lastModified; protected $lastModified;
protected $requestedTime; protected $requestedTime;
protected $cacheModified; protected $cacheModified;
public function __construct(array $config, array $groups) public function __construct(array $config, string $configFile)
{ {
$group = $_GET['g']; $group = $_GET['g'];
$groups = $config['groups'];
$this->jsRoot = $config['js_root']; $this->jsRoot = $config['js_root'];
$this->jsGroup = $groups[$group]; $this->jsGroup = $groups[$group];
$this->jsGroupsFile = $config['js_groups_file']; $this->configFile = $configFile;
$this->cacheFile = "{$this->jsRoot}cache/{$group}"; $this->cacheFile = "{$this->jsRoot}cache/{$group}";
$this->lastModified = $this->getLastModified(); $this->lastModified = $this->getLastModified();
@ -99,7 +118,7 @@ class JSMin extends BaseMin {
protected function closureCall(array $options) protected function closureCall(array $options)
{ {
$formFields = http_build_query($options); $formFields = http_build_query($options);
$request = (new Request) $request = (new Request)
->setMethod('POST') ->setMethod('POST')
->setUri('http://closure-compiler.appspot.com/compile') ->setUri('http://closure-compiler.appspot.com/compile')
@ -109,7 +128,7 @@ class JSMin extends BaseMin {
'Content-type' => 'application/x-www-form-urlencoded' 'Content-type' => 'application/x-www-form-urlencoded'
]) ])
->setBody($formFields); ->setBody($formFields);
$response = wait((new Client)->request($request, [ $response = wait((new Client)->request($request, [
Client::OP_AUTO_ENCODING => false Client::OP_AUTO_ENCODING => false
])); ]));
@ -128,7 +147,7 @@ class JSMin extends BaseMin {
$errorRes = $this->closureCall($options); $errorRes = $this->closureCall($options);
$errorJson = $errorRes->getBody(); $errorJson = $errorRes->getBody();
$errorObj = Json::decode($errorJson) ?: (object)[]; $errorObj = Json::decode($errorJson) ?: (object)[];
// Show error if exists // Show error if exists
if ( ! empty($errorObj->errors) || ! empty($errorObj->serverErrors)) 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 //Add this page too, as well as the groups file
$modified[] = filemtime(__FILE__); $modified[] = filemtime(__FILE__);
$modified[] = filemtime($this->jsGroupsFile); $modified[] = filemtime($this->configFile);
rsort($modified); rsort($modified);
$lastModified = $modified[0]; $lastModified = $modified[0];
@ -227,14 +246,97 @@ class JSMin extends BaseMin {
{ {
$this->sendFinalOutput($js, 'application/javascript', $this->lastModified); $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 // ! Start Minifying
// -------------------------------------------------------------------------- // --------------------------------------------------------------------------
$config = require_once('../app/appConf/minify_config.php'); $configFile = realpath(__DIR__ . '/../app/appConf/minify_config.php');
$groups = require_once($config['js_groups_file']); $config = require_once($configFile);
$groups = $config['groups'];
$cacheDir = "{$config['js_root']}cache"; $cacheDir = "{$config['js_root']}cache";
if ( ! is_dir($cacheDir)) if ( ! is_dir($cacheDir))
@ -249,11 +351,11 @@ if ( ! array_key_exists($_GET['g'], $groups))
try try
{ {
new JSMin($config, $groups); new JSMin($config, $configFile);
} }
catch (FileNotChangedException $e) catch (FileNotChangedException $e)
{ {
BaseMin::send304(); JSMin::send304();
} }
//end of js.php //end of js.php

View File

@ -8,7 +8,7 @@
// Action to increment episode count // Action to increment episode count
_.on('body.anime.list', 'click', '.plus_one', (e) => { _.on('body.anime.list', 'click', '.plus_one', (e) => {
let parentSel = _.closestParent(e.target, 'article'); 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 totalCount = parseInt(_.$('.total_number', parentSel)[0].textContent, 10);
let title = _.$('.name a', parentSel)[0].textContent; let title = _.$('.name a', parentSel)[0].textContent;

View File

@ -9,7 +9,7 @@
let thisSel = e.target; let thisSel = e.target;
let parentSel = _.closestParent(e.target, 'article'); let parentSel = _.closestParent(e.target, 'article');
let type = thisSel.classList.contains('plus_one_chapter') ? 'chapter' : 'volume'; 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 total = parseInt(_.$(`.${type}_count`, parentSel)[0].textContent, 10);
let mangaName = _.$('.name', parentSel)[0].textContent; let mangaName = _.$('.name', parentSel)[0].textContent;

View File

@ -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);
}
}

View File

@ -1,14 +1,13 @@
{ {
"scripts": { "scripts": {
"build": "postcss -u postcss-import --autoprefixer.browsers \"> 5%\" -u postcss-cssnext -o css/base.css css/base.myth.css", "build": "node ./css.js",
"watch": "postcss -u postcss-import --autoprefixer.browsers \"> 5%\" -u postcss-cssnext -w -o css/base.css css/base.myth.css" "watch": "watch 'npm run build' --filter=./cssfilter.js"
}, },
"devDependencies": { "devDependencies": {
"autoprefixer": "^6.6.1", "cssnano": "^3.10.0",
"npm-run-all": "^4.0.0",
"postcss-cachify": "^1.3.1", "postcss-cachify": "^1.3.1",
"postcss-cli": "^2.6.0",
"postcss-cssnext": "^2.9.0", "postcss-cssnext": "^2.9.0",
"postcss-import": "^9.0.0" "postcss-import": "^9.0.0",
"watch": "^1.0.2"
} }
} }

View File

@ -1,8 +1,9 @@
{{#data}} {{#data}}
<article class="media search"> <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}}" /> <input type="radio" class="big-check" id="{{attributes.slug}}" name="id" value="{{id}}" />
<label for="{{attributes.slug}}"> <label for="{{attributes.slug}}">
<img src="/public/images/anime/{{id}}.jpg" alt="" width="220" />
<span class="name"> <span class="name">
{{attributes.canonicalTitle}} {{attributes.canonicalTitle}}
<br /> <br />

View File

@ -1,8 +1,9 @@
{{#data}} {{#data}}
<article class="media search"> <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}}" /> <input type="radio" class="big-check" id="{{attributes.slug}}" name="id" value="{{id}}" />
<label for="{{attributes.slug}}"> <label for="{{attributes.slug}}">
<img src="/public/images/manga/{{id}}.jpg" alt="" width="220" />
<span class="name"> <span class="name">
{{attributes.canonicalTitle}} {{attributes.canonicalTitle}}
<br /> <br />

File diff suppressed because it is too large Load Diff

View File

@ -54,7 +54,9 @@ class JsonAPI {
]; ];
// Reorganize included data // Reorganize included data
$included = static::organizeIncluded($data['included']); $included = (array_key_exists('included', $data))
? static::organizeIncluded($data['included'])
: [];
// Inline organized data // Inline organized data
foreach($data['data'] as $i => &$item) foreach($data['data'] as $i => &$item)
@ -125,23 +127,13 @@ class JsonAPI {
$typeKey = $props['data'][$j]['type']; $typeKey = $props['data'][$j]['type'];
$relationship =& $item['relationships'][$relType]; $relationship =& $item['relationships'][$relType];
unset($relationship['data'][$j]);
if (empty($relationship['data']))
{
unset($relationship['data']);
}
if ($relType === $typeKey) if ($relType === $typeKey)
{ {
$relationship[$idKey] = $included[$typeKey][$idKey]; $relationship[$idKey] = $included[$typeKey][$idKey];
continue; continue;
} }
$relationship[$typeKey][$idKey] = array_merge( $relationship[$typeKey][$idKey][$j] = $included[$typeKey][$idKey];
$included[$typeKey][$idKey],
$relationship[$typeKey][$idKey] ?? []
);
} }
} }
} }
@ -149,6 +141,8 @@ class JsonAPI {
} }
} }
$data['data']['included'] = $included;
return $data['data']; return $data['data'];
} }
@ -202,11 +196,11 @@ class JsonAPI {
{ {
foreach($items as $id => $item) 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) 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']])) if (array_key_exists($props['data']['id'], $organized[$props['data']['type']]))
{ {

View File

@ -219,11 +219,11 @@ class Kitsu {
foreach($existingTitles as $existing) foreach($existingTitles as $existing)
{ {
$isSubset = stripos($existing, $title) !== FALSE; $isSubset = mb_substr_count($existing, $title) > 0;
$diff = levenshtein($existing, $title); $diff = levenshtein($existing, $title);
$onlydifferentCase = (mb_strtolower($existing) === mb_strtolower($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; return FALSE;
} }

View File

@ -22,7 +22,7 @@ use function Amp\wait;
use Amp\Artax\{Client, Request}; use Amp\Artax\{Client, Request};
use Aviat\AnimeClient\AnimeClient; use Aviat\AnimeClient\AnimeClient;
use Aviat\AnimeClient\API\Kitsu as K; use Aviat\AnimeClient\API\{FailedResponseException, Kitsu as K};
use Aviat\Ion\Json; use Aviat\Ion\Json;
trait KitsuTrait { trait KitsuTrait {
@ -142,8 +142,10 @@ trait KitsuTrait {
{ {
if ($logger) 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); return Json::decode($response->getBody(), TRUE);

View File

@ -47,7 +47,7 @@ class Model {
use ContainerAware; use ContainerAware;
use KitsuTrait; use KitsuTrait;
const FULL_TRANSFORMED_LIST_CACHE_KEY = 'kitsu-full-organized-anime-list'; const LIST_PAGE_SIZE = 100;
/** /**
* Class to map anime list items * Class to map anime list items
@ -160,13 +160,16 @@ class Model {
*/ */
public function getCharacter(string $slug): array public function getCharacter(string $slug): array
{ {
// @todo catch non-existent characters and show 404
$data = $this->getRequest('/characters', [ $data = $this->getRequest('/characters', [
'query' => [ 'query' => [
'filter' => [ '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 $malId
* @param string $type "anime" or "manga" * @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 = [ $options = [
'query' => [ 'query' => [
@ -254,6 +257,11 @@ class Model {
$raw = $this->getRequest('mappings', $options); $raw = $this->getRequest('mappings', $options);
if ( ! array_key_exists('included', $raw))
{
return NULL;
}
return $raw['included'][0]['id']; return $raw['included'][0]['id'];
} }
@ -277,7 +285,7 @@ class Model {
} }
$transformed = $this->animeTransformer->transform($baseData); $transformed = $this->animeTransformer->transform($baseData);
$transformed['included'] = $baseData['included']; $transformed['included'] = JsonAPI::organizeIncluded($baseData['included']);
return $transformed; return $transformed;
} }
@ -374,7 +382,7 @@ class Model {
{ {
$status = $options['filter']['status'] ?? ''; $status = $options['filter']['status'] ?? '';
$count = $this->getAnimeListCount($status); $count = $this->getAnimeListCount($status);
$size = 100; $size = static::LIST_PAGE_SIZE;
$pages = ceil($count / $size); $pages = ceil($count / $size);
$requester = new ParallelAPIRequest(); $requester = new ParallelAPIRequest();
@ -460,7 +468,7 @@ class Model {
* @param array $options * @param array $options
* @return Request * @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' 'include' => 'anime.mappings'
]): Request ]): Request
{ {
@ -618,7 +626,7 @@ class Model {
{ {
$status = $options['filter']['status'] ?? ''; $status = $options['filter']['status'] ?? '';
$count = $this->getMangaListCount($status); $count = $this->getMangaListCount($status);
$size = 100; $size = static::LIST_PAGE_SIZE;
$pages = ceil($count / $size); $pages = ceil($count / $size);
$requester = new ParallelAPIRequest(); $requester = new ParallelAPIRequest();
@ -668,7 +676,7 @@ class Model {
* @param array $options * @param array $options
* @return Request * @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' 'include' => 'manga.mappings'
]): Request ]): Request
{ {
@ -821,6 +829,7 @@ class Model {
} }
$baseData = $data['data']['attributes']; $baseData = $data['data']['attributes'];
$baseData['id'] = $data['id'];
$baseData['included'] = $data['included']; $baseData['included'] = $data['included'];
return $baseData; return $baseData;
} }
@ -856,6 +865,7 @@ class Model {
} }
$baseData = $data['data'][0]['attributes']; $baseData = $data['data'][0]['attributes'];
$baseData['id'] = $data['data'][0]['id'];
$baseData['included'] = $data['included']; $baseData['included'] = $data['included'];
return $baseData; return $baseData;
} }

View File

@ -40,7 +40,9 @@ class AnimeListTransformer extends AbstractTransformer {
$genres = array_column($anime['relationships']['genres'], 'name') ?? []; $genres = array_column($anime['relationships']['genres'], 'name') ?? [];
sort($genres); 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 $total_episodes = array_key_exists('episodeCount', $anime) && (int) $anime['episodeCount'] !== 0
? (int) $anime['episodeCount'] ? (int) $anime['episodeCount']
@ -68,7 +70,7 @@ class AnimeListTransformer extends AbstractTransformer {
'id' => $item['id'], 'id' => $item['id'],
'mal_id' => $MALid, 'mal_id' => $MALid,
'episodes' => [ 'episodes' => [
'watched' => (int) $item['attributes']['progress'] !== '0' 'watched' => (int) $item['attributes']['progress'] !== 0
? (int) $item['attributes']['progress'] ? (int) $item['attributes']['progress']
: '-', : '-',
'total' => $total_episodes, 'total' => $total_episodes,
@ -80,6 +82,7 @@ class AnimeListTransformer extends AbstractTransformer {
'ended' => $anime['endDate'] 'ended' => $anime['endDate']
], ],
'anime' => [ 'anime' => [
'id' => $animeId,
'age_rating' => $anime['ageRating'], 'age_rating' => $anime['ageRating'],
'title' => $anime['canonicalTitle'], 'title' => $anime['canonicalTitle'],
'titles' => Kitsu::filterTitles($anime), 'titles' => Kitsu::filterTitles($anime),
@ -93,7 +96,7 @@ class AnimeListTransformer extends AbstractTransformer {
'notes' => $item['attributes']['notes'], 'notes' => $item['attributes']['notes'],
'rewatching' => (bool) $item['attributes']['reconsuming'], 'rewatching' => (bool) $item['attributes']['reconsuming'],
'rewatched' => (int) $item['attributes']['reconsumeCount'], 'rewatched' => (int) $item['attributes']['reconsumeCount'],
'user_rating' => ($rating === 0) ? '-' : (int) $rating, 'user_rating' => $rating,
'private' => (bool) $item['attributes']['private'] ?? FALSE, 'private' => (bool) $item['attributes']['private'] ?? FALSE,
]; ];
} }
@ -118,12 +121,16 @@ class AnimeListTransformer extends AbstractTransformer {
'reconsuming' => $rewatching, 'reconsuming' => $rewatching,
'reconsumeCount' => $item['rewatched'], 'reconsumeCount' => $item['rewatched'],
'notes' => $item['notes'], 'notes' => $item['notes'],
'progress' => $item['episodes_watched'],
'private' => $privacy '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; $untransformed['data']['rating'] = $item['user_rating'] / 2;
} }

View File

@ -36,10 +36,11 @@ class AnimeTransformer extends AbstractTransformer {
$item['included'] = JsonAPI::organizeIncludes($item['included']); $item['included'] = JsonAPI::organizeIncludes($item['included']);
$item['genres'] = array_column($item['included']['genres'], 'name') ?? []; $item['genres'] = array_column($item['included']['genres'], 'name') ?? [];
sort($item['genres']); sort($item['genres']);
$titles = Kitsu::filterTitles($item); $titles = Kitsu::filterTitles($item);
return [ return [
'id' => $item['id'],
'slug' => $item['slug'], 'slug' => $item['slug'],
'title' => $titles[0], 'title' => $titles[0],
'titles' => $titles, 'titles' => $titles,

View File

@ -42,18 +42,22 @@ class MangaListTransformer extends AbstractTransformer {
$genres = array_column($manga['relationships']['genres'], 'name') ?? []; $genres = array_column($manga['relationships']['genres'], 'name') ?? [];
sort($genres); sort($genres);
$rating = (is_numeric($item['attributes']['rating'])) $rating = (int) $item['attributes']['rating'] !== 0
? intval(2 * $item['attributes']['rating']) ? (int) 2 * $item['attributes']['rating']
: '-'; : '-';
$totalChapters = ($manga['chapterCount'] > 0) $totalChapters = ((int) $manga['chapterCount'] !== 0)
? $manga['chapterCount'] ? $manga['chapterCount']
: '-'; : '-';
$totalVolumes = ($manga['volumeCount'] > 0) $totalVolumes = ((int) $manga['volumeCount'] !== 0)
? $manga['volumeCount'] ? $manga['volumeCount']
: '-'; : '-';
$readChapters = ((int) $item['attributes']['progress'] !== 0)
? $item['attributes']['progress']
: '-';
$MALid = NULL; $MALid = NULL;
if (array_key_exists('mappings', $manga['relationships'])) if (array_key_exists('mappings', $manga['relationships']))
@ -72,7 +76,7 @@ class MangaListTransformer extends AbstractTransformer {
'id' => $item['id'], 'id' => $item['id'],
'mal_id' => $MALid, 'mal_id' => $MALid,
'chapters' => [ 'chapters' => [
'read' => $item['attributes']['progress'], 'read' => $readChapters,
'total' => $totalChapters 'total' => $totalChapters
], ],
'volumes' => [ 'volumes' => [
@ -80,6 +84,7 @@ class MangaListTransformer extends AbstractTransformer {
'total' => $totalVolumes 'total' => $totalVolumes
], ],
'manga' => [ 'manga' => [
'id' => $mangaId,
'titles' => Kitsu::filterTitles($manga), 'titles' => Kitsu::filterTitles($manga),
'alternate_title' => NULL, 'alternate_title' => NULL,
'slug' => $manga['slug'], 'slug' => $manga['slug'],
@ -113,14 +118,18 @@ class MangaListTransformer extends AbstractTransformer {
'mal_id' => $item['mal_id'], 'mal_id' => $item['mal_id'],
'data' => [ 'data' => [
'status' => $item['status'], 'status' => $item['status'],
'progress' => (int)$item['chapters_read'],
'reconsuming' => $rereading, 'reconsuming' => $rereading,
'reconsumeCount' => (int)$item['reread_count'], 'reconsumeCount' => (int)$item['reread_count'],
'notes' => $item['notes'], '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; $map['data']['rating'] = $item['new_rating'] / 2;
} }

View File

@ -33,7 +33,7 @@ class MangaTransformer extends AbstractTransformer {
public function transform($item) public function transform($item)
{ {
$genres = []; $genres = [];
foreach($item['included'] as $included) foreach($item['included'] as $included)
{ {
if ($included['type'] === 'genres') if ($included['type'] === 'genres')
@ -41,10 +41,11 @@ class MangaTransformer extends AbstractTransformer {
$genres[] = $included['attributes']['name']; $genres[] = $included['attributes']['name'];
} }
} }
sort($genres); sort($genres);
return [ return [
'id' => $item['id'],
'title' => $item['canonicalTitle'], 'title' => $item['canonicalTitle'],
'en_title' => $item['titles']['en'], 'en_title' => $item['titles']['en'],
'jp_title' => $item['titles']['en_jp'], 'jp_title' => $item['titles']['en_jp'],

View File

@ -44,9 +44,7 @@ class AnimeListTransformer extends AbstractTransformer {
{ {
$map = [ $map = [
'id' => $item['mal_id'], 'id' => $item['mal_id'],
'data' => [ 'data' => []
'episode' => $item['data']['progress']
]
]; ];
$data =& $item['data']; $data =& $item['data'];
@ -55,6 +53,10 @@ class AnimeListTransformer extends AbstractTransformer {
{ {
switch($key) switch($key)
{ {
case 'progress':
$map['data']['episode'] = $value;
break;
case 'notes': case 'notes':
$map['data']['comments'] = $value; $map['data']['comments'] = $value;
break; break;

View File

@ -44,9 +44,7 @@ class MangaListTransformer extends AbstractTransformer {
{ {
$map = [ $map = [
'id' => $item['mal_id'], 'id' => $item['mal_id'],
'data' => [ 'data' => []
'chapter' => $item['data']['progress']
]
]; ];
$data =& $item['data']; $data =& $item['data'];
@ -55,6 +53,10 @@ class MangaListTransformer extends AbstractTransformer {
{ {
switch($key) switch($key)
{ {
case 'progress':
$map['data']['chapter'] = $value;
break;
case 'notes': case 'notes':
$map['data']['comments'] = $value; $map['data']['comments'] = $value;
break; break;

View File

@ -63,61 +63,48 @@ class SyncKitsuWithMal extends BaseCommand {
$this->kitsuModel = $this->container->get('kitsu-model'); $this->kitsuModel = $this->container->get('kitsu-model');
$this->malModel = $this->container->get('mal-model'); $this->malModel = $this->container->get('mal-model');
$this->syncAnime(); $this->sync('anime');
$this->syncManga(); $this->sync('manga');
} }
public function syncAnime() public function sync(string $type)
{ {
$malCount = count($this->malModel->getAnimeList()); $uType = ucfirst($type);
$kitsuCount = $this->kitsuModel->getAnimeListCount(); $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 MAL {$type} list items: {$malCount}");
$this->echoBox("Number of Kitsu anime list items: {$kitsuCount}"); $this->echoBox("Number of Kitsu {$type} list items: {$kitsuCount}");
$data = $this->diffAnimeLists(); $data = $this->diffLists($type);
$this->echoBox("Number of anime items that need to be added to MAL: " . count($data['addToMAL']));
if ( ! empty($data['addToMAL'])) if ( ! empty($data['addToMAL']))
{ {
$this->echoBox("Adding missing anime list items to MAL"); $count = count($data['addToMAL']);
$this->createMALListItems($data['addToMAL'], 'anime'); $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'])) if ( ! empty($data['addToKitsu']))
{ {
$this->echoBox("Adding missing anime list items to Kitsu"); $count = count($data['addToKitsu']);
$this->createKitsuListItems($data['addToKitsu'], 'anime'); $this->echoBox("Adding {$count} missing {$type} list items to Kitsu");
} $this->createKitsuListItems($data['addToKitsu'], $type);
}
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');
} }
$this->echoBox('Number of manga items that need to be added to Kitsu: ' . count($data['addToKitsu'])); if ( ! empty($data['updateMAL']))
if ( ! empty($data['addToKitsu']))
{ {
$this->echoBox("Adding missing manga list items to Kitsu"); $count = count($data['updateMAL']);
$this->createKitsuListItems($data['addToKitsu'], 'manga'); $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; return $output;
} }
public function formatMALList(string $type): array
{
if ($type === 'anime')
{
return $this->formatMALAnimeList();
}
if ($type === 'manga')
{
return $this->formatMALMangaList();
}
}
public function formatMALAnimeList() public function formatMALAnimeList()
{ {
$orig = $this->malModel->getAnimeList(); $orig = $this->malModel->getAnimeList();
@ -149,10 +149,6 @@ class SyncKitsuWithMal extends BaseCommand {
'status' => AnimeWatchingStatus::MAL_TO_KITSU[$item['my_status']], 'status' => AnimeWatchingStatus::MAL_TO_KITSU[$item['my_status']],
'progress' => $item['my_watched_episodes'], 'progress' => $item['my_watched_episodes'],
'reconsuming' => (bool) $item['my_rewatching'], 'reconsuming' => (bool) $item['my_rewatching'],
'reconsumeCount' => array_key_exists('times_rewatched', $item)
? $item['times_rewatched']
: 0,
// 'notes' => ,
'rating' => $item['my_score'] / 2, 'rating' => $item['my_score'] / 2,
'updatedAt' => (new \DateTime()) 'updatedAt' => (new \DateTime())
->setTimestamp((int)$item['my_last_updated']) ->setTimestamp((int)$item['my_last_updated'])
@ -179,10 +175,6 @@ class SyncKitsuWithMal extends BaseCommand {
'progress' => $item['my_read_chapters'], 'progress' => $item['my_read_chapters'],
'volumes' => $item['my_read_volumes'], 'volumes' => $item['my_read_volumes'],
'reconsuming' => (bool) $item['my_rereadingg'], 'reconsuming' => (bool) $item['my_rereadingg'],
/* 'reconsumeCount' => array_key_exists('times_rewatched', $item)
? $item['times_rewatched']
: 0, */
// 'notes' => ,
'rating' => $item['my_score'] / 2, 'rating' => $item['my_score'] / 2,
'updatedAt' => (new \DateTime()) 'updatedAt' => (new \DateTime())
->setTimestamp((int)$item['my_last_updated']) ->setTimestamp((int)$item['my_last_updated'])
@ -194,18 +186,18 @@ class SyncKitsuWithMal extends BaseCommand {
return $output; 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 = JsonAPI::organizeIncludes($data['included']);
$includes['mappings'] = $this->filterMappings($includes['mappings']); $includes['mappings'] = $this->filterMappings($includes['mappings'], $type);
$output = []; $output = [];
foreach($data['data'] as $listItem) foreach($data['data'] as $listItem)
{ {
$animeId = $listItem['relationships']['anime']['data']['id']; $id = $listItem['relationships'][$type]['data']['id'];
$potentialMappings = $includes['anime'][$animeId]['relationships']['mappings']; $potentialMappings = $includes[$type][$id]['relationships']['mappings'];
$malId = NULL; $malId = NULL;
foreach ($potentialMappings as $mappingId) foreach ($potentialMappings as $mappingId)
@ -232,94 +224,14 @@ class SyncKitsuWithMal extends BaseCommand {
return $output; return $output;
} }
public function filterKitsuMangaList() public function diffLists(string $type = 'anime'): array
{
$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()
{ {
// Get libraryEntries with media.mappings from Kitsu // Get libraryEntries with media.mappings from Kitsu
// Organize mappings, and ignore entries without mappings // Organize mappings, and ignore entries without mappings
$kitsuList = $this->filterKitsuAnimeList(); $kitsuList = $this->formatKitsuList($type);
// Get MAL list data // Get MAL list data
$malList = $this->formatMALAnimeList(); $malList = $this->formatMALList($type);
$itemsToAddToMAL = []; $itemsToAddToMAL = [];
$itemsToAddToKitsu = []; $itemsToAddToKitsu = [];
@ -334,8 +246,8 @@ class SyncKitsuWithMal extends BaseCommand {
{ {
// print_r($malList[$mid]); // print_r($malList[$mid]);
$itemsToAddToKitsu[] = array_merge($malList[$mid]['data'], [ $itemsToAddToKitsu[] = array_merge($malList[$mid]['data'], [
'id' => $this->kitsuModel->getKitsuIdFromMALId($mid), 'id' => $this->kitsuModel->getKitsuIdFromMALId($mid, $type),
'type' => 'anime' 'type' => $type
]); ]);
} }
@ -343,8 +255,23 @@ class SyncKitsuWithMal extends BaseCommand {
{ {
if (in_array($kitsuItem['malId'], $malIds)) if (in_array($kitsuItem['malId'], $malIds))
{ {
// Eventually, compare the list entries, and determine which $item = $this->compareListItems($kitsuItem, $malList[$kitsuItem['malId']]);
// needs to be updated
if (is_null($item))
{
continue;
}
if (in_array('kitsu', $item['updateType']))
{
$kitsuUpdateItems[] = $item['data'];
}
if (in_array('mal', $item['updateType']))
{
$malUpdateItems[] = $item['data'];
}
continue; 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 [ return [
'addToMAL' => $itemsToAddToMAL, 'addToMAL' => $itemsToAddToMAL,
'updateMAL' => $malUpdateItems, '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') public function createKitsuListItems($itemsToAdd, $type = 'anime')
{ {
$requester = new ParallelAPIRequest(); $requester = new ParallelAPIRequest();

View File

@ -239,6 +239,13 @@ class Controller {
*/ */
protected function renderFullPage($view, string $template, array $data) 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)); $view->appendOutput($this->loadPartial($view, 'header', $data));
if (array_key_exists('message', $data) && is_array($data['message'])) if (array_key_exists('message', $data) && is_array($data['message']))

View File

@ -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'];
} }
} }

View File

@ -14,38 +14,154 @@
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/ */
namespace Aviat\AnimeClient\Controller; namespace Aviat\AnimeClient\Controller;
use Aviat\AnimeClient\Controller as BaseController; use Aviat\AnimeClient\Controller as BaseController;
use Aviat\AnimeClient\API\JsonAPI;
/** use Aviat\Ion\ArrayWrapper;
* Controller for character description pages
*/ /**
class Character extends BaseController { * Controller for character description pages
*/
public function index(string $slug) class Character extends BaseController {
{
$model = $this->container->get('kitsu-model'); use ArrayWrapper;
$data = $model->getCharacter($slug); public function index(string $slug)
{
if (( ! array_key_exists('data', $data)) || empty($data['data'])) $model = $this->container->get('kitsu-model');
{
return $this->notFound( $rawData = $model->getCharacter($slug);
$this->formatTitle(
'Characters', if (( ! array_key_exists('data', $rawData)) || empty($rawData['data']))
'Character not found' {
), return $this->notFound(
'Character Not Found' $this->formatTitle(
); 'Characters',
} 'Character not found'
),
$this->outputHTML('character', [ 'Character Not Found'
'title' => $this->formatTitle( );
'Characters', }
$data['data'][0]['attributes']['name']
), $data = JsonAPI::organizeData($rawData);
'data' => $data['data'][0]['attributes']
]); $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;
}
} }

View File

@ -16,10 +16,16 @@
namespace Aviat\AnimeClient\Controller; namespace Aviat\AnimeClient\Controller;
use function Amp\wait;
use Amp\Artax\Client;
use Aviat\AnimeClient\Controller as BaseController; use Aviat\AnimeClient\Controller as BaseController;
use Aviat\AnimeClient\API\JsonAPI; use Aviat\AnimeClient\API\JsonAPI;
use Aviat\Ion\View\HtmlView; use Aviat\Ion\View\HtmlView;
/**
* Controller for handling routes that don't fit elsewhere
*/
class Index extends BaseController { class Index extends BaseController {
/** /**
@ -105,7 +111,7 @@ class Index extends BaseController {
$data = $model->getUserData($username); $data = $model->getUserData($username);
$orgData = JsonAPI::organizeData($data); $orgData = JsonAPI::organizeData($data);
$this->outputHTML('me', [ $this->outputHTML('me', [
'title' => 'About' . $this->config->get('whose_list'), 'title' => 'About ' . $this->config->get('whose_list'),
'data' => $orgData[0], 'data' => $orgData[0],
'attributes' => $orgData[0]['attributes'], 'attributes' => $orgData[0]['attributes'],
'relationships' => $orgData[0]['relationships'], '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 private function organizeFavorites(array $rawfavorites): array
{ {
// return $rawfavorites; // return $rawfavorites;
$output = []; $output = [];
unset($rawfavorites['data']);
foreach($rawfavorites as $item) foreach($rawfavorites as $item)
{ {
$rank = $item['attributes']['favRank']; $rank = $item['attributes']['favRank'];
@ -126,7 +176,7 @@ class Index extends BaseController {
$output[$key] = $output[$key] ?? []; $output[$key] = $output[$key] ?? [];
foreach ($fav as $id => $data) foreach ($fav as $id => $data)
{ {
$output[$key][$rank] = $data['attributes']; $output[$key][$rank] = array_merge(['id' => $id], $data['attributes']);
} }
} }

View File

@ -26,6 +26,7 @@ use const Aviat\AnimeClient\{
use function Aviat\Ion\_dir; use function Aviat\Ion\_dir;
use Aviat\AnimeClient\API\FailedResponseException;
use Aviat\Ion\Di\ContainerInterface; use Aviat\Ion\Di\ContainerInterface;
use Aviat\Ion\Friend; use Aviat\Ion\Friend;
@ -256,13 +257,24 @@ class Dispatcher extends RoutingBase {
{ {
$logger = $this->container->getLogger('default'); $logger = $this->container->getLogger('default');
$controller = new $controllerName($this->container); try
{
$controller = new $controllerName($this->container);
// Run the appropriate controller method // Run the appropriate controller method
$logger->debug('Dispatcher - controller arguments'); $logger->debug('Dispatcher - controller arguments', $params);
$logger->debug(print_r($params, TRUE));
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);
} }
/** /**

View File

@ -98,7 +98,7 @@ class MenuGenerator extends UrlGenerator {
foreach ($menuConfig as $title => $path) foreach ($menuConfig as $title => $path)
{ {
$has = $this->string($this->path())->contains($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); $link = $this->helper->a($this->url($path), $title);

View File

@ -22,33 +22,27 @@ use Aviat\Ion\Friend;
use Aviat\Ion\Json; use Aviat\Ion\Json;
class AnimeListTransformerTest extends AnimeClientTestCase { class AnimeListTransformerTest extends AnimeClientTestCase {
protected $dir; protected $dir;
protected $beforeTransform; protected $beforeTransform;
protected $afterTransform; protected $afterTransform;
protected $transformer; protected $transformer;
public function setUp() public function setUp()
{ {
parent::setUp(); parent::setUp();
$this->dir = AnimeClientTestCase::TEST_DATA_DIR . '/Kitsu'; $this->dir = AnimeClientTestCase::TEST_DATA_DIR . '/Kitsu';
$this->beforeTransform = Json::decodeFile("{$this->dir}/animeListItemBeforeTransform.json"); $this->beforeTransform = Json::decodeFile("{$this->dir}/animeListItemBeforeTransform.json");
$this->afterTransform = Json::decodeFile("{$this->dir}/animeListItemAfterTransform.json");
$this->transformer = new AnimeListTransformer(); $this->transformer = new AnimeListTransformer();
} }
public function testTransform() public function testTransform()
{ {
$expected = $this->afterTransform;
$actual = $this->transformer->transform($this->beforeTransform); $actual = $this->transformer->transform($this->beforeTransform);
$this->assertMatchesSnapshot($actual);
// Json::encodeFile("{$this->dir}/animeListItemAfterTransform.json", $actual);
$this->assertEquals($expected, $actual);
} }
public function dataUntransform() public function dataUntransform()
{ {
return [[ return [[
@ -60,19 +54,6 @@ class AnimeListTransformerTest extends AnimeClientTestCase {
'rewatched' => 0, 'rewatched' => 0,
'notes' => 'Very formulaic.', 'notes' => 'Very formulaic.',
'edit' => true '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' => [ 'input' => [
@ -86,29 +67,29 @@ class AnimeListTransformerTest extends AnimeClientTestCase {
'edit' => 'true', 'edit' => 'true',
'private' => 'On', 'private' => 'On',
'rewatching' => 'On' 'rewatching' => 'On'
], ]
'expected' => [ ], [
'id' => 14047981, 'input' => [
'mal_id' => '12345', 'id' => 14047983,
'data' => [ 'mal_id' => '12347',
'status' => 'current', 'watching_status' => 'current',
'rating' => 4, 'user_rating' => 0,
'reconsuming' => true, 'episodes_watched' => 12,
'reconsumeCount' => 0, 'rewatched' => 0,
'notes' => 'Very formulaic.', 'notes' => '',
'progress' => 38, 'edit' => 'true',
'private' => true, 'private' => 'On',
] 'rewatching' => 'On'
] ]
]]; ]];
} }
/** /**
* @dataProvider dataUntransform * @dataProvider dataUntransform
*/ */
public function testUntransform($input, $expected) public function testUntransform($input)
{ {
$actual = $this->transformer->untransform($input); $actual = $this->transformer->untransform($input);
$this->assertEquals($expected, $actual); $this->assertMatchesSnapshot($actual);
} }
} }

View File

@ -22,29 +22,27 @@ use Aviat\Ion\Friend;
use Aviat\Ion\Json; use Aviat\Ion\Json;
class AnimeTransformerTest extends AnimeClientTestCase { class AnimeTransformerTest extends AnimeClientTestCase {
protected $dir; protected $dir;
protected $beforeTransform; protected $beforeTransform;
protected $afterTransform; protected $afterTransform;
protected $transformer; protected $transformer;
public function setUp() public function setUp()
{ {
parent::setUp(); parent::setUp();
$this->dir = AnimeClientTestCase::TEST_DATA_DIR . '/Kitsu'; $this->dir = AnimeClientTestCase::TEST_DATA_DIR . '/Kitsu';
$this->beforeTransform = Json::decodeFile("{$this->dir}/animeBeforeTransform.json"); $this->beforeTransform = Json::decodeFile("{$this->dir}/animeBeforeTransform.json");
$this->afterTransform = Json::decodeFile("{$this->dir}/animeAfterTransform.json");
$this->transformer = new AnimeTransformer(); $this->transformer = new AnimeTransformer();
} }
public function testTransform() public function testTransform()
{ {
$expected = $this->afterTransform; $expected = $this->afterTransform;
$actual = $this->transformer->transform($this->beforeTransform); $actual = $this->transformer->transform($this->beforeTransform);
// Json::encodeFile("{$this->dir}/animeAfterTransform.json", $actual);
$this->assertMatchesSnapshot($actual);
$this->assertEquals($expected, $actual);
} }
} }

View File

@ -47,19 +47,15 @@ class MangaListTransformerTest extends AnimeClientTestCase {
} }
$this->beforeTransform = $rawBefore['data']; $this->beforeTransform = $rawBefore['data'];
$this->afterTransform = Json::decodeFile("{$this->dir}/mangaListAfterTransform.json"); // $this->afterTransform = Json::decodeFile("{$this->dir}/mangaListAfterTransform.json");
$this->transformer = new MangaListTransformer(); $this->transformer = new MangaListTransformer();
} }
public function testTransform() public function testTransform()
{ {
$expected = $this->afterTransform;
$actual = $this->transformer->transformCollection($this->beforeTransform); $actual = $this->transformer->transformCollection($this->beforeTransform);
$this->assertMatchesSnapshot($actual);
// Json::encodeFile("{$this->dir}/mangaListAfterTransform.json", $actual);
$this->assertEquals($expected, $actual);
} }
public function testUntransform() public function testUntransform()
@ -69,6 +65,7 @@ class MangaListTransformerTest extends AnimeClientTestCase {
'mal_id' => '26769', 'mal_id' => '26769',
'chapters_read' => 67, 'chapters_read' => 67,
'manga' => [ 'manga' => [
'id' => '12345',
'titles' => ["Bokura wa Minna Kawaisou"], 'titles' => ["Bokura wa Minna Kawaisou"],
'alternate_title' => NULL, 'alternate_title' => NULL,
'slug' => "bokura-wa-minna-kawaisou", 'slug' => "bokura-wa-minna-kawaisou",

View File

@ -22,32 +22,29 @@ use Aviat\AnimeClient\Tests\AnimeClientTestCase;
use Aviat\Ion\Json; use Aviat\Ion\Json;
class MangaTransformerTest extends AnimeClientTestCase { class MangaTransformerTest extends AnimeClientTestCase {
protected $dir; protected $dir;
protected $beforeTransform; protected $beforeTransform;
protected $afterTransform; protected $afterTransform;
protected $transformer; protected $transformer;
public function setUp() public function setUp()
{ {
parent::setUp(); parent::setUp();
$this->dir = AnimeClientTestCase::TEST_DATA_DIR . '/Kitsu'; $this->dir = AnimeClientTestCase::TEST_DATA_DIR . '/Kitsu';
$data = Json::decodeFile("{$this->dir}/mangaBeforeTransform.json"); $data = Json::decodeFile("{$this->dir}/mangaBeforeTransform.json");
$baseData = $data['data'][0]['attributes']; $baseData = $data['data'][0]['attributes'];
$baseData['included'] = $data['included']; $baseData['included'] = $data['included'];
$baseData['id'] = $data['data'][0]['id'];
$this->beforeTransform = $baseData; $this->beforeTransform = $baseData;
$this->afterTransform = Json::decodeFile("{$this->dir}/mangaAfterTransform.json");
$this->transformer = new MangaTransformer(); $this->transformer = new MangaTransformer();
} }
public function testTransform() public function testTransform()
{ {
$actual = $this->transformer->transform($this->beforeTransform); $actual = $this->transformer->transform($this->beforeTransform);
$expected = $this->afterTransform; $this->assertMatchesSnapshot($actual);
//Json::encodeFile("{$this->dir}/mangaAfterTransform.json", $actual);
$this->assertEquals($expected, $actual);
} }
} }

View File

@ -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,
);

View File

@ -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,
),
);

View File

@ -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,
),
);

View File

@ -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,
),
);

View File

@ -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',
),
),
),
);

View File

@ -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,
),
);

View File

@ -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',
),
);

View File

@ -23,6 +23,7 @@ use function Aviat\Ion\_dir;
use Aura\Web\WebFactory; use Aura\Web\WebFactory;
use Aviat\Ion\Json; use Aviat\Ion\Json;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Spatie\Snapshots\MatchesSnapshots;
use Zend\Diactoros\{ use Zend\Diactoros\{
Response as HttpResponse, Response as HttpResponse,
ServerRequestFactory ServerRequestFactory
@ -36,6 +37,9 @@ define('TEST_VIEW_DIR', __DIR__ . '/test_views');
* Base class for TestCases * Base class for TestCases
*/ */
class AnimeClientTestCase extends TestCase { class AnimeClientTestCase extends TestCase {
use MatchesSnapshots;
// Test directory constants // Test directory constants
const ROOT_DIR = ROOT_DIR; const ROOT_DIR = ROOT_DIR;
const SRC_DIR = SRC_DIR; const SRC_DIR = SRC_DIR;

View File

@ -1,4 +1,5 @@
{ {
"id": 32344,
"slug": "attack-on-titan", "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)", "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, "coverImageTopOffset": 263,