Merge remote-tracking branch 'origin/develop'

This commit is contained in:
Timothy Warren 2018-08-14 11:37:43 -04:00
commit 6df059fe75
109 changed files with 3021 additions and 1392 deletions

View File

@ -3,8 +3,6 @@
Update your anime/manga list on Kitsu.io and MyAnimeList.net Update your anime/manga list on Kitsu.io and MyAnimeList.net
[![Build Status](https://travis-ci.org/timw4mail/HummingBirdAnimeClient.svg?branch=master)](https://travis-ci.org/timw4mail/HummingBirdAnimeClient) [![Build Status](https://travis-ci.org/timw4mail/HummingBirdAnimeClient.svg?branch=master)](https://travis-ci.org/timw4mail/HummingBirdAnimeClient)
[![build status](https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient/badges/develop/build.svg)](https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient/commits/develop)
[![coverage report](https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient/badges/develop/coverage.svg)](https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient/commits/develop)
[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/timw4mail/HummingBirdAnimeClient/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/timw4mail/HummingBirdAnimeClient/?branch=master) [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/timw4mail/HummingBirdAnimeClient/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/timw4mail/HummingBirdAnimeClient/?branch=master)
[[Hosted Example](https://list.timshomepage.net)] [[Hosted Example](https://list.timshomepage.net)]
@ -52,12 +50,8 @@ Update your anime/manga list on Kitsu.io and MyAnimeList.net
* public/images/manga * public/images/manga
5. Make sure the `console` script is executable 5. Make sure the `console` script is executable
### Using MAL API
1. Update `app/config/mal.toml` with your username and password
2. Enable MAL api in `app/config/config.toml`
### Server Setup ### Server Setup
See the [wiki](https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient/wikis/home) See the [wiki](https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient/wiki)
for more in-depth information for more in-depth information

View File

@ -8,13 +8,12 @@
* *
* @package HummingbirdAnimeClient * @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net> * @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2017 Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License * @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.0 * @version 4.0
* @link https://github.com/timw4mail/HummingBirdAnimeClient * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/ */
use const Aviat\AnimeClient\{ use const Aviat\AnimeClient\{
DEFAULT_CONTROLLER_METHOD, DEFAULT_CONTROLLER_METHOD,
DEFAULT_CONTROLLER DEFAULT_CONTROLLER
@ -184,6 +183,16 @@ return [
// --------------------------------------------------------------------- // ---------------------------------------------------------------------
// Default / Shared routes // Default / Shared routes
// --------------------------------------------------------------------- // ---------------------------------------------------------------------
'anilist-redirect' => [
'path' => '/anilist-redirect',
'action' => 'anilistRedirect',
'controller' => DEFAULT_CONTROLLER,
],
'anilist-oauth' => [
'path' => '/anilist-oauth',
'action' => 'anilistCallback',
'controller' => DEFAULT_CONTROLLER,
],
'image_proxy' => [ 'image_proxy' => [
'path' => '/public/images/{type}/{file}', 'path' => '/public/images/{type}/{file}',
'action' => 'images', 'action' => 'images',

View File

@ -20,6 +20,7 @@ use Aura\Html\HelperLocatorFactory;
use Aura\Router\RouterContainer; use Aura\Router\RouterContainer;
use Aura\Session\SessionFactory; use Aura\Session\SessionFactory;
use Aviat\AnimeClient\API\{ use Aviat\AnimeClient\API\{
Anilist,
Kitsu, Kitsu,
MAL, MAL,
Kitsu\KitsuRequestBuilder, Kitsu\KitsuRequestBuilder,
@ -45,11 +46,14 @@ return function (array $configArray = []) {
$appLogger = new Logger('animeclient'); $appLogger = new Logger('animeclient');
$appLogger->pushHandler(new RotatingFileHandler(__DIR__ . '/logs/app.log', Logger::NOTICE)); $appLogger->pushHandler(new RotatingFileHandler(__DIR__ . '/logs/app.log', Logger::NOTICE));
$anilistRequestLogger = new Logger('anilist-request');
$anilistRequestLogger->pushHandler(new RotatingFileHandler(__DIR__ . '/logs/anilist_request.log', Logger::NOTICE));
$kitsuRequestLogger = new Logger('kitsu-request'); $kitsuRequestLogger = new Logger('kitsu-request');
$kitsuRequestLogger->pushHandler(new RotatingFileHandler(__DIR__ . '/logs/kitsu_request.log', Logger::NOTICE)); $kitsuRequestLogger->pushHandler(new RotatingFileHandler(__DIR__ . '/logs/kitsu_request.log', Logger::NOTICE));
$malRequestLogger = new Logger('mal-request'); $malRequestLogger = new Logger('mal-request');
$malRequestLogger->pushHandler(new RotatingFileHandler(__DIR__ . '/logs/mal_request.log', Logger::NOTICE)); $malRequestLogger->pushHandler(new RotatingFileHandler(__DIR__ . '/logs/mal_request.log', Logger::NOTICE));
$container->setLogger($appLogger); $container->setLogger($appLogger);
$container->setLogger($anilistRequestLogger, 'anilist-request');
$container->setLogger($kitsuRequestLogger, 'kitsu-request'); $container->setLogger($kitsuRequestLogger, 'kitsu-request');
$container->setLogger($malRequestLogger, 'mal-request'); $container->setLogger($malRequestLogger, 'mal-request');

View File

@ -11,8 +11,5 @@ whose_list = "Tim"
# do you wish to show the anime collection? # do you wish to show the anime collection?
show_anime_collection = true show_anime_collection = true
# do you have a My Anime List account set up in mal.toml?
use_mal_api = false
# path to public directory on the server # path to public directory on the server
asset_dir = "/../../public" asset_dir = "/../../public"

View File

@ -1,6 +0,0 @@
################################################################################
# My Anime LIst Integration Config #
################################################################################
username = "timw4mail"
password = "mysecretpassword"

View File

@ -13,7 +13,7 @@
</section> </section>
</section> </section>
<br /> <br />
<table class="form"> <table class="invisible form">
<tbody> <tbody>
<tr> <tr>
<td><label for="status">Watching Status</label></td> <td><label for="status">Watching Status</label></td>

View File

@ -1,4 +1,4 @@
<main> <main class="media-list">
<?php if ($auth->isAuthenticated()): ?> <?php if ($auth->isAuthenticated()): ?>
<a class="bracketed" href="<?= $url->generate('anime.add.get') ?>">Add Item</a> <a class="bracketed" href="<?= $url->generate('anime.add.get') ?>">Add Item</a>
<?php endif ?> <?php endif ?>
@ -24,27 +24,13 @@
<img src="<?= $urlGenerator->assetUrl("images/anime/{$item['anime']['id']}.jpg") ?>" 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']) ?> <span class="canonical"><?= $item['anime']['title'] ?></span>
<?php foreach ($item['anime']['titles'] as $title): ?> <?php foreach ($item['anime']['titles'] as $title): ?>
<br /><small><?= $title ?></small> <br /><small><?= $title ?></small>
<?php endforeach ?> <?php endforeach ?>
</a> </a>
</div> </div>
<div class="table"> <div class="table">
<?php if ($auth->isAuthenticated()): ?>
<div class="row">
<span class="edit">
<a class="bracketed" title="Edit information about this anime" href="<?=
$url->generate('edit', [
'controller' => 'anime',
'id' => $item['id'],
'status' => $item['watching_status']
]);
?>">Edit</a>
</span>
</div>
<?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): ?>
@ -77,6 +63,20 @@
</div> </div>
<?php endif ?> <?php endif ?>
<?php if ($auth->isAuthenticated()): ?>
<div class="row">
<span class="edit">
<a class="bracketed" title="Edit information about this anime" href="<?=
$url->generate('edit', [
'controller' => 'anime',
'id' => $item['id'],
'status' => $item['watching_status']
]);
?>">Edit</a>
</span>
</div>
<?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:
@ -85,7 +85,7 @@
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="media_type"><?= $escape->html($item['anime']['type']) ?></div> <div class="media_type"><?= $escape->html($item['anime']['show_type']) ?></div>
<div class="airing_status"><?= $escape->html($item['airing']['status']) ?></div> <div class="airing_status"><?= $escape->html($item['airing']['status']) ?></div>
<div class="age_rating"><?= $escape->html($item['anime']['age_rating']) ?></div> <div class="age_rating"><?= $escape->html($item['anime']['age_rating']) ?></div>
</div> </div>

View File

@ -38,7 +38,7 @@
</table> </table>
</div> </div>
<div> <div>
<h2><a rel="external" href="<?= $show_data['url'] ?>"><?= array_shift($show_data['titles']) ?></a></h2> <h2><a rel="external" href="<?= $show_data['url'] ?>"><?= $show_data['title'] ?></a></h2>
<?php foreach ($show_data['titles'] as $title): ?> <?php foreach ($show_data['titles'] as $title): ?>
<h3><?= $title ?></h3> <h3><?= $title ?></h3>
<?php endforeach ?> <?php endforeach ?>

View File

@ -2,23 +2,25 @@
<main> <main>
<h2>Edit Anime List Item</h2> <h2>Edit Anime List Item</h2>
<form action="<?= $action ?>" method="post"> <form action="<?= $action ?>" method="post">
<table class="form"> <table class="invisible form">
<thead> <thead>
<tr> <tr>
<th> <th>
<h3><?= $escape->html(array_shift($item['anime']['titles'])) ?></h3> <h3><?= $escape->html($item['anime']['title']) ?></h3>
<?php foreach($item['anime']['titles'] as $title): ?> <?php foreach($item['anime']['titles'] as $title): ?>
<h4><?= $escape->html($title) ?></h4> <h4><?= $escape->html($title) ?></h4>
<?php endforeach ?> <?php endforeach ?>
</th> </th>
<th>
<article class="media">
<?= $helper->img($urlGenerator->assetUrl('images/anime', "{$item['anime']['id']}.jpg")) ?>
</article>
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr>
<td rowspan="9">
<article class="media">
<?= $helper->img($urlGenerator->assetUrl('images/anime', "{$item['anime']['id']}.jpg")) ?>
</article>
</td>
</tr>
<tr> <tr>
<td><label for="private">Is Private?</label></td> <td><label for="private">Is Private?</label></td>
<td> <td>

View File

@ -1,4 +1,4 @@
<main> <main class="media-list">
<?php if ($auth->isAuthenticated()): ?> <?php if ($auth->isAuthenticated()): ?>
<a class="bracketed" href="<?= $url->generate('anime.add.get') ?>">Add Item</a> <a class="bracketed" href="<?= $url->generate('anime.add.get') ?>">Add Item</a>
<?php endif ?> <?php endif ?>
@ -42,7 +42,7 @@
<?php endif ?> <?php endif ?>
<td class="justify"> <td class="justify">
<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']) ?> <?= $item['anime']['title'] ?>
</a> </a>
<?php foreach ($item['anime']['titles'] as $title): ?> <?php foreach ($item['anime']['titles'] as $title): ?>
<br/><?= $title ?> <br/><?= $title ?>
@ -50,7 +50,7 @@
</td> </td>
<td><?= $item['airing']['status'] ?></td> <td><?= $item['airing']['status'] ?></td>
<td><?= $item['user_rating'] ?> / 10 </td> <td><?= $item['user_rating'] ?> / 10 </td>
<td><?= $item['anime']['type'] ?></td> <td><?= $item['anime']['show_type'] ?></td>
<td id="<?= $item['anime']['slug'] ?>"> <td id="<?= $item['anime']['slug'] ?>">
Episodes: <br /> Episodes: <br />
<span class="completed_number"><?= $item['episodes']['watched'] ?></span>&nbsp;/&nbsp;<span class="total_number"><?= $item['episodes']['total'] ?></span> <span class="completed_number"><?= $item['episodes']['watched'] ?></span>&nbsp;/&nbsp;<span class="total_number"><?= $item['episodes']['total'] ?></span>
@ -83,8 +83,8 @@
<p><?= $escape->html($item['notes']) ?></p> <p><?= $escape->html($item['notes']) ?></p>
</td> </td>
<td class="align_left"> <td class="align_left">
<?php sort($item['anime']['genres']) ?> <?php sort($item['anime']->genres) ?>
<?= implode(', ', $item['anime']['genres']) ?> <?= implode(', ', $item['anime']->genres) ?>
</td> </td>
</tr> </tr>
<?php endforeach ?> <?php endforeach ?>

View File

@ -13,7 +13,7 @@
</section> </section>
</section> </section>
<br /> <br />
<table class="form"> <table class="invisible form">
<tbody> <tbody>
<tr> <tr>
<td><label for="media_id">Media</label></td> <td><label for="media_id">Media</label></td>

View File

@ -0,0 +1,27 @@
<article class="media" id="a-<?= $item['hummingbird_id'] ?>">
<img src="<?= $urlGenerator->assetUrl("images/anime/{$item['hummingbird_id']}.jpg") ?>"
alt="<?= $item['title'] ?> cover image"/>
<div class="name">
<a href="<?= $url->generate('anime.details', ['id' => $item['slug']]) ?>">
<?= $item['title'] ?>
<?= ($item['alternate_title'] != "") ? "<small><br />{$item['alternate_title']}</small>" : ""; ?>
</a>
</div>
<div class="table">
<?php if ($auth->isAuthenticated()): ?>
<div class="row">
<span class="edit">
<a class="bracketed"
href="<?= $url->generate($collection_type . '.collection.edit.get', [
'id' => $item['hummingbird_id']
]) ?>">Edit</a>
</span>
</div>
<?php endif ?>
<div class="row">
<div class="completion">Episodes: <?= $item['episode_count'] ?></div>
<div class="media_type"><?= $item['show_type'] ?></div>
<div class="age_rating"><?= $item['age_rating'] ?></div>
</div>
</div>
</article>

View File

@ -1,44 +1,25 @@
<main> <main class="media-list">
<?php if ($auth->isAuthenticated()): ?> <?php if ($auth->isAuthenticated()): ?>
<a class="bracketed" href="<?= $url->generate($collection_type . '.collection.add.get') ?>">Add Item</a> <a class="bracketed" href="<?= $url->generate($collection_type . '.collection.add.get') ?>">Add Item</a>
<?php endif ?> <?php endif ?>
<?php if (empty($sections)): ?> <?php if (empty($sections)): ?>
<h3>There's nothing here!</h3> <h3>There's nothing here!</h3>
<?php else: ?> <?php else: ?>
<div class="tabs">
<?php $i = 0; ?>
<?php foreach ($sections as $name => $items): ?> <?php foreach ($sections as $name => $items): ?>
<section class="status"> <input <?= $i === 0 ? 'checked="checked"' : '' ?> type="radio" id="collection-tab-<?= $i ?>" name="collection-tabs" />
<label for="collection-tab-<?= $i ?>"><?= $name ?></label>
<div class="content">
<h2><?= $name ?></h2> <h2><?= $name ?></h2>
<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'] ?>"> <?php include __DIR__ . '/cover-item.php'; ?>
<img src="<?= $urlGenerator->assetUrl("images/anime/{$item['hummingbird_id']}.jpg") ?>"
alt="<?= $item['title'] ?> cover image" />
<div class="name">
<a href="<?= $url->generate('anime.details', ['id' => $item['slug']]) ?>">
<?= $item['title'] ?>
<?= ($item['alternate_title'] != "") ? "<small><br />{$item['alternate_title']}</small>" : ""; ?>
</a>
</div>
<div class="table">
<?php if ($auth->isAuthenticated()): ?>
<div class="row">
<span class="edit">
<a class="bracketed" href="<?= $url->generate($collection_type . '.collection.edit.get', [
'id' => $item['hummingbird_id']
]) ?>">Edit</a>
</span>
</div>
<?php endif ?>
<div class="row">
<div class="completion">Episodes: <?= $item['episode_count'] ?></div>
<div class="media_type"><?= $item['show_type'] ?></div>
<div class="age_rating"><?= $item['age_rating'] ?></div>
</div>
</div>
</article>
<?php endforeach ?> <?php endforeach ?>
</section> </section>
</section> </div>
<?php $i++; ?>
<?php endforeach ?> <?php endforeach ?>
</div>
<?php endif ?> <?php endif ?>
</main> </main>

View File

@ -2,7 +2,7 @@
<main> <main>
<h2>Edit Anime Collection Item</h2> <h2>Edit Anime Collection Item</h2>
<form action="<?= $action_url ?>" method="post"> <form action="<?= $action_url ?>" method="post">
<table class="form"> <table class="invisible form" style="border:0">
<thead> <thead>
<tr> <tr>
<th> <th>
@ -11,17 +11,19 @@
<h4><?= $item['alternate_title'] ?></h4> <h4><?= $item['alternate_title'] ?></h4>
<?php endif ?> <?php endif ?>
</th> </th>
<th>
<article class="media">
<?= $helper->img($urlGenerator->assetUrl("images/anime/{$item['hummingbird_id']}.jpg")); ?>
</article>
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<td><label for="media_id">Media</label></td> <td rowspan="4" class="align_center">
<td> <article class="media">
<?= $helper->img($urlGenerator->assetUrl("images/anime/{$item['hummingbird_id']}.jpg")); ?>
</article>
</td>
</tr>
<tr>
<td class="align_right"><label for="media_id">Media</label></td>
<td class="align_left">
<select name="media_id" id="media_id"> <select name="media_id" id="media_id">
<?php foreach($media_items as $id => $name): ?> <?php foreach($media_items as $id => $name): ?>
<option <?= $item['media_id'] == $id ? 'selected="selected"' : '' ?> value="<?= $id ?>"><?= $name ?></option> <option <?= $item['media_id'] == $id ? 'selected="selected"' : '' ?> value="<?= $id ?>"><?= $name ?></option>

View File

@ -0,0 +1,19 @@
<tr>
<?php if ($auth->isAuthenticated()): ?>
<td>
<a class="bracketed"
href="<?= $url->generate($collection_type . '.collection.edit.get', ['id' => $item['hummingbird_id']]) ?>">Edit</a>
</td>
<?php endif ?>
<td class="align_left">
<a href="<?= $url->generate('anime.details', ['id' => $item['slug']]) ?>">
<?= $item['title'] ?>
</a>
<?= (!empty($item['alternate_title'])) ? " <br /><small> " . $item['alternate_title'] . "</small>" : "" ?>
</td>
<td><?= $item['episode_count'] ?></td>
<td><?= $item['episode_length'] ?></td>
<td><?= $item['show_type'] ?></td>
<td><?= $item['age_rating'] ?></td>
<td class="align_left"><?= $item['notes'] ?></td>
</tr>

View File

@ -5,13 +5,19 @@
<?php if (empty($sections)): ?> <?php if (empty($sections)): ?>
<h3>There's nothing here!</h3> <h3>There's nothing here!</h3>
<?php else: ?> <?php else: ?>
<?php $i = 0; ?>
<div class="tabs">
<?php foreach ($sections as $name => $items): ?> <?php foreach ($sections as $name => $items): ?>
<input <?= $i === 0 ? 'checked="checked"' : '' ?> type="radio" id="collection-tab-<?= $i ?>"
name="collection-tabs"/>
<label for="collection-tab-<?= $i ?>"><?= $name ?></label>
<div class="content">
<h2><?= $name ?></h2> <h2><?= $name ?></h2>
<table> <table>
<thead> <thead>
<tr> <tr>
<?php if ($auth->isAuthenticated()): ?> <?php if ($auth->isAuthenticated()): ?>
<th>Actions</th> <td>Actions</td>
<?php endif ?> <?php endif ?>
<th>Title</th> <th>Title</th>
<th>Episode Count</th> <th>Episode Count</th>
@ -23,29 +29,14 @@
</thead> </thead>
<tbody> <tbody>
<?php foreach ($items as $item): ?> <?php foreach ($items as $item): ?>
<tr> <?php include __DIR__ . '/list-item.php' ?>
<?php if($auth->isAuthenticated()): ?>
<td>
<a class="bracketed" href="<?= $url->generate($collection_type . '.collection.edit.get', ['id' => $item['hummingbird_id']]) ?>">Edit</a>
</td>
<?php endif ?>
<td class="align_left">
<a href="<?= $url->generate('anime.details', ['id' => $item['slug']]) ?>">
<?= $item['title'] ?>
</a>
<?= ( ! empty($item['alternate_title'])) ? " <br /><small> " . $item['alternate_title'] . "</small>" : "" ?>
</td>
<td><?= $item['episode_count'] ?></td>
<td><?= $item['episode_length'] ?></td>
<td><?= $item['show_type'] ?></td>
<td><?= $item['age_rating'] ?></td>
<td class="align_left"><?= $item['notes'] ?></td>
</tr>
<?php endforeach ?> <?php endforeach ?>
</tbody> </tbody>
</table> </table>
<br /> </div>
<?php $i++ ?>
<?php endforeach ?> <?php endforeach ?>
</div>
<?php endif ?> <?php endif ?>
</main> </main>
<script defer="defer" src="<?= $urlGenerator->assetUrl('js.php/g/table') ?>"></script> <script defer="defer" src="<?= $urlGenerator->assetUrl('js.php/g/table') ?>"></script>

View File

@ -1,11 +1,11 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<title><?= $title ?></title>
<meta charset="utf-8"/> <meta charset="utf-8"/>
<title><?= $title ?></title>
<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=1" />
<link rel="stylesheet" href="<?= $urlGenerator->assetUrl('css/app.min.css') ?>" /> <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') ?>">

View File

@ -7,7 +7,7 @@ $lastSegment = $urlGenerator->lastSegment();
$extraSegment = $lastSegment === 'list' ? '/list' : ''; $extraSegment = $lastSegment === 'list' ? '/list' : '';
?> ?>
<h1 class="flex flex-align-end flex-wrap"> <div id="main-nav" 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): ?>
<?= $helper->a( <?= $helper->a(
@ -67,7 +67,7 @@ $extraSegment = $lastSegment === 'list' ? '/list' : '';
[<?= $helper->a($url->generate('login'), "{$whose} Login") ?>] [<?= $helper->a($url->generate('login'), "{$whose} Login") ?>]
<?php endif ?> <?php endif ?>
</span> </span>
</h1> </div>
<nav> <nav>
<?php if ($container->get('util')->isViewPage()): ?> <?php if ($container->get('util')->isViewPage()): ?>
<?= $helper->menu($menu_name) ?> <?= $helper->menu($menu_name) ?>

View File

@ -13,7 +13,7 @@
</section> </section>
</section> </section>
<br /> <br />
<table class="form"> <table class="invisible form">
<tbody> <tbody>
<tr> <tr>
<td><label for="status">Reading Status</label></td> <td><label for="status">Reading Status</label></td>

View File

@ -1,4 +1,4 @@
<main> <main class="media-list">
<?php if ($auth->isAuthenticated()): ?> <?php if ($auth->isAuthenticated()): ?>
<a class="bracketed" href="<?= $url->generate('manga.add.get') ?>">Add Item</a> <a class="bracketed" href="<?= $url->generate('manga.add.get') ?>">Add Item</a>
<?php endif ?> <?php endif ?>
@ -26,7 +26,7 @@
<img src="<?= $urlGenerator->assetUrl('images/manga', "{$item['manga']['id']}.jpg") ?>" /> <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($item['manga']['title']) ?>
<?php foreach($item['manga']['titles'] as $title): ?> <?php foreach($item['manga']['titles'] as $title): ?>
<br /><small><?= $title ?></small> <br /><small><?= $title ?></small>
<?php endforeach ?> <?php endforeach ?>
@ -73,8 +73,8 @@
Chapters: <span class="chapters_read"><?= $item['chapters']['read'] ?></span> / Chapters: <span class="chapters_read"><?= $item['chapters']['read'] ?></span> /
<span class="chapter_count"><?= $item['chapters']['total'] ?></span> <span class="chapter_count"><?= $item['chapters']['total'] ?></span>
</div> </div>
</div> <?php /* </div>
<div class="row"> <div class="row"> */ ?>
<div class="volume_completion"> <div class="volume_completion">
Volumes: <span class="volume_count"><?= $item['volumes']['total'] ?></span> Volumes: <span class="volume_count"><?= $item['volumes']['total'] ?></span>
</div> </div>

View File

@ -1,26 +1,28 @@
<?php if ($auth->isAuthenticated()): ?> <?php if ($auth->isAuthenticated()): ?>
<main> <main>
<h1> <h2>
Edit <?= $item['manga']['titles'][0] ?> Edit Manga List Item
</h1> </h2>
<form action="<?= $action ?>" method="post"> <form action="<?= $action ?>" method="post">
<table class="form"> <table class="invisible form">
<thead> <thead>
<tr> <tr>
<th> <th>
<h3><?= $escape->html(array_shift($item['manga']['titles'])) ?></h3> <h3><?= $escape->html($item['manga']['title']) ?></h3>
<?php foreach ($item['manga']['titles'] as $title): ?> <?php foreach ($item['manga']['titles'] as $title): ?>
<h4><?= $escape->html($title) ?></h4> <h4><?= $escape->html($title) ?></h4>
<?php endforeach ?> <?php endforeach ?>
</th>
<th>
<article class="media">
<?= $helper->img($urlGenerator->assetUrl('images/manga', "{$item['manga']['id']}.jpg")); ?>
</article>
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr>
<td rowspan="9">
<article class="media">
<?= $helper->img($urlGenerator->assetUrl('images/manga', "{$item['manga']['id']}.jpg")); ?>
</article>
</td>
</tr>
<tr> <tr>
<td><label for="status">Reading Status</label></td> <td><label for="status">Reading Status</label></td>
<td> <td>
@ -35,21 +37,24 @@
<tr> <tr>
<td><label for="series_rating">Rating</label></td> <td><label for="series_rating">Rating</label></td>
<td> <td>
<input type="number" min="0" max="10" maxlength="2" name="new_rating" value="<?= $item['user_rating'] ?>" id="series_rating" size="2" /> / 10 <input type="number" min="0" max="10" maxlength="2" name="new_rating"
value="<?= $item['user_rating'] ?>" id="series_rating" size="2"/> / 10
</td> </td>
</tr> </tr>
<tr> <tr>
<td><label for="chapters_read">Chapters Read</label></td> <td><label for="chapters_read">Chapters Read</label></td>
<td> <td>
<input type="number" min="0" name="chapters_read" id="chapters_read" value="<?= $item['chapters']['read'] ?>" /> / <?= $item['chapters']['total'] ?> <input type="number" min="0" name="chapters_read" id="chapters_read"
value="<?= $item['chapters']['read'] ?>"/> / <?= $item['chapters']['total'] ?>
</td> </td>
</tr> </tr>
<? /*<tr> <tr>
<td><label for="volumes_read">Volumes Read</label></td> <td><label for="volumes_read">Volumes Read</label></td>
<td> <td>
<input type="number" min="0" name="volumes_read" id="volumes_read" value="<?= $item['volumes']['read'] ?>" /> / <?= $item['volumes']['total'] ?> <?php /*<input type="number" disabled="disabled" min="0" name="volumes_read" id="volumes_read" value="" /> */ ?>
- / <?= $item['volumes']['total'] ?>
</td> </td>
</tr> */ ?> </tr>
<tr> <tr>
<td><label for="rereading_flag">Rereading?</label></td> <td><label for="rereading_flag">Rereading?</label></td>
<td> <td>
@ -61,7 +66,8 @@
<tr> <tr>
<td><label for="reread_count">Reread Count</label></td> <td><label for="reread_count">Reread Count</label></td>
<td> <td>
<input type="number" min="0" id="reread_count" name="reread_count" value="<?= $item['reread'] ?>" /> <input type="number" min="0" id="reread_count" name="reread_count"
value="<?= $item['reread'] ?>"/>
</td> </td>
</tr> </tr>
<tr> <tr>

View File

@ -1,4 +1,4 @@
<main> <main class="media-list">
<?php if ($auth->isAuthenticated()): ?> <?php if ($auth->isAuthenticated()): ?>
<a class="bracketed" href="<?= $url->generate('manga.add.get') ?>">Add Item</a> <a class="bracketed" href="<?= $url->generate('manga.add.get') ?>">Add Item</a>
<?php endif ?> <?php endif ?>
@ -14,7 +14,7 @@
<thead> <thead>
<tr> <tr>
<?php if ($auth->isAuthenticated()): ?> <?php if ($auth->isAuthenticated()): ?>
<th>&nbsp;</th> <td>&nbsp;</td>
<?php endif ?> <?php endif ?>
<th>Title</th> <th>Title</th>
<th>Rating</th> <th>Rating</th>
@ -39,7 +39,7 @@
<?php endif ?> <?php endif ?>
<td class="align_left"> <td class="align_left">
<a href="<?= $url->generate('manga.details', ['id' => $item['manga']['slug']]) ?>"> <a href="<?= $url->generate('manga.details', ['id' => $item['manga']['slug']]) ?>">
<?= array_shift($item['manga']['titles']) ?> <?= $item['manga']['title'] ?>
</a> </a>
<?php foreach($item['manga']['titles'] as $title): ?> <?php foreach($item['manga']['titles'] as $title): ?>
<br /><?= $title ?> <br /><?= $title ?>

View File

@ -39,6 +39,7 @@
"phpmd/phpmd": "^2.4", "phpmd/phpmd": "^2.4",
"phpstan/phpstan": "^0.9.1", "phpstan/phpstan": "^0.9.1",
"phpunit/phpunit": "^6.0", "phpunit/phpunit": "^6.0",
"roave/security-advisories": "dev-master",
"robmorgan/phinx": "^0.9.1", "robmorgan/phinx": "^0.9.1",
"sebastian/phpcpd": "^3.0", "sebastian/phpcpd": "^3.0",
"spatie/phpunit-snapshot-assertions": "^1.2.0", "spatie/phpunit-snapshot-assertions": "^1.2.0",

View File

@ -28,9 +28,12 @@ if ($timezone === '' || $timezone === FALSE)
// Load composer autoloader // Load composer autoloader
require_once __DIR__ . '/vendor/autoload.php'; require_once __DIR__ . '/vendor/autoload.php';
// if (array_key_exists('ENV', $_ENV) && $_ENV['ENV'] === 'development')
{
$whoops = new \Whoops\Run; $whoops = new \Whoops\Run;
$whoops->pushHandler(new \Whoops\Handler\PrettyPageHandler); $whoops->pushHandler(new \Whoops\Handler\PrettyPageHandler);
$whoops->register(); $whoops->register();
}
// Define base directories // Define base directories
$APP_DIR = _dir(__DIR__, 'app'); $APP_DIR = _dir(__DIR__, 'app');

View File

@ -7,7 +7,7 @@ const atImport = require('postcss-import');
const cssNext = require('postcss-cssnext'); const cssNext = require('postcss-cssnext');
const cssNano = require('cssnano'); const cssNano = require('cssnano');
const css = fs.readFileSync('css/base.css', 'utf8'); const css = fs.readFileSync('css/base.css', 'utf-8');
postcss() postcss()
.use(atImport()) .use(atImport())

File diff suppressed because one or more lines are too long

View File

@ -1,8 +1,10 @@
@import "./marx.css"; @import "./marx.css";
:root { :root {
--blue-link: rgb(18, 113, 219);
--link-shadow: 1px 1px 1px #000; --link-shadow: 1px 1px 1px #000;
--shadow: 1px 2px 1px rgba(0, 0, 0, 0.85); --white-link-shadow: 1px 1px 1px #fff;
--shadow: 2px 2px 2px #000;
--title-overlay: rgba(0, 0, 0, 0.45); --title-overlay: rgba(0, 0, 0, 0.45);
--title-overlay-fallback: #000; --title-overlay-fallback: #000;
--text-color: #ffffff; --text-color: #ffffff;
@ -13,9 +15,13 @@
--radius: 5px; --radius: 5px;
} }
template, [hidden="hidden"], .media[hidden] {display:none} template, [hidden="hidden"], .media[hidden] {
display: none
}
body {margin: 0.5em;} body {
margin: 0.5em;
}
button { button {
background: rgba(255, 255, 255, 0.65); background: rgba(255, 255, 255, 0.65);
@ -23,7 +29,7 @@ button {
} }
table { table {
min-width:85%; /* min-width: 85%; */
margin: 0 auto; margin: 0 auto;
} }
@ -56,40 +62,87 @@ a:hover, a:active {
.bracketed { .bracketed {
color: var(--edit-link-color); color: var(--edit-link-color);
} }
.bracketed, h1 a {
.bracketed, #main-nav a {
text-shadow: var(--link-shadow); text-shadow: var(--link-shadow);
} }
.bracketed:before {content: '[\00a0'}
.bracketed:after {content: '\00a0]'} .bracketed:before {
content: '[\00a0'
}
.bracketed:after {
content: '\00a0]'
}
.bracketed:hover, .bracketed:active { .bracketed:hover, .bracketed:active {
color: var(--edit-link-hover-color) color: var(--edit-link-hover-color)
} }
.grow-1 {flex-grow: 1} .grow-1 {
.flex-wrap {flex-wrap: wrap} flex-grow: 1
.flex-no-wrap {flex-wrap: nowrap} }
.flex-align-end {align-items: flex-end}
.flex-align-space-around {align-content: space-around} .flex-wrap {
.flex-justify-space-around {justify-content: space-around} flex-wrap: wrap
.flex-self-center {align-self:center} }
.flex {display: flex}
.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 { .small-font {
font-size: 1.6rem; font-size: 1.6rem;
} }
.justify {text-align:justify} .justify {
.align_center {text-align:center !important} text-align: justify
.align_left {text-align:left !important} }
.align_right {text-align:right !important}
.valign_top {vertical-align:top} .align_center {
text-align: center !important
}
.no_border {border:none} .align_left {
text-align: left !important
}
.align_right {
text-align: right !important
}
.valign_top {
vertical-align: top
}
.no_border {
border: none
}
.media-wrap { .media-wrap {
text-align: center; text-align: center;
margin: 0 auto; margin: 0 auto;
position: relative;
} }
.danger { .danger {
@ -111,6 +164,7 @@ a:hover, a:active {
padding: 0 0.5em; padding: 0 0.5em;
padding: 0 0.5rem; padding: 0 0.5rem;
} }
.user-btn:hover, .user-btn:active { .user-btn:hover, .user-btn:active {
border-color: var(--edit-link-hover-color); border-color: var(--edit-link-hover-color);
background-color: var(--edit-link-hover-color); background-color: var(--edit-link-hover-color);
@ -120,6 +174,20 @@ a:hover, a:active {
width: 100%; width: 100%;
} }
/* -----------------------------------------------------------------------------
Main Nav
------------------------------------------------------------------------------*/
#main-nav {
font-family: var(--default-font-list);
margin: 2em 0 1.6em;
margin: 2rem 0 1.6rem;
border-bottom: .1rem solid rgba(0, 0, 0, 0.2);
font-size: 3.6em;
font-size: 3.6rem;
font-style: normal;
font-weight: 500;
}
/* ----------------------------------------------------------------------------- /* -----------------------------------------------------------------------------
CSS loading icon CSS loading icon
------------------------------------------------------------------------------*/ ------------------------------------------------------------------------------*/
@ -197,17 +265,22 @@ a:hover, a:active {
.sorting_desc { .sorting_desc {
vertical-align: text-bottom; vertical-align: text-bottom;
} }
.sorting::before { .sorting::before {
content: " ↕\00a0"; content: " ↕\00a0";
} }
.sorting_asc::before { .sorting_asc::before {
content: " ↑\00a0"; content: " ↑\00a0";
} }
.sorting_desc::before { .sorting_desc::before {
content: " ↓\00a0"; content: " ↓\00a0";
} }
.form { width:100%; } .form {
/* width: 100%; */
}
.form thead th, .form thead tr { .form thead th, .form thead tr {
background: inherit; background: inherit;
@ -219,14 +292,16 @@ a:hover, a:active {
min-width: 25px; min-width: 25px;
max-width: 30%; max-width: 30%;
} }
.form tr > td:nth-child(even) { .form tr > td:nth-child(even) {
text-align: left; text-align: left;
width:70%; /* width: 70%; */
} }
.invisible tbody > tr:nth-child(odd) { .invisible tbody > tr:nth-child(odd) {
background: inherit; background: inherit;
} }
.invisible tr, .invisible td, .invisible th { .invisible tr, .invisible td, .invisible th {
border: 0; border: 0;
} }
@ -280,6 +355,7 @@ a:hover, a:active {
border: 1px solid #1f8454; border: 1px solid #1f8454;
background: #70dda9; background: #70dda9;
} }
.message.success .icon::after { .message.success .icon::after {
content: '✔' content: '✔'
} }
@ -305,6 +381,7 @@ a:hover, a:active {
width: 220px; width: 220px;
height: 311px; height: 311px;
margin: var(--normal-padding); margin: var(--normal-padding);
z-index: 0;
} }
.media > img, .media > img,
@ -322,11 +399,10 @@ a:hover, a:active {
.medium_metadata > div, .medium_metadata > div,
.row { .row {
text-shadow: var(--shadow); text-shadow: var(--shadow);
background: var(--title-overlay-fallback);
background: var(--title-overlay);
color: var(--text-color); color: var(--text-color);
padding: var(--normal-padding); padding: var(--normal-padding);
text-align: right; text-align: right;
z-index: 2;
} }
.media_type, .age_rating { .media_type, .age_rating {
@ -349,42 +425,74 @@ a:hover, a:active {
position: absolute; position: absolute;
top: 0; top: 0;
} }
.small_character:hover > .name,
.character:hover > .name, .media > .name a {
.media:hover > .name, transition: none;
.media:hover > .media_metadata > div, }
.media:hover > .medium_metadata > div,
.media:hover > .table .row .media .name a::before {
{ /* background: var(--title-overlay-fallback);
transition: .25s ease; background: var(--title-overlay); */
content: '';
display: block;
height: 311px;
left: 0;
position: absolute;
top: 0;
width: 220px;
z-index: -1; /* Put the pseudo-element behind its parent */
}
.media-list .media:hover .name a::before {
/* transition: .25s ease; */
background: rgba(0, 0, 0, 0.75); background: rgba(0, 0, 0, 0.75);
} }
.media > .name span.canonical {
font-weight: bold;
}
.media > .name small {
font-weight: normal;
}
.media:hover .name {
background: rgba(0, 0, 0, 0.75);
}
.media-list .media > .name a:hover,
.media-list .media > .name a:hover small {
color: var(--blue-link);
}
.media:hover > button[hidden], .media:hover > button[hidden],
.media:hover > .edit_buttons[hidden] .media:hover > .edit_buttons[hidden] {
{
transition: .25s ease; transition: .25s ease;
display: block; display: block;
} }
.media:hover {
transition: .25s ease;
}
.small_character > .name a, .small_character > .name a,
.small_character > .name a small, .small_character > .name a small,
.character > .name a, .character > .name a,
.character > .name a small, .character > .name a small,
.media > .name a, .media > .name a,
.media > .name a small .media > .name a small {
{
background: none; background: none;
color: #fff; color: #fff;
text-shadow: var(--shadow); text-shadow: var(--shadow);
} }
/* ----------------------------------------------------------------------------- /* -----------------------------------------------------------------------------
Anime-list-specific styles Anime-list-specific styles
------------------------------------------------------------------------------*/ ------------------------------------------------------------------------------*/
.anime .name, .manga .name { .anime .name, .manga .name {
background: var(--title-overlay-fallback);
background: var(--title-overlay);
text-align: center; text-align: center;
width: 100%; width: 100%;
padding: 0.5em 0.25em; padding: 0.5em 0.25em;
@ -401,7 +509,6 @@ a:hover, a:active {
text-align: center; text-align: center;
} }
.anime .table, .manga .table { .anime .table, .manga .table {
position: absolute; position: absolute;
bottom: 0; bottom: 0;
@ -411,8 +518,6 @@ a:hover, a:active {
.anime .row, .manga .row { .anime .row, .manga .row {
width: 100%; width: 100%;
background: var(--title-overlay-fallback);
background: var(--title-overlay);
display: flex; display: flex;
align-content: space-around; align-content: space-around;
justify-content: space-around; justify-content: space-around;
@ -422,6 +527,7 @@ a:hover, a:active {
.anime .row > span, .manga .row > span { .anime .row > span, .manga .row > span {
text-align: left; text-align: left;
z-index: 2;
} }
.anime .row > div, .manga .row > div { .anime .row > div, .manga .row > div {
@ -430,14 +536,26 @@ a:hover, a:active {
align-self: center; align-self: center;
text-align: center; text-align: center;
vertical-align: middle; vertical-align: middle;
z-index: 2;
} }
.anime .media > button.plus_one { .anime .media > button.plus_one {
border-color: hsla(0, 0%, 100%, .65);
position: absolute; position: absolute;
top: 138px; top: 138px;
top: calc(50% - 21.5px); top: calc(50% - 21.5px);
left: 44px; left: 44px;
left: calc(50% - 66.5px); left: calc(50% - 66.5px);
z-index: 50;
}
.anime .media > button.plus_one:hover {
color: hsla(0, 0%, 100%, .65);
background: #888;
}
.anime .media > button.plus_one:active {
background: #444;
} }
/* ----------------------------------------------------------------------------- /* -----------------------------------------------------------------------------
@ -448,7 +566,7 @@ a:hover, a:active {
} }
.manga .media { .manga .media {
border:1px solid #ddd; /* border: 1px solid #ddd; */
height: 310px; height: 310px;
margin: 0.25em; margin: 0.25em;
} }
@ -456,11 +574,25 @@ a:hover, a:active {
.manga .media > .edit_buttons { .manga .media > .edit_buttons {
position: absolute; position: absolute;
top: 86px; top: 86px;
top: calc(50% - 58.5px); /* top: calc(50% - 58.5px); */
top: calc(50% - 22.4px);
left: 43.5px; left: 43.5px;
left: calc(50% - 66.5px); left: calc(50% - 66.5px);
z-index: 40;
} }
.manga .media > .edit_buttons button {
border-color: hsla(0, 0%, 100%, .65);
}
.manga .media > .edit_buttons:hover button {
color: hsla(0, 0%, 100%, .65);
background: #888;
}
.manga .media > .edit_buttons button:active {
background: #444;
}
/* ----------------------------------------------------------------------------- /* -----------------------------------------------------------------------------
Search page styles Search page styles
@ -493,11 +625,13 @@ a:hover, a:active {
left: 0; left: 0;
height: 100%; height: 100%;
width: 100%; width: 100%;
z-index: 5;
} }
#series_list article.media { #series_list article.media {
position: relative; position: relative;
} }
#series_list .name, #series_list .name label { #series_list .name, #series_list .name label {
position: absolute; position: absolute;
display: block; display: block;
@ -508,6 +642,7 @@ a:hover, a:active {
vertical-align: middle; vertical-align: middle;
line-height: 1.25em; line-height: 1.25em;
} }
#series_list .name small { #series_list .name small {
color: #fff; color: #fff;
} }
@ -546,6 +681,7 @@ a:hover, a:active {
.details .media_details { .details .media_details {
max-width: 300px; max-width: 300px;
} }
.details .media_details td { .details .media_details td {
padding: 0 1.5rem; padding: 0 1.5rem;
} }
@ -559,18 +695,25 @@ a:hover, a:active {
white-space: nowrap; white-space: nowrap;
text-align: right; text-align: right;
} }
.details .media_details td:nth-child(even) { .details .media_details td:nth-child(even) {
text-align: left; text-align: left;
} }
.character, .character,
.small_character { .small_character {
background: rgba(0,0,0,0.5); /* background: rgba(0,0,0,0.5); */
width: 225px; width: 225px;
height: 350px; height: 350px;
vertical-align: middle; vertical-align: middle;
white-space: nowrap; white-space: nowrap;
} }
.character:hover .name,
.small_character:hover .name {
background: rgba(0, 0, 0, 0.8);
}
.small_character a { .small_character a {
display: inline-block; display: inline-block;
width: 100%; width: 100%;
@ -646,9 +789,19 @@ a:hover, a:active {
vertical-align: middle; vertical-align: middle;
} }
.cover_streaming_link {
display:none;
}
.media:hover .cover_streaming_link {
display: block;
}
.cover_streaming_link .streaming-logo { .cover_streaming_link .streaming-logo {
width: 20px; width: 20px;
height: 20px; height: 20px;
-webkit-filter: drop-shadow(0 -1px 4px #fff);
filter: drop-shadow(0 -1px 4px #fff);
} }
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
@ -686,3 +839,75 @@ a:hover, a:active {
.loading-content .cssload-inner.cssload-three { .loading-content .cssload-inner.cssload-three {
border-color: #fff border-color: #fff
} }
/* ----------------------------------------------------------------------------
CSS Tabs
-----------------------------------------------------------------------------*/
.tabs {
display: flex;
flex-wrap: wrap;
background: #efefef;
box-shadow: 0 48px 80px -32px rgba(0, 0, 0, 0.3);
margin-top: 1.5em;
}
.tabs label {
border: 1px solid #e5e5e5;
width: 100%;
padding: 20px 30px;
background: #e5e5e5;
cursor: pointer;
font-weight: bold;
font-size: 18px;
color: #7f7f7f;
transition: background 0.1s, color 0.1s;
/* margin-left: 4em; */
}
.tabs label:hover {
background: #d8d8d8;
}
.tabs label:active {
background: #ccc;
}
.tabs [type=radio]:focus + label {
box-shadow: inset 0px 0px 0px 3px #2aa1c0;
z-index: 1;
}
.tabs [type=radio] {
position: absolute;
opacity: 0;
}
.tabs [type=radio]:checked + label {
border-bottom: 1px solid #fff;
background: #fff;
color: #000;
}
.tabs [type=radio]:checked + label + .content {
border: 1px solid #e5e5e5;
border-top: 0;
display: block;
padding: 20px 30px 30px;
background: #fff;
width: 100%;
}
.tabs .content {
display: none;
}
@media (min-width: 600px) {
.tabs label {
width: auto;
}
.tabs .content {
order: 99;
}
}

View File

@ -22,7 +22,7 @@ use Aviat\AnimeClient\API\HummingbirdClient;
use Aviat\Ion\{Json, JsonException}; use Aviat\Ion\{Json, JsonException};
// Include Amp and Artax // Include Amp and Artax
require_once('../vendor/autoload.php'); require_once '../vendor/autoload.php';
//Creative rewriting of /g/groupname to ?g=groupname //Creative rewriting of /g/groupname to ?g=groupname
$pi = $_SERVER['PATH_INFO']; $pi = $_SERVER['PATH_INFO'];
@ -318,10 +318,11 @@ class JSMin {
$lastModifiedDate = gmdate('D, d M Y H:i:s', $lastModified); $lastModifiedDate = gmdate('D, d M Y H:i:s', $lastModified);
$expiresDate = gmdate('D, d M Y H:i:s', $expires); $expiresDate = gmdate('D, d M Y H:i:s', $expires);
header("Content-Type: {$mimeType}; charset=utf8"); header("Content-Type: {$mimeType}; charset=utf-8");
header("Cache-control: public, max-age=691200, must-revalidate"); header('Cache-control: public, max-age=691200, must-revalidate');
header("Last-Modified: {$lastModifiedDate} GMT");
header("Expires: {$expiresDate} GMT"); header("Expires: {$expiresDate} GMT");
header("Last-Modified: {$lastModifiedDate} GMT");
header('X-Content-Type-Options: no-sniff');
echo $content; echo $content;
@ -335,7 +336,7 @@ class JSMin {
*/ */
public static function send304() public static function send304()
{ {
header("status: 304 Not Modified", true, 304); header('status: 304 Not Modified', true, 304);
} }
} }

View File

@ -39,8 +39,17 @@
data, data,
dataType: 'json', dataType: 'json',
type: 'POST', type: 'POST',
success: () => { success: (res) => {
if (data.data.status === 'completed') { const resData = JSON.parse(res);
if (resData.errors) {
_.hide(_.$('#loading-shadow')[ 0 ]);
_.showMessage('error', `Failed to update ${title}. `);
_.scrollToTop();
return;
}
if (resData.data.attributes.status === 'completed') {
_.hide(parentSel); _.hide(parentSel);
} }

View File

@ -273,7 +273,7 @@ var AnimeClient = (function(w) {
responseText = request.responseText; responseText = request.responseText;
} }
if (request.status > 400) { if (request.status > 299) {
config.error.call(null, request.status, responseText, request.response); config.error.call(null, request.status, responseText, request.response);
} else { } else {
config.success.call(null, responseText, request.status); config.success.call(null, responseText, request.status);

View File

@ -1,16 +1,15 @@
const LightTableSorter = (function() { 'use strict';
let _cellIndex, _onClickEvent, _order, _reset, _sort, _text, _th, _toggle; const LightTableSorter = (() => {
_th = null; let th = null;
_cellIndex = null; let cellIndex = null;
_order = ''; let order = '';
_text = function(row) { const text = (row) => {
return row.cells.item(_cellIndex).textContent.toLowerCase(); return row.cells.item(cellIndex).textContent.toLowerCase();
}; };
_sort = function(a, b) { const sort = (a, b) => {
let n, textA, textB; let textA = text(a);
textA = _text(a); let textB = text(b);
textB = _text(b); const n = parseInt(textA, 10);
n = parseInt(textA, 10);
if (n) { if (n) {
textA = n; textA = n;
textB = parseInt(textB, 10); textB = parseInt(textB, 10);
@ -23,51 +22,49 @@ const LightTableSorter = (function() {
} }
return 0; return 0;
}; };
_toggle = function() { const toggle = () => {
let c; const c = order !== 'sorting_asc' ? 'sorting_asc' : 'sorting_desc';
c = _order !== 'sorting_asc' ? 'sorting_asc' : 'sorting_desc'; th.className = (th.className.replace(order, '') + ' ' + c).trim();
_th.className = (_th.className.replace(_order, '') + ' ' + c).trim(); return order = c;
return _order = c;
}; };
_reset = function() { const reset = () => {
_th.className = _th.className.replace('sorting_asc', 'sorting').replace('sorting_desc', 'sorting'); th.classList.remove('sorting_asc', 'sorting_desc');
return _order = ''; th.classList.add('sorting');
return order = '';
}; };
_onClickEvent = function(e) { const onClickEvent = (e) => {
let row, rows, tbody, _i, _len; if (th && (cellIndex !== e.target.cellIndex)) {
if (_th && (_cellIndex !== e.target.cellIndex)) { reset();
_reset();
} }
_th = e.target; th = e.target;
if (_th.nodeName.toLowerCase() === 'th') { if (th.nodeName.toLowerCase() === 'th') {
_cellIndex = _th.cellIndex; cellIndex = th.cellIndex;
tbody = _th.offsetParent.getElementsByTagName('tbody')[0]; const tbody = th.offsetParent.getElementsByTagName('tbody')[0];
rows = tbody.rows; let rows = Array.from(tbody.rows);
if (rows) { if (rows) {
rows = Array.prototype.slice.call(rows, 0); rows.sort(sort);
rows = Array.prototype.sort.call(rows, _sort); if (order === 'sorting_asc') {
if (_order === 'sorting_asc') { rows.reverse();
Array.prototype.reverse.call(rows);
} }
_toggle(); toggle();
tbody.innerHtml = ''; tbody.innerHtml = '';
for (_i = 0, _len = rows.length; _i < _len; _i++) {
row = rows[_i]; rows.forEach(row => {
tbody.appendChild(row); tbody.appendChild(row);
} });
} }
} }
}; };
return { return {
init: function() { init: () => {
let ths = document.getElementsByTagName('th'); let ths = document.getElementsByTagName('th');
let _results = []; let results = [];
for (let _i = 0, _len = ths.length; _i < _len; _i++) { for (let i = 0, len = ths.length; i < len; i++) {
let th = ths[_i]; let th = ths[i];
th.className = 'sorting'; th.classList.add('sorting');
_results.push(th.onclick = _onClickEvent); results.push(th.onclick = onClickEvent);
} }
return _results; return results;
} }
}; };
})(); })();

0
public/js/cache/.gitkeep vendored Normal file → Executable file
View File

View File

@ -4,10 +4,10 @@
"watch": "watch 'npm run build' --filter=./cssfilter.js" "watch": "watch 'npm run build' --filter=./cssfilter.js"
}, },
"devDependencies": { "devDependencies": {
"cssnano": "^3.10.0", "cssnano": "^4.0.5",
"postcss-cachify": "^1.3.1", "postcss-cachify": "^1.3.1",
"postcss-cssnext": "^3.0.0", "postcss-cssnext": "^3.0.0",
"postcss-import": "^10.0.0", "postcss-import": "^12.0.0",
"watch": "^1.0.2" "watch": "^1.0.2"
} }
} }

File diff suppressed because it is too large Load Diff

73
src/API/Anilist.php Normal file
View File

@ -0,0 +1,73 @@
<?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 - 2018 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.0
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\API;
use Aviat\AnimeClient\API\Enum\{
AnimeWatchingStatus\Kitsu as KAWS,
MangaReadingStatus\Kitsu as KMRS
};
use Aviat\AnimeClient\API\Enum\{
AnimeWatchingStatus\Anilist as AnimeWatchingStatus,
MangaReadingStatus\Anilist as MangaReadingStatus
};
/**
* Constants and mappings for the Anilist API
*/
final class Anilist {
const AUTH_URL = 'https://anilist.co/api/v2/oauth/authorize';
const BASE_URL = 'https://graphql.anilist.co';
const KITSU_ANILIST_WATCHING_STATUS_MAP = [
KAWS::WATCHING => AnimeWatchingStatus::WATCHING,
KAWS::COMPLETED => AnimeWatchingStatus::COMPLETED,
KAWS::ON_HOLD => AnimeWatchingStatus::ON_HOLD,
KAWS::DROPPED => AnimeWatchingStatus::DROPPED,
KAWS::PLAN_TO_WATCH => AnimeWatchingStatus::PLAN_TO_WATCH,
];
const ANILIST_KITSU_WATCHING_STATUS_MAP = [
'CURRENT' => KAWS::WATCHING,
'COMPLETED' => KAWS::COMPLETED,
'PAUSED' => KAWS::ON_HOLD,
'DROPPED' => KAWS::DROPPED,
'PLANNING' => KAWS::PLAN_TO_WATCH,
];
public static function getIdToWatchingStatusMap()
{
return [
'CURRENT' => AnimeWatchingStatus::WATCHING,
'COMPLETED' => AnimeWatchingStatus::COMPLETED,
'PAUSED' => AnimeWatchingStatus::ON_HOLD,
'DROPPED' => AnimeWatchingStatus::DROPPED,
'PLANNING' => AnimeWatchingStatus::PLAN_TO_WATCH,
'REPEATING' => AnimeWatchingStatus::WATCHING,
];
}
public static function getIdToReadingStatusMap()
{
return [
'CURRENT' => MangaReadingStatus::READING,
'COMPLETED' => MangaReadingStatus::COMPLETED,
'PAUSED' => MangaReadingStatus::ON_HOLD,
'DROPPED' => MangaReadingStatus::DROPPED,
'PLANNING' => MangaReadingStatus::PLAN_TO_READ
];
}
}

View File

@ -0,0 +1,49 @@
<?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 - 2018 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.0
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\API\Anilist;
use const Aviat\AnimeClient\USER_AGENT;
use Aviat\AnimeClient\API\APIRequestBuilder;
final class AnilistRequestBuilder extends APIRequestBuilder {
/**
* The base url for api requests
* @var string $base_url
*/
protected $baseUrl = 'https://kitsu.io/api/edge/';
/**
* Valid HTTP request methods
* @var array
*/
protected $validMethods = ['POST'];
/**
* HTTP headers to send with every request
*
* @var array
*/
protected $defaultHeaders = [
'User-Agent' => USER_AGENT,
'Accept' => 'application/vnd.api+json',
'Content-Type' => 'application/vnd.api+json',
'CLIENT_ID' => 'dd031b32d2f56c990b1425efe6c42ad847e7fe3ab46bf1299f05ecd856bdb7dd',
'CLIENT_SECRET' => '54d7307928f63414defd96399fc31ba847961ceaecef3a5fd93144e960c0e151',
];
}

View File

@ -0,0 +1,179 @@
<?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 - 2018 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.0
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\API\MAL;
use function Amp\Promise\wait;
use Aviat\AnimeClient\API\{
Anilist,
HummingbirdClient
};
trait AnilistTrait {
/**
* The request builder for the MAL API
* @var AnilistRequestBuilder
*/
protected $requestBuilder;
/**
* The base url for api requests
* @var string $base_url
*/
protected $baseUrl = Anilist::BASE_URL;
/**
* HTTP headers to send with every request
*
* @var array
*/
protected $defaultHeaders = [
'Accept' => 'application/json',
'Accept-Encoding' => 'gzip',
'Content-type' => 'application/json',
'User-Agent' => "Tim's Anime Client/4.0"
];
/**
* Set the request builder object
*
* @param MALRequestBuilder $requestBuilder
* @return self
*/
public function setRequestBuilder($requestBuilder): self
{
$this->requestBuilder = $requestBuilder;
return $this;
}
/**
* Create a request object
*
* @param string $type
* @param string $url
* @param array $options
* @return \Amp\Artax\Response
*/
public function setUpRequest(string $type, string $url, array $options = [])
{
$config = $this->container->get('config');
$request = $this->requestBuilder
->newRequest($type, $url)
->setBasicAuth($config->get(['mal','username']), $config->get(['mal','password']));
if (array_key_exists('query', $options))
{
$request = $request->setQuery($options['query']);
}
if (array_key_exists('body', $options))
{
$request = $request->setBody($options['body']);
}
return $request->getFullRequest();
}
/**
* Make a request
*
* @param string $type
* @param string $url
* @param array $options
* @return \Amp\Artax\Response
*/
private function getResponse(string $type, string $url, array $options = [])
{
$logger = NULL;
if ($this->getContainer())
{
$logger = $this->container->getLogger('mal-request');
}
$request = $this->setUpRequest($type, $url, $options);
$response = wait((new HummingbirdClient)->request($request));
$logger->debug('MAL api response', [
'status' => $response->getStatus(),
'reason' => $response->getReason(),
'body' => $response->getBody(),
'headers' => $response->getHeaders(),
'requestHeaders' => $request->getHeaders(),
]);
return $response;
}
/**
* Make a request
*
* @param string $type
* @param string $url
* @param array $options
* @return array
*/
private function request(string $type, string $url, array $options = []): array
{
$logger = NULL;
if ($this->getContainer())
{
$logger = $this->container->getLogger('anilist-request');
}
$response = $this->getResponse($type, $url, $options);
if ((int) $response->getStatus() > 299 OR (int) $response->getStatus() < 200)
{
if ($logger)
{
$logger->warning('Non 200 response for api call', (array)$response->getBody());
}
}
return XML::toArray(wait($response->getBody()));
}
/**
* Remove some boilerplate for post requests
*
* @param mixed ...$args
* @return array
*/
protected function postRequest(...$args): array
{
$logger = NULL;
if ($this->getContainer())
{
$logger = $this->container->getLogger('anilist-request');
}
$response = $this->getResponse('POST', ...$args);
$validResponseCodes = [200, 201];
if ( ! \in_array((int) $response->getStatus(), $validResponseCodes, TRUE))
{
if ($logger)
{
$logger->warning('Non 201 response for POST api call', (array)$response->getBody());
}
}
return XML::toArray($response->getBody());
}
}

View File

@ -0,0 +1,109 @@
<?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 - 2018 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.0
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\API\Anilist;
use Amp\Artax\{FormBody, Request};
use Aviat\AnimeClient\API\{
XML
};
use Aviat\AnimeClient\Types\AbstractType;
use Aviat\Ion\Di\ContainerAware;
/**
* CRUD operations for MAL list items
*/
final class ListItem {
use ContainerAware;
use AnilistTrait;
/**
* Create a list item
*
* @param array $data
* @param string $type
* @return Request
*/
public function create(array $data, string $type = 'anime'): Request
{
$id = $data['id'];
$createData = [
'id' => $id,
'data' => XML::toXML([
'entry' => $data['data']
])
];
$config = $this->container->get('config');
return $this->requestBuilder->newRequest('POST', "{$type}list/add/{$id}.xml")
->setFormFields($createData)
->setBasicAuth($config->get(['mal','username']), $config->get(['mal', 'password']))
->getFullRequest();
}
/**
* Delete a list item
*
* @param string $id
* @param string $type
* @return Request
*/
public function delete(string $id, string $type = 'anime'): Request
{
$config = $this->container->get('config');
return $this->requestBuilder->newRequest('DELETE', "{$type}list/delete/{$id}.xml")
->setFormFields([
'id' => $id
])
->setBasicAuth($config->get(['mal','username']), $config->get(['mal', 'password']))
->getFullRequest();
// return $response->getBody() === 'Deleted'
}
public function get(string $id): array
{
return [];
}
/**
* Update a list item
*
* @param string $id
* @param AbstractType $data
* @param string $type
* @return Request
*/
public function update(string $id, AbstractType $data, string $type = 'anime'): Request
{
$config = $this->container->get('config');
$xml = XML::toXML(['entry' => $data]);
$body = new FormBody();
$body->addField('id', $id);
$body->addField('data', $xml);
return $this->requestBuilder->newRequest('POST', "{$type}list/update/{$id}.xml")
->setFormFields([
'id' => $id,
'data' => $xml
])
->setBasicAuth($config->get(['mal','username']), $config->get(['mal', 'password']))
->getFullRequest();
}
}

23
src/API/Anilist/Model.php Normal file
View File

@ -0,0 +1,23 @@
<?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 - 2018 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.0
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\API\Anilist;
/**
* Anilist API Model
*/
final class Model {
}

View File

@ -0,0 +1,31 @@
<?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 - 2018 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.0
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\API\Enum\AnimeWatchingStatus;
use Aviat\Ion\Enum;
/**
* Possible values for watching status for the current anime
*/
final class Anilist extends Enum {
const WATCHING = 'CURRENT';
const COMPLETED = 'COMPLETED';
const ON_HOLD = 'PAUSED';
const DROPPED = 'DROPPED';
const PLAN_TO_WATCH = 'PLANNING';
const REPEATING = 'REPEATING';
}

View File

@ -21,7 +21,7 @@ use Aviat\Ion\Enum;
/** /**
* Possible values for watching status for the current anime * Possible values for watching status for the current anime
*/ */
class Kitsu extends Enum { final class Kitsu extends Enum {
const WATCHING = 'current'; const WATCHING = 'current';
const PLAN_TO_WATCH = 'planned'; const PLAN_TO_WATCH = 'planned';
const ON_HOLD = 'on_hold'; const ON_HOLD = 'on_hold';

View File

@ -21,7 +21,7 @@ use Aviat\Ion\Enum;
/** /**
* Possible values for watching status for the current anime * Possible values for watching status for the current anime
*/ */
class MAL extends Enum { final class MAL extends Enum {
const WATCHING = 1; const WATCHING = 1;
const COMPLETED = 2; const COMPLETED = 2;
const ON_HOLD = 3; const ON_HOLD = 3;

View File

@ -21,7 +21,7 @@ use Aviat\Ion\Enum as Enum;
/** /**
* Possible values for current watching status of anime * Possible values for current watching status of anime
*/ */
class Route extends Enum { final class Route extends Enum {
const ALL = 'all'; const ALL = 'all';
const WATCHING = 'watching'; const WATCHING = 'watching';
const PLAN_TO_WATCH = 'plan_to_watch'; const PLAN_TO_WATCH = 'plan_to_watch';

View File

@ -21,7 +21,7 @@ use Aviat\Ion\Enum as Enum;
/** /**
* Possible values for current watching status of anime * Possible values for current watching status of anime
*/ */
class Title extends Enum { final class Title extends Enum {
const ALL = 'All'; const ALL = 'All';
const WATCHING = 'Currently Watching'; const WATCHING = 'Currently Watching';
const PLAN_TO_WATCH = 'Plan to Watch'; const PLAN_TO_WATCH = 'Plan to Watch';

View File

@ -0,0 +1,31 @@
<?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 - 2018 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.0
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\API\Enum\MangaReadingStatus;
use Aviat\Ion\Enum;
/**
* Possible values for watching status for the current anime
*/
final class Anilist extends Enum {
const WATCHING = 'CURRENT';
const COMPLETED = 'COMPLETED';
const ON_HOLD = 'PAUSED';
const DROPPED = 'DROPPED';
const PLAN_TO_WATCH = 'PLANNING';
const REPEATING = 'REPEATING';
}

View File

@ -21,7 +21,7 @@ use Aviat\Ion\Enum;
/** /**
* Possible values for current reading status of manga * Possible values for current reading status of manga
*/ */
class Kitsu extends Enum { final class Kitsu extends Enum {
const READING = 'current'; const READING = 'current';
const PLAN_TO_READ = 'planned'; const PLAN_TO_READ = 'planned';
const DROPPED = 'dropped'; const DROPPED = 'dropped';

View File

@ -21,7 +21,7 @@ use Aviat\Ion\Enum;
/** /**
* Possible values for watching status for the current anime * Possible values for watching status for the current anime
*/ */
class MAL extends Enum { final class MAL extends Enum {
const READING = 'reading'; const READING = 'reading';
const COMPLETED = 'completed'; const COMPLETED = 'completed';
const ON_HOLD = 'onhold'; const ON_HOLD = 'onhold';

View File

@ -16,12 +16,12 @@
namespace Aviat\AnimeClient\API\Enum\MangaReadingStatus; namespace Aviat\AnimeClient\API\Enum\MangaReadingStatus;
use Aviat\Ion\Enum as Enum; use Aviat\Ion\Enum;
/** /**
* Possible values for current reading status of manga * Possible values for current reading status of manga
*/ */
class Route extends Enum { final class Route extends Enum {
const ALL = 'all'; const ALL = 'all';
const READING = 'reading'; const READING = 'reading';
const PLAN_TO_READ = 'plan_to_read'; const PLAN_TO_READ = 'plan_to_read';

View File

@ -16,12 +16,12 @@
namespace Aviat\AnimeClient\API\Enum\MangaReadingStatus; namespace Aviat\AnimeClient\API\Enum\MangaReadingStatus;
use Aviat\Ion\Enum as Enum; use Aviat\Ion\Enum;
/** /**
* Possible values for current reading status of manga * Possible values for current reading status of manga
*/ */
class Title extends Enum { final class Title extends Enum {
const ALL = 'All'; const ALL = 'All';
const READING = 'Currently Reading'; const READING = 'Currently Reading';
const PLAN_TO_READ = 'Plan to Read'; const PLAN_TO_READ = 'Plan to Read';

View File

@ -19,7 +19,7 @@ namespace Aviat\AnimeClient\API;
/** /**
* Class encapsulating Json API data structure for a request or response * Class encapsulating Json API data structure for a request or response
*/ */
class JsonAPI { final class JsonAPI {
/** /**
* The full data array * The full data array

View File

@ -22,7 +22,7 @@ use DateTimeImmutable;
/** /**
* Data massaging helpers for the Kitsu API * Data massaging helpers for the Kitsu API
*/ */
class Kitsu { final class Kitsu {
const AUTH_URL = 'https://kitsu.io/api/oauth/token'; const AUTH_URL = 'https://kitsu.io/api/oauth/token';
const AUTH_USER_ID_KEY = 'kitsu-auth-userid'; const AUTH_USER_ID_KEY = 'kitsu-auth-userid';
const AUTH_TOKEN_CACHE_KEY = 'kitsu-auth-token'; const AUTH_TOKEN_CACHE_KEY = 'kitsu-auth-token';
@ -143,7 +143,10 @@ class Kitsu {
*/ */
public static function parseStreamingLinks(array $included): array public static function parseStreamingLinks(array $included): array
{ {
if ( ! array_key_exists('streamingLinks', $included)) if (
( ! array_key_exists('streamingLinks', $included)) ||
count($included['streamingLinks']) === 0
)
{ {
return []; return [];
} }
@ -152,7 +155,16 @@ class Kitsu {
foreach ($included['streamingLinks'] as $streamingLink) foreach ($included['streamingLinks'] as $streamingLink)
{ {
$host = parse_url($streamingLink['url'], \PHP_URL_HOST); $url = $streamingLink['url'];
// 'Fix' links that start with the hostname,
// rather than a protocol
if (strpos($url, '//') === FALSE)
{
$url = '//' . $url;
}
$host = parse_url($url, \PHP_URL_HOST);
$links[] = [ $links[] = [
'meta' => static::getServiceMetaData($host), 'meta' => static::getServiceMetaData($host),
@ -182,17 +194,7 @@ class Kitsu {
if (count($anime['relationships']['streamingLinks']) > 0) if (count($anime['relationships']['streamingLinks']) > 0)
{ {
foreach ($anime['relationships']['streamingLinks'] as $streamingLink) return static::parseStreamingLinks($anime['relationships']);
{
$host = parse_url($streamingLink['url'], \PHP_URL_HOST);
$links[] = [
'meta' => static::getServiceMetaData($host),
'link' => $streamingLink['url'],
'subs' => $streamingLink['subs'],
'dubs' => $streamingLink['dubs']
];
}
} }
return $links; return $links;
@ -243,10 +245,9 @@ class Kitsu {
foreach($existingTitles as $existing) foreach($existingTitles as $existing)
{ {
$isSubset = mb_substr_count($existing, $title) > 0; $isSubset = mb_substr_count($existing, $title) > 0;
$diff = levenshtein($existing, $title); $diff = levenshtein(mb_strtolower($existing), mb_strtolower($title));
$onlydifferentCase = (mb_strtolower($existing) === mb_strtolower($title));
if ($diff <= 3 OR $isSubset OR $onlydifferentCase OR mb_strlen($title) > 55 OR mb_strlen($existing) > 60) if ($diff <= 4 || $isSubset || mb_strlen($title) > 45 || mb_strlen($existing) > 50)
{ {
return FALSE; return FALSE;
} }

View File

@ -28,7 +28,7 @@ use Exception;
/** /**
* Kitsu API Authentication * Kitsu API Authentication
*/ */
class Auth { final class Auth {
use CacheTrait; use CacheTrait;
use ContainerAware; use ContainerAware;
@ -37,14 +37,14 @@ class Auth {
* *
* @var Model * @var Model
*/ */
protected $model; private $model;
/** /**
* Session object * Session object
* *
* @var \Aura\Session\Segment * @var \Aura\Session\Segment
*/ */
protected $segment; private $segment;
/** /**
* Constructor * Constructor

View File

@ -21,7 +21,7 @@ use Aviat\Ion\Enum as BaseEnum;
/** /**
* Status of when anime is being/was/will be aired * Status of when anime is being/was/will be aired
*/ */
class AnimeAiringStatus extends BaseEnum { final class AnimeAiringStatus extends BaseEnum {
const NOT_YET_AIRED = 'Not Yet Aired'; const NOT_YET_AIRED = 'Not Yet Aired';
const AIRING = 'Currently Airing'; const AIRING = 'Currently Airing';
const FINISHED_AIRING = 'Finished Airing'; const FINISHED_AIRING = 'Finished Airing';

View File

@ -18,7 +18,7 @@ namespace Aviat\AnimeClient\API\Kitsu;
use Aviat\AnimeClient\API\APIRequestBuilder; use Aviat\AnimeClient\API\APIRequestBuilder;
class KitsuRequestBuilder extends APIRequestBuilder { final class KitsuRequestBuilder extends APIRequestBuilder {
/** /**
* The base url for api requests * The base url for api requests

View File

@ -25,39 +25,17 @@ use Aviat\AnimeClient\API\{
HummingbirdClient, HummingbirdClient,
ListItemInterface ListItemInterface
}; };
use Aviat\AnimeClient\Types\FormItemData;
use Aviat\Ion\Di\ContainerAware; use Aviat\Ion\Di\ContainerAware;
use Aviat\Ion\Json; use Aviat\Ion\Json;
/** /**
* CRUD operations for Kitsu list items * CRUD operations for Kitsu list items
*/ */
class ListItem implements ListItemInterface { final class ListItem implements ListItemInterface {
use ContainerAware; use ContainerAware;
use KitsuTrait; use KitsuTrait;
private function getAuthHeader()
{
$cache = $this->getContainer()->get('cache');
$cacheItem = $cache->getItem('kitsu-auth-token');
$sessionSegment = $this->getContainer()
->get('session')
->getSegment(SESSION_SEGMENT);
if ($sessionSegment->get('auth_token') !== NULL)
{
$token = $sessionSegment->get('auth_token');
return "bearer {$token}";
}
if ($cacheItem->isHit())
{
$token = $cacheItem->get();
return "bearer {$token}";
}
return FALSE;
}
public function create(array $data): Request public function create(array $data): Request
{ {
$body = [ $body = [
@ -134,7 +112,7 @@ class ListItem implements ListItemInterface {
return Json::decode(wait($response->getBody())); return Json::decode(wait($response->getBody()));
} }
public function update(string $id, array $data): Request public function update(string $id, FormItemData $data): Request
{ {
$authHeader = $this->getAuthHeader(); $authHeader = $this->getAuthHeader();
$requestData = [ $requestData = [
@ -155,4 +133,25 @@ class ListItem implements ListItemInterface {
return $request->getFullRequest(); return $request->getFullRequest();
} }
private function getAuthHeader()
{
$cache = $this->getContainer()->get('cache');
$cacheItem = $cache->getItem('kitsu-auth-token');
$sessionSegment = $this->getContainer()
->get('session')
->getSegment(SESSION_SEGMENT);
if ($sessionSegment->get('auth_token') !== NULL) {
$token = $sessionSegment->get('auth_token');
return "bearer {$token}";
}
if ($cacheItem->isHit()) {
$token = $cacheItem->get();
return "bearer {$token}";
}
return FALSE;
}
} }

View File

@ -26,7 +26,6 @@ use Aviat\AnimeClient\API\{
ParallelAPIRequest ParallelAPIRequest
}; };
use Aviat\AnimeClient\API\Enum\{ use Aviat\AnimeClient\API\Enum\{
AnimeWatchingStatus\Title,
AnimeWatchingStatus\Kitsu as KitsuWatchingStatus, AnimeWatchingStatus\Kitsu as KitsuWatchingStatus,
MangaReadingStatus\Kitsu as KitsuReadingStatus MangaReadingStatus\Kitsu as KitsuReadingStatus
}; };
@ -37,17 +36,25 @@ use Aviat\AnimeClient\API\Kitsu\Transformer\{
MangaTransformer, MangaTransformer,
MangaListTransformer MangaListTransformer
}; };
use Aviat\AnimeClient\Types\{
AbstractType,
Anime,
FormItem,
FormItemData,
AnimeListItem,
MangaPage
};
use Aviat\Ion\{Di\ContainerAware, Json}; use Aviat\Ion\{Di\ContainerAware, Json};
/** /**
* Kitsu API Model * Kitsu API Model
*/ */
class Model { final class Model {
use CacheTrait; use CacheTrait;
use ContainerAware; use ContainerAware;
use KitsuTrait; use KitsuTrait;
const LIST_PAGE_SIZE = 100; private const LIST_PAGE_SIZE = 100;
/** /**
* Class to map anime list items * Class to map anime list items
@ -56,27 +63,27 @@ class Model {
* *
* @var AnimeListTransformer * @var AnimeListTransformer
*/ */
protected $animeListTransformer; private $animeListTransformer;
/** /**
* @var AnimeTransformer * @var AnimeTransformer
*/ */
protected $animeTransformer; private $animeTransformer;
/** /**
* @var ListItem * @var ListItem
*/ */
protected $listItem; private $listItem;
/** /**
* @var MangaTransformer * @var MangaTransformer
*/ */
protected $mangaTransformer; private $mangaTransformer;
/** /**
* @var MangaListTransformer * @var MangaListTransformer
*/ */
protected $mangaListTransformer; private $mangaListTransformer;
/** /**
* Constructor * Constructor
@ -314,15 +321,15 @@ class Model {
* Get information about a particular anime * Get information about a particular anime
* *
* @param string $slug * @param string $slug
* @return array * @return Anime
*/ */
public function getAnime(string $slug): array public function getAnime(string $slug): Anime
{ {
$baseData = $this->getRawMediaData('anime', $slug); $baseData = $this->getRawMediaData('anime', $slug);
if (empty($baseData)) if (empty($baseData))
{ {
return []; return new Anime();
} }
$transformed = $this->animeTransformer->transform($baseData); $transformed = $this->animeTransformer->transform($baseData);
@ -417,7 +424,7 @@ class Model {
* @param array $options * @param array $options
* @return array * @return array
*/ */
public function getFullAnimeList(array $options = [ public function getFullRawAnimeList(array $options = [
'include' => 'anime.mappings' 'include' => 'anime.mappings'
]): array ]): array
{ {
@ -441,10 +448,10 @@ class Model {
foreach($responses as $response) foreach($responses as $response)
{ {
$data = Json::decode($response); $data = Json::decode($response);
$output = array_merge_recursive($output, $data); $output[] = $data;
} }
return $output; return array_merge_recursive(...$output);
} }
/** /**
@ -547,7 +554,7 @@ class Model {
'sort' => '-updated_at' 'sort' => '-updated_at'
]; ];
return $this->getFullAnimeList($options); return $this->getFullRawAnimeList($options);
} }
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
@ -558,15 +565,15 @@ class Model {
* Get information about a particular manga * Get information about a particular manga
* *
* @param string $slug * @param string $slug
* @return array * @return MangaPage
*/ */
public function getManga(string $slug): array public function getManga(string $slug): MangaPage
{ {
$baseData = $this->getRawMediaData('manga', $slug); $baseData = $this->getRawMediaData('manga', $slug);
if (empty($baseData)) if (empty($baseData))
{ {
return []; return new MangaPage([]);
} }
$transformed = $this->mangaTransformer->transform($baseData); $transformed = $this->mangaTransformer->transform($baseData);
@ -580,7 +587,7 @@ class Model {
* @param string $mangaId * @param string $mangaId
* @return array * @return array
*/ */
public function getMangaById(string $mangaId): array public function getMangaById(string $mangaId): MangaPage
{ {
$baseData = $this->getRawMediaDataById('manga', $mangaId); $baseData = $this->getRawMediaDataById('manga', $mangaId);
return $this->mangaTransformer->transform($baseData); return $this->mangaTransformer->transform($baseData);
@ -680,7 +687,7 @@ class Model {
* @param array $options * @param array $options
* @return array * @return array
*/ */
public function getFullMangaList(array $options = [ public function getFullRawMangaList(array $options = [
'include' => 'manga.mappings' 'include' => 'manga.mappings'
]): array ]): array
{ {
@ -704,10 +711,10 @@ class Model {
foreach($responses as $response) foreach($responses as $response)
{ {
$data = Json::decode($response); $data = Json::decode($response);
$output = array_merge_recursive($output, $data); $output[] = $data;
} }
return $output; return array_merge_recursive(...$output);
} }
/** /**
@ -804,9 +811,9 @@ class Model {
* Get the data for a specific list item, generally for editing * Get the data for a specific list item, generally for editing
* *
* @param string $listId - The unique identifier of that list item * @param string $listId - The unique identifier of that list item
* @return array * @return mixed
*/ */
public function getListItem(string $listId): array public function getListItem(string $listId)
{ {
$baseData = $this->listItem->get($listId); $baseData = $this->listItem->get($listId);
$included = JsonAPI::organizeIncludes($baseData['included']); $included = JsonAPI::organizeIncludes($baseData['included']);
@ -814,12 +821,12 @@ class Model {
switch (TRUE) switch (TRUE)
{ {
case in_array('anime', array_keys($included)): case array_key_exists('anime', $included): // in_array('anime', array_keys($included)):
$included = JsonAPI::inlineIncludedRelationships($included, 'anime'); $included = JsonAPI::inlineIncludedRelationships($included, 'anime');
$baseData['data']['included'] = $included; $baseData['data']['included'] = $included;
return $this->animeListTransformer->transform($baseData['data']); return $this->animeListTransformer->transform($baseData['data']);
case in_array('manga', array_keys($included)): case array_key_exists('manga', $included): // in_array('manga', array_keys($included)):
$included = JsonAPI::inlineIncludedRelationships($included, 'manga'); $included = JsonAPI::inlineIncludedRelationships($included, 'manga');
$baseData['data']['included'] = $included; $baseData['data']['included'] = $included;
$baseData['data']['manga'] = $baseData['included'][0]; $baseData['data']['manga'] = $baseData['included'][0];
@ -833,10 +840,10 @@ class Model {
/** /**
* Modify a list item * Modify a list item
* *
* @param array $data * @param FormItem $data
* @return Request * @return Request
*/ */
public function updateListItem(array $data): Request public function updateListItem(FormItem $data): Request
{ {
return $this->listItem->update($data['id'], $data['data']); return $this->listItem->update($data['id'], $data['data']);
} }

View File

@ -17,21 +17,27 @@
namespace Aviat\AnimeClient\API\Kitsu\Transformer; namespace Aviat\AnimeClient\API\Kitsu\Transformer;
use Aviat\AnimeClient\API\Kitsu; use Aviat\AnimeClient\API\Kitsu;
use Aviat\AnimeClient\Types\{
Anime,
AnimeFormItem,
AnimeFormItemData,
AnimeListItem
};
use Aviat\Ion\Transformer\AbstractTransformer; use Aviat\Ion\Transformer\AbstractTransformer;
/** /**
* Transformer for anime list * Transformer for anime list
*/ */
class AnimeListTransformer extends AbstractTransformer { final class AnimeListTransformer extends AbstractTransformer {
/** /**
* Convert raw api response to a more * Convert raw api response to a more
* logical and workable structure * logical and workable structure
* *
* @param array $item API library item * @param array $item API library item
* @return array * @return AnimeListItem
*/ */
public function transform($item): array public function transform($item): AnimeListItem
{ {
$included = $item['included']; $included = $item['included'];
$animeId = $item['relationships']['media']['data']['id']; $animeId = $item['relationships']['media']['data']['id'];
@ -66,7 +72,10 @@ class AnimeListTransformer extends AbstractTransformer {
? Kitsu::parseListItemStreamingLinks($included, $animeId) ? Kitsu::parseListItemStreamingLinks($included, $animeId)
: []; : [];
return [ $titles = Kitsu::filterTitles($anime);
$title = array_shift($titles);
return new AnimeListItem([
'id' => $item['id'], 'id' => $item['id'],
'mal_id' => $MALid, 'mal_id' => $MALid,
'episodes' => [ 'episodes' => [
@ -81,24 +90,24 @@ class AnimeListTransformer extends AbstractTransformer {
'started' => $anime['startDate'], 'started' => $anime['startDate'],
'ended' => $anime['endDate'] 'ended' => $anime['endDate']
], ],
'anime' => [ 'anime' => new Anime([
'id' => $animeId, 'id' => $animeId,
'age_rating' => $anime['ageRating'], 'age_rating' => $anime['ageRating'],
'title' => $anime['canonicalTitle'], 'title' => $title,
'titles' => Kitsu::filterTitles($anime), 'titles' => $titles,
'slug' => $anime['slug'], 'slug' => $anime['slug'],
'type' => $this->string($anime['showType'])->upperCaseFirst()->__toString(), 'show_type' => $this->string($anime['showType'])->upperCaseFirst()->__toString(),
'image' => $anime['posterImage']['small'], 'cover_image' => $anime['posterImage']['small'],
'genres' => $genres, 'genres' => $genres,
'streaming_links' => $streamingLinks, 'streaming_links' => $streamingLinks,
], ]),
'watching_status' => $item['attributes']['status'], 'watching_status' => $item['attributes']['status'],
'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, 'user_rating' => $rating,
'private' => $item['attributes']['private'] ?? FALSE, 'private' => $item['attributes']['private'] ?? FALSE,
]; ]);
} }
/** /**
@ -106,14 +115,14 @@ class AnimeListTransformer extends AbstractTransformer {
* api response format * api response format
* *
* @param array $item Transformed library item * @param array $item Transformed library item
* @return array API library item * @return AnimeFormItem API library item
*/ */
public function untransform($item): array public function untransform($item): AnimeFormItem
{ {
$privacy = (array_key_exists('private', $item) && $item['private']); $privacy = (array_key_exists('private', $item) && $item['private']);
$rewatching = (array_key_exists('rewatching', $item) && $item['rewatching']); $rewatching = (array_key_exists('rewatching', $item) && $item['rewatching']);
$untransformed = [ $untransformed = new AnimeFormItem([
'id' => $item['id'], 'id' => $item['id'],
'mal_id' => $item['mal_id'] ?? NULL, 'mal_id' => $item['mal_id'] ?? NULL,
'data' => [ 'data' => [
@ -123,7 +132,7 @@ class AnimeListTransformer extends AbstractTransformer {
'notes' => $item['notes'], 'notes' => $item['notes'],
'private' => $privacy 'private' => $privacy
] ]
]; ]);
if (is_numeric($item['episodes_watched']) && $item['episodes_watched'] > 0) if (is_numeric($item['episodes_watched']) && $item['episodes_watched'] > 0)
{ {

View File

@ -17,31 +17,32 @@
namespace Aviat\AnimeClient\API\Kitsu\Transformer; namespace Aviat\AnimeClient\API\Kitsu\Transformer;
use Aviat\AnimeClient\API\{JsonAPI, Kitsu}; use Aviat\AnimeClient\API\{JsonAPI, Kitsu};
use Aviat\AnimeClient\Types\Anime;
use Aviat\Ion\Transformer\AbstractTransformer; use Aviat\Ion\Transformer\AbstractTransformer;
/** /**
* Transformer for anime description page * Transformer for anime description page
*/ */
class AnimeTransformer extends AbstractTransformer { final class AnimeTransformer extends AbstractTransformer {
/** /**
* Convert raw api response to a more * Convert raw api response to a more
* logical and workable structure * logical and workable structure
* *
* @param array $item API library item * @param array $item API library item
* @return array * @return Anime
*/ */
public function transform($item): array public function transform($item): Anime
{ {
$item['included'] = JsonAPI::organizeIncludes($item['included']); $item['included'] = JsonAPI::organizeIncludes($item['included']);
$genres = $item['included']['categories'] ?? []; $genres = $item['included']['categories'] ?? [];
$item['genres'] = array_column($genres, 'title') ?? []; $item['genres'] = array_column($genres, 'title') ?? [];
sort($item['genres']); sort($item['genres']);
$titles = Kitsu::filterTitles($item); $title = $item['canonicalTitle'];
$titles = array_diff($item['titles'], [$title]);
return [ return new Anime([
'age_rating' => $item['ageRating'], 'age_rating' => $item['ageRating'],
'age_rating_guide' => $item['ageRatingGuide'], 'age_rating_guide' => $item['ageRatingGuide'],
'cover_image' => $item['posterImage']['small'], 'cover_image' => $item['posterImage']['small'],
@ -54,10 +55,10 @@ class AnimeTransformer extends AbstractTransformer {
'status' => Kitsu::getAiringStatus($item['startDate'], $item['endDate']), 'status' => Kitsu::getAiringStatus($item['startDate'], $item['endDate']),
'streaming_links' => Kitsu::parseStreamingLinks($item['included']), 'streaming_links' => Kitsu::parseStreamingLinks($item['included']),
'synopsis' => $item['synopsis'], 'synopsis' => $item['synopsis'],
'title' => $titles[0], 'title' => $title,
'titles' => $titles, 'titles' => $titles,
'trailer_id' => $item['youtubeVideoId'], 'trailer_id' => $item['youtubeVideoId'],
'url' => "https://kitsu.io/anime/{$item['slug']}", 'url' => "https://kitsu.io/anime/{$item['slug']}",
]; ]);
} }
} }

View File

@ -17,13 +17,17 @@
namespace Aviat\AnimeClient\API\Kitsu\Transformer; namespace Aviat\AnimeClient\API\Kitsu\Transformer;
use Aviat\AnimeClient\API\Kitsu; use Aviat\AnimeClient\API\Kitsu;
use Aviat\AnimeClient\Types\{
MangaFormItem, MangaFormItemData,
MangaListItem, MangaListItemDetail
};
use Aviat\Ion\StringWrapper; use Aviat\Ion\StringWrapper;
use Aviat\Ion\Transformer\AbstractTransformer; use Aviat\Ion\Transformer\AbstractTransformer;
/** /**
* Data transformation class for zippered Hummingbird manga * Data transformation class for zippered Hummingbird manga
*/ */
class MangaListTransformer extends AbstractTransformer { final class MangaListTransformer extends AbstractTransformer {
use StringWrapper; use StringWrapper;
@ -31,9 +35,9 @@ class MangaListTransformer extends AbstractTransformer {
* Remap zipped anime data to a more logical form * Remap zipped anime data to a more logical form
* *
* @param array $item manga entry item * @param array $item manga entry item
* @return array * @return MangaListItem
*/ */
public function transform($item): array public function transform($item): MangaListItem
{ {
$included = $item['included']; $included = $item['included'];
$mangaId = $item['relationships']['media']['data']['id']; $mangaId = $item['relationships']['media']['data']['id'];
@ -72,7 +76,10 @@ class MangaListTransformer extends AbstractTransformer {
} }
} }
$map = [ $titles = Kitsu::filterTitles($manga);
$title = array_shift($titles);
$map = new MangaListItem([
'id' => $item['id'], 'id' => $item['id'],
'mal_id' => $MALid, 'mal_id' => $MALid,
'chapters' => [ 'chapters' => [
@ -83,22 +90,22 @@ class MangaListTransformer extends AbstractTransformer {
'read' => '-', //$item['attributes']['volumes_read'], 'read' => '-', //$item['attributes']['volumes_read'],
'total' => $totalVolumes 'total' => $totalVolumes
], ],
'manga' => [ 'manga' => new MangaListItemDetail([
'id' => $mangaId,
'titles' => Kitsu::filterTitles($manga),
'alternate_title' => NULL,
'slug' => $manga['slug'],
'url' => 'https://kitsu.io/manga/' . $manga['slug'],
'type' => $manga['mangaType'],
'image' => $manga['posterImage']['small'],
'genres' => $genres, 'genres' => $genres,
], 'id' => $mangaId,
'image' => $manga['posterImage']['small'],
'slug' => $manga['slug'],
'title' => $title,
'titles' => $titles,
'type' => $manga['mangaType'],
'url' => 'https://kitsu.io/manga/' . $manga['slug'],
]),
'reading_status' => $item['attributes']['status'], 'reading_status' => $item['attributes']['status'],
'notes' => $item['attributes']['notes'], 'notes' => $item['attributes']['notes'],
'rereading' => (bool)$item['attributes']['reconsuming'], 'rereading' => (bool)$item['attributes']['reconsuming'],
'reread' => $item['attributes']['reconsumeCount'], 'reread' => $item['attributes']['reconsumeCount'],
'user_rating' => $rating, 'user_rating' => $rating,
]; ]);
return $map; return $map;
} }
@ -107,22 +114,22 @@ class MangaListTransformer extends AbstractTransformer {
* Untransform data to update the api * Untransform data to update the api
* *
* @param array $item * @param array $item
* @return array * @return MangaFormItem
*/ */
public function untransform($item): array public function untransform($item): MangaFormItem
{ {
$rereading = array_key_exists('rereading', $item) && (bool)$item['rereading']; $rereading = array_key_exists('rereading', $item) && (bool)$item['rereading'];
$map = [ $map = new MangaFormItem([
'id' => $item['id'], 'id' => $item['id'],
'mal_id' => $item['mal_id'], 'mal_id' => $item['mal_id'],
'data' => [ 'data' => new MangaFormItemData([
'status' => $item['status'], 'status' => $item['status'],
'reconsuming' => $rereading, 'reconsuming' => $rereading,
'reconsumeCount' => (int)$item['reread_count'], 'reconsumeCount' => (int)$item['reread_count'],
'notes' => $item['notes'], 'notes' => $item['notes'],
], ]),
]; ]);
if (is_numeric($item['chapters_read']) && $item['chapters_read'] > 0) if (is_numeric($item['chapters_read']) && $item['chapters_read'] > 0)
{ {

View File

@ -16,35 +16,37 @@
namespace Aviat\AnimeClient\API\Kitsu\Transformer; namespace Aviat\AnimeClient\API\Kitsu\Transformer;
use Aviat\AnimeClient\Types\MangaPage;
use Aviat\Ion\Transformer\AbstractTransformer; use Aviat\Ion\Transformer\AbstractTransformer;
/** /**
* Transformer for anime description page * Transformer for anime description page
*/ */
class MangaTransformer extends AbstractTransformer { final class MangaTransformer extends AbstractTransformer {
/** /**
* Convert raw api response to a more * Convert raw api response to a more
* logical and workable structure * logical and workable structure
* *
* @param array $item API library item * @param array $item API library item
* @return array * @return MangaPage
*/ */
public function transform($item) public function transform($item): MangaPage
{ {
// \dump($item);
$genres = []; $genres = [];
foreach($item['included'] as $included) foreach($item['included'] as $included)
{ {
if ($included['type'] === 'genres') if ($included['type'] === 'categories')
{ {
$genres[] = $included['attributes']['name']; $genres[] = $included['attributes']['title'];
} }
} }
sort($genres); sort($genres);
return [ return new MangaPage([
'id' => $item['id'], 'id' => $item['id'],
'title' => $item['canonicalTitle'], 'title' => $item['canonicalTitle'],
'en_title' => $item['titles']['en'], 'en_title' => $item['titles']['en'],
@ -56,7 +58,7 @@ class MangaTransformer extends AbstractTransformer {
'synopsis' => $item['synopsis'], 'synopsis' => $item['synopsis'],
'url' => "https://kitsu.io/manga/{$item['slug']}", 'url' => "https://kitsu.io/manga/{$item['slug']}",
'genres' => $genres, 'genres' => $genres,
]; ]);
} }
private function count(int $value = NULL) private function count(int $value = NULL)

View File

@ -17,6 +17,7 @@
namespace Aviat\AnimeClient\API; namespace Aviat\AnimeClient\API;
use Amp\Artax\Request; use Amp\Artax\Request;
use Aviat\AnimeClient\Types\FormItemData;
/** /**
* Common interface for anime and manga list item CRUD * Common interface for anime and manga list item CRUD
@ -43,10 +44,10 @@ interface ListItemInterface {
* Update a list item * Update a list item
* *
* @param string $id - The id of the list item to update * @param string $id - The id of the list item to update
* @param array $data - The data with which to update the list item * @param FormItemData $data - The data with which to update the list item
* @return Request * @return Request
*/ */
public function update(string $id, array $data): Request; public function update(string $id, FormItemData $data): Request;
/** /**
* Delete a list item * Delete a list item

View File

@ -28,7 +28,7 @@ use Aviat\AnimeClient\API\Enum\{
/** /**
* Constants and mappings for the My Anime List API * Constants and mappings for the My Anime List API
*/ */
class MAL { final class MAL {
const AUTH_URL = 'https://myanimelist.net/api/account/verify_credentials.xml'; const AUTH_URL = 'https://myanimelist.net/api/account/verify_credentials.xml';
const BASE_URL = 'https://myanimelist.net/api/'; const BASE_URL = 'https://myanimelist.net/api/';

View File

@ -20,12 +20,13 @@ use Amp\Artax\{FormBody, Request};
use Aviat\AnimeClient\API\{ use Aviat\AnimeClient\API\{
XML XML
}; };
use Aviat\AnimeClient\Types\AbstractType;
use Aviat\Ion\Di\ContainerAware; use Aviat\Ion\Di\ContainerAware;
/** /**
* CRUD operations for MAL list items * CRUD operations for MAL list items
*/ */
class ListItem { final class ListItem {
use ContainerAware; use ContainerAware;
use MALTrait; use MALTrait;
@ -84,11 +85,11 @@ class ListItem {
* Update a list item * Update a list item
* *
* @param string $id * @param string $id
* @param array $data * @param AbstractType $data
* @param string $type * @param string $type
* @return Request * @return Request
*/ */
public function update(string $id, array $data, string $type = 'anime'): Request public function update(string $id, AbstractType $data, string $type = 'anime'): Request
{ {
$config = $this->container->get('config'); $config = $this->container->get('config');

View File

@ -16,12 +16,14 @@
namespace Aviat\AnimeClient\API\MAL; namespace Aviat\AnimeClient\API\MAL;
use const Aviat\AnimeClient\USER_AGENT;
use Aviat\AnimeClient\API\{ use Aviat\AnimeClient\API\{
APIRequestBuilder, APIRequestBuilder,
MAL as M MAL as M
}; };
class MALRequestBuilder extends APIRequestBuilder { final class MALRequestBuilder extends APIRequestBuilder {
/** /**
* The base url for api requests * The base url for api requests
@ -38,7 +40,7 @@ class MALRequestBuilder extends APIRequestBuilder {
'Accept' => 'text/xml', 'Accept' => 'text/xml',
'Accept-Encoding' => 'gzip', 'Accept-Encoding' => 'gzip',
'Content-type' => 'application/x-www-form-urlencoded', 'Content-type' => 'application/x-www-form-urlencoded',
'User-Agent' => "Tim's Anime Client/4.0" 'User-Agent' => USER_AGENT,
]; ];
/** /**

View File

@ -24,12 +24,13 @@ use Aviat\AnimeClient\API\MAL\{
}; };
use Aviat\AnimeClient\API\XML; use Aviat\AnimeClient\API\XML;
use Aviat\AnimeClient\API\Mapping\{AnimeWatchingStatus, MangaReadingStatus}; use Aviat\AnimeClient\API\Mapping\{AnimeWatchingStatus, MangaReadingStatus};
use Aviat\AnimeClient\Types\{Anime, FormItem};
use Aviat\Ion\Di\ContainerAware; use Aviat\Ion\Di\ContainerAware;
/** /**
* MyAnimeList API Model * MyAnimeList API Model
*/ */
class Model { final class Model {
use ContainerAware; use ContainerAware;
use MALTrait; use MALTrait;
@ -147,11 +148,11 @@ class Model {
/** /**
* Update a list item * Update a list item
* *
* @param array $data * @param FormItem $data
* @param string $type "anime" or "manga" * @param string $type "anime" or "manga"
* @return Request * @return Request
*/ */
public function updateListItem(array $data, string $type = 'anime'): Request public function updateListItem(FormItem $data, string $type = 'anime'): Request
{ {
$updateData = []; $updateData = [];

View File

@ -17,12 +17,13 @@
namespace Aviat\AnimeClient\API\MAL\Transformer; namespace Aviat\AnimeClient\API\MAL\Transformer;
use Aviat\AnimeClient\API\Mapping\AnimeWatchingStatus; use Aviat\AnimeClient\API\Mapping\AnimeWatchingStatus;
use Aviat\AnimeClient\Types\{AnimeFormItem, AnimeFormItemData};
use Aviat\Ion\Transformer\AbstractTransformer; use Aviat\Ion\Transformer\AbstractTransformer;
/** /**
* Transformer for updating MAL List * Transformer for updating MAL List
*/ */
class AnimeListTransformer extends AbstractTransformer { final class AnimeListTransformer extends AbstractTransformer {
/** /**
* Identity transformation * Identity transformation
* *
@ -38,16 +39,14 @@ class AnimeListTransformer extends AbstractTransformer {
* Transform Kitsu episode data to MAL episode data * Transform Kitsu episode data to MAL episode data
* *
* @param array $item * @param array $item
* @return array * @return AnimeFormItem
*/ */
public function untransform(array $item): array public function untransform(array $item): AnimeFormItem
{ {
$map = [ $map = new AnimeFormItem([
'id' => $item['mal_id'], 'id' => $item['mal_id'],
'data' => [] 'data' => new AnimeFormItemData([]),
]; ]);
$data =& $item['data'];
foreach($item['data'] as $key => $value) foreach($item['data'] as $key => $value)
{ {

View File

@ -22,7 +22,7 @@ use Aviat\Ion\Transformer\AbstractTransformer;
/** /**
* Transformer for updating MAL List * Transformer for updating MAL List
*/ */
class MangaListTransformer extends AbstractTransformer { final class MangaListTransformer extends AbstractTransformer {
/** /**
* Identity transformation * Identity transformation
* *

View File

@ -16,14 +16,30 @@
namespace Aviat\AnimeClient\API\Mapping; namespace Aviat\AnimeClient\API\Mapping;
use Aviat\AnimeClient\API\Enum\AnimeWatchingStatus\{Kitsu, MAL, Route, Title}; use Aviat\AnimeClient\API\Enum\AnimeWatchingStatus\{Anilist, Kitsu, MAL, Route, Title};
use Aviat\Ion\Enum; use Aviat\Ion\Enum;
/** /**
* Anime watching status mappings, among Kitsu, MAL, Page titles * Anime watching status mappings, among Kitsu, MAL, Page titles
* and url route segments * and url route segments
*/ */
class AnimeWatchingStatus extends Enum { final class AnimeWatchingStatus extends Enum {
const ANILIST_TO_KITSU = [
Anilist::WATCHING => Kitsu::WATCHING,
Anilist::PLAN_TO_WATCH => Kitsu::PLAN_TO_WATCH,
Anilist::COMPLETED => Kitsu::COMPLETED,
Anilist::ON_HOLD => Kitsu::ON_HOLD,
Anilist::DROPPED => Kitsu::DROPPED
];
const KITSU_TO_ANILIST = [
Kitsu::WATCHING => Anilist::WATCHING,
Kitsu::PLAN_TO_WATCH => Anilist::PLAN_TO_WATCH,
Kitsu::COMPLETED => Anilist::COMPLETED,
Kitsu::ON_HOLD => Anilist::ON_HOLD,
Kitsu::DROPPED => Anilist::DROPPED
];
const KITSU_TO_MAL = [ const KITSU_TO_MAL = [
Kitsu::WATCHING => MAL::WATCHING, Kitsu::WATCHING => MAL::WATCHING,
Kitsu::PLAN_TO_WATCH => MAL::PLAN_TO_WATCH, Kitsu::PLAN_TO_WATCH => MAL::PLAN_TO_WATCH,

View File

@ -16,14 +16,31 @@
namespace Aviat\AnimeClient\API\Mapping; namespace Aviat\AnimeClient\API\Mapping;
use Aviat\AnimeClient\API\Enum\MangaReadingStatus\{Kitsu, MAL, Title, Route}; use Aviat\AnimeClient\API\Enum\MangaReadingStatus\{Anilist, Kitsu, MAL, Title, Route};
use Aviat\Ion\Enum; use Aviat\Ion\Enum;
/** /**
* Manga reading status mappings, among Kitsu, MAL, Page titles * Manga reading status mappings, among Kitsu, MAL, Page titles
* and url route segments * and url route segments
*/ */
class MangaReadingStatus extends Enum { final class MangaReadingStatus extends Enum {
const ANILIST_TO_KITSU = [
Anilist::READING => Kitsu::READING,
Anilist::PLAN_TO_READ => Kitsu::PLAN_TO_READ,
Anilist::COMPLETED => Kitsu::COMPLETED,
Anilist::ON_HOLD => Kitsu::ON_HOLD,
Anilist::DROPPED => Kitsu::DROPPED
];
const KITSU_TO_ANILIST = [
Kitsu::READING => Anilist::READING,
Kitsu::PLAN_TO_READ => Anilist::PLAN_TO_READ,
Kitsu::COMPLETED => Anilist::COMPLETED,
Kitsu::ON_HOLD => Anilist::ON_HOLD,
Kitsu::DROPPED => Anilist::DROPPED
];
const KITSU_TO_MAL = [ const KITSU_TO_MAL = [
Kitsu::READING => MAL::READING, Kitsu::READING => MAL::READING,
Kitsu::PLAN_TO_READ => MAL::PLAN_TO_READ, Kitsu::PLAN_TO_READ => MAL::PLAN_TO_READ,

View File

@ -22,14 +22,14 @@ use function Amp\Promise\{all, wait};
/** /**
* Class to simplify making and validating simultaneous requests * Class to simplify making and validating simultaneous requests
*/ */
class ParallelAPIRequest { final class ParallelAPIRequest {
/** /**
* Set of requests to make in parallel * Set of requests to make in parallel
* *
* @var array * @var array
*/ */
protected $requests = []; private $requests = [];
/** /**
* Add a request * Add a request
@ -76,9 +76,7 @@ class ParallelAPIRequest {
{ {
$promises[$key] = call(function () use ($client, $url) { $promises[$key] = call(function () use ($client, $url) {
$response = yield $client->request($url); $response = yield $client->request($url);
$body = yield $response->getBody(); return yield $response->getBody();
return $body;
}); });
} }

View File

@ -21,7 +21,7 @@ use DOMDocument, DOMNode, DOMNodeList, InvalidArgumentException;
/** /**
* XML <=> PHP Array codec * XML <=> PHP Array codec
*/ */
class XML { final class XML {
/** /**
* XML representation of the data * XML representation of the data

View File

@ -19,7 +19,7 @@ namespace Aviat\AnimeClient\Command;
/** /**
* Clears the API Cache * Clears the API Cache
*/ */
class CacheClear extends BaseCommand { final class CacheClear extends BaseCommand {
/** /**
* Clear the API cache * Clear the API cache
* *

View File

@ -19,7 +19,7 @@ namespace Aviat\AnimeClient\Command;
/** /**
* Clears the API Cache * Clears the API Cache
*/ */
class CachePrime extends BaseCommand { final class CachePrime extends BaseCommand {
/** /**
* Clear, then prime the API cache * Clear, then prime the API cache
* *

View File

@ -32,7 +32,7 @@ use DateTime;
/** /**
* Clears the API Cache * Clears the API Cache
*/ */
class SyncLists extends BaseCommand { final class SyncLists extends BaseCommand {
/** /**
* Model for making requests to Kitsu API * Model for making requests to Kitsu API

View File

@ -20,6 +20,7 @@ use Aviat\AnimeClient\Controller as BaseController;
use Aviat\AnimeClient\API\Kitsu\Transformer\AnimeListTransformer; use Aviat\AnimeClient\API\Kitsu\Transformer\AnimeListTransformer;
use Aviat\AnimeClient\API\Enum\AnimeWatchingStatus\Kitsu as KitsuWatchingStatus; use Aviat\AnimeClient\API\Enum\AnimeWatchingStatus\Kitsu as KitsuWatchingStatus;
use Aviat\AnimeClient\API\Mapping\AnimeWatchingStatus; use Aviat\AnimeClient\API\Mapping\AnimeWatchingStatus;
use Aviat\AnimeClient\Types\AnimeFormItem;
use Aviat\Ion\Di\ContainerInterface; use Aviat\Ion\Di\ContainerInterface;
use Aviat\Ion\Json; use Aviat\Ion\Json;
use Aviat\Ion\StringWrapper; use Aviat\Ion\StringWrapper;
@ -27,7 +28,7 @@ use Aviat\Ion\StringWrapper;
/** /**
* Controller for Anime-related pages * Controller for Anime-related pages
*/ */
class Anime extends BaseController { final class Anime extends BaseController {
use StringWrapper; use StringWrapper;
@ -201,7 +202,7 @@ class Anime extends BaseController {
// large form-based updates // large form-based updates
$transformer = new AnimeListTransformer(); $transformer = new AnimeListTransformer();
$postData = $transformer->untransform($data); $postData = $transformer->untransform($data);
$fullResult = $this->model->updateLibraryItem($postData); $fullResult = $this->model->updateLibraryItem(new AnimeFormItem($postData));
if ($fullResult['statusCode'] === 200) if ($fullResult['statusCode'] === 200)
{ {
@ -232,7 +233,7 @@ class Anime extends BaseController {
$data = $this->request->getParsedBody(); $data = $this->request->getParsedBody();
} }
$response = $this->model->updateLibraryItem($data); $response = $this->model->updateLibraryItem(new AnimeFormItem($data));
$this->cache->clear(); $this->cache->clear();
$this->outputJSON($response['body'], $response['statusCode']); $this->outputJSON($response['body'], $response['statusCode']);
@ -277,7 +278,7 @@ class Anime extends BaseController {
$show_data = $this->model->getAnime($animeId); $show_data = $this->model->getAnime($animeId);
$characters = []; $characters = [];
if (empty($show_data)) if ($show_data->title === '')
{ {
$this->notFound( $this->notFound(
$this->config->get('whose_list') . $this->config->get('whose_list') .
@ -301,7 +302,7 @@ class Anime extends BaseController {
'title' => $this->formatTitle( 'title' => $this->formatTitle(
$this->config->get('whose_list') . "'s Anime List", $this->config->get('whose_list') . "'s Anime List",
'Anime', 'Anime',
$show_data['titles'][0] $show_data->title
), ),
'characters' => $characters, 'characters' => $characters,
'show_data' => $show_data, 'show_data' => $show_data,

View File

@ -26,7 +26,7 @@ use Aviat\Ion\Di\ContainerInterface;
/** /**
* Controller for Anime collection pages * Controller for Anime collection pages
*/ */
class AnimeCollection extends BaseController { final class AnimeCollection extends BaseController {
/** /**
* The anime collection model * The anime collection model

View File

@ -23,7 +23,7 @@ use Aviat\Ion\ArrayWrapper;
/** /**
* Controller for character description pages * Controller for character description pages
*/ */
class Character extends BaseController { final class Character extends BaseController {
use ArrayWrapper; use ArrayWrapper;

View File

@ -25,7 +25,7 @@ use Aviat\Ion\View\HtmlView;
/** /**
* Controller for handling routes that don't fit elsewhere * Controller for handling routes that don't fit elsewhere
*/ */
class Index extends BaseController { final class Index extends BaseController {
/** /**
* Purges the API cache * Purges the API cache
@ -72,6 +72,24 @@ class Index extends BaseController {
], $view); ], $view);
} }
/**
* Redirect to Anilist to start Oauth flow
*/
public function anilistRedirect()
{
}
/**
* Oauth callback for Anilist API
*/
public function anilistCallback()
{
$this->outputHTML('blank', [
'title' => 'Oauth!'
]);
}
/** /**
* Attempt login authentication * Attempt login authentication
* *

View File

@ -20,13 +20,14 @@ use Aviat\AnimeClient\Controller;
use Aviat\AnimeClient\API\Kitsu\Transformer\MangaListTransformer; use Aviat\AnimeClient\API\Kitsu\Transformer\MangaListTransformer;
use Aviat\AnimeClient\API\Mapping\MangaReadingStatus; use Aviat\AnimeClient\API\Mapping\MangaReadingStatus;
use Aviat\AnimeClient\Model\Manga as MangaModel; use Aviat\AnimeClient\Model\Manga as MangaModel;
use Aviat\AnimeClient\Types\MangaFormItem;
use Aviat\Ion\Di\ContainerInterface; use Aviat\Ion\Di\ContainerInterface;
use Aviat\Ion\{Json, StringWrapper}; use Aviat\Ion\{Json, StringWrapper};
/** /**
* Controller for manga list * Controller for manga list
*/ */
class Manga extends Controller { final class Manga extends Controller {
use StringWrapper; use StringWrapper;
@ -200,7 +201,7 @@ class Manga extends Controller {
// large form-based updates // large form-based updates
$transformer = new MangaListTransformer(); $transformer = new MangaListTransformer();
$post_data = $transformer->untransform($data); $post_data = $transformer->untransform($data);
$full_result = $this->model->updateLibraryItem($post_data); $full_result = $this->model->updateLibraryItem(new MangaFormItem($post_data));
if ($full_result['statusCode'] === 200) if ($full_result['statusCode'] === 200)
{ {
@ -234,7 +235,7 @@ class Manga extends Controller {
$data = $this->request->getParsedBody(); $data = $this->request->getParsedBody();
} }
$response = $this->model->updateLibraryItem($data); $response = $this->model->updateLibraryItem(new MangaFormItem($data));
$this->cache->clear(); $this->cache->clear();
$this->outputJSON($response['body'], $response['statusCode']); $this->outputJSON($response['body'], $response['statusCode']);

View File

@ -26,7 +26,7 @@ use Aviat\Ion\Di\ContainerInterface;
/** /**
* Controller for manga collection pages * Controller for manga collection pages
*/ */
class MangaCollection extends BaseController { final class MangaCollection extends BaseController {
/** /**
* The manga collection model * The manga collection model

View File

@ -28,7 +28,7 @@ use Aviat\Ion\StringWrapper;
/** /**
* Basic routing/ dispatch * Basic routing/ dispatch
*/ */
class Dispatcher extends RoutingBase { final class Dispatcher extends RoutingBase {
use StringWrapper; use StringWrapper;

View File

@ -17,13 +17,14 @@
namespace Aviat\AnimeClient\Helper; namespace Aviat\AnimeClient\Helper;
use Aviat\AnimeClient\MenuGenerator; use Aviat\AnimeClient\MenuGenerator;
use Aviat\Ion\Di\ContainerAware;
/** /**
* MenuGenerator helper wrapper * MenuGenerator helper wrapper
*/ */
class Menu { final class Menu {
use \Aviat\Ion\Di\ContainerAware; use ContainerAware;
/** /**
* Create the html for the selected menu * Create the html for the selected menu

View File

@ -25,7 +25,7 @@ use Aviat\Ion\Exception\ConfigException;
/** /**
* Helper object to manage menu creation and selection * Helper object to manage menu creation and selection
*/ */
class MenuGenerator extends UrlGenerator { final class MenuGenerator extends UrlGenerator {
use ArrayWrapper; use ArrayWrapper;
use StringWrapper; use StringWrapper;

View File

@ -44,7 +44,7 @@ class API {
foreach ($array as $key => $item) foreach ($array as $key => $item)
{ {
$sort[$key] = $item[$sortKey]['titles'][0]; $sort[$key] = $item[$sortKey]['title'];
} }
array_multisort($sort, SORT_ASC, $array); array_multisort($sort, SORT_ASC, $array);

View File

@ -18,6 +18,11 @@ namespace Aviat\AnimeClient\Model;
use Aviat\AnimeClient\API\ParallelAPIRequest; use Aviat\AnimeClient\API\ParallelAPIRequest;
use Aviat\AnimeClient\API\Mapping\AnimeWatchingStatus; use Aviat\AnimeClient\API\Mapping\AnimeWatchingStatus;
use Aviat\AnimeClient\Types\{
Anime as AnimeType,
AnimeFormItem,
AnimeListItem
};
use Aviat\Ion\Di\ContainerInterface; use Aviat\Ion\Di\ContainerInterface;
use Aviat\Ion\Json; use Aviat\Ion\Json;
@ -93,9 +98,9 @@ class Anime extends API {
* Get information about an anime from its slug * Get information about an anime from its slug
* *
* @param string $slug * @param string $slug
* @return array * @return AnimeType
*/ */
public function getAnime(string $slug): array public function getAnime(string $slug): AnimeType
{ {
return $this->kitsuModel->getAnime($slug); return $this->kitsuModel->getAnime($slug);
} }
@ -127,9 +132,9 @@ class Anime extends API {
* for editing/updating that item * for editing/updating that item
* *
* @param string $itemId * @param string $itemId
* @return array * @return AnimeListItem
*/ */
public function getLibraryItem(string $itemId): array public function getLibraryItem(string $itemId): AnimeListItem
{ {
return $this->kitsuModel->getListItem($itemId); return $this->kitsuModel->getListItem($itemId);
} }
@ -166,10 +171,10 @@ class Anime extends API {
/** /**
* Update a list entry * Update a list entry
* *
* @param array $data * @param AnimeFormItem $data
* @return array * @return array
*/ */
public function updateLibraryItem(array $data): array public function updateLibraryItem(AnimeFormItem $data): array
{ {
$requester = new ParallelAPIRequest(); $requester = new ParallelAPIRequest();

View File

@ -22,7 +22,7 @@ use PDO;
/** /**
* Model for getting anime collection data * Model for getting anime collection data
*/ */
class AnimeCollection extends Collection { final class AnimeCollection extends Collection {
/** /**
* Anime API Model * Anime API Model

View File

@ -21,14 +21,18 @@ use Aviat\AnimeClient\API\{
Mapping\MangaReadingStatus, Mapping\MangaReadingStatus,
ParallelAPIRequest ParallelAPIRequest
}; };
use Aviat\AnimeClient\Types\{
MangaFormItem,
MangaListItem,
MangaPage
};
use Aviat\Ion\Di\ContainerInterface; use Aviat\Ion\Di\ContainerInterface;
use Aviat\Ion\Json; use Aviat\Ion\Json;
/** /**
* Model for handling requests dealing with the manga list * Model for handling requests dealing with the manga list
*/ */
class Manga extends API class Manga extends API {
{
/** /**
* Model for making requests to Kitsu API * Model for making requests to Kitsu API
* @var \Aviat\AnimeClient\API\Kitsu\Model * @var \Aviat\AnimeClient\API\Kitsu\Model
@ -67,21 +71,28 @@ class Manga extends API
{ {
if ($status === 'All') if ($status === 'All')
{ {
return $this->kitsuModel->getFullOrganizedMangaList(); $data = $this->kitsuModel->getFullOrganizedMangaList();
foreach($data as &$section)
{
$this->sortByName($section, 'manga');
}
return $data;
} }
$APIstatus = MangaReadingStatus::TITLE_TO_KITSU[$status]; $APIstatus = MangaReadingStatus::TITLE_TO_KITSU[$status];
$data = $this->kitsuModel->getMangaList($APIstatus); $data = $this->mapByStatus($this->kitsuModel->getMangaList($APIstatus));
return $this->mapByStatus($data)[$status]; $this->sortByName($data[$status], 'manga');
return $data[$status];
} }
/** /**
* Get the details of a manga * Get the details of a manga
* *
* @param string $manga_id * @param string $manga_id
* @return array * @return MangaPage
*/ */
public function getManga($manga_id): array public function getManga($manga_id): MangaPage
{ {
return $this->kitsuModel->getManga($manga_id); return $this->kitsuModel->getManga($manga_id);
} }
@ -90,9 +101,9 @@ class Manga extends API
* Get anime by its kitsu id * Get anime by its kitsu id
* *
* @param string $animeId * @param string $animeId
* @return array * @return MangaPage
*/ */
public function getMangaById(string $animeId): array public function getMangaById(string $animeId): MangaPage
{ {
return $this->kitsuModel->getMangaById($animeId); return $this->kitsuModel->getMangaById($animeId);
} }
@ -102,9 +113,9 @@ class Manga extends API
* for editing/updating that item * for editing/updating that item
* *
* @param string $itemId * @param string $itemId
* @return array * @return MangaListItem
*/ */
public function getLibraryItem(string $itemId): array public function getLibraryItem(string $itemId): MangaListItem
{ {
return $this->kitsuModel->getListItem($itemId); return $this->kitsuModel->getListItem($itemId);
} }
@ -141,10 +152,10 @@ class Manga extends API
/** /**
* Update a list entry * Update a list entry
* *
* @param array $data * @param MangaFormItem $data
* @return array * @return array
*/ */
public function updateLibraryItem(array $data): array public function updateLibraryItem(MangaFormItem $data): array
{ {
$requester = new ParallelAPIRequest(); $requester = new ParallelAPIRequest();
@ -221,9 +232,7 @@ class Manga extends API
$output[$key][] = $entry; $output[$key][] = $entry;
} }
foreach ($output as &$val) { unset($entry);
$this->sortByName($val, 'manga');
}
return $output; return $output;
} }

View File

@ -22,7 +22,7 @@ use PDO;
/** /**
* Model for getting anime collection data * Model for getting anime collection data
*/ */
class MangaCollection extends Collection { final class MangaCollection extends Collection {
/** /**
* Manga API Model * Manga API Model

157
src/Types/AbstractType.php Normal file
View File

@ -0,0 +1,157 @@
<?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 - 2018 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.0
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Types;
use ArrayAccess;
use LogicException;
abstract class AbstractType implements ArrayAccess {
/**
* Populate values for unserializing data
*
* @param $properties
* @return mixed
*/
public static function __set_state($properties)
{
return new static($properties);
}
/**
* Sets the properties by using the constructor
*
* @param mixed $data
*/
public function __construct($data = [])
{
$typeKeys = array_keys((array)$this);
$dataKeys = array_keys((array)$data);
$unsetKeys = array_diff($typeKeys, $dataKeys);
foreach ($data as $key => $value)
{
$this->__set($key, $value);
}
// Remove unset keys so that they aren't serialized
foreach ($unsetKeys as $k)
{
unset($this->$k);
}
}
/**
* See if a property is set
*
* @param $name
* @return bool
*/
public function __isset($name): bool
{
return property_exists($this, $name);
}
/**
* Set a property on the type object
*
* @param string $name
* @param mixed $value
* @return void
*/
public function __set($name, $value): void
{
$setterMethod = 'set' . ucfirst($name);
if (method_exists($this, $setterMethod))
{
$this->$setterMethod($value);
return;
}
if (!property_exists($this, $name))
{
$existing = json_encode($this);
throw new LogicException("Trying to set non-existent property: '$name'. Existing properties: $existing");
}
$this->$name = $value;
}
/**
* Get a property from the type object
*
* @param string $name
* @return mixed
*/
public function __get($name)
{
if (property_exists($this, $name))
{
return $this->$name;
}
throw new LogicException("Trying to get non-existent property: '$name'");
}
/**
* Implementing ArrayAccess
*
* @param $offset
* @return bool
*/
public function offsetExists($offset): bool
{
return $this->__isset($offset);
}
/**
* Implementing ArrayAccess
*
* @param $offset
* @return mixed
*/
public function offsetGet($offset)
{
return $this->__get($offset);
}
/**
* Implementing ArrayAccess
*
* @param $offset
* @param $value
*/
public function offsetSet($offset, $value): void
{
$this->__set($offset, $value);
}
/**
* Implementing ArrayAccess
*
* @param $offset
*/
public function offsetUnset($offset): void
{
if ($this->offsetExists($offset))
{
unset($this->$offset);
}
}
}

40
src/Types/Anime.php Normal file
View File

@ -0,0 +1,40 @@
<?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 - 2018 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.0
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Types;
/**
* Type representing an Anime object for display
*/
final class Anime extends AbstractType {
public $age_rating;
public $age_rating_guide;
public $cover_image;
public $episode_count;
public $episode_length;
public $genres;
public $id;
public $included;
public $show_type;
public $slug;
public $status;
public $streaming_links;
public $synopsis;
public $title;
public $titles;
public $trailer_id;
public $url;
}

View File

@ -0,0 +1,27 @@
<?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 - 2018 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.0
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Types;
/**
* Type representing an Anime object for display
*/
final class AnimeFormItem extends FormItem {
public function setData($value): void
{
$this->data = new AnimeFormItemData($value);
}
}

View File

@ -0,0 +1,22 @@
<?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 - 2018 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.0
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Types;
/**
* Type representing an Anime object for display
*/
final class AnimeFormItemData extends FormItemData {}

View File

@ -0,0 +1,42 @@
<?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 - 2018 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.0
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Types;
/**
* Type representing an Anime object for display
*/
final class AnimeListItem extends AbstractType {
public $id;
public $mal_id;
public $episodes = [
'length' => 0,
'total' => 0,
'watched' => '',
];
public $airing = [
'status' => '',
'started' => '',
'ended' => '',
];
public $anime;
public $watching_status;
public $notes;
public $rewatching;
public $rewatched;
public $user_rating;
public $private;
}

29
src/Types/FormItem.php Normal file
View File

@ -0,0 +1,29 @@
<?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 - 2018 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.0
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Types;
/**
* Type representing an Anime object for display
*/
abstract class FormItem extends AbstractType {
public $id;
public $mal_id;
public $data;
abstract public function setData($value): void;
}

View File

@ -0,0 +1,30 @@
<?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 - 2018 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.0
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Types;
/**
* Type representing an Anime object for display
*/
abstract class FormItemData extends AbstractType {
public $notes;
public $private;
public $progress;
public $rating;
public $reconsumeCount;
public $reconsuming;
public $status;
}

View File

@ -0,0 +1,28 @@
<?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 - 2018 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.0
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Types;
/**
* Form data for updating a Manga List item
*/
final class MangaFormItem extends FormItem {
public function setData($value): void
{
$this->data = new MangaFormItemData($value);
}
}

View File

@ -0,0 +1,19 @@
<?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 - 2018 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.0
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Types;
final class MangaFormItemData extends FormItemData {}

View File

@ -0,0 +1,40 @@
<?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 - 2018 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.0
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Types;
/**
* Type representing an Anime object for display
*/
final class MangaListItem extends AbstractType {
public $id;
public $mal_id;
public $chapters = [
'read' => 0,
'total' => 0,
];
public $volumes = [
'read' => '-',
'total' => 0,
];
public $manga;
public $reading_status;
public $notes;
public $rereading;
public $reread;
public $user_rating;
}

View File

@ -0,0 +1,31 @@
<?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 - 2018 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.0
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Types;
/**
* Type representing the manga represented by the list item
*/
final class MangaListItemDetail extends AbstractType {
public $genres;
public $id;
public $image;
public $slug;
public $title;
public $titles;
public $type;
public $url;
}

35
src/Types/MangaPage.php Normal file
View File

@ -0,0 +1,35 @@
<?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 - 2018 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.0
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Types;
/**
* Type representing an Anime object for display
*/
final class MangaPage extends AbstractType {
public $chapter_count;
public $cover_image;
public $en_title;
public $genres;
public $id;
public $included;
public $jp_title;
public $manga_type;
public $synopsis;
public $title;
public $url;
public $volume_count;
}

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