Merge remote-tracking branch 'origin/develop'

This commit is contained in:
Timothy Warren 2021-04-23 19:01:21 -04:00
commit 0f4383563f
96 changed files with 20464 additions and 3602 deletions

8
Jenkinsfile vendored
View File

@ -18,7 +18,8 @@ pipeline {
} }
} }
steps { steps {
sh 'apk add --no-cache git' sh 'apk add --no-cache git icu-dev'
sh 'docker-php-ext-configure intl && docker-php-ext-install intl'
sh 'php ./vendor/bin/phpunit --colors=never' sh 'php ./vendor/bin/phpunit --colors=never'
} }
} }
@ -30,14 +31,15 @@ pipeline {
} }
} }
steps { steps {
sh 'apk add --no-cache git' sh 'apk add --no-cache git icu-dev'
sh 'docker-php-ext-configure intl && docker-php-ext-install intl'
sh 'php ./vendor/bin/phpunit --colors=never' sh 'php ./vendor/bin/phpunit --colors=never'
} }
} }
stage('Code Cleanliness') { stage('Code Cleanliness') {
agent any agent any
steps { steps {
sh "php8 ./vendor/bin/phpstan analyse -c phpstan.neon -n --no-progress --no-ansi --error-format=checkstyle | awk '{\$1=\$1;print}' > build/logs/phpstan.log" sh "php ./vendor/bin/phpstan analyse -c phpstan.neon -n --no-progress --no-ansi --error-format=checkstyle | awk '{\$1=\$1;print}' > build/logs/phpstan.log"
recordIssues( recordIssues(
failOnError: false, failOnError: false,
tools: [phpStan(reportEncoding: 'UTF-8', pattern: 'build/logs/phpstan.log')] tools: [phpStan(reportEncoding: 'UTF-8', pattern: 'build/logs/phpstan.log')]

View File

@ -1,316 +0,0 @@
<?php declare(strict_types=1);
use Robo\Tasks;
if ( ! function_exists('glob_recursive'))
{
// Does not support flag GLOB_BRACE
function glob_recursive($pattern, $flags = 0)
{
$files = glob($pattern, $flags);
foreach (glob(dirname($pattern).'/*', GLOB_ONLYDIR|GLOB_NOSORT) as $dir)
{
$files = array_merge($files, glob_recursive($dir.'/'.basename($pattern), $flags));
}
return $files;
}
}
/**
* This is project's console commands configuration for Robo task runner.
*
* @see http://robo.li/
*/
class RoboFile extends Tasks {
/**
* Directories used by analysis tools
*
* @var array
*/
protected array $taskDirs = [
'build/logs',
'build/pdepend',
'build/phpdox',
];
/**
* Directories to remove with the clean task
*
* @var array
*/
protected array $cleanDirs = [
'coverage',
'docs',
'phpdoc',
'build/logs',
'build/phpdox',
'build/pdepend'
];
/**
* Do static analysis tasks
*/
public function analyze(): void
{
$this->prepare();
$this->lint();
$this->phploc(TRUE);
$this->phpcs(TRUE);
$this->phpmd(TRUE);
$this->dependencyReport();
$this->phpcpdReport();
}
/**
* Run all tests, generate coverage, generate docs, generate code statistics
*/
public function build(): void
{
$this->analyze();
$this->coverage();
$this->docs();
}
/**
* Cleanup temporary files
*/
public function clean(): void
{
$cleanFiles = [
'build/humbug.json',
'build/humbug-log.txt',
];
array_map(static function ($file) {
@unlink($file);
}, $cleanFiles);
// So the task doesn't complain,
// make any 'missing' dirs to cleanup
array_map(static function ($dir) {
if ( ! is_dir($dir))
{
`mkdir -p {$dir}`;
}
}, $this->cleanDirs);
$this->_cleanDir($this->cleanDirs);
$this->_deleteDir($this->cleanDirs);
}
/**
* Run unit tests and generate coverage reports
*/
public function coverage(): void
{
$this->_run(['phpdbg -qrr -- vendor/bin/phpunit -c build']);
}
/**
* Generate documentation with phpdox
*/
public function docs(): void
{
$cmd_parts = [
'vendor/bin/phpdox',
];
$this->_run($cmd_parts, ' && ');
}
/**
* Verify that source files are valid
*/
public function lint(): void
{
$files = $this->getAllSourceFiles();
$chunks = array_chunk($files, (int)shell_exec('getconf _NPROCESSORS_ONLN'));
foreach($chunks as $chunk)
{
$this->parallelLint($chunk);
}
}
/**
* Run the phpcs tool
*
* @param bool $report - if true, generates reports instead of direct output
*/
public function phpcs($report = FALSE): void
{
$report_cmd_parts = [
'vendor/bin/phpcs',
'--standard=./build/phpcs.xml',
'--report-checkstyle=./build/logs/phpcs.xml',
];
$normal_cmd_parts = [
'vendor/bin/phpcs',
'--standard=./build/phpcs.xml',
];
$cmd_parts = ($report) ? $report_cmd_parts : $normal_cmd_parts;
$this->_run($cmd_parts);
}
public function phpmd($report = FALSE): void
{
$report_cmd_parts = [
'vendor/bin/phpmd',
'./src',
'xml',
'cleancode,codesize,controversial,design,naming,unusedcode',
'--exclude ParallelAPIRequest',
'--reportfile ./build/logs/phpmd.xml'
];
$normal_cmd_parts = [
'vendor/bin/phpmd',
'./src',
'ansi',
'cleancode,codesize,controversial,design,naming,unusedcode',
'--exclude ParallelAPIRequest'
];
$cmd_parts = ($report) ? $report_cmd_parts : $normal_cmd_parts;
$this->_run($cmd_parts);
}
/**
* Run the phploc tool
*
* @param bool $report - if true, generates reports instead of direct output
*/
public function phploc($report = FALSE): void
{
// Command for generating reports
$report_cmd_parts = [
'vendor/bin/phploc',
'--count-tests',
'--log-csv=build/logs/phploc.csv',
'--log-xml=build/logs/phploc.xml',
'src',
'tests'
];
// Command for generating direct output
$normal_cmd_parts = [
'vendor/bin/phploc',
'--count-tests',
'src',
'tests'
];
$cmd_parts = ($report) ? $report_cmd_parts : $normal_cmd_parts;
$this->_run($cmd_parts);
}
/**
* Create temporary directories
*/
public function prepare(): void
{
array_map([$this, '_mkdir'], $this->taskDirs);
}
/**
* Lint php files and run unit tests
*/
public function test(): void
{
$this->lint();
$this->_run(['vendor/bin/phpunit']);
}
/**
* Create pdepend reports
*/
protected function dependencyReport(): void
{
$cmd_parts = [
'vendor/bin/pdepend',
'--jdepend-xml=build/logs/jdepend.xml',
'--jdepend-chart=build/pdepend/dependencies.svg',
'--overview-pyramid=build/pdepend/overview-pyramid.svg',
'src'
];
$this->_run($cmd_parts);
}
/**
* Get the total list of source files, including tests
*
* @return array
*/
protected function getAllSourceFiles(): array
{
$files = array_merge(
glob_recursive('build/*.php'),
glob_recursive('src/*.php'),
glob_recursive('src/**/*.php'),
glob_recursive('tests/*.php'),
glob_recursive('tests/**/*.php'),
glob('*.php')
);
$files = array_filter($files, static function(string $value) {
return strpos($value, '__snapshots__') === FALSE;
});
sort($files);
return $files;
}
/**
* Run php's linter in one parallel task for the passed chunk
*
* @param array $chunk
*/
protected function parallelLint(array $chunk): void
{
$task = $this->taskParallelExec()
->timeout(5)
->printed(FALSE);
foreach($chunk as $file)
{
$task = $task->process("php -l {$file}");
}
$task->run();
}
/**
* Generate copy paste detector report
*/
protected function phpcpdReport(): void
{
$cmd_parts = [
'vendor/bin/phpcpd',
'--log-pmd build/logs/pmd-cpd.xml',
'src'
];
$this->_run($cmd_parts);
}
/**
* Shortcut for joining an array of command arguments
* and then running it
*
* @param array $cmd_parts - command arguments
* @param string $join_on - what to join the command arguments with
*/
protected function _run(array $cmd_parts, $join_on = ' '): void
{
$this->taskExec(implode($join_on, $cmd_parts))->run();
}
}

View File

@ -2,16 +2,16 @@
/** /**
* Hummingbird Anime List Client * Hummingbird Anime List Client
* *
* An API client for Kitsu and MyAnimeList to manage anime and manga watch lists * An API client for Kitsu to manage anime and manga watch lists
* *
* PHP version 7 * PHP version 8
* *
* @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 - 2021 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 5.2
* @link https://github.com/timw4mail/HummingBirdAnimeClient * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/ */
use function Aviat\AnimeClient\loadConfig; use function Aviat\AnimeClient\loadConfig;
@ -21,12 +21,13 @@ use function Aviat\AnimeClient\loadConfig;
// //
// You shouldn't generally need to change anything below this line // You shouldn't generally need to change anything below this line
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
$APP_DIR = realpath(__DIR__ . '/../'); $APP_DIR = dirname(__DIR__);
$ROOT_DIR = realpath("{$APP_DIR}/../"); $ROOT_DIR = dirname($APP_DIR);
$tomlConfig = loadConfig(__DIR__); $tomlConfig = loadConfig(__DIR__);
return array_merge($tomlConfig, [ return array_merge($tomlConfig, [
'root' => $ROOT_DIR,
'asset_dir' => "{$ROOT_DIR}/public", 'asset_dir' => "{$ROOT_DIR}/public",
'base_config_dir' => __DIR__, 'base_config_dir' => __DIR__,
'config_dir' => "{$APP_DIR}/config", 'config_dir' => "{$APP_DIR}/config",

View File

@ -2,15 +2,15 @@
/** /**
* Hummingbird Anime List Client * Hummingbird Anime List Client
* *
* An API client for Kitsu and MyAnimeList to manage anime and manga watch lists * An API client for Kitsu to manage anime and manga watch lists
* *
* PHP version 7 * PHP version 8
* *
* @package HummingbirdAnimeClient * @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net> * @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2018 Timothy J. Warren * @copyright 2015 - 2021 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 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/ */

View File

@ -34,11 +34,11 @@ use Psr\SimpleCache\CacheInterface;
use function Aviat\Ion\_dir; use function Aviat\Ion\_dir;
if ( ! defined('APP_DIR')) if ( ! defined('HB_APP_DIR'))
{ {
define('APP_DIR', __DIR__); define('HB_APP_DIR', __DIR__);
define('ROOT_DIR', dirname(APP_DIR)); define('ROOT_DIR', dirname(HB_APP_DIR));
define('TEMPLATE_DIR', _dir(APP_DIR, 'templates')); define('TEMPLATE_DIR', _dir(HB_APP_DIR, 'templates'));
} }
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
@ -50,7 +50,7 @@ return static function (array $configArray = []): Container {
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// Logging // Logging
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
$LOG_DIR = _dir(APP_DIR, 'logs'); $LOG_DIR = _dir(HB_APP_DIR, 'logs');
$appLogger = new Logger('animeclient'); $appLogger = new Logger('animeclient');
$appLogger->pushHandler(new RotatingFileHandler(_dir($LOG_DIR, 'app.log'), 2, Logger::WARNING)); $appLogger->pushHandler(new RotatingFileHandler(_dir($LOG_DIR, 'app.log'), 2, Logger::WARNING));

View File

@ -11,12 +11,6 @@
</div> </div>
</section> </section>
<script nomodule="nomodule" src="https://polyfill.io/v3/polyfill.min.js?features=es5%2CObject.assign"></script> <script nomodule="nomodule" src="https://polyfill.io/v3/polyfill.min.js?features=es5%2CObject.assign"></script>
<?php if ($auth->isAuthenticated()): ?> <script async="async" defer="defer" src="<?= $urlGenerator->assetUrl('js/scripts.min.js') ?>"></script>
<script nomodule='nomodule' async="async" defer="defer" src="<?= $urlGenerator->assetUrl('js/scripts.min.js') ?>"></script>
<script type="module" src="<?= $urlGenerator->assetUrl('es/scripts.js') ?>"></script>
<?php else: ?>
<script nomodule="nomodule" async="async" defer="defer" src="<?= $urlGenerator->assetUrl('js/anon.min.js') ?>"></script>
<script type="module" src="<?= $urlGenerator->assetUrl('es/anon.js') ?>"></script>
<?php endif ?>
</body> </body>
</html> </html>

View File

@ -5,13 +5,13 @@ namespace Aviat\AnimeClient;
$whose = $config->get('whose_list') . "'s "; $whose = $config->get('whose_list') . "'s ";
$lastSegment = $urlGenerator->lastSegment(); $lastSegment = $urlGenerator->lastSegment();
$extraSegment = $lastSegment === 'list' ? '/list' : ''; $extraSegment = $lastSegment === 'list' ? '/list' : '';
$hasAnime = stripos($GLOBALS['_SERVER']['REQUEST_URI'], 'anime') !== FALSE; $hasAnime = str_contains($GLOBALS['_SERVER']['REQUEST_URI'], 'anime');
$hasManga = stripos($GLOBALS['_SERVER']['REQUEST_URI'], 'manga') !== FALSE; $hasManga = str_contains($GLOBALS['_SERVER']['REQUEST_URI'], 'manga');
?> ?>
<div id="main-nav" 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( ! str_contains($route_path, 'collection')): ?>
<?= $helper->a( <?= $helper->a(
$urlGenerator->defaultUrl($url_type), $urlGenerator->defaultUrl($url_type),
$whose . ucfirst($url_type) . ' List', $whose . ucfirst($url_type) . ' List',

View File

@ -2,6 +2,7 @@
declare(strict_types=1); declare(strict_types=1);
$file_patterns = [ $file_patterns = [
'app/appConf/*.php',
'app/bootstrap.php', 'app/bootstrap.php',
'migrations/*.php', 'migrations/*.php',
'src/**/*.php', 'src/**/*.php',
@ -16,7 +17,7 @@ if ( ! function_exists('glob_recursive'))
{ {
// Does not support flag GLOB_BRACE // Does not support flag GLOB_BRACE
function glob_recursive($pattern, $flags = 0) function glob_recursive(string $pattern, int $flags = 0): array
{ {
$files = glob($pattern, $flags); $files = glob($pattern, $flags);
@ -57,17 +58,21 @@ function get_text_to_replace(array $tokens): string
return $output; return $output;
} }
function get_tokens($source): array function get_tokens(string $source): array
{ {
return token_get_all($source); return token_get_all($source);
} }
function replace_files(array $files, $template) function replace_files(array $files, string $template): void
{ {
print_r($files); print_r($files);
foreach ($files as $file) foreach ($files as $file)
{ {
$source = file_get_contents($file); $source = file_get_contents($file);
if ($source === FALSE)
{
continue;
}
if (stripos($source, 'namespace') === FALSE) if (stripos($source, 'namespace') === FALSE)
{ {

View File

@ -43,26 +43,25 @@
"aviat/query": "^3.0.0", "aviat/query": "^3.0.0",
"danielstjules/stringy": "^3.1.0", "danielstjules/stringy": "^3.1.0",
"ext-dom": "*", "ext-dom": "*",
"ext-iconv": "*",
"ext-intl": "*", "ext-intl": "*",
"ext-json": "*", "ext-json": "*",
"ext-gd": "*", "ext-gd": "*",
"ext-pdo": "*", "ext-pdo": "*",
"filp/whoops": "^2.1",
"laminas/laminas-diactoros": "^2.5.0", "laminas/laminas-diactoros": "^2.5.0",
"laminas/laminas-httphandlerrunner": "^1.1.0", "laminas/laminas-httphandlerrunner": "^1.1.0",
"maximebf/consolekit": "^1.0.3", "maximebf/consolekit": "^1.0.3",
"monolog/monolog": "^2.0.2", "monolog/monolog": "^2.0.2",
"php": "^8.0.0", "php": ">= 8.0.0",
"psr/container": "^1.0.0", "psr/container": "^1.0.0",
"psr/http-message": "^1.0.1", "psr/http-message": "^1.0.1",
"psr/log": "^1.1.3", "psr/log": "^1.1.3",
"robmorgan/phinx": "^0.12.4", "robmorgan/phinx": "^0.12.4",
"symfony/var-dumper": "^5.0.7", "symfony/polyfill-mbstring": "^1.0.0",
"symfony/polyfill-util": "^1.0.0",
"tracy/tracy": "^2.8.0",
"yosymfony/toml": "^1.0.4" "yosymfony/toml": "^1.0.4"
}, },
"require-dev": { "require-dev": {
"consolidation/robo": "^2.0.0",
"pdepend/pdepend": "^2.", "pdepend/pdepend": "^2.",
"phploc/phploc": "^7.0.0", "phploc/phploc": "^7.0.0",
"phpmd/phpmd": "^2.8.2", "phpmd/phpmd": "^2.8.2",

View File

@ -26,7 +26,7 @@ try
'sync:lists' => Command\SyncLists::class 'sync:lists' => Command\SyncLists::class
]))->run(); ]))->run();
} }
catch (\Exception $e) catch (\Throwable)
{ {
} }

View File

@ -1,68 +0,0 @@
import compiler from '@ampproject/rollup-plugin-closure-compiler';
const plugins = [
compiler({
assumeFunctionWrapper: true,
compilationLevel: 'WHITESPACE_ONLY', //'ADVANCED',
createSourceMap: true,
env: 'BROWSER',
languageIn: 'ECMASCRIPT_2018',
languageOut: 'ES3'
})
];
const defaultOutput = {
format: 'iife',
sourcemap: true,
}
const nonModules = [{
input: './js/anon.js',
output: {
...defaultOutput,
file: '../public/js/anon.min.js',
sourcemapFile: '../public/js/anon.min.js.map',
},
plugins,
}, {
input: './js/index.js',
output: {
...defaultOutput,
file: '../public/js/scripts.min.js',
sourcemapFile: '../public/js/scripts.min.js.map',
},
plugins,
}, {
input: './js/base/sort-tables.js',
output: {
...defaultOutput,
file: '../public/js/tables.min.js',
sourcemapFile: '../public/js/tables.min.js.map',
},
plugins,
}];
const moduleOutput = {
format: 'es',
sourcemap: false,
}
let modules = [{
input: './js/anon.js',
output: {
...moduleOutput,
file: '../public/es/anon.js',
},
}, {
input: './js/index.js',
output: {
...moduleOutput,
file: '../public/es/scripts.js',
},
}];
// Return the config array for rollup
export default [
...nonModules,
...modules,
];

View File

@ -9,7 +9,7 @@ const matches = (elm, selector) => {
return i > -1; return i > -1;
} }
export const AnimeClient = { const AnimeClient = {
/** /**
* Placeholder function * Placeholder function
*/ */
@ -18,8 +18,8 @@ export const AnimeClient = {
* DOM selector * DOM selector
* *
* @param {string} selector - The dom selector string * @param {string} selector - The dom selector string
* @param {object} [context] * @param {Element} [context]
* @return {[HTMLElement]} - array of dom elements * @return array of dom elements
*/ */
$(selector, context = null) { $(selector, context = null) {
if (typeof selector !== 'string') { if (typeof selector !== 'string') {
@ -60,7 +60,7 @@ export const AnimeClient = {
/** /**
* Hide the selected element * Hide the selected element
* *
* @param {string|Element} sel - the selector of the element to hide * @param {string|Element|Element[]} sel - the selector of the element to hide
* @return {void} * @return {void}
*/ */
hide (sel) { hide (sel) {
@ -77,7 +77,7 @@ export const AnimeClient = {
/** /**
* UnHide the selected element * UnHide the selected element
* *
* @param {string|Element} sel - the selector of the element to hide * @param {string|Element|Element[]} sel - the selector of the element to hide
* @return {void} * @return {void}
*/ */
show (sel) { show (sel) {
@ -116,9 +116,9 @@ export const AnimeClient = {
/** /**
* Finds the closest parent element matching the passed selector * Finds the closest parent element matching the passed selector
* *
* @param {HTMLElement} current - the current HTMLElement * @param {Element} current - the current Element
* @param {string} parentSelector - selector for the parent element * @param {string} parentSelector - selector for the parent element
* @return {HTMLElement|null} - the parent element * @return {Element|null} - the parent element
*/ */
closestParent (current, parentSelector) { closestParent (current, parentSelector) {
if (Element.prototype.closest !== undefined) { if (Element.prototype.closest !== undefined) {
@ -204,9 +204,9 @@ function delegateEvent(sel, target, event, listener) {
/** /**
* Add an event listener * Add an event listener
* *
* @param {string|HTMLElement} sel - the parent selector to bind to * @param {string|Element} sel - the parent selector to bind to
* @param {string} event - event name(s) to bind * @param {string} event - event name(s) to bind
* @param {string|HTMLElement|function} target - the element to directly bind the event to * @param {string|Element|function} target - the element to directly bind the event to
* @param {function} [listener] - event listener callback * @param {function} [listener] - event listener callback
* @return {void} * @return {void}
*/ */

View File

@ -71,7 +71,7 @@ _.on('body.anime.list', 'click', '.plus-one', (e) => {
success: (res) => { success: (res) => {
const resData = JSON.parse(res); const resData = JSON.parse(res);
if (resData.errors) { if (resData.error) {
_.hide('#loading-shadow'); _.hide('#loading-shadow');
_.showMessage('error', `Failed to update ${title}. `); _.showMessage('error', `Failed to update ${title}. `);
_.scrollToTop(); _.scrollToTop();

View File

@ -1,227 +0,0 @@
/*
* classList.js: Cross-browser full element.classList implementation.
* 2014-07-23
*
* By Eli Grey, http://eligrey.com
* Public Domain.
* NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
*/
/*global self, document, DOMException */
/*! @source http://purl.eligrey.com/github/classList.js/blob/master/classList.js*/
if ("document" in self) {
// Full polyfill for browsers with no classList support
if (!("classList" in document.createElement("_"))) {
(function(view) {
"use strict";
if (!('Element' in view)) return;
var
classListProp = "classList",
protoProp = "prototype",
elemCtrProto = view.Element[protoProp],
objCtr = Object,
strTrim = String[protoProp].trim || function() {
return this.replace(/^\s+|\s+$/g, "");
},
arrIndexOf = Array[protoProp].indexOf || function(item) {
var
i = 0,
len = this.length;
for (; i < len; i++) {
if (i in this && this[i] === item) {
return i;
}
}
return -1;
}
// Vendors: please allow content code to instantiate DOMExceptions
,
DOMEx = function(type, message) {
this.name = type;
this.code = DOMException[type];
this.message = message;
},
checkTokenAndGetIndex = function(classList, token) {
if (token === "") {
throw new DOMEx(
"SYNTAX_ERR", "An invalid or illegal string was specified"
);
}
if (/\s/.test(token)) {
throw new DOMEx(
"INVALID_CHARACTER_ERR", "String contains an invalid character"
);
}
return arrIndexOf.call(classList, token);
},
ClassList = function(elem) {
var
trimmedClasses = strTrim.call(elem.getAttribute("class") || ""),
classes = trimmedClasses ? trimmedClasses.split(/\s+/) : [],
i = 0,
len = classes.length;
for (; i < len; i++) {
this.push(classes[i]);
}
this._updateClassName = function() {
elem.setAttribute("class", this.toString());
};
},
classListProto = ClassList[protoProp] = [],
classListGetter = function() {
return new ClassList(this);
};
// Most DOMException implementations don't allow calling DOMException's toString()
// on non-DOMExceptions. Error's toString() is sufficient here.
DOMEx[protoProp] = Error[protoProp];
classListProto.item = function(i) {
return this[i] || null;
};
classListProto.contains = function(token) {
token += "";
return checkTokenAndGetIndex(this, token) !== -1;
};
classListProto.add = function() {
var
tokens = arguments,
i = 0,
l = tokens.length,
token, updated = false;
do {
token = tokens[i] + "";
if (checkTokenAndGetIndex(this, token) === -1) {
this.push(token);
updated = true;
}
}
while (++i < l);
if (updated) {
this._updateClassName();
}
};
classListProto.remove = function() {
var
tokens = arguments,
i = 0,
l = tokens.length,
token, updated = false,
index;
do {
token = tokens[i] + "";
index = checkTokenAndGetIndex(this, token);
while (index !== -1) {
this.splice(index, 1);
updated = true;
index = checkTokenAndGetIndex(this, token);
}
}
while (++i < l);
if (updated) {
this._updateClassName();
}
};
classListProto.toggle = function(token, force) {
token += "";
var
result = this.contains(token),
method = result ?
force !== true && "remove" :
force !== false && "add";
if (method) {
this[method](token);
}
if (force === true || force === false) {
return force;
} else {
return !result;
}
};
classListProto.toString = function() {
return this.join(" ");
};
if (objCtr.defineProperty) {
var classListPropDesc = {
get: classListGetter,
enumerable: true,
configurable: true
};
try {
objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc);
} catch (ex) { // IE 8 doesn't support enumerable:true
if (ex.number === -0x7FF5EC54) {
classListPropDesc.enumerable = false;
objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc);
}
}
} else if (objCtr[protoProp].__defineGetter__) {
elemCtrProto.__defineGetter__(classListProp, classListGetter);
}
}(self));
} else {
// There is full or partial native classList support, so just check if we need
// to normalize the add/remove and toggle APIs.
(function() {
"use strict";
var testElement = document.createElement("_");
testElement.classList.add("c1", "c2");
// Polyfill for IE 10/11 and Firefox <26, where classList.add and
// classList.remove exist but support only one argument at a time.
if (!testElement.classList.contains("c2")) {
var createMethod = function(method) {
var original = DOMTokenList.prototype[method];
DOMTokenList.prototype[method] = function(token) {
var i, len = arguments.length;
for (i = 0; i < len; i++) {
token = arguments[i];
original.call(this, token);
}
};
};
createMethod('add');
createMethod('remove');
}
testElement.classList.toggle("c3", false);
// Polyfill for IE 10 and Firefox <24, where classList.toggle does not
// support the second argument.
if (testElement.classList.contains("c3")) {
var _toggle = DOMTokenList.prototype.toggle;
DOMTokenList.prototype.toggle = function(token, force) {
if (1 in arguments && !this.contains(token) === !force) {
return force;
} else {
return _toggle.call(this, token);
}
};
}
testElement = null;
}());
}
}

View File

@ -16,7 +16,7 @@ _.on('.media-filter', 'input', filterMedia);
/** /**
* Hide the html element attached to the event * Hide the html element attached to the event
* *
* @param event * @param {MouseEvent} event
* @return void * @return void
*/ */
function hide (event) { function hide (event) {
@ -26,7 +26,7 @@ function hide (event) {
/** /**
* Confirm deletion of an item * Confirm deletion of an item
* *
* @param event * @param {MouseEvent} event
* @return void * @return void
*/ */
function confirmDelete (event) { function confirmDelete (event) {
@ -52,7 +52,7 @@ function clearAPICache () {
/** /**
* Scroll to the accordion/vertical tab section just opened * Scroll to the accordion/vertical tab section just opened
* *
* @param event * @param {InputEvent} event
* @return void * @return void
*/ */
function scrollToSection (event) { function scrollToSection (event) {
@ -70,7 +70,7 @@ function scrollToSection (event) {
/** /**
* Filter an anime or manga list * Filter an anime or manga list
* *
* @param event * @param {InputEvent} event
* @return void * @return void
*/ */
function filterMedia (event) { function filterMedia (event) {

View File

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

View File

@ -72,14 +72,22 @@ _.on('.manga.list', 'click', '.edit-buttons button', (e) => {
dataType: 'json', dataType: 'json',
type: 'POST', type: 'POST',
mimeType: 'application/json', mimeType: 'application/json',
success: () => { success: (res) => {
const resData = JSON.parse(res)
if (resData.error) {
_.hide('#loading-shadow');
_.showMessage('error', `Failed to update ${mangaName}. `);
_.scrollToTop();
return;
}
if (String(data.data.status).toUpperCase() === 'COMPLETED') { if (String(data.data.status).toUpperCase() === 'COMPLETED') {
_.hide(parentSel); _.hide(parentSel);
} }
_.hide('#loading-shadow'); _.hide('#loading-shadow');
_.$(`.${type}s_read`, parentSel)[ 0 ].textContent = completed; _.$(`.${type}s_read`, parentSel)[ 0 ].textContent = String(completed);
_.showMessage('success', `Successfully updated ${mangaName}`); _.showMessage('success', `Successfully updated ${mangaName}`);
_.scrollToTop(); _.scrollToTop();
}, },

View File

@ -1,9 +1,8 @@
import _ from './anime-client.js'; import _ from './anime-client.js';
(() => { (() => {
// Var is intentional let hidden = null;
var hidden = null; let visibilityChange = null;
var visibilityChange = null;
if (typeof document.hidden !== "undefined") { if (typeof document.hidden !== "undefined") {
hidden = "hidden"; hidden = "hidden";

View File

@ -1,5 +1,4 @@
import './events.js'; // Start the service worker, if you can
if ('serviceWorker' in navigator) { if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js').then(reg => { navigator.serviceWorker.register('/sw.js').then(reg => {
console.log('Service worker registered', reg.scope); console.log('Service worker registered', reg.scope);
@ -7,4 +6,3 @@ if ('serviceWorker' in navigator) {
console.error('Failed to register service worker', error); console.error('Failed to register service worker', error);
}); });
} }

View File

@ -8,12 +8,10 @@ _.on('main', 'change', '.big-check', (e) => {
}); });
export function renderAnimeSearchResults (data) { export function renderAnimeSearchResults (data) {
const results = []; return data.map(item => {
data.forEach(item => {
const titles = item.titles.join('<br />'); const titles = item.titles.join('<br />');
results.push(` return `
<article class="media search"> <article class="media search">
<div class="name"> <div class="name">
<input type="radio" class="mal-check" id="mal_${item.slug}" name="mal_id" value="${item.mal_id}" /> <input type="radio" class="mal-check" id="mal_${item.slug}" name="mal_id" value="${item.mal_id}" />
@ -38,19 +36,14 @@ export function renderAnimeSearchResults (data) {
</div> </div>
</div> </div>
</article> </article>
`); `;
}); }).join('');
return results.join('');
} }
export function renderMangaSearchResults (data) { export function renderMangaSearchResults (data) {
const results = []; return data.map(item => {
data.forEach(item => {
const titles = item.titles.join('<br />'); const titles = item.titles.join('<br />');
return `
results.push(`
<article class="media search"> <article class="media search">
<div class="name"> <div class="name">
<input type="radio" id="mal_${item.slug}" name="mal_id" value="${item.mal_id}" /> <input type="radio" id="mal_${item.slug}" name="mal_id" value="${item.mal_id}" />
@ -75,8 +68,6 @@ export function renderMangaSearchResults (data) {
</div> </div>
</div> </div>
</article> </article>
`); `;
}); }).join('');
return results.join('');
} }

View File

@ -3,19 +3,19 @@
"scripts": { "scripts": {
"build": "npm run build:css && npm run build:js", "build": "npm run build:css && npm run build:js",
"build:css": "node ./css.js", "build:css": "node ./css.js",
"build:js": "rollup -c ./build-js.js", "build:js": "spack",
"watch:css": "watch 'npm run build:css' --filter=./cssfilter.js", "watch:css": "watch 'npm run build:css' --filter=./cssfilter.js",
"watch:js": "watch 'npm run build:js' ./js", "watch:js": "watch 'npm run build:js' ./js",
"watch": "concurrently \"npm:watch:css\" \"npm:watch:js\" --kill-others" "watch": "concurrently \"npm:watch:css\" \"npm:watch:js\" --kill-others"
}, },
"devDependencies": { "devDependencies": {
"@ampproject/rollup-plugin-closure-compiler": "^0.25.2", "@swc/cli": "^0.1.39",
"concurrently": "^5.1.0", "@swc/core": "^1.2.54",
"cssnano": "^4.1.10", "concurrently": "^6.0.2",
"postcss": "^7.0.27", "cssnano": "^5.0.1",
"postcss-import": "^12.0.1", "postcss": "^8.2.6",
"postcss-import": "^14.0.0",
"postcss-preset-env": "^6.7.0", "postcss-preset-env": "^6.7.0",
"rollup": "^2.4.0",
"watch": "^1.0.2" "watch": "^1.0.2"
} }
} }

View File

@ -0,0 +1,19 @@
module.exports = {
entry: {
'scripts.min': __dirname + '/js/index.js',
'tables.min': __dirname + '/js/base/sort-tables.js',
},
output: {
path: '../public/js',
},
options: {
jsc: {
target: 'es3',
loose: true,
},
minify: true,
module: {
type: 'es6'
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -17,9 +17,7 @@
namespace Aviat\AnimeClient; namespace Aviat\AnimeClient;
use Aviat\AnimeClient\Types\Config as ConfigType; use Aviat\AnimeClient\Types\Config as ConfigType;
use Whoops\Handler\PrettyPageHandler; use Tracy\Debugger;
use Whoops\Run;
use function Aviat\Ion\_dir; use function Aviat\Ion\_dir;
setlocale(LC_CTYPE, 'en_US'); setlocale(LC_CTYPE, 'en_US');
@ -27,12 +25,9 @@ setlocale(LC_CTYPE, 'en_US');
// Load composer autoloader // Load composer autoloader
require_once __DIR__ . '/vendor/autoload.php'; require_once __DIR__ . '/vendor/autoload.php';
if (file_exists('.is-dev')) Debugger::$strictMode = true;
{ Debugger::$showBar = false;
$whoops = new Run; Debugger::enable(Debugger::DEVELOPMENT, __DIR__ . '/app/logs');
$whoops->pushHandler(new PrettyPageHandler);
$whoops->register();
}
// Define base directories // Define base directories
$APP_DIR = _dir(__DIR__, 'app'); $APP_DIR = _dir(__DIR__, 'app');

View File

@ -4,6 +4,7 @@ parameters:
inferPrivatePropertyTypeFromConstructor: true inferPrivatePropertyTypeFromConstructor: true
level: 8 level: 8
paths: paths:
- app/appConf
- src - src
- ./console - ./console
- index.php - index.php

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,463 +0,0 @@
// -------------------------------------------------------------------------
// ! Base
// -------------------------------------------------------------------------
const matches = (elm, selector) => {
let m = (elm.document || elm.ownerDocument).querySelectorAll(selector);
let i = matches.length;
while (--i >= 0 && m.item(i) !== elm) {} return i > -1;
};
const AnimeClient = {
/**
* Placeholder function
*/
noop: () => {},
/**
* DOM selector
*
* @param {string} selector - The dom selector string
* @param {object} [context]
* @return {[HTMLElement]} - array of dom elements
*/
$(selector, context = null) {
if (typeof selector !== 'string') {
return selector;
}
context = (context !== null && context.nodeType === 1)
? context
: document;
let elements = [];
if (selector.match(/^#([\w]+$)/)) {
elements.push(document.getElementById(selector.split('#')[1]));
} else {
elements = [].slice.apply(context.querySelectorAll(selector));
}
return elements;
},
/**
* Does the selector exist on the current page?
*
* @param {string} selector
* @returns {boolean}
*/
hasElement (selector) {
return AnimeClient.$(selector).length > 0;
},
/**
* Scroll to the top of the Page
*
* @return {void}
*/
scrollToTop () {
const el = AnimeClient.$('header')[0];
el.scrollIntoView(true);
},
/**
* Hide the selected element
*
* @param {string|Element} sel - the selector of the element to hide
* @return {void}
*/
hide (sel) {
if (typeof sel === 'string') {
sel = AnimeClient.$(sel);
}
if (Array.isArray(sel)) {
sel.forEach(el => el.setAttribute('hidden', 'hidden'));
} else {
sel.setAttribute('hidden', 'hidden');
}
},
/**
* UnHide the selected element
*
* @param {string|Element} sel - the selector of the element to hide
* @return {void}
*/
show (sel) {
if (typeof sel === 'string') {
sel = AnimeClient.$(sel);
}
if (Array.isArray(sel)) {
sel.forEach(el => el.removeAttribute('hidden'));
} else {
sel.removeAttribute('hidden');
}
},
/**
* Display a message box
*
* @param {string} type - message type: info, error, success
* @param {string} message - the message itself
* @return {void}
*/
showMessage (type, message) {
let template =
`<div class='message ${type}'>
<span class='icon'></span>
${message}
<span class='close'></span>
</div>`;
let sel = AnimeClient.$('.message');
if (sel[0] !== undefined) {
sel[0].remove();
}
AnimeClient.$('header')[0].insertAdjacentHTML('beforeend', template);
},
/**
* Finds the closest parent element matching the passed selector
*
* @param {HTMLElement} current - the current HTMLElement
* @param {string} parentSelector - selector for the parent element
* @return {HTMLElement|null} - the parent element
*/
closestParent (current, parentSelector) {
if (Element.prototype.closest !== undefined) {
return current.closest(parentSelector);
}
while (current !== document.documentElement) {
if (matches(current, parentSelector)) {
return current;
}
current = current.parentElement;
}
return null;
},
/**
* Generate a full url from a relative path
*
* @param {string} path - url path
* @return {string} - full url
*/
url (path) {
let uri = `//${document.location.host}`;
uri += (path.charAt(0) === '/') ? path : `/${path}`;
return uri;
},
/**
* Throttle execution of a function
*
* @see https://remysharp.com/2010/07/21/throttling-function-calls
* @see https://jsfiddle.net/jonathansampson/m7G64/
* @param {Number} interval - the minimum throttle time in ms
* @param {Function} fn - the function to throttle
* @param {Object} [scope] - the 'this' object for the function
* @return {Function}
*/
throttle (interval, fn, scope) {
let wait = false;
return function (...args) {
const context = scope || this;
if ( ! wait) {
fn.apply(context, args);
wait = true;
setTimeout(function() {
wait = false;
}, interval);
}
};
},
};
// -------------------------------------------------------------------------
// ! Events
// -------------------------------------------------------------------------
function addEvent(sel, event, listener) {
// Recurse!
if (! event.match(/^([\w\-]+)$/)) {
event.split(' ').forEach((evt) => {
addEvent(sel, evt, listener);
});
}
sel.addEventListener(event, listener, false);
}
function delegateEvent(sel, target, event, listener) {
// Attach the listener to the parent
addEvent(sel, event, (e) => {
// Get live version of the target selector
AnimeClient.$(target, sel).forEach((element) => {
if(e.target == element) {
listener.call(element, e);
e.stopPropagation();
}
});
});
}
/**
* Add an event listener
*
* @param {string|HTMLElement} sel - the parent selector to bind to
* @param {string} event - event name(s) to bind
* @param {string|HTMLElement|function} target - the element to directly bind the event to
* @param {function} [listener] - event listener callback
* @return {void}
*/
AnimeClient.on = (sel, event, target, listener) => {
if (listener === undefined) {
listener = target;
AnimeClient.$(sel).forEach((el) => {
addEvent(el, event, listener);
});
} else {
AnimeClient.$(sel).forEach((el) => {
delegateEvent(el, target, event, listener);
});
}
};
// -------------------------------------------------------------------------
// ! Ajax
// -------------------------------------------------------------------------
/**
* Url encoding for non-get requests
*
* @param data
* @returns {string}
* @private
*/
function ajaxSerialize(data) {
let pairs = [];
Object.keys(data).forEach((name) => {
let value = data[name].toString();
name = encodeURIComponent(name);
value = encodeURIComponent(value);
pairs.push(`${name}=${value}`);
});
return pairs.join('&');
}
/**
* Make an ajax request
*
* Config:{
* data: // data to send with the request
* type: // http verb of the request, defaults to GET
* success: // success callback
* error: // error callback
* }
*
* @param {string} url - the url to request
* @param {Object} config - the configuration object
* @return {XMLHttpRequest}
*/
AnimeClient.ajax = (url, config) => {
// Set some sane defaults
const defaultConfig = {
data: {},
type: 'GET',
dataType: '',
success: AnimeClient.noop,
mimeType: 'application/x-www-form-urlencoded',
error: AnimeClient.noop
};
config = {
...defaultConfig,
...config,
};
let request = new XMLHttpRequest();
let method = String(config.type).toUpperCase();
if (method === 'GET') {
url += (url.match(/\?/))
? ajaxSerialize(config.data)
: `?${ajaxSerialize(config.data)}`;
}
request.open(method, url);
request.onreadystatechange = () => {
if (request.readyState === 4) {
let responseText = '';
if (request.responseType === 'json') {
responseText = JSON.parse(request.responseText);
} else {
responseText = request.responseText;
}
if (request.status > 299) {
config.error.call(null, request.status, responseText, request.response);
} else {
config.success.call(null, responseText, request.status);
}
}
};
if (config.dataType === 'json') {
config.data = JSON.stringify(config.data);
config.mimeType = 'application/json';
} else {
config.data = ajaxSerialize(config.data);
}
request.setRequestHeader('Content-Type', config.mimeType);
if (method === 'GET') {
request.send(null);
} else {
request.send(config.data);
}
return request
};
/**
* Do a get request
*
* @param {string} url
* @param {object|function} data
* @param {function} [callback]
* @return {XMLHttpRequest}
*/
AnimeClient.get = (url, data, callback = null) => {
if (callback === null) {
callback = data;
data = {};
}
return AnimeClient.ajax(url, {
data,
success: callback
});
};
// ----------------------------------------------------------------------------
// Event subscriptions
// ----------------------------------------------------------------------------
AnimeClient.on('header', 'click', '.message', hide);
AnimeClient.on('form.js-delete', 'submit', confirmDelete);
AnimeClient.on('.js-clear-cache', 'click', clearAPICache);
AnimeClient.on('.vertical-tabs input', 'change', scrollToSection);
AnimeClient.on('.media-filter', 'input', filterMedia);
// ----------------------------------------------------------------------------
// Handler functions
// ----------------------------------------------------------------------------
/**
* Hide the html element attached to the event
*
* @param event
* @return void
*/
function hide (event) {
AnimeClient.hide(event.target);
}
/**
* Confirm deletion of an item
*
* @param event
* @return void
*/
function confirmDelete (event) {
const proceed = confirm('Are you ABSOLUTELY SURE you want to delete this item?');
if (proceed === false) {
event.preventDefault();
event.stopPropagation();
}
}
/**
* Clear the API cache, and show a message if the cache is cleared
*
* @return void
*/
function clearAPICache () {
AnimeClient.get('/cache_purge', () => {
AnimeClient.showMessage('success', 'Successfully purged api cache');
});
}
/**
* Scroll to the accordion/vertical tab section just opened
*
* @param event
* @return void
*/
function scrollToSection (event) {
const el = event.currentTarget.parentElement;
const rect = el.getBoundingClientRect();
const top = rect.top + window.pageYOffset;
window.scrollTo({
top,
behavior: 'smooth',
});
}
/**
* Filter an anime or manga list
*
* @param event
* @return void
*/
function filterMedia (event) {
const rawFilter = event.target.value;
const filter = new RegExp(rawFilter, 'i');
// console.log('Filtering items by: ', filter);
if (rawFilter !== '') {
// Filter the cover view
AnimeClient.$('article.media').forEach(article => {
const titleLink = AnimeClient.$('.name a', article)[0];
const title = String(titleLink.textContent).trim();
if ( ! filter.test(title)) {
AnimeClient.hide(article);
} else {
AnimeClient.show(article);
}
});
// Filter the list view
AnimeClient.$('table.media-wrap tbody tr').forEach(tr => {
const titleCell = AnimeClient.$('td.align-left', tr)[0];
const titleLink = AnimeClient.$('a', titleCell)[0];
const linkTitle = String(titleLink.textContent).trim();
const textTitle = String(titleCell.textContent).trim();
if ( ! (filter.test(linkTitle) || filter.test(textTitle))) {
AnimeClient.hide(tr);
} else {
AnimeClient.show(tr);
}
});
} else {
AnimeClient.show('article.media');
AnimeClient.show('table.media-wrap tbody tr');
}
}
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js').then(reg => {
console.log('Service worker registered', reg.scope);
}).catch(error => {
console.error('Failed to register service worker', error);
});
}

View File

@ -1,769 +0,0 @@
// -------------------------------------------------------------------------
// ! Base
// -------------------------------------------------------------------------
const matches = (elm, selector) => {
let m = (elm.document || elm.ownerDocument).querySelectorAll(selector);
let i = matches.length;
while (--i >= 0 && m.item(i) !== elm) {} return i > -1;
};
const AnimeClient = {
/**
* Placeholder function
*/
noop: () => {},
/**
* DOM selector
*
* @param {string} selector - The dom selector string
* @param {object} [context]
* @return {[HTMLElement]} - array of dom elements
*/
$(selector, context = null) {
if (typeof selector !== 'string') {
return selector;
}
context = (context !== null && context.nodeType === 1)
? context
: document;
let elements = [];
if (selector.match(/^#([\w]+$)/)) {
elements.push(document.getElementById(selector.split('#')[1]));
} else {
elements = [].slice.apply(context.querySelectorAll(selector));
}
return elements;
},
/**
* Does the selector exist on the current page?
*
* @param {string} selector
* @returns {boolean}
*/
hasElement (selector) {
return AnimeClient.$(selector).length > 0;
},
/**
* Scroll to the top of the Page
*
* @return {void}
*/
scrollToTop () {
const el = AnimeClient.$('header')[0];
el.scrollIntoView(true);
},
/**
* Hide the selected element
*
* @param {string|Element} sel - the selector of the element to hide
* @return {void}
*/
hide (sel) {
if (typeof sel === 'string') {
sel = AnimeClient.$(sel);
}
if (Array.isArray(sel)) {
sel.forEach(el => el.setAttribute('hidden', 'hidden'));
} else {
sel.setAttribute('hidden', 'hidden');
}
},
/**
* UnHide the selected element
*
* @param {string|Element} sel - the selector of the element to hide
* @return {void}
*/
show (sel) {
if (typeof sel === 'string') {
sel = AnimeClient.$(sel);
}
if (Array.isArray(sel)) {
sel.forEach(el => el.removeAttribute('hidden'));
} else {
sel.removeAttribute('hidden');
}
},
/**
* Display a message box
*
* @param {string} type - message type: info, error, success
* @param {string} message - the message itself
* @return {void}
*/
showMessage (type, message) {
let template =
`<div class='message ${type}'>
<span class='icon'></span>
${message}
<span class='close'></span>
</div>`;
let sel = AnimeClient.$('.message');
if (sel[0] !== undefined) {
sel[0].remove();
}
AnimeClient.$('header')[0].insertAdjacentHTML('beforeend', template);
},
/**
* Finds the closest parent element matching the passed selector
*
* @param {HTMLElement} current - the current HTMLElement
* @param {string} parentSelector - selector for the parent element
* @return {HTMLElement|null} - the parent element
*/
closestParent (current, parentSelector) {
if (Element.prototype.closest !== undefined) {
return current.closest(parentSelector);
}
while (current !== document.documentElement) {
if (matches(current, parentSelector)) {
return current;
}
current = current.parentElement;
}
return null;
},
/**
* Generate a full url from a relative path
*
* @param {string} path - url path
* @return {string} - full url
*/
url (path) {
let uri = `//${document.location.host}`;
uri += (path.charAt(0) === '/') ? path : `/${path}`;
return uri;
},
/**
* Throttle execution of a function
*
* @see https://remysharp.com/2010/07/21/throttling-function-calls
* @see https://jsfiddle.net/jonathansampson/m7G64/
* @param {Number} interval - the minimum throttle time in ms
* @param {Function} fn - the function to throttle
* @param {Object} [scope] - the 'this' object for the function
* @return {Function}
*/
throttle (interval, fn, scope) {
let wait = false;
return function (...args) {
const context = scope || this;
if ( ! wait) {
fn.apply(context, args);
wait = true;
setTimeout(function() {
wait = false;
}, interval);
}
};
},
};
// -------------------------------------------------------------------------
// ! Events
// -------------------------------------------------------------------------
function addEvent(sel, event, listener) {
// Recurse!
if (! event.match(/^([\w\-]+)$/)) {
event.split(' ').forEach((evt) => {
addEvent(sel, evt, listener);
});
}
sel.addEventListener(event, listener, false);
}
function delegateEvent(sel, target, event, listener) {
// Attach the listener to the parent
addEvent(sel, event, (e) => {
// Get live version of the target selector
AnimeClient.$(target, sel).forEach((element) => {
if(e.target == element) {
listener.call(element, e);
e.stopPropagation();
}
});
});
}
/**
* Add an event listener
*
* @param {string|HTMLElement} sel - the parent selector to bind to
* @param {string} event - event name(s) to bind
* @param {string|HTMLElement|function} target - the element to directly bind the event to
* @param {function} [listener] - event listener callback
* @return {void}
*/
AnimeClient.on = (sel, event, target, listener) => {
if (listener === undefined) {
listener = target;
AnimeClient.$(sel).forEach((el) => {
addEvent(el, event, listener);
});
} else {
AnimeClient.$(sel).forEach((el) => {
delegateEvent(el, target, event, listener);
});
}
};
// -------------------------------------------------------------------------
// ! Ajax
// -------------------------------------------------------------------------
/**
* Url encoding for non-get requests
*
* @param data
* @returns {string}
* @private
*/
function ajaxSerialize(data) {
let pairs = [];
Object.keys(data).forEach((name) => {
let value = data[name].toString();
name = encodeURIComponent(name);
value = encodeURIComponent(value);
pairs.push(`${name}=${value}`);
});
return pairs.join('&');
}
/**
* Make an ajax request
*
* Config:{
* data: // data to send with the request
* type: // http verb of the request, defaults to GET
* success: // success callback
* error: // error callback
* }
*
* @param {string} url - the url to request
* @param {Object} config - the configuration object
* @return {XMLHttpRequest}
*/
AnimeClient.ajax = (url, config) => {
// Set some sane defaults
const defaultConfig = {
data: {},
type: 'GET',
dataType: '',
success: AnimeClient.noop,
mimeType: 'application/x-www-form-urlencoded',
error: AnimeClient.noop
};
config = {
...defaultConfig,
...config,
};
let request = new XMLHttpRequest();
let method = String(config.type).toUpperCase();
if (method === 'GET') {
url += (url.match(/\?/))
? ajaxSerialize(config.data)
: `?${ajaxSerialize(config.data)}`;
}
request.open(method, url);
request.onreadystatechange = () => {
if (request.readyState === 4) {
let responseText = '';
if (request.responseType === 'json') {
responseText = JSON.parse(request.responseText);
} else {
responseText = request.responseText;
}
if (request.status > 299) {
config.error.call(null, request.status, responseText, request.response);
} else {
config.success.call(null, responseText, request.status);
}
}
};
if (config.dataType === 'json') {
config.data = JSON.stringify(config.data);
config.mimeType = 'application/json';
} else {
config.data = ajaxSerialize(config.data);
}
request.setRequestHeader('Content-Type', config.mimeType);
if (method === 'GET') {
request.send(null);
} else {
request.send(config.data);
}
return request
};
/**
* Do a get request
*
* @param {string} url
* @param {object|function} data
* @param {function} [callback]
* @return {XMLHttpRequest}
*/
AnimeClient.get = (url, data, callback = null) => {
if (callback === null) {
callback = data;
data = {};
}
return AnimeClient.ajax(url, {
data,
success: callback
});
};
// ----------------------------------------------------------------------------
// Event subscriptions
// ----------------------------------------------------------------------------
AnimeClient.on('header', 'click', '.message', hide);
AnimeClient.on('form.js-delete', 'submit', confirmDelete);
AnimeClient.on('.js-clear-cache', 'click', clearAPICache);
AnimeClient.on('.vertical-tabs input', 'change', scrollToSection);
AnimeClient.on('.media-filter', 'input', filterMedia);
// ----------------------------------------------------------------------------
// Handler functions
// ----------------------------------------------------------------------------
/**
* Hide the html element attached to the event
*
* @param event
* @return void
*/
function hide (event) {
AnimeClient.hide(event.target);
}
/**
* Confirm deletion of an item
*
* @param event
* @return void
*/
function confirmDelete (event) {
const proceed = confirm('Are you ABSOLUTELY SURE you want to delete this item?');
if (proceed === false) {
event.preventDefault();
event.stopPropagation();
}
}
/**
* Clear the API cache, and show a message if the cache is cleared
*
* @return void
*/
function clearAPICache () {
AnimeClient.get('/cache_purge', () => {
AnimeClient.showMessage('success', 'Successfully purged api cache');
});
}
/**
* Scroll to the accordion/vertical tab section just opened
*
* @param event
* @return void
*/
function scrollToSection (event) {
const el = event.currentTarget.parentElement;
const rect = el.getBoundingClientRect();
const top = rect.top + window.pageYOffset;
window.scrollTo({
top,
behavior: 'smooth',
});
}
/**
* Filter an anime or manga list
*
* @param event
* @return void
*/
function filterMedia (event) {
const rawFilter = event.target.value;
const filter = new RegExp(rawFilter, 'i');
// console.log('Filtering items by: ', filter);
if (rawFilter !== '') {
// Filter the cover view
AnimeClient.$('article.media').forEach(article => {
const titleLink = AnimeClient.$('.name a', article)[0];
const title = String(titleLink.textContent).trim();
if ( ! filter.test(title)) {
AnimeClient.hide(article);
} else {
AnimeClient.show(article);
}
});
// Filter the list view
AnimeClient.$('table.media-wrap tbody tr').forEach(tr => {
const titleCell = AnimeClient.$('td.align-left', tr)[0];
const titleLink = AnimeClient.$('a', titleCell)[0];
const linkTitle = String(titleLink.textContent).trim();
const textTitle = String(titleCell.textContent).trim();
if ( ! (filter.test(linkTitle) || filter.test(textTitle))) {
AnimeClient.hide(tr);
} else {
AnimeClient.show(tr);
}
});
} else {
AnimeClient.show('article.media');
AnimeClient.show('table.media-wrap tbody tr');
}
}
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js').then(reg => {
console.log('Service worker registered', reg.scope);
}).catch(error => {
console.error('Failed to register service worker', error);
});
}
(() => {
// Var is intentional
var hidden = null;
var visibilityChange = null;
if (typeof document.hidden !== "undefined") {
hidden = "hidden";
visibilityChange = "visibilitychange";
} else if (typeof document.msHidden !== "undefined") {
hidden = "msHidden";
visibilityChange = "msvisibilitychange";
} else if (typeof document.webkitHidden !== "undefined") {
hidden = "webkitHidden";
visibilityChange = "webkitvisibilitychange";
}
function handleVisibilityChange() {
// Check the user's session to see if they are currently logged-in
// when the page becomes visible
if ( ! document[hidden]) {
AnimeClient.get('/heartbeat', (beat) => {
const status = JSON.parse(beat);
// If the session is expired, immediately reload so that
// you can't attempt to do an action that requires authentication
if (status.hasAuth !== true) {
document.removeEventListener(visibilityChange, handleVisibilityChange, false);
location.reload();
}
});
}
}
if (hidden === null) {
console.info('Page visibility API not supported, JS session check will not work');
} else {
document.addEventListener(visibilityChange, handleVisibilityChange, false);
}
})();
// Click on hidden MAL checkbox so
// that MAL id is passed
AnimeClient.on('main', 'change', '.big-check', (e) => {
const id = e.target.id;
document.getElementById(`mal_${id}`).checked = true;
});
function renderAnimeSearchResults (data) {
const results = [];
data.forEach(item => {
const titles = item.titles.join('<br />');
results.push(`
<article class="media search">
<div class="name">
<input type="radio" class="mal-check" id="mal_${item.slug}" name="mal_id" value="${item.mal_id}" />
<input type="radio" class="big-check" id="${item.slug}" name="id" value="${item.id}" />
<label for="${item.slug}">
<picture width="220">
<source srcset="/public/images/anime/${item.id}.webp" type="image/webp" />
<source srcset="/public/images/anime/${item.id}.jpg" type="image/jpeg" />
<img src="/public/images/anime/${item.id}.jpg" alt="" width="220" />
</picture>
<span class="name">
${item.canonicalTitle}<br />
<small>${titles}</small>
</span>
</label>
</div>
<div class="table">
<div class="row">
<span class="edit">
<a class="bracketed" href="/anime/details/${item.slug}">Info Page</a>
</span>
</div>
</div>
</article>
`);
});
return results.join('');
}
function renderMangaSearchResults (data) {
const results = [];
data.forEach(item => {
const titles = item.titles.join('<br />');
results.push(`
<article class="media search">
<div class="name">
<input type="radio" id="mal_${item.slug}" name="mal_id" value="${item.mal_id}" />
<input type="radio" class="big-check" id="${item.slug}" name="id" value="${item.id}" />
<label for="${item.slug}">
<picture width="220">
<source srcset="/public/images/manga/${item.id}.webp" type="image/webp" />
<source srcset="/public/images/manga/${item.id}.jpg" type="image/jpeg" />
<img src="/public/images/manga/${item.id}.jpg" alt="" width="220" />
</picture>
<span class="name">
${item.canonicalTitle}<br />
<small>${titles}</small>
</span>
</label>
</div>
<div class="table">
<div class="row">
<span class="edit">
<a class="bracketed" href="/manga/details/${item.slug}">Info Page</a>
</span>
</div>
</div>
</article>
`);
});
return results.join('');
}
const search = (query) => {
// Show the loader
AnimeClient.show('.cssload-loader');
// Do the api search
return AnimeClient.get(AnimeClient.url('/anime-collection/search'), { query }, (searchResults, status) => {
searchResults = JSON.parse(searchResults);
// Hide the loader
AnimeClient.hide('.cssload-loader');
// Show the results
AnimeClient.$('#series-list')[ 0 ].innerHTML = renderAnimeSearchResults(searchResults);
});
};
if (AnimeClient.hasElement('.anime #search')) {
let prevRequest = null;
AnimeClient.on('#search', 'input', AnimeClient.throttle(250, (e) => {
const query = encodeURIComponent(e.target.value);
if (query === '') {
return;
}
if (prevRequest !== null) {
prevRequest.abort();
}
prevRequest = search(query);
}));
}
// Action to increment episode count
AnimeClient.on('body.anime.list', 'click', '.plus-one', (e) => {
let parentSel = AnimeClient.closestParent(e.target, 'article');
let watchedCount = parseInt(AnimeClient.$('.completed_number', parentSel)[ 0 ].textContent, 10) || 0;
let totalCount = parseInt(AnimeClient.$('.total_number', parentSel)[ 0 ].textContent, 10);
let title = AnimeClient.$('.name a', parentSel)[ 0 ].textContent;
// Setup the update data
let data = {
id: parentSel.dataset.kitsuId,
mal_id: parentSel.dataset.malId,
data: {
progress: watchedCount + 1
}
};
// If the episode count is 0, and incremented,
// change status to currently watching
if (isNaN(watchedCount) || watchedCount === 0) {
data.data.status = 'CURRENT';
}
// If you increment at the last episode, mark as completed
if ((!isNaN(watchedCount)) && (watchedCount + 1) === totalCount) {
data.data.status = 'COMPLETED';
}
AnimeClient.show('#loading-shadow');
// okay, lets actually make some changes!
AnimeClient.ajax(AnimeClient.url('/anime/increment'), {
data,
dataType: 'json',
type: 'POST',
success: (res) => {
const resData = JSON.parse(res);
if (resData.errors) {
AnimeClient.hide('#loading-shadow');
AnimeClient.showMessage('error', `Failed to update ${title}. `);
AnimeClient.scrollToTop();
return;
}
if (resData.data.libraryEntry.update.libraryEntry.status === 'COMPLETED') {
AnimeClient.hide(parentSel);
}
AnimeClient.hide('#loading-shadow');
AnimeClient.showMessage('success', `Successfully updated ${title}`);
AnimeClient.$('.completed_number', parentSel)[ 0 ].textContent = ++watchedCount;
AnimeClient.scrollToTop();
},
error: () => {
AnimeClient.hide('#loading-shadow');
AnimeClient.showMessage('error', `Failed to update ${title}. `);
AnimeClient.scrollToTop();
}
});
});
const search$1 = (query) => {
AnimeClient.show('.cssload-loader');
return AnimeClient.get(AnimeClient.url('/manga/search'), { query }, (searchResults, status) => {
searchResults = JSON.parse(searchResults);
AnimeClient.hide('.cssload-loader');
AnimeClient.$('#series-list')[ 0 ].innerHTML = renderMangaSearchResults(searchResults);
});
};
if (AnimeClient.hasElement('.manga #search')) {
let prevRequest = null;
AnimeClient.on('#search', 'input', AnimeClient.throttle(250, (e) => {
let query = encodeURIComponent(e.target.value);
if (query === '') {
return;
}
if (prevRequest !== null) {
prevRequest.abort();
}
prevRequest = search$1(query);
}));
}
/**
* Javascript for editing manga, if logged in
*/
AnimeClient.on('.manga.list', 'click', '.edit-buttons button', (e) => {
let thisSel = e.target;
let parentSel = AnimeClient.closestParent(e.target, 'article');
let type = thisSel.classList.contains('plus-one-chapter') ? 'chapter' : 'volume';
let completed = parseInt(AnimeClient.$(`.${type}s_read`, parentSel)[ 0 ].textContent, 10) || 0;
let total = parseInt(AnimeClient.$(`.${type}_count`, parentSel)[ 0 ].textContent, 10);
let mangaName = AnimeClient.$('.name', parentSel)[ 0 ].textContent;
if (isNaN(completed)) {
completed = 0;
}
// Setup the update data
let data = {
id: parentSel.dataset.kitsuId,
mal_id: parentSel.dataset.malId,
data: {
progress: completed
}
};
// If the episode count is 0, and incremented,
// change status to currently reading
if (isNaN(completed) || completed === 0) {
data.data.status = 'CURRENT';
}
// If you increment at the last chapter, mark as completed
if ((!isNaN(completed)) && (completed + 1) === total) {
data.data.status = 'COMPLETED';
}
// Update the total count
data.data.progress = ++completed;
AnimeClient.show('#loading-shadow');
AnimeClient.ajax(AnimeClient.url('/manga/increment'), {
data,
dataType: 'json',
type: 'POST',
mimeType: 'application/json',
success: () => {
if (String(data.data.status).toUpperCase() === 'COMPLETED') {
AnimeClient.hide(parentSel);
}
AnimeClient.hide('#loading-shadow');
AnimeClient.$(`.${type}s_read`, parentSel)[ 0 ].textContent = completed;
AnimeClient.showMessage('success', `Successfully updated ${mangaName}`);
AnimeClient.scrollToTop();
},
error: () => {
AnimeClient.hide('#loading-shadow');
AnimeClient.showMessage('error', `Failed to update ${mangaName}`);
AnimeClient.scrollToTop();
}
});
});

14
public/js/anon.min.js vendored
View File

@ -1,14 +0,0 @@
(function(){var matches=function(elm,selector){var m=(elm.document||elm.ownerDocument).querySelectorAll(selector);var i=matches.length;while(--i>=0&&m.item(i)!==elm);return i>-1};var AnimeClient={noop:function(){},$:function(selector,context){context=context===undefined?null:context;if(typeof selector!=="string")return selector;context=context!==null&&context.nodeType===1?context:document;var elements=[];if(selector.match(/^#([\w]+$)/))elements.push(document.getElementById(selector.split("#")[1]));
else elements=[].slice.apply(context.querySelectorAll(selector));return elements},hasElement:function(selector){return AnimeClient.$(selector).length>0},scrollToTop:function(){var el=AnimeClient.$("header")[0];el.scrollIntoView(true)},hide:function(sel){if(typeof sel==="string")sel=AnimeClient.$(sel);if(Array.isArray(sel))sel.forEach(function(el){return el.setAttribute("hidden","hidden")});else sel.setAttribute("hidden","hidden")},show:function(sel){if(typeof sel==="string")sel=AnimeClient.$(sel);
if(Array.isArray(sel))sel.forEach(function(el){return el.removeAttribute("hidden")});else sel.removeAttribute("hidden")},showMessage:function(type,message){var template="<div class='message "+type+"'>\n\t\t\t\t<span class='icon'></span>\n\t\t\t\t"+message+"\n\t\t\t\t<span class='close'></span>\n\t\t\t</div>";var sel=AnimeClient.$(".message");if(sel[0]!==undefined)sel[0].remove();AnimeClient.$("header")[0].insertAdjacentHTML("beforeend",template)},closestParent:function(current,parentSelector){if(Element.prototype.closest!==
undefined)return current.closest(parentSelector);while(current!==document.documentElement){if(matches(current,parentSelector))return current;current=current.parentElement}return null},url:function(path){var uri="//"+document.location.host;uri+=path.charAt(0)==="/"?path:"/"+path;return uri},throttle:function(interval,fn,scope){var wait=false;return function(args){var $jscomp$restParams=[];for(var $jscomp$restIndex=0;$jscomp$restIndex<arguments.length;++$jscomp$restIndex)$jscomp$restParams[$jscomp$restIndex-
0]=arguments[$jscomp$restIndex];{var args$0=$jscomp$restParams;var context=scope||this;if(!wait){fn.apply(context,args$0);wait=true;setTimeout(function(){wait=false},interval)}}}}};function addEvent(sel,event,listener){if(!event.match(/^([\w\-]+)$/))event.split(" ").forEach(function(evt){addEvent(sel,evt,listener)});sel.addEventListener(event,listener,false)}function delegateEvent(sel,target,event,listener){addEvent(sel,event,function(e){AnimeClient.$(target,sel).forEach(function(element){if(e.target==
element){listener.call(element,e);e.stopPropagation()}})})}AnimeClient.on=function(sel,event,target,listener){if(listener===undefined){listener=target;AnimeClient.$(sel).forEach(function(el){addEvent(el,event,listener)})}else AnimeClient.$(sel).forEach(function(el){delegateEvent(el,target,event,listener)})};function ajaxSerialize(data){var pairs=[];Object.keys(data).forEach(function(name){var value=data[name].toString();name=encodeURIComponent(name);value=encodeURIComponent(value);pairs.push(name+
"="+value)});return pairs.join("&")}AnimeClient.ajax=function(url,config){var defaultConfig={data:{},type:"GET",dataType:"",success:AnimeClient.noop,mimeType:"application/x-www-form-urlencoded",error:AnimeClient.noop};config=Object.assign({},defaultConfig,config);var request=new XMLHttpRequest;var method=String(config.type).toUpperCase();if(method==="GET")url+=url.match(/\?/)?ajaxSerialize(config.data):"?"+ajaxSerialize(config.data);request.open(method,url);request.onreadystatechange=function(){if(request.readyState===
4){var responseText="";if(request.responseType==="json")responseText=JSON.parse(request.responseText);else responseText=request.responseText;if(request.status>299)config.error.call(null,request.status,responseText,request.response);else config.success.call(null,responseText,request.status)}};if(config.dataType==="json"){config.data=JSON.stringify(config.data);config.mimeType="application/json"}else config.data=ajaxSerialize(config.data);request.setRequestHeader("Content-Type",config.mimeType);if(method===
"GET")request.send(null);else request.send(config.data);return request};AnimeClient.get=function(url,data,callback){callback=callback===undefined?null:callback;if(callback===null){callback=data;data={}}return AnimeClient.ajax(url,{data:data,success:callback})};AnimeClient.on("header","click",".message",hide);AnimeClient.on("form.js-delete","submit",confirmDelete);AnimeClient.on(".js-clear-cache","click",clearAPICache);AnimeClient.on(".vertical-tabs input","change",scrollToSection);AnimeClient.on(".media-filter",
"input",filterMedia);function hide(event){AnimeClient.hide(event.target)}function confirmDelete(event){var proceed=confirm("Are you ABSOLUTELY SURE you want to delete this item?");if(proceed===false){event.preventDefault();event.stopPropagation()}}function clearAPICache(){AnimeClient.get("/cache_purge",function(){AnimeClient.showMessage("success","Successfully purged api cache")})}function scrollToSection(event){var el=event.currentTarget.parentElement;var rect=el.getBoundingClientRect();var top=
rect.top+window.pageYOffset;window.scrollTo({top:top,behavior:"smooth"})}function filterMedia(event){var rawFilter=event.target.value;var filter=new RegExp(rawFilter,"i");if(rawFilter!==""){AnimeClient.$("article.media").forEach(function(article){var titleLink=AnimeClient.$(".name a",article)[0];var title=String(titleLink.textContent).trim();if(!filter.test(title))AnimeClient.hide(article);else AnimeClient.show(article)});AnimeClient.$("table.media-wrap tbody tr").forEach(function(tr){var titleCell=
AnimeClient.$("td.align-left",tr)[0];var titleLink=AnimeClient.$("a",titleCell)[0];var linkTitle=String(titleLink.textContent).trim();var textTitle=String(titleCell.textContent).trim();if(!(filter.test(linkTitle)||filter.test(textTitle)))AnimeClient.hide(tr);else AnimeClient.show(tr)})}else{AnimeClient.show("article.media");AnimeClient.show("table.media-wrap tbody tr")}}if("serviceWorker"in navigator)navigator.serviceWorker.register("/sw.js").then(function(reg){console.log("Service worker registered",
reg.scope)})["catch"](function(error){console.error("Failed to register service worker",error)})})()
//# sourceMappingURL=anon.min.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1 @@
(function(){var LightTableSorter=function(){var th=null;var cellIndex=null;var order="";var text=function(row){return row.cells.item(cellIndex).textContent.toLowerCase()};var sort=function(a,b){var textA=text(a);var textB=text(b);var n=parseInt(textA,10);if(n){textA=n;textB=parseInt(textB,10)}if(textA>textB)return 1;if(textA<textB)return-1;return 0};var toggle=function(){var c=order!=="sorting-asc"?"sorting-asc":"sorting-desc";th.className=(th.className.replace(order,"")+" "+c).trim();return order= var LightTableSorter=function(){var th=null;var cellIndex=null;var order='';var text=function(row){return row.cells.item(cellIndex).textContent.toLowerCase();};var sort=function(a,b){var textA=text(a);var textB=text(b);var n=parseInt(textA,10);if(n){textA=n;textB=parseInt(textB,10);}if(textA>textB)return 1;if(textA<textB)return -1;return 0;};var toggle=function(){var c=order!=='sorting-asc'?'sorting-asc':'sorting-desc';th.className=(th.className.replace(order,'')+' '+c).trim();return order=c;};var reset=function(){th.classList.remove('sorting-asc','sorting-desc');th.classList.add('sorting');return order='';};var onClickEvent=function(e){if(th&&cellIndex!==e.target.cellIndex)reset();th=e.target;if(th.nodeName.toLowerCase()==='th'){cellIndex=th.cellIndex;var tbody=th.offsetParent.getElementsByTagName('tbody')[0];var rows=Array.from(tbody.rows);if(rows){rows.sort(sort);if(order==='sorting-asc')rows.reverse();toggle();tbody.innerHtml='';rows.forEach(function(row){tbody.appendChild(row);});}}};return {init:function(){var ths=document.getElementsByTagName('th');var results=[];for(var i=0,len=ths.length;i<len;i++){var th1=ths[i];th1.classList.add('sorting');results.push(th1.onclick=onClickEvent);}return results;}};}();LightTableSorter.init();
c};var reset=function(){th.classList.remove("sorting-asc","sorting-desc");th.classList.add("sorting");return order=""};var onClickEvent=function(e){if(th&&cellIndex!==e.target.cellIndex)reset();th=e.target;if(th.nodeName.toLowerCase()==="th"){cellIndex=th.cellIndex;var tbody=th.offsetParent.getElementsByTagName("tbody")[0];var rows=Array.from(tbody.rows);if(rows){rows.sort(sort);if(order==="sorting-asc")rows.reverse();toggle();tbody.innerHtml="";rows.forEach(function(row){tbody.appendChild(row)})}}};
return{init:function(){var ths=document.getElementsByTagName("th");var results=[];for(var i=0,len=ths.length;i<len;i++){var th$0=ths[i];th$0.classList.add("sorting");results.push(th$0.onclick=onClickEvent)}return results}}}();LightTableSorter.init()})()
//# sourceMappingURL=tables.min.js.map

View File

@ -1 +1 @@
{"version":3,"file":"tables.min.js.map","sources":["../../frontEndSrc/js/base/sort-tables.js"],"sourcesContent":["const LightTableSorter = (() => {\n\tlet th = null;\n\tlet cellIndex = null;\n\tlet order = '';\n\tconst text = (row) => row.cells.item(cellIndex).textContent.toLowerCase();\n\tconst sort = (a, b) => {\n\t\tlet textA = text(a);\n\t\tlet textB = text(b);\n\t\tconst n = parseInt(textA, 10);\n\t\tif (n) {\n\t\t\ttextA = n;\n\t\t\ttextB = parseInt(textB, 10);\n\t\t}\n\t\tif (textA > textB) {\n\t\t\treturn 1;\n\t\t}\n\t\tif (textA < textB) {\n\t\t\treturn -1;\n\t\t}\n\t\treturn 0;\n\t};\n\tconst toggle = () => {\n\t\tconst c = order !== 'sorting-asc' ? 'sorting-asc' : 'sorting-desc';\n\t\tth.className = (th.className.replace(order, '') + ' ' + c).trim();\n\t\treturn order = c;\n\t};\n\tconst reset = () => {\n\t\tth.classList.remove('sorting-asc', 'sorting-desc');\n\t\tth.classList.add('sorting');\n\t\treturn order = '';\n\t};\n\tconst onClickEvent = (e) => {\n\t\tif (th && (cellIndex !== e.target.cellIndex)) {\n\t\t\treset();\n\t\t}\n\t\tth = e.target;\n\t\tif (th.nodeName.toLowerCase() === 'th') {\n\t\t\tcellIndex = th.cellIndex;\n\t\t\tconst tbody = th.offsetParent.getElementsByTagName('tbody')[0];\n\t\t\tlet rows = Array.from(tbody.rows);\n\t\t\tif (rows) {\n\t\t\t\trows.sort(sort);\n\t\t\t\tif (order === 'sorting-asc') {\n\t\t\t\t\trows.reverse();\n\t\t\t\t}\n\t\t\t\ttoggle();\n\t\t\t\ttbody.innerHtml = '';\n\n\t\t\t\trows.forEach(row => {\n\t\t\t\t\ttbody.appendChild(row);\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t};\n\treturn {\n\t\tinit: () => {\n\t\t\tlet ths = document.getElementsByTagName('th');\n\t\t\tlet results = [];\n\t\t\tfor (let i = 0, len = ths.length; i < len; i++) {\n\t\t\t\tlet th = ths[i];\n\t\t\t\tth.classList.add('sorting');\n\t\t\t\tresults.push(th.onclick = onClickEvent);\n\t\t\t}\n\t\t\treturn results;\n\t\t}\n\t};\n})();\n\nLightTableSorter.init();"],"names":["th","cellIndex","order","text","row","cells","item","textContent","toLowerCase","sort","a","b","textA","textB","n","parseInt","toggle","c","className","trim","replace","reset","classList","remove","add","onClickEvent","e","target","nodeName","tbody","offsetParent","getElementsByTagName","rows","Array","from","reverse","innerHtml","forEach","appendChild","init","ths","document","results","i","len","length","push","onclick","LightTableSorter"],"mappings":"YAAA,gCACC,IAAIA,GAAK,IACT,KAAIC,UAAY,IAChB,KAAIC,MAAQ,EACZ,KAAMC,KAAOA,QAAA,CAACC,GAAD,CAAS,CAAA,MAAAA,IAAAC,MAAAC,KAAA,CAAeL,SAAf,CAAAM,YAAAC,YAAA,EAAA,CACtB,KAAMC,KAAOA,QAAA,CAACC,CAAD,CAAIC,CAAJ,CAAU,CACtB,IAAIC,MAAQT,IAAA,CAAKO,CAAL,CACZ,KAAIG,MAAQV,IAAA,CAAKQ,CAAL,CACZ,KAAMG,EAAIC,QAAA,CAASH,KAAT,CAAgB,EAAhB,CACV,IAAIE,CAAJ,CAAO,CACNF,KAAA,CAAQE,CACRD,MAAA,CAAQE,QAAA,CAASF,KAAT,CAAgB,EAAhB,CAFF,CAIP,GAAID,KAAJ,CAAYC,KAAZ,CACC,MAAO,EAER,IAAID,KAAJ,CAAYC,KAAZ,CACC,MAAO,EAER,OAAO,EAde,CAgBvB,KAAMG,OAASA,QAAA,EAAM,CACpB,IAAMC,EAAIf,KAAA,GAAU,aAAV,CAA0B,aAA1B,CAA0C,cACpDF,GAAAkB,UAAA,CAAeC,CAACnB,EAAAkB,UAAAE,QAAA,CAAqBlB,KAArB,CAA4B,EAA5B,CAADiB,CAAmC,GAAnCA,CAAyCF,CAAzCE,MAAA,EACf,OAAOjB,MAAP;AAAee,CAHK,CAKrB,KAAMI,MAAQA,QAAA,EAAM,CACnBrB,EAAAsB,UAAAC,OAAA,CAAoB,aAApB,CAAmC,cAAnC,CACAvB,GAAAsB,UAAAE,IAAA,CAAiB,SAAjB,CACA,OAAOtB,MAAP,CAAe,EAHI,CAKpB,KAAMuB,aAAeA,QAAA,CAACC,CAAD,CAAO,CAC3B,GAAI1B,EAAJ,EAAWC,SAAX,GAAyByB,CAAAC,OAAA1B,UAAzB,CACCoB,KAAA,EAEDrB,GAAA,CAAK0B,CAAAC,OACL,IAAI3B,EAAA4B,SAAApB,YAAA,EAAJ,GAAkC,IAAlC,CAAwC,CACvCP,SAAA,CAAYD,EAAAC,UACZ,KAAM4B,MAAQ7B,EAAA8B,aAAAC,qBAAA,CAAqC,OAArC,CAAA,CAA8C,CAA9C,CACd,KAAIC,KAAOC,KAAAC,KAAA,CAAWL,KAAAG,KAAX,CACX,IAAIA,IAAJ,CAAU,CACTA,IAAAvB,KAAA,CAAUA,IAAV,CACA,IAAIP,KAAJ,GAAc,aAAd,CACC8B,IAAAG,QAAA,EAEDnB,OAAA,EACAa,MAAAO,UAAA,CAAkB,EAElBJ,KAAAK,QAAA,CAAa,QAAA,CAAAjC,GAAA,CAAO,CACnByB,KAAAS,YAAA,CAAkBlC,GAAlB,CADmB,CAApB,CARS,CAJ6B,CALb,CAuB5B;MAAO,CACNmC,KAAMA,QAAA,EAAM,CACX,IAAIC,IAAMC,QAAAV,qBAAA,CAA8B,IAA9B,CACV,KAAIW,QAAU,EACd,KAAK,IAAIC,EAAI,CAAR,CAAWC,IAAMJ,GAAAK,OAAtB,CAAkCF,CAAlC,CAAsCC,GAAtC,CAA2CD,CAAA,EAA3C,CAAgD,CAC/C,IAAI3C,KAAKwC,GAAA,CAAIG,CAAJ,CACT3C,KAAAsB,UAAAE,IAAA,CAAiB,SAAjB,CACAkB,QAAAI,KAAA,CAAa9C,IAAA+C,QAAb,CAA0BtB,YAA1B,CAH+C,CAKhD,MAAOiB,QARI,CADN,IAcRM,iBAAAT,KAAA;"} {"version":3,"sources":["/var/www/htdocs/github.timshomepage.net/animeclient/frontEndSrc/js/base/sort-tables.js"],"sourcesContent":["const LightTableSorter = (() => {\n\tlet th = null;\n\tlet cellIndex = null;\n\tlet order = '';\n\tconst text = (row) => row.cells.item(cellIndex).textContent.toLowerCase();\n\tconst sort = (a, b) => {\n\t\tlet textA = text(a);\n\t\tlet textB = text(b);\n\t\tconst n = parseInt(textA, 10);\n\t\tif (n) {\n\t\t\ttextA = n;\n\t\t\ttextB = parseInt(textB, 10);\n\t\t}\n\t\tif (textA > textB) {\n\t\t\treturn 1;\n\t\t}\n\t\tif (textA < textB) {\n\t\t\treturn -1;\n\t\t}\n\t\treturn 0;\n\t};\n\tconst toggle = () => {\n\t\tconst c = order !== 'sorting-asc' ? 'sorting-asc' : 'sorting-desc';\n\t\tth.className = (th.className.replace(order, '') + ' ' + c).trim();\n\t\treturn order = c;\n\t};\n\tconst reset = () => {\n\t\tth.classList.remove('sorting-asc', 'sorting-desc');\n\t\tth.classList.add('sorting');\n\t\treturn order = '';\n\t};\n\tconst onClickEvent = (e) => {\n\t\tif (th && (cellIndex !== e.target.cellIndex)) {\n\t\t\treset();\n\t\t}\n\t\tth = e.target;\n\t\tif (th.nodeName.toLowerCase() === 'th') {\n\t\t\tcellIndex = th.cellIndex;\n\t\t\tconst tbody = th.offsetParent.getElementsByTagName('tbody')[0];\n\t\t\tlet rows = Array.from(tbody.rows);\n\t\t\tif (rows) {\n\t\t\t\trows.sort(sort);\n\t\t\t\tif (order === 'sorting-asc') {\n\t\t\t\t\trows.reverse();\n\t\t\t\t}\n\t\t\t\ttoggle();\n\t\t\t\ttbody.innerHtml = '';\n\n\t\t\t\trows.forEach(row => {\n\t\t\t\t\ttbody.appendChild(row);\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t};\n\treturn {\n\t\tinit: () => {\n\t\t\tlet ths = document.getElementsByTagName('th');\n\t\t\tlet results = [];\n\t\t\tfor (let i = 0, len = ths.length; i < len; i++) {\n\t\t\t\tlet th = ths[i];\n\t\t\t\tth.classList.add('sorting');\n\t\t\t\tresults.push(th.onclick = onClickEvent);\n\t\t\t}\n\t\t\treturn results;\n\t\t}\n\t};\n})();\n\nLightTableSorter.init();"],"names":[],"mappings":"IAAM,gBAAgB,gBACjB,EAAE,CAAG,IAAI,KACT,SAAS,CAAG,IAAI,KAChB,KAAK,QACH,IAAI,UAAI,GAAG,SAAK,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,EAAE,WAAW,CAAC,WAAW,SACjE,IAAI,UAAI,CAAC,CAAE,CAAC,MACb,KAAK,CAAG,IAAI,CAAC,CAAC,MACd,KAAK,CAAG,IAAI,CAAC,CAAC,MACZ,CAAC,CAAG,QAAQ,CAAC,KAAK,CAAE,EAAE,KACxB,CAAC,EACJ,KAAK,CAAG,CAAC,CACT,KAAK,CAAG,QAAQ,CAAC,KAAK,CAAE,EAAE,MAEvB,KAAK,CAAG,KAAK,QACT,CAAC,IAEL,KAAK,CAAG,KAAK,QACT,EAAE,QAEH,CAAC,OAEH,MAAM,gBACL,CAAC,CAAG,KAAK,IAAK,WAAa,GAAG,WAAa,GAAG,YAAc,EAClE,EAAE,CAAC,SAAS,EAAI,EAAE,CAAC,SAAS,CAAC,OAAO,CAAC,KAAK,MAAQ,CAAG,EAAG,CAAC,EAAE,IAAI,UACxD,KAAK,CAAG,CAAC,OAEX,KAAK,YACV,EAAE,CAAC,SAAS,CAAC,MAAM,EAAC,WAAa,GAAE,YAAc,GACjD,EAAE,CAAC,SAAS,CAAC,GAAG,EAAC,OAAS,UACnB,KAAK,UAEP,YAAY,UAAI,CAAC,KAClB,EAAE,EAAK,SAAS,GAAK,CAAC,CAAC,MAAM,CAAC,SAAS,CAC1C,KAAK,GAEN,EAAE,CAAG,CAAC,CAAC,MAAM,IACT,EAAE,CAAC,QAAQ,CAAC,WAAW,MAAO,EAAI,GACrC,SAAS,CAAG,EAAE,CAAC,SAAS,KAClB,KAAK,CAAG,EAAE,CAAC,YAAY,CAAC,oBAAoB,EAAC,KAAO,GAAE,CAAC,MACzD,IAAI,CAAG,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,KAC5B,IAAI,EACP,IAAI,CAAC,IAAI,CAAC,IAAI,KACV,KAAK,IAAK,WAAa,EAC1B,IAAI,CAAC,OAAO,GAEb,MAAM,GACN,KAAK,CAAC,SAAS,IAEf,IAAI,CAAC,OAAO,UAAC,GAAG,EACf,KAAK,CAAC,WAAW,CAAC,GAAG,iBAMxB,IAAI,gBACC,GAAG,CAAG,QAAQ,CAAC,oBAAoB,EAAC,EAAI,OACxC,OAAO,YACF,CAAC,CAAG,CAAC,CAAE,GAAG,CAAG,GAAG,CAAC,MAAM,CAAE,CAAC,CAAG,GAAG,CAAE,CAAC,QACvC,GAAE,CAAG,GAAG,CAAC,CAAC,EACd,GAAE,CAAC,SAAS,CAAC,GAAG,EAAC,OAAS,GAC1B,OAAO,CAAC,IAAI,CAAC,GAAE,CAAC,OAAO,CAAG,YAAY,UAEhC,OAAO,QAKjB,gBAAgB,CAAC,IAAI"}

View File

@ -272,7 +272,7 @@ abstract class APIRequestBuilder {
throw new InvalidArgumentException('Invalid HTTP method'); throw new InvalidArgumentException('Invalid HTTP method');
} }
$realUrl = (strpos($uri, '//') !== FALSE) $realUrl = (str_contains($uri, '//'))
? $uri ? $uri
: $this->baseUrl . $uri; : $this->baseUrl . $uri;
@ -297,7 +297,7 @@ abstract class APIRequestBuilder {
*/ */
private function buildUri(): Request private function buildUri(): Request
{ {
$url = (strpos($this->path, '//') !== FALSE) $url = (str_contains($this->path, '//'))
? $this->path ? $this->path
: $this->baseUrl . $this->path; : $this->baseUrl . $this->path;
@ -314,11 +314,11 @@ abstract class APIRequestBuilder {
/** /**
* Reset the class state for a new request * Reset the class state for a new request
* *
* @param string $url * @param string|null $url
* @param string $type * @param string $type
* @return void * @return void
*/ */
private function resetState($url, $type = 'GET'): void private function resetState(?string $url, $type = 'GET'): void
{ {
$requestUrl = $url ?: $this->baseUrl; $requestUrl = $url ?: $this->baseUrl;

View File

@ -187,7 +187,7 @@ type AiringProgression {
watching: Int watching: Int
} }
"Media Airing Schedule" "Media Airing Schedule. NOTE: We only aim to guarantee that FUTURE airing data is present and accurate."
type AiringSchedule { type AiringSchedule {
"The time the episode airs at" "The time the episode airs at"
airingAt: Int! airingAt: Int!
@ -225,6 +225,10 @@ type AniChartUser {
"A character that features in an anime or manga" "A character that features in an anime or manga"
type Character { type Character {
"The character's age. Note this is a string, not an int, it may contain further text and additional ages."
age: String
"The character's birth date"
dateOfBirth: FuzzyDate
"A general description of the character" "A general description of the character"
description( description(
"Return the string in pre-parsed html instead of markdown" "Return the string in pre-parsed html instead of markdown"
@ -232,14 +236,19 @@ type Character {
): String ): String
"The amount of user's who have favourited the character" "The amount of user's who have favourited the character"
favourites: Int favourites: Int
"The character's gender. Usually Male, Female, or Non-binary but can be any string."
gender: String
"The id of the character" "The id of the character"
id: Int! id: Int!
"Character images" "Character images"
image: CharacterImage image: CharacterImage
"If the character is marked as favourite by the currently authenticated user" "If the character is marked as favourite by the currently authenticated user"
isFavourite: Boolean! isFavourite: Boolean!
"If the character is blocked from being added to favourites"
isFavouriteBlocked: Boolean!
"Media that includes the character" "Media that includes the character"
media( media(
onList: Boolean,
"The page" "The page"
page: Int, page: Int,
"The amount of entries per page, max 25" "The amount of entries per page, max 25"
@ -271,9 +280,13 @@ type CharacterEdge {
id: Int id: Int
"The media the character is in" "The media the character is in"
media: [Media] media: [Media]
"Media specific character name"
name: String
node: Character node: Character
"The characters role in the media" "The characters role in the media"
role: CharacterRole role: CharacterRole
"The voice actors of the character with role date"
voiceActorRoles(language: StaffLanguage, sort: [StaffSort]): [StaffRoleType]
"The voice actors of the character" "The voice actors of the character"
voiceActors(language: StaffLanguage, sort: [StaffSort]): [Staff] voiceActors(language: StaffLanguage, sort: [StaffSort]): [Staff]
} }
@ -289,12 +302,16 @@ type CharacterImage {
type CharacterName { type CharacterName {
"Other names the character might be referred to as" "Other names the character might be referred to as"
alternative: [String] alternative: [String]
"Other names the character might be referred to as but are spoilers"
alternativeSpoiler: [String]
"The character's given name" "The character's given name"
first: String first: String
"The character's full name" "The character's first and last name"
full: String full: String
"The character's surname" "The character's surname"
last: String last: String
"The character's middle name"
middle: String
"The character's full name in their native language" "The character's full name in their native language"
native: String native: String
} }
@ -543,6 +560,8 @@ type InternalPage {
id_not: Int, id_not: Int,
"Filter by character id" "Filter by character id"
id_not_in: [Int], id_not_in: [Int],
"Filter by character by if its their birthday today"
isBirthday: Boolean,
"Filter by search query" "Filter by search query"
search: String, search: String,
"The order the results will be returned in" "The order the results will be returned in"
@ -855,7 +874,7 @@ type InternalPage {
mediaType: MediaType, mediaType: MediaType,
"The order the results will be returned in" "The order the results will be returned in"
sort: [ReviewSort], sort: [ReviewSort],
"Filter by media id" "Filter by user id"
userId: Int userId: Int
): [Review] ): [Review]
revisionHistory( revisionHistory(
@ -879,6 +898,8 @@ type InternalPage {
id_not: Int, id_not: Int,
"Filter by the staff id" "Filter by the staff id"
id_not_in: [Int], id_not_in: [Int],
"Filter by staff by if its their birthday today"
isBirthday: Boolean,
"Filter by search query" "Filter by search query"
search: String, search: String,
"The order the results will be returned in" "The order the results will be returned in"
@ -1114,7 +1135,10 @@ type Media {
startDate: FuzzyDate startDate: FuzzyDate
stats: MediaStats stats: MediaStats
"The current releasing status of the media" "The current releasing status of the media"
status: MediaStatus status(
"Provide 2 to use new version 2 of sources enum"
version: Int
): MediaStatus
"Data and links to legal streaming episodes on external sites" "Data and links to legal streaming episodes on external sites"
streamingEpisodes: [MediaStreamingEpisode] streamingEpisodes: [MediaStreamingEpisode]
"The companies who produced the media" "The companies who produced the media"
@ -1151,10 +1175,14 @@ type Media {
type MediaCharacter { type MediaCharacter {
"The characters in the media voiced by the parent actor" "The characters in the media voiced by the parent actor"
character: Character character: Character
"Media specific character name"
characterName: String
dubGroup: String
"The id of the connection" "The id of the connection"
id: Int id: Int
"The characters role in the media" "The characters role in the media"
role: CharacterRole role: CharacterRole
roleNotes: String
"The voice actor of the character" "The voice actor of the character"
voiceActor: Staff voiceActor: Staff
} }
@ -1179,10 +1207,14 @@ type MediaCoverImage {
"Media connection edge" "Media connection edge"
type MediaEdge { type MediaEdge {
"Media specific character name"
characterName: String
"The characters role in the media" "The characters role in the media"
characterRole: CharacterRole characterRole: CharacterRole
"The characters in the media voiced by the parent actor" "The characters in the media voiced by the parent actor"
characters: [Character] characters: [Character]
"Used for grouping roles where multiple dubs exist for the same language. Either dubbing company name or language variant."
dubGroup: String
"The order the media should be displayed from the users favourites" "The order the media should be displayed from the users favourites"
favouriteOrder: Int favouriteOrder: Int
"The id of the connection" "The id of the connection"
@ -1195,8 +1227,12 @@ type MediaEdge {
"Provide 2 to use new version 2 of relation enum" "Provide 2 to use new version 2 of relation enum"
version: Int version: Int
): MediaRelation ): MediaRelation
"Notes regarding the VA's role for the character"
roleNotes: String
"The role of the staff member in the production of the media" "The role of the staff member in the production of the media"
staffRole: String staffRole: String
"The voice actors of the character with role date"
voiceActorRoles(language: StaffLanguage, sort: [StaffSort]): [StaffRoleType]
"The voice actors of the character" "The voice actors of the character"
voiceActors(language: StaffLanguage, sort: [StaffSort]): [Staff] voiceActors(language: StaffLanguage, sort: [StaffSort]): [Staff]
} }
@ -1297,8 +1333,7 @@ type MediaListOptions {
sharedTheme: Json @deprecated(reason : "No longer used") sharedTheme: Json @deprecated(reason : "No longer used")
"If the shared theme should be used instead of the individual list themes" "If the shared theme should be used instead of the individual list themes"
sharedThemeEnabled: Boolean @deprecated(reason : "No longer used") sharedThemeEnabled: Boolean @deprecated(reason : "No longer used")
"(Site only) If the user should be using legacy css-supporting list versions" useLegacyLists: Boolean @deprecated(reason : "No longer used")
useLegacyLists: Boolean
} }
"A user's list options for anime or manga lists" "A user's list options for anime or manga lists"
@ -1388,12 +1423,15 @@ type MediaSubmissionComparison {
type MediaSubmissionEdge { type MediaSubmissionEdge {
character: Character character: Character
characterName: String
characterRole: CharacterRole characterRole: CharacterRole
characterSubmission: Character characterSubmission: Character
dubGroup: String
"The id of the direct submission" "The id of the direct submission"
id: Int id: Int
isMain: Boolean isMain: Boolean
media: Media media: Media
roleNotes: String
staff: Staff staff: Staff
staffRole: String staffRole: String
staffSubmission: Staff staffSubmission: Staff
@ -1814,6 +1852,8 @@ type Mutation {
UpdateUser( UpdateUser(
"User's about/bio text" "User's about/bio text"
about: String, about: String,
"Minutes between activity for them to be merged together. 0 is Never, Above 2 weeks (20160 mins) is Always."
activityMergeTime: Int,
"If the user should get notifications when a show they are watching aires" "If the user should get notifications when a show they are watching aires"
airingNotifications: Boolean, airingNotifications: Boolean,
"The user's anime list options" "The user's anime list options"
@ -1832,6 +1872,8 @@ type Mutation {
rowOrder: String, rowOrder: String,
"The user's list scoring system" "The user's list scoring system"
scoreFormat: ScoreFormat, scoreFormat: ScoreFormat,
"Timezone offset format: -?HH:MM"
timezone: String,
"User's title language" "User's title language"
titleLanguage: UserTitleLanguage titleLanguage: UserTitleLanguage
): User ): User
@ -1958,6 +2000,8 @@ type Page {
id_not: Int, id_not: Int,
"Filter by character id" "Filter by character id"
id_not_in: [Int], id_not_in: [Int],
"Filter by character by if its their birthday today"
isBirthday: Boolean,
"Filter by search query" "Filter by search query"
search: String, search: String,
"The order the results will be returned in" "The order the results will be returned in"
@ -2258,7 +2302,7 @@ type Page {
mediaType: MediaType, mediaType: MediaType,
"The order the results will be returned in" "The order the results will be returned in"
sort: [ReviewSort], sort: [ReviewSort],
"Filter by media id" "Filter by user id"
userId: Int userId: Int
): [Review] ): [Review]
staff( staff(
@ -2270,6 +2314,8 @@ type Page {
id_not: Int, id_not: Int,
"Filter by the staff id" "Filter by the staff id"
id_not_in: [Int], id_not_in: [Int],
"Filter by staff by if its their birthday today"
isBirthday: Boolean,
"Filter by search query" "Filter by search query"
search: String, search: String,
"The order the results will be returned in" "The order the results will be returned in"
@ -2467,6 +2513,8 @@ type Query {
id_not: Int, id_not: Int,
"Filter by character id" "Filter by character id"
id_not_in: [Int], id_not_in: [Int],
"Filter by character by if its their birthday today"
isBirthday: Boolean,
"Filter by search query" "Filter by search query"
search: String, search: String,
"The order the results will be returned in" "The order the results will be returned in"
@ -2837,7 +2885,7 @@ type Query {
mediaType: MediaType, mediaType: MediaType,
"The order the results will be returned in" "The order the results will be returned in"
sort: [ReviewSort], sort: [ReviewSort],
"Filter by media id" "Filter by user id"
userId: Int userId: Int
): Review ): Review
"Site statistics query" "Site statistics query"
@ -2852,6 +2900,8 @@ type Query {
id_not: Int, id_not: Int,
"Filter by the staff id" "Filter by the staff id"
id_not_in: [Int], id_not_in: [Int],
"Filter by staff by if its their birthday today"
isBirthday: Boolean,
"Filter by search query" "Filter by search query"
search: String, search: String,
"The order the results will be returned in" "The order the results will be returned in"
@ -3127,6 +3177,17 @@ type SiteTrendEdge {
"Voice actors or production staff" "Voice actors or production staff"
type Staff { type Staff {
"The person's age in years"
age: Int
"Media the actor voiced characters in. (Same data as characters with media as node instead of characters)"
characterMedia(
onList: Boolean,
"The page"
page: Int,
"The amount of entries per page, max 25"
perPage: Int,
sort: [MediaSort]
): MediaConnection
"Characters voiced by the actor" "Characters voiced by the actor"
characters( characters(
"The page" "The page"
@ -3135,6 +3196,8 @@ type Staff {
perPage: Int, perPage: Int,
sort: [CharacterSort] sort: [CharacterSort]
): CharacterConnection ): CharacterConnection
dateOfBirth: FuzzyDate
dateOfDeath: FuzzyDate
"A general description of the staff member" "A general description of the staff member"
description( description(
"Return the string in pre-parsed html instead of markdown" "Return the string in pre-parsed html instead of markdown"
@ -3142,24 +3205,35 @@ type Staff {
): String ): String
"The amount of user's who have favourited the staff member" "The amount of user's who have favourited the staff member"
favourites: Int favourites: Int
"The staff's gender. Usually Male, Female, or Non-binary but can be any string."
gender: String
"The persons birthplace or hometown"
homeTown: String
"The id of the staff member" "The id of the staff member"
id: Int! id: Int!
"The staff images" "The staff images"
image: StaffImage image: StaffImage
"If the staff member is marked as favourite by the currently authenticated user" "If the staff member is marked as favourite by the currently authenticated user"
isFavourite: Boolean! isFavourite: Boolean!
"The primary language of the staff member" "If the staff member is blocked from being added to favourites"
language: StaffLanguage isFavouriteBlocked: Boolean!
"The primary language the staff member dub's in"
language: StaffLanguage @deprecated(reason : "Replaced with languageV2")
"The primary language of the staff member. Current values: Japanese, English, Korean, Italian, Spanish, Portuguese, French, German, Hebrew, Hungarian, Chinese, Arabic, Filipino, Catalan"
languageV2: String
"Notes for site moderators" "Notes for site moderators"
modNotes: String modNotes: String
"The names of the staff member" "The names of the staff member"
name: StaffName name: StaffName
"The person's primary occupations"
primaryOccupations: [String]
"The url for the staff page on the AniList website" "The url for the staff page on the AniList website"
siteUrl: String siteUrl: String
"Staff member that the submission is referencing" "Staff member that the submission is referencing"
staff: Staff staff: Staff
"Media where the staff member has a production role" "Media where the staff member has a production role"
staffMedia( staffMedia(
onList: Boolean,
"The page" "The page"
page: Int, page: Int,
"The amount of entries per page, max 25" "The amount of entries per page, max 25"
@ -3174,6 +3248,8 @@ type Staff {
"Submitter for the submission" "Submitter for the submission"
submitter: User submitter: User
updatedAt: Int @deprecated(reason : "No data available") updatedAt: Int @deprecated(reason : "No data available")
"[startYear, endYear] (If the 2nd value is not present staff is still active)"
yearsActive: [Int]
} }
type StaffConnection { type StaffConnection {
@ -3207,14 +3283,26 @@ type StaffName {
alternative: [String] alternative: [String]
"The person's given name" "The person's given name"
first: String first: String
"The person's full name" "The person's first and last name"
full: String full: String
"The person's surname" "The person's surname"
last: String last: String
"The person's middle name"
middle: String
"The person's full name in their native language" "The person's full name in their native language"
native: String native: String
} }
"Voice actor role for a character"
type StaffRoleType {
"Used for grouping roles where multiple dubs exist for the same language. Either dubbing company name or language variant."
dubGroup: String
"Notes regarding the VA's role for the character"
roleNotes: String
"The voice actors of the character"
voiceActor: Staff
}
"User's staff statistics" "User's staff statistics"
type StaffStats { type StaffStats {
amount: Int amount: Int
@ -3264,6 +3352,7 @@ type Studio {
media( media(
"If the studio was the primary animation studio of the media" "If the studio was the primary animation studio of the media"
isMain: Boolean, isMain: Boolean,
onList: Boolean,
"The page" "The page"
page: Int, page: Int,
"The amount of entries per page, max 25" "The amount of entries per page, max 25"
@ -3663,6 +3752,8 @@ type UserModData {
"A user's general options" "A user's general options"
type UserOptions { type UserOptions {
"Minutes between activity for them to be merged together. 0 is Never, Above 2 weeks (20160 mins) is Always."
activityMergeTime: Int
"Whether the user receives notifications when a show they are watching aires" "Whether the user receives notifications when a show they are watching aires"
airingNotifications: Boolean airingNotifications: Boolean
"Whether the user has enabled viewing of 18+ content" "Whether the user has enabled viewing of 18+ content"
@ -3671,6 +3762,8 @@ type UserOptions {
notificationOptions: [NotificationOption] notificationOptions: [NotificationOption]
"Profile highlight color (blue, purple, pink, orange, red, green, gray)" "Profile highlight color (blue, purple, pink, orange, red, green, gray)"
profileColor: String profileColor: String
"The user's timezone offset (Auth user only)"
timezone: String
"The language the user wants to see media titles in" "The language the user wants to see media titles in"
titleLanguage: UserTitleLanguage titleLanguage: UserTitleLanguage
} }
@ -3853,6 +3946,8 @@ enum CharacterSort {
FAVOURITES_DESC FAVOURITES_DESC
ID ID
ID_DESC ID_DESC
"Order manually decided by moderators"
RELEVANCE
ROLE ROLE
ROLE_DESC ROLE_DESC
SEARCH_MATCH SEARCH_MATCH
@ -4058,6 +4153,8 @@ enum MediaStatus {
CANCELLED CANCELLED
"Has completed and is no longer being released" "Has completed and is no longer being released"
FINISHED FINISHED
"Version 2 only. Is currently paused from releasing and will resume at a later date"
HIATUS
"To be released at a later date" "To be released at a later date"
NOT_YET_RELEASED NOT_YET_RELEASED
"Currently releasing" "Currently releasing"
@ -4231,6 +4328,8 @@ enum StaffSort {
ID_DESC ID_DESC
LANGUAGE LANGUAGE
LANGUAGE_DESC LANGUAGE_DESC
"Order manually decided by moderators"
RELEVANCE
ROLE ROLE
ROLE_DESC ROLE_DESC
SEARCH_MATCH SEARCH_MATCH
@ -4343,10 +4442,14 @@ input AniChartHighlightInput {
input CharacterNameInput { input CharacterNameInput {
"Other names the character might be referred by" "Other names the character might be referred by"
alternative: [String] alternative: [String]
"Other names the character might be referred to as but are spoilers"
alternativeSpoiler: [String]
"The character's given name" "The character's given name"
first: String first: String
"The character's surname" "The character's surname"
last: String last: String
"The character's middle name"
middle: String
"The character's full name in their native language" "The character's full name in their native language"
native: String native: String
} }
@ -4413,6 +4516,8 @@ input StaffNameInput {
first: String first: String
"The person's surname" "The person's surname"
last: String last: String
"The person's middle name"
middle: String
"The person's full name in their native language" "The person's full name in their native language"
native: String native: String
} }

View File

@ -1,6 +1,6 @@
query ($slug: String!) { query ($slug: String!) {
findProfileBySlug(slug: $slug) { findProfileBySlug(slug: $slug) {
libraryEvents(first: 100) { libraryEvents(first: 100, kind: [PROGRESSED, UPDATED]) {
nodes { nodes {
id id
changedData changedData

View File

@ -104,7 +104,7 @@ final class AnimeListTransformer extends AbstractTransformer {
'notes' => $item['notes'], 'notes' => $item['notes'],
'rewatching' => (bool) $item['reconsuming'], 'rewatching' => (bool) $item['reconsuming'],
'rewatched' => (int) $item['reconsumeCount'], 'rewatched' => (int) $item['reconsumeCount'],
'user_rating' => $rating, 'user_rating' => (is_string($rating)) ? $rating : (int) $rating,
'private' => $item['private'] ?? FALSE, 'private' => $item['private'] ?? FALSE,
]); ]);
} }

View File

@ -20,16 +20,6 @@ interface AmountConsumed {
units: Int! units: Int!
} }
"Generic error fields used by all errors."
interface Base {
"The error code."
code: String
"A description of the error"
message: String!
"Which input value this error came from"
path: [String!]
}
"Generic Category Breakdown based on Media" "Generic Category Breakdown based on Media"
interface CategoryBreakdown { interface CategoryBreakdown {
"A Map of category_id -> count for all categories present on the library entries" "A Map of category_id -> count for all categories present on the library entries"
@ -65,6 +55,16 @@ interface Episodic {
totalLength: Int totalLength: Int
} }
"Generic error fields used by all errors."
interface Error {
"The error code."
code: String
"A description of the error"
message: String!
"Which input value this error came from"
path: [String!]
}
"A media in the Kitsu database" "A media in the Kitsu database"
interface Media { interface Media {
"The recommended minimum age group for this media" "The recommended minimum age group for this media"
@ -117,6 +117,8 @@ interface Media {
): MappingConnection! ): MappingConnection!
"The time of the next release of this media" "The time of the next release of this media"
nextRelease: ISO8601DateTime nextRelease: ISO8601DateTime
"The country in which the media was primarily produced"
originalLocale: String
"The poster image of this media" "The poster image of this media"
posterImage: Image! posterImage: Image!
"The companies which helped to produce this media" "The companies which helped to produce this media"
@ -318,6 +320,8 @@ type Anime implements Episodic & Media & WithTimestamps {
): MappingConnection! ): MappingConnection!
"The time of the next release of this media" "The time of the next release of this media"
nextRelease: ISO8601DateTime nextRelease: ISO8601DateTime
"The country in which the media was primarily produced"
originalLocale: String
"The poster image of this media" "The poster image of this media"
posterImage: Image! posterImage: Image!
"The companies which helped to produce this media" "The companies which helped to produce this media"
@ -445,15 +449,13 @@ type AnimeConnection {
"Autogenerated return type of AnimeCreate" "Autogenerated return type of AnimeCreate"
type AnimeCreatePayload { type AnimeCreatePayload {
anime: Anime anime: Anime
"Graphql Errors" errors: [Error!]
errors: [Generic!]
} }
"Autogenerated return type of AnimeDelete" "Autogenerated return type of AnimeDelete"
type AnimeDeletePayload { type AnimeDeletePayload {
anime: GenericDelete anime: GenericDelete
"Graphql Errors" errors: [Error!]
errors: [Generic!]
} }
"An edge in a connection." "An edge in a connection."
@ -464,7 +466,7 @@ type AnimeEdge {
node: Anime node: Anime
} }
type AnimeMutation { type AnimeMutations {
"Create an Anime." "Create an Anime."
create( create(
"Create an Anime." "Create an Anime."
@ -485,8 +487,7 @@ type AnimeMutation {
"Autogenerated return type of AnimeUpdate" "Autogenerated return type of AnimeUpdate"
type AnimeUpdatePayload { type AnimeUpdatePayload {
anime: Anime anime: Anime
"Graphql Errors" errors: [Error!]
errors: [Generic!]
} }
"Information about a specific Category" "Information about a specific Category"
@ -651,7 +652,7 @@ type Comment implements WithTimestamps {
contentFormatted: String! contentFormatted: String!
createdAt: ISO8601DateTime! createdAt: ISO8601DateTime!
id: ID! id: ID!
"Users who liked this comment." "Users who liked this comment"
likes( likes(
"Returns the elements in the list that come after the specified cursor." "Returns the elements in the list that come after the specified cursor."
after: String, after: String,
@ -660,13 +661,14 @@ type Comment implements WithTimestamps {
"Returns the first _n_ elements from the list." "Returns the first _n_ elements from the list."
first: Int, first: Int,
"Returns the last _n_ elements from the list." "Returns the last _n_ elements from the list."
last: Int last: Int,
sort: [CommentLikeSortOption]
): ProfileConnection! ): ProfileConnection!
"The parent comment if this comment was a reply to another." "The parent comment if this comment was a reply to another."
parent: Comment parent: Comment
"The post that this comment is attached to." "The post that this comment is attached to."
post: Post! post: Post!
"All replies to a specific comment." "Replies to this comment"
replies( replies(
"Returns the elements in the list that come after the specified cursor." "Returns the elements in the list that come after the specified cursor."
after: String, after: String,
@ -675,7 +677,8 @@ type Comment implements WithTimestamps {
"Returns the first _n_ elements from the list." "Returns the first _n_ elements from the list."
first: Int, first: Int,
"Returns the last _n_ elements from the list." "Returns the last _n_ elements from the list."
last: Int last: Int,
sort: [CommentSortOption]
): CommentConnection! ): CommentConnection!
updatedAt: ISO8601DateTime! updatedAt: ISO8601DateTime!
} }
@ -736,15 +739,13 @@ type EpisodeConnection {
"Autogenerated return type of EpisodeCreate" "Autogenerated return type of EpisodeCreate"
type EpisodeCreatePayload { type EpisodeCreatePayload {
episode: Episode episode: Episode
"Graphql Errors" errors: [Error!]
errors: [Generic!]
} }
"Autogenerated return type of EpisodeDelete" "Autogenerated return type of EpisodeDelete"
type EpisodeDeletePayload { type EpisodeDeletePayload {
episode: GenericDelete episode: GenericDelete
"Graphql Errors" errors: [Error!]
errors: [Generic!]
} }
"An edge in a connection." "An edge in a connection."
@ -755,7 +756,7 @@ type EpisodeEdge {
node: Episode node: Episode
} }
type EpisodeMutation { type EpisodeMutations {
"Create an Episode." "Create an Episode."
create( create(
"Create an Episode" "Create an Episode"
@ -776,8 +777,7 @@ type EpisodeMutation {
"Autogenerated return type of EpisodeUpdate" "Autogenerated return type of EpisodeUpdate"
type EpisodeUpdatePayload { type EpisodeUpdatePayload {
episode: Episode episode: Episode
"Graphql Errors" errors: [Error!]
errors: [Generic!]
} }
"Favorite media, characters, and people for a user" "Favorite media, characters, and people for a user"
@ -811,7 +811,7 @@ type FavoriteEdge {
node: Favorite node: Favorite
} }
type Generic implements Base { type Generic implements Error {
"The error code." "The error code."
code: String code: String
"A description of the error" "A description of the error"
@ -991,15 +991,13 @@ type LibraryEntryConnection {
"Autogenerated return type of LibraryEntryCreate" "Autogenerated return type of LibraryEntryCreate"
type LibraryEntryCreatePayload { type LibraryEntryCreatePayload {
"Graphql Errors" errors: [Error!]
errors: [Generic!]
libraryEntry: LibraryEntry libraryEntry: LibraryEntry
} }
"Autogenerated return type of LibraryEntryDelete" "Autogenerated return type of LibraryEntryDelete"
type LibraryEntryDeletePayload { type LibraryEntryDeletePayload {
"Graphql Errors" errors: [Error!]
errors: [Generic!]
libraryEntry: GenericDelete libraryEntry: GenericDelete
} }
@ -1011,7 +1009,7 @@ type LibraryEntryEdge {
node: LibraryEntry node: LibraryEntry
} }
type LibraryEntryMutation { type LibraryEntryMutations {
"Create a library entry" "Create a library entry"
create( create(
"Create a Library Entry" "Create a Library Entry"
@ -1061,50 +1059,43 @@ type LibraryEntryMutation {
"Autogenerated return type of LibraryEntryUpdate" "Autogenerated return type of LibraryEntryUpdate"
type LibraryEntryUpdatePayload { type LibraryEntryUpdatePayload {
"Graphql Errors" errors: [Error!]
errors: [Generic!]
libraryEntry: LibraryEntry libraryEntry: LibraryEntry
} }
"Autogenerated return type of LibraryEntryUpdateProgressById" "Autogenerated return type of LibraryEntryUpdateProgressById"
type LibraryEntryUpdateProgressByIdPayload { type LibraryEntryUpdateProgressByIdPayload {
"Graphql Errors" errors: [Error!]
errors: [Generic!]
libraryEntry: LibraryEntry libraryEntry: LibraryEntry
} }
"Autogenerated return type of LibraryEntryUpdateProgressByMedia" "Autogenerated return type of LibraryEntryUpdateProgressByMedia"
type LibraryEntryUpdateProgressByMediaPayload { type LibraryEntryUpdateProgressByMediaPayload {
"Graphql Errors" errors: [Error!]
errors: [Generic!]
libraryEntry: LibraryEntry libraryEntry: LibraryEntry
} }
"Autogenerated return type of LibraryEntryUpdateRatingById" "Autogenerated return type of LibraryEntryUpdateRatingById"
type LibraryEntryUpdateRatingByIdPayload { type LibraryEntryUpdateRatingByIdPayload {
"Graphql Errors" errors: [Error!]
errors: [Generic!]
libraryEntry: LibraryEntry libraryEntry: LibraryEntry
} }
"Autogenerated return type of LibraryEntryUpdateRatingByMedia" "Autogenerated return type of LibraryEntryUpdateRatingByMedia"
type LibraryEntryUpdateRatingByMediaPayload { type LibraryEntryUpdateRatingByMediaPayload {
"Graphql Errors" errors: [Error!]
errors: [Generic!]
libraryEntry: LibraryEntry libraryEntry: LibraryEntry
} }
"Autogenerated return type of LibraryEntryUpdateStatusById" "Autogenerated return type of LibraryEntryUpdateStatusById"
type LibraryEntryUpdateStatusByIdPayload { type LibraryEntryUpdateStatusByIdPayload {
"Graphql Errors" errors: [Error!]
errors: [Generic!]
libraryEntry: LibraryEntry libraryEntry: LibraryEntry
} }
"Autogenerated return type of LibraryEntryUpdateStatusByMedia" "Autogenerated return type of LibraryEntryUpdateStatusByMedia"
type LibraryEntryUpdateStatusByMediaPayload { type LibraryEntryUpdateStatusByMediaPayload {
"Graphql Errors" errors: [Error!]
errors: [Generic!]
libraryEntry: LibraryEntry libraryEntry: LibraryEntry
} }
@ -1145,13 +1136,6 @@ type LibraryEventEdge {
node: LibraryEvent node: LibraryEvent
} }
"Autogenerated return type of LockPost"
type LockPostPayload {
"Graphql Errors"
errors: [Generic!]
post: Post
}
type Manga implements Media & WithTimestamps { type Manga implements Media & WithTimestamps {
"The recommended minimum age group for this media" "The recommended minimum age group for this media"
ageRating: AgeRatingEnum ageRating: AgeRatingEnum
@ -1219,6 +1203,8 @@ type Manga implements Media & WithTimestamps {
): MappingConnection! ): MappingConnection!
"The time of the next release of this media" "The time of the next release of this media"
nextRelease: ISO8601DateTime nextRelease: ISO8601DateTime
"The country in which the media was primarily produced"
originalLocale: String
"The poster image of this media" "The poster image of this media"
posterImage: Image! posterImage: Image!
"The companies which helped to produce this media" "The companies which helped to produce this media"
@ -1361,15 +1347,13 @@ type MappingConnection {
"Autogenerated return type of MappingCreate" "Autogenerated return type of MappingCreate"
type MappingCreatePayload { type MappingCreatePayload {
"Graphql Errors" errors: [Error!]
errors: [Generic!]
mapping: Mapping mapping: Mapping
} }
"Autogenerated return type of MappingDelete" "Autogenerated return type of MappingDelete"
type MappingDeletePayload { type MappingDeletePayload {
"Graphql Errors" errors: [Error!]
errors: [Generic!]
mapping: GenericDelete mapping: GenericDelete
} }
@ -1381,7 +1365,7 @@ type MappingEdge {
node: Mapping node: Mapping
} }
type MappingMutation { type MappingMutations {
"Create a Mapping" "Create a Mapping"
create( create(
"Create a Mapping" "Create a Mapping"
@ -1401,8 +1385,7 @@ type MappingMutation {
"Autogenerated return type of MappingUpdate" "Autogenerated return type of MappingUpdate"
type MappingUpdatePayload { type MappingUpdatePayload {
"Graphql Errors" errors: [Error!]
errors: [Generic!]
mapping: Mapping mapping: Mapping
} }
@ -1584,12 +1567,12 @@ type MediaStaffEdge {
} }
type Mutation { type Mutation {
anime: AnimeMutation anime: AnimeMutations!
episode: EpisodeMutation episode: EpisodeMutations!
libraryEntry: LibraryEntryMutation libraryEntry: LibraryEntryMutations!
mapping: MappingMutation mapping: MappingMutations!
post: PostMutation post: PostMutations!
pro: ProMutation! pro: ProMutations!
} }
"Information about pagination in a connection." "Information about pagination in a connection."
@ -1649,7 +1632,7 @@ type Person implements WithTimestamps {
type Post implements WithTimestamps { type Post implements WithTimestamps {
"The user who created this post." "The user who created this post."
author: Profile! author: Profile!
"All comments related to this post." "All comments on this post"
comments( comments(
"Returns the elements in the list that come after the specified cursor." "Returns the elements in the list that come after the specified cursor."
after: String, after: String,
@ -1658,7 +1641,8 @@ type Post implements WithTimestamps {
"Returns the first _n_ elements from the list." "Returns the first _n_ elements from the list."
first: Int, first: Int,
"Returns the last _n_ elements from the list." "Returns the last _n_ elements from the list."
last: Int last: Int,
sort: [CommentSortOption]
): CommentConnection! ): CommentConnection!
"Unmodified content." "Unmodified content."
content: String! content: String!
@ -1681,7 +1665,7 @@ type Post implements WithTimestamps {
isNsfw: Boolean! isNsfw: Boolean!
"If this post spoils the tagged media." "If this post spoils the tagged media."
isSpoiler: Boolean! isSpoiler: Boolean!
"Users that have liked this post." "Users that have liked this post"
likes( likes(
"Returns the elements in the list that come after the specified cursor." "Returns the elements in the list that come after the specified cursor."
after: String, after: String,
@ -1690,7 +1674,8 @@ type Post implements WithTimestamps {
"Returns the first _n_ elements from the list." "Returns the first _n_ elements from the list."
first: Int, first: Int,
"Returns the last _n_ elements from the list." "Returns the last _n_ elements from the list."
last: Int last: Int,
sort: [PostLikeSortOption]
): ProfileConnection! ): ProfileConnection!
"When this post was locked." "When this post was locked."
lockedAt: ISO8601DateTime lockedAt: ISO8601DateTime
@ -1723,32 +1708,54 @@ type PostEdge {
node: Post node: Post
} }
type PostMutation { "Autogenerated return type of PostLock"
type PostLockPayload {
errors: [Error!]
post: Post
}
type PostMutations {
"Lock a Post." "Lock a Post."
lock( lock(
"Lock a Post." "Lock a Post."
input: LockInput! input: LockInput!
): LockPostPayload ): PostLockPayload
"Unlock a Post." "Unlock a Post."
unlock( unlock(
"Unlock a Post." "Unlock a Post."
input: UnlockInput! input: UnlockInput!
): UnlockPostPayload ): PostUnlockPayload
} }
type ProMutation { "Autogenerated return type of PostUnlock"
type PostUnlockPayload {
errors: [Error!]
post: Post
}
type ProMutations {
"Set the user's discord tag" "Set the user's discord tag"
setDiscord( setDiscord(
"Your discord tag (Name#1234)" "Your discord tag (Name#1234)"
discord: String! discord: String!
): SetDiscordPayload ): ProSetDiscordPayload
"Set the user's Hall-of-Fame message" "Set the user's Hall-of-Fame message"
setMessage( setMessage(
"The message to set for your Hall of Fame entry" "The message to set for your Hall of Fame entry"
message: String! message: String!
): SetMessagePayload ): ProSetMessagePayload
"End the user's pro subscription" "End the user's pro subscription"
unsubscribe: UnsubscribePayload unsubscribe: ProUnsubscribePayload
}
"Autogenerated return type of ProSetDiscord"
type ProSetDiscordPayload {
discord: String!
}
"Autogenerated return type of ProSetMessage"
type ProSetMessagePayload {
message: String!
} }
"A subscription to Kitsu PRO" "A subscription to Kitsu PRO"
@ -1763,6 +1770,11 @@ type ProSubscription implements WithTimestamps {
updatedAt: ISO8601DateTime! updatedAt: ISO8601DateTime!
} }
"Autogenerated return type of ProUnsubscribe"
type ProUnsubscribePayload {
expiresAt: ISO8601DateTime
}
"A company involved in the creation or localization of media" "A company involved in the creation or localization of media"
type Producer implements WithTimestamps { type Producer implements WithTimestamps {
createdAt: ISO8601DateTime! createdAt: ISO8601DateTime!
@ -1814,7 +1826,8 @@ type Profile implements WithTimestamps {
"Returns the first _n_ elements from the list." "Returns the first _n_ elements from the list."
first: Int, first: Int,
"Returns the last _n_ elements from the list." "Returns the last _n_ elements from the list."
last: Int last: Int,
sort: [FollowSortOption]
): ProfileConnection! ): ProfileConnection!
"People the user is following" "People the user is following"
following( following(
@ -1825,7 +1838,8 @@ type Profile implements WithTimestamps {
"Returns the first _n_ elements from the list." "Returns the first _n_ elements from the list."
first: Int, first: Int,
"Returns the last _n_ elements from the list." "Returns the last _n_ elements from the list."
last: Int last: Int,
sort: [FollowSortOption]
): ProfileConnection! ): ProfileConnection!
"What the user identifies as" "What the user identifies as"
gender: String gender: String
@ -1870,7 +1884,8 @@ type Profile implements WithTimestamps {
"Returns the first _n_ elements from the list." "Returns the first _n_ elements from the list."
first: Int, first: Int,
"Returns the last _n_ elements from the list." "Returns the last _n_ elements from the list."
last: Int last: Int,
sort: [PostSortOption]
): PostConnection! ): PostConnection!
"The message this user has submitted to the Hall of Fame" "The message this user has submitted to the Hall of Fame"
proMessage: String proMessage: String
@ -2223,20 +2238,6 @@ type Session {
profile: Profile profile: Profile
} }
"Autogenerated return type of SetDiscord"
type SetDiscordPayload {
discord: String!
"Graphql Errors"
errors: [Generic!]
}
"Autogenerated return type of SetMessage"
type SetMessagePayload {
"Graphql Errors"
errors: [Generic!]
message: String!
}
"A link to a user's profile on an external site." "A link to a user's profile on an external site."
type SiteLink implements WithTimestamps { type SiteLink implements WithTimestamps {
"The user profile the site is linked to." "The user profile the site is linked to."
@ -2349,20 +2350,6 @@ type TitlesList {
localized(locales: [String!]): Map! localized(locales: [String!]): Map!
} }
"Autogenerated return type of UnlockPost"
type UnlockPostPayload {
"Graphql Errors"
errors: [Generic!]
post: Post
}
"Autogenerated return type of Unsubscribe"
type UnsubscribePayload {
"Graphql Errors"
errors: [Generic!]
expiresAt: ISO8601DateTime
}
"The media video." "The media video."
type Video implements Streamable & WithTimestamps { type Video implements Streamable & WithTimestamps {
createdAt: ISO8601DateTime! createdAt: ISO8601DateTime!
@ -2464,6 +2451,23 @@ enum CharacterRoleEnum {
RECURRING RECURRING
} }
enum CommentLikeSortEnum {
CREATED_AT
FOLLOWING
}
enum CommentSortEnum {
CREATED_AT
FOLLOWING
LIKES_COUNT
}
enum FollowSortEnum {
CREATED_AT
FOLLOWING_FOLLOWED
FOLLOWING_FOLLOWER
}
enum LibraryEntryStatusEnum { enum LibraryEntryStatusEnum {
"The user completed this media." "The user completed this media."
COMPLETED COMPLETED
@ -2554,6 +2558,15 @@ enum MediaTypeEnum {
MANGA MANGA
} }
enum PostLikeSortEnum {
CREATED_AT
FOLLOWING
}
enum PostSortEnum {
CREATED_AT
}
enum ProTierEnum { enum ProTierEnum {
"Aozora Pro (only hides ads)" "Aozora Pro (only hides ads)"
AO_PRO @deprecated(reason : "No longer for sale") AO_PRO @deprecated(reason : "No longer for sale")
@ -2618,6 +2631,11 @@ enum SitePermissionEnum {
DATABASE_MOD DATABASE_MOD
} }
enum SortDirection {
ASCENDING
DESCENDING
}
enum TitleLanguagePreferenceEnum { enum TitleLanguagePreferenceEnum {
"Prefer the most commonly-used title for media" "Prefer the most commonly-used title for media"
CANONICAL CANONICAL
@ -2658,6 +2676,16 @@ input AnimeUpdateInput {
youtubeTrailerVideoId: String youtubeTrailerVideoId: String
} }
input CommentLikeSortOption {
direction: SortDirection!
on: CommentLikeSortEnum!
}
input CommentSortOption {
direction: SortDirection!
on: CommentSortEnum!
}
input EpisodeCreateInput { input EpisodeCreateInput {
description: Map description: Map
length: Int length: Int
@ -2679,6 +2707,11 @@ input EpisodeUpdateInput {
titles: TitlesListInput titles: TitlesListInput
} }
input FollowSortOption {
direction: SortDirection!
on: FollowSortEnum!
}
input GenericDeleteInput { input GenericDeleteInput {
id: ID! id: ID!
} }
@ -2768,8 +2801,19 @@ input MappingUpdateInput {
itemType: MappingItemEnum itemType: MappingItemEnum
} }
input PostLikeSortOption {
direction: SortDirection!
on: PostLikeSortEnum!
}
input PostSortOption {
direction: SortDirection!
on: PostSortEnum!
}
input TitlesListInput { input TitlesListInput {
alternatives: [String!] alternatives: [String!]
canonical: String
canonicalLocale: String canonicalLocale: String
localized: Map localized: Map
} }

View File

@ -186,7 +186,7 @@ function checkFolderPermissions(ConfigInterface $config): array
$errors = []; $errors = [];
$publicDir = $config->get('asset_dir'); $publicDir = $config->get('asset_dir');
$APP_DIR = _dir(dirname(__DIR__, 2), '/app'); $APP_DIR = _dir($config->get('root'), 'app');
$pathMap = [ $pathMap = [
'app/config' => "{$APP_DIR}/config", 'app/config' => "{$APP_DIR}/config",
@ -211,7 +211,9 @@ function checkFolderPermissions(ConfigInterface $config): array
if ( ! $writable) if ( ! $writable)
{ {
// @codeCoverageIgnoreStart
$errors['writable'][] = $pretty; $errors['writable'][] = $pretty;
// @codeCoverageIgnoreEnd
} }
} }
@ -292,6 +294,7 @@ function getLocalImg (string $kitsuUrl, $webp = TRUE): string
/** /**
* Create a transparent placeholder image * Create a transparent placeholder image
* *
* @codeCoverageIgnore
* @param string $path * @param string $path
* @param int|null $width * @param int|null $width
* @param int|null $height * @param int|null $height
@ -378,7 +381,6 @@ function colNotEmpty(array $search, string $key): bool
* *
* @param CacheInterface $cache * @param CacheInterface $cache
* @return bool * @return bool
* @throws Throwable
*/ */
function clearCache(CacheInterface $cache): bool function clearCache(CacheInterface $cache): bool
{ {
@ -393,9 +395,7 @@ function clearCache(CacheInterface $cache): bool
$userData = array_filter((array)$userData, static fn ($value) => $value !== NULL); $userData = array_filter((array)$userData, static fn ($value) => $value !== NULL);
$cleared = $cache->clear(); $cleared = $cache->clear();
$saved = ( ! empty($userData)) $saved = ( ! empty($userData)) ? $cache->setMultiple($userData) : TRUE;
? $cache->setMultiple($userData)
: TRUE;
return $cleared && $saved; return $cleared && $saved;
} }

View File

@ -61,14 +61,16 @@ abstract class BaseCommand extends Command {
if ($fgColor !== NULL) if ($fgColor !== NULL)
{ {
$fgColor = (string)$fgColor; $fgColor = (int)$fgColor;
} }
if ($bgColor !== NULL) if ($bgColor !== NULL)
{ {
$bgColor = (string)$bgColor; $bgColor = (int)$bgColor;
} }
// color message // Colorize the CLI output
// the documented type for the function is wrong
// @phpstan-ignore-next-line
$message = Colors::colorize($message, $fgColor, $bgColor); $message = Colors::colorize($message, $fgColor, $bgColor);
// create the box // create the box
@ -142,13 +144,16 @@ abstract class BaseCommand extends Command {
{ {
if ($fgColor !== NULL) if ($fgColor !== NULL)
{ {
$fgColor = (string)$fgColor; $fgColor = (int)$fgColor;
} }
if ($bgColor !== NULL) if ($bgColor !== NULL)
{ {
$bgColor = (string)$bgColor; $bgColor = (int)$bgColor;
} }
// Colorize the CLI output
// the documented type for the function is wrong
// @phpstan-ignore-next-line
$message = Colors::colorize($message, $fgColor, $bgColor); $message = Colors::colorize($message, $fgColor, $bgColor);
$this->getConsole()->writeln($message); $this->getConsole()->writeln($message);
} }

View File

@ -16,6 +16,7 @@
namespace Aviat\AnimeClient\Command; namespace Aviat\AnimeClient\Command;
use Aviat\Ion\JsonException;
use ConsoleKit\Widgets; use ConsoleKit\Widgets;
use Aviat\AnimeClient\API\{ use Aviat\AnimeClient\API\{
@ -288,7 +289,15 @@ final class SyncLists extends BaseCommand {
// This uses a static so I don't have to fetch this list twice for a count // This uses a static so I don't have to fetch this list twice for a count
if ($list[$type] === NULL) if ($list[$type] === NULL)
{ {
$list[$type] = $this->anilistModel->getSyncList(strtoupper($type)); try
{
$list[$type] = $this->anilistModel->getSyncList(strtoupper($type));
}
catch (JsonException)
{
$this->echoErrorBox('Anlist API exception. Can not sync.');
die();
}
} }
return $list[$type]; return $list[$type];
@ -354,7 +363,7 @@ final class SyncLists extends BaseCommand {
'progress' => $listItem['progress'], 'progress' => $listItem['progress'],
// Comparision is done on 1-10 scale, // Comparision is done on 1-10 scale,
// Kitsu returns 1-20 scale. // Kitsu returns 1-20 scale.
'rating' => $listItem['rating'] / 2, 'rating' => (int) $listItem['rating'] / 2,
'reconsumeCount' => $listItem['reconsumeCount'], 'reconsumeCount' => $listItem['reconsumeCount'],
'reconsuming' => $listItem['reconsuming'], 'reconsuming' => $listItem['reconsuming'],
'status' => strtolower($listItem['status']), 'status' => strtolower($listItem['status']),
@ -404,7 +413,7 @@ final class SyncLists extends BaseCommand {
$malIds = array_keys($anilistList); $malIds = array_keys($anilistList);
$kitsuMalIds = array_map('intval', array_column($kitsuList, 'malId')); $kitsuMalIds = array_map('intval', array_column($kitsuList, 'malId'));
$missingMalIds = array_filter(array_diff($kitsuMalIds, $malIds), fn ($id) => ! in_array($id, $kitsuMalIds)); $missingMalIds = array_filter($malIds, fn ($id) => ! in_array($id, $kitsuMalIds));
// Add items on Anilist, but not Kitsu to Kitsu // Add items on Anilist, but not Kitsu to Kitsu
foreach($missingMalIds as $mid) foreach($missingMalIds as $mid)
@ -599,7 +608,7 @@ final class SyncLists extends BaseCommand {
$kitsuItem['data']['ratingTwenty'] !== 0 $kitsuItem['data']['ratingTwenty'] !== 0
) )
{ {
$update['data']['ratingTwenty'] = $kitsuItem['data']['ratingTwenty']; $update['data']['ratingTwenty'] = $kitsuItem['data']['rating'];
$return['updateType'][] = Enum\API::ANILIST; $return['updateType'][] = Enum\API::ANILIST;
} }
else if($dateDiff === self::ANILIST_GREATER && $anilistItem['data']['rating'] !== 0) else if($dateDiff === self::ANILIST_GREATER && $anilistItem['data']['rating'] !== 0)
@ -683,7 +692,7 @@ final class SyncLists extends BaseCommand {
// Anilist returns a rating between 1-100 // Anilist returns a rating between 1-100
// Kitsu expects a rating from 1-20 // Kitsu expects a rating from 1-20
'rating' => (((int)$anilistItem['data']['rating']) > 0) 'rating' => (((int)$anilistItem['data']['rating']) > 0)
? $anilistItem['data']['rating'] / 5 ? (int) $anilistItem['data']['rating'] / 5
: 0, : 0,
'reconsumeCount' => $anilistItem['data']['reconsumeCount'], 'reconsumeCount' => $anilistItem['data']['reconsumeCount'],
'reconsuming' => $anilistItem['data']['reconsuming'], 'reconsuming' => $anilistItem['data']['reconsuming'],
@ -738,7 +747,7 @@ final class SyncLists extends BaseCommand {
$responseData = Json::decode($response); $responseData = Json::decode($response);
$id = $itemsToUpdate[$key]['id']; $id = $itemsToUpdate[$key]['id'];
$mal_id = $itemsToUpdate[$key]['mal_id']; $mal_id = $itemsToUpdate[$key]['mal_id'] ?? NULL;
if ( ! array_key_exists('errors', $responseData)) if ( ! array_key_exists('errors', $responseData))
{ {
$verb = ($action === SyncAction::UPDATE) ? 'updated' : 'created'; $verb = ($action === SyncAction::UPDATE) ? 'updated' : 'created';

View File

@ -126,6 +126,7 @@ class Controller {
/** /**
* Set the current url in the session as the target of a future redirect * Set the current url in the session as the target of a future redirect
* *
* @codeCoverageIgnore
* @param string|NULL $url * @param string|NULL $url
* @throws ContainerException * @throws ContainerException
* @throws NotFoundException * @throws NotFoundException
@ -141,7 +142,7 @@ class Controller {
$util = $this->container->get('util'); $util = $this->container->get('util');
$doubleFormPage = $serverParams['HTTP_REFERER'] === $this->request->getUri(); $doubleFormPage = $serverParams['HTTP_REFERER'] === $this->request->getUri();
$isLoginPage = (bool) strpos($serverParams['HTTP_REFERER'], 'login'); $isLoginPage = str_contains($serverParams['HTTP_REFERER'], 'login');
// Don't attempt to set the redirect url if // Don't attempt to set the redirect url if
// the page is one of the form type pages, // the page is one of the form type pages,
@ -166,6 +167,7 @@ class Controller {
* *
* If one is not set, redirect to default url * If one is not set, redirect to default url
* *
* @codeCoverageIgnore
* @throws InvalidArgumentException * @throws InvalidArgumentException
* @return void * @return void
*/ */
@ -179,6 +181,7 @@ class Controller {
/** /**
* Check if the current user is authenticated, else error and exit * Check if the current user is authenticated, else error and exit
* @codeCoverageIgnore
*/ */
protected function checkAuth(): void protected function checkAuth(): void
{ {
@ -195,12 +198,10 @@ class Controller {
/** /**
* Get the string output of a partial template * Get the string output of a partial template
* *
* @codeCoverageIgnore
* @param HtmlView $view * @param HtmlView $view
* @param string $template * @param string $template
* @param array $data * @param array $data
* @throws InvalidArgumentException
* @throws ContainerException
* @throws NotFoundException
* @return string * @return string
*/ */
protected function loadPartial(HtmlView $view, string $template, array $data = []): string protected function loadPartial(HtmlView $view, string $template, array $data = []): string
@ -229,19 +230,18 @@ class Controller {
/** /**
* Render a template with header and footer * Render a template with header and footer
* *
* @codeCoverageIgnore
* @param HtmlView $view * @param HtmlView $view
* @param string $template * @param string $template
* @param array $data * @param array $data
* @return HtmlView * @return HtmlView
* @throws ContainerException
* @throws NotFoundException
*/ */
protected function renderFullPage(HtmlView $view, string $template, array $data): HtmlView protected function renderFullPage(HtmlView $view, string $template, array $data): HtmlView
{ {
$csp = [ $csp = [
"default-src 'self'", "default-src 'self'",
"object-src 'none'", "object-src 'none'",
'frame-src *.youtube.com', "child-src 'self' *.youtube.com polyfill.io",
]; ];
$view->addHeader('Content-Security-Policy', implode('; ', $csp)); $view->addHeader('Content-Security-Policy', implode('; ', $csp));
@ -261,11 +261,10 @@ class Controller {
/** /**
* 404 action * 404 action
* *
* @codeCoverageIgnore
* @param string $title * @param string $title
* @param string $message * @param string $message
* @throws InvalidArgumentException * @throws InvalidArgumentException
* @throws ContainerException
* @throws NotFoundException
* @return void * @return void
*/ */
public function notFound( public function notFound(
@ -283,13 +282,12 @@ class Controller {
/** /**
* Display a generic error page * Display a generic error page
* *
* @codeCoverageIgnore
* @param int $httpCode * @param int $httpCode
* @param string $title * @param string $title
* @param string $message * @param string $message
* @param string $longMessage * @param string $longMessage
* @throws InvalidArgumentException * @throws InvalidArgumentException
* @throws ContainerException
* @throws NotFoundException
* @return void * @return void
*/ */
public function errorPage(int $httpCode, string $title, string $message, string $longMessage = ''): void public function errorPage(int $httpCode, string $title, string $message, string $longMessage = ''): void
@ -304,6 +302,7 @@ class Controller {
/** /**
* Redirect to the default controller/url from an empty path * Redirect to the default controller/url from an empty path
* *
* @codeCoverageIgnore
* @throws InvalidArgumentException * @throws InvalidArgumentException
* @return void * @return void
*/ */
@ -317,6 +316,7 @@ class Controller {
* Set a session flash variable to display a message on * Set a session flash variable to display a message on
* next page load * next page load
* *
* @codeCoverageIgnore
* @param string $message * @param string $message
* @param string $type * @param string $type
* @return void * @return void
@ -352,12 +352,11 @@ class Controller {
/** /**
* Add a message box to the page * Add a message box to the page
* *
* @codeCoverageIgnore
* @param HtmlView $view * @param HtmlView $view
* @param string $type * @param string $type
* @param string $message * @param string $message
* @throws InvalidArgumentException * @throws InvalidArgumentException
* @throws ContainerException
* @throws NotFoundException
* @return string * @return string
*/ */
protected function showMessage(HtmlView $view, string $type, string $message): string protected function showMessage(HtmlView $view, string $type, string $message): string
@ -371,13 +370,12 @@ class Controller {
/** /**
* Output a template to HTML, using the provided data * Output a template to HTML, using the provided data
* *
* @codeCoverageIgnore
* @param string $template * @param string $template
* @param array $data * @param array $data
* @param HtmlView|NULL $view * @param HtmlView|NULL $view
* @param int $code * @param int $code
* @throws InvalidArgumentException * @throws InvalidArgumentException
* @throws ContainerException
* @throws NotFoundException
* @return void * @return void
*/ */
protected function outputHTML(string $template, array $data = [], $view = NULL, int $code = 200): void protected function outputHTML(string $template, array $data = [], $view = NULL, int $code = 200): void
@ -394,6 +392,7 @@ class Controller {
/** /**
* Output a JSON Response * Output a JSON Response
* *
* @codeCoverageIgnore
* @param mixed $data * @param mixed $data
* @param int $code - the http status code * @param int $code - the http status code
* @throws DoubleRenderException * @throws DoubleRenderException
@ -410,6 +409,7 @@ class Controller {
/** /**
* Redirect to the selected page * Redirect to the selected page
* *
* @codeCoverageIgnore
* @param string $url * @param string $url
* @param int $code * @param int $code
* @return void * @return void

View File

@ -251,7 +251,7 @@ final class Anime extends BaseController {
{ {
$this->checkAuth(); $this->checkAuth();
if (stripos($this->request->getHeader('content-type')[0], 'application/json') !== FALSE) if (str_contains($this->request->getHeader('content-type')[0], 'application/json'))
{ {
$data = Json::decode((string)$this->request->getBody()); $data = Json::decode((string)$this->request->getBody());
} }
@ -302,8 +302,6 @@ final class Anime extends BaseController {
* View details of an anime * View details of an anime
* *
* @param string $id * @param string $id
* @throws ContainerException
* @throws NotFoundException
* @throws InvalidArgumentException * @throws InvalidArgumentException
* @return void * @return void
*/ */

View File

@ -64,8 +64,6 @@ final class Manga extends Controller {
* *
* @param string $status * @param string $status
* @param string $view * @param string $view
* @throws ContainerException
* @throws NotFoundException
* @throws InvalidArgumentException * @throws InvalidArgumentException
* @return void * @return void
*/ */
@ -251,7 +249,7 @@ final class Manga extends Controller {
{ {
$this->checkAuth(); $this->checkAuth();
if (stripos($this->request->getHeader('content-type')[0], 'application/json') !== FALSE) if (str_contains($this->request->getHeader('content-type')[0], 'application/json'))
{ {
$data = Json::decode((string)$this->request->getBody()); $data = Json::decode((string)$this->request->getBody());
} }
@ -298,8 +296,6 @@ final class Manga extends Controller {
* View details of an manga * View details of an manga
* *
* @param string $id * @param string $id
* @throws ContainerException
* @throws NotFoundException
* @throws InvalidArgumentException * @throws InvalidArgumentException
* @throws Throwable * @throws Throwable
* @return void * @return void
@ -331,8 +327,6 @@ final class Manga extends Controller {
/** /**
* View details of a random manga * View details of a random manga
* *
* @throws ContainerException
* @throws NotFoundException
* @throws InvalidArgumentException * @throws InvalidArgumentException
* @throws Throwable * @throws Throwable
* @return void * @return void

View File

@ -314,10 +314,8 @@ final class Dispatcher extends RoutingBase {
/** /**
* Get the appropriate params for the error page * Get the appropriate params for the error page
* passed on the failed route * passed on the failed route
*
* @return array|false
*/ */
protected function getErrorParams() protected function getErrorParams(): array
{ {
$logger = $this->container->getLogger(); $logger = $this->container->getLogger();
$failure = $this->matcher->getFailedRoute(); $failure = $this->matcher->getFailedRoute();

View File

@ -25,22 +25,6 @@ final class Picture {
use ContainerAware; use ContainerAware;
private const MIME_MAP = [
'apng' => 'image/vnd.mozilla.apng',
'bmp' => 'image/bmp',
'gif' => 'image/gif',
'ico' => 'image/x-icon',
'jpeg' => 'image/jpeg',
'jpf' => 'image/jpx',
'jpg' => 'image/jpeg',
'jpx' => 'image/jpx',
'png' => 'image/png',
'svg' => 'image/svg+xml',
'tif' => 'image/tiff',
'tiff' => 'image/tiff',
'webp' => 'image/webp',
];
private const SIMPLE_IMAGE_TYPES = [ private const SIMPLE_IMAGE_TYPES = [
'gif', 'gif',
'jpeg', 'jpeg',
@ -68,12 +52,12 @@ final class Picture {
// If it is a placeholder image, make the // If it is a placeholder image, make the
// fallback a png, not a jpg // fallback a png, not a jpg
if (strpos($uri, 'placeholder') !== FALSE) if (str_contains($uri, 'placeholder'))
{ {
$fallbackExt = 'png'; $fallbackExt = 'png';
} }
if (strpos($uri, '//') === FALSE) if ( ! str_contains($uri, '//'))
{ {
$uri = $urlGenerator->assetUrl($uri); $uri = $urlGenerator->assetUrl($uri);
} }
@ -82,22 +66,34 @@ final class Picture {
$ext = array_pop($urlParts); $ext = array_pop($urlParts);
$path = implode('.', $urlParts); $path = implode('.', $urlParts);
$mime = array_key_exists($ext, static::MIME_MAP) $mime = match ($ext) {
? static::MIME_MAP[$ext] 'avif' => 'image/avif',
: 'image/jpeg'; 'apng' => 'image/vnd.mozilla.apng',
'bmp' => 'image/bmp',
'gif' => 'image/gif',
'ico' => 'image/x-icon',
'jpf', 'jpx' => 'image/jpx',
'png' => 'image/png',
'svg' => 'image/svg+xml',
'tif', 'tiff' => 'image/tiff',
'webp' => 'image/webp',
default => 'image/jpeg',
};
$fallbackMime = array_key_exists($fallbackExt, static::MIME_MAP) $fallbackMime = match ($fallbackExt) {
? static::MIME_MAP[$fallbackExt] 'gif' => 'image/gif',
: 'image/jpeg'; 'png' => 'image/png',
default => 'image/jpeg',
};
// For image types that are well-established, just return a // For image types that are well-established, just return a
// simple <img /> element instead // simple <img /> element instead
if ( if (
$ext === $fallbackExt || $ext === $fallbackExt ||
\in_array($ext, static::SIMPLE_IMAGE_TYPES, TRUE) \in_array($ext, Picture::SIMPLE_IMAGE_TYPES, TRUE)
) )
{ {
$attrs = ( ! empty($imgAttrs)) $attrs = (count($imgAttrs) > 1)
? $imgAttrs ? $imgAttrs
: $picAttrs; : $picAttrs;

View File

@ -236,6 +236,9 @@ abstract class AbstractType implements ArrayAccess, Countable {
return TRUE; return TRUE;
} }
/**
* @codeCoverageIgnore
*/
final protected function fromObject(mixed $parent = null): float|null|bool|int|array|string final protected function fromObject(mixed $parent = null): float|null|bool|int|array|string
{ {
$object = $parent ?? $this; $object = $parent ?? $this;

View File

@ -34,9 +34,6 @@ class Anime extends AbstractType {
public array $genres = []; public array $genres = [];
/**
* @var string
*/
public string $id = ''; public string $id = '';
public array $included = []; public array $included = [];

View File

@ -46,9 +46,6 @@ final class AnimeListItem extends AbstractType {
public int $rewatched = 0; public int $rewatched = 0;
/**
* @var string|int
*/
public string|int $user_rating = ''; public string|int $user_rating = '';
/** /**

View File

@ -24,9 +24,6 @@ final class Character extends AbstractType {
public ?string $description; public ?string $description;
/**
* @var string
*/
public string $id; public string $id;
public array $included = []; public array $included = [];

View File

@ -32,6 +32,8 @@ class Config extends AbstractType {
// Settings in config.toml // Settings in config.toml
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
public string $root; // Path to app root
public ?string $asset_path; // Path to public folder for urls public ?string $asset_path; // Path to public folder for urls
/** /**
@ -62,8 +64,6 @@ class Config extends AbstractType {
/** /**
* Default list view type * Default list view type
* 'cover_view' or 'list_view' * 'cover_view' or 'list_view'
*
* @var string
*/ */
public ?string $default_view_type; public ?string $default_view_type;
@ -71,21 +71,13 @@ class Config extends AbstractType {
public bool $secure_urls = TRUE; public bool $secure_urls = TRUE;
/**
* @var string|bool
*/
public string|bool $show_anime_collection = FALSE; public string|bool $show_anime_collection = FALSE;
/**
* @var string|bool
*/
public string|bool $show_manga_collection = FALSE; public string|bool $show_manga_collection = FALSE;
/** /**
* CSS theme: light, dark, or auto-switching * CSS theme: light, dark, or auto-switching
* 'auto', 'light', or 'dark' * 'auto', 'light', or 'dark'
*
* @var string|null
*/ */
public ?string $theme = 'auto'; public ?string $theme = 'auto';

View File

@ -19,10 +19,7 @@ namespace Aviat\AnimeClient\Types\Config;
use Aviat\AnimeClient\Types\AbstractType; use Aviat\AnimeClient\Types\AbstractType;
class Anilist extends AbstractType { class Anilist extends AbstractType {
/** public bool|string $enabled = FALSE;
* @var bool|string
*/
public $enabled = FALSE;
public ?string $client_id; public ?string $client_id;
@ -30,10 +27,7 @@ class Anilist extends AbstractType {
public ?string $access_token; public ?string $access_token;
/** public int|string|null $access_token_expires;
* @var int|string|null
*/
public $access_token_expires;
public ?string $refresh_token; public ?string $refresh_token;

View File

@ -23,10 +23,7 @@ class Cache extends AbstractType {
public ?string $host; public ?string $host;
/** public string|int|null $port;
* @var string|int|null
*/
public $port;
public ?string $database; public ?string $database;

View File

@ -19,38 +19,18 @@ namespace Aviat\AnimeClient\Types\Config;
use Aviat\AnimeClient\Types\AbstractType; use Aviat\AnimeClient\Types\AbstractType;
class Database extends AbstractType { class Database extends AbstractType {
/**
* @var string
*/
public string $type = 'sqlite'; public string $type = 'sqlite';
/**
* @var string|null
*/
public ?string $host; public ?string $host;
/**
* @var string|null
*/
public ?string $user; public ?string $user;
/**
* @var string|null
*/
public ?string $pass; public ?string $pass;
/** public string|int|null $port;
* @var string|int|null
*/
public $port;
/**
* @var string|null
*/
public ?string $database; public ?string $database;
/**
* @var string|null
*/
public ?string $file; public ?string $file;
} }

View File

@ -20,14 +20,8 @@ namespace Aviat\AnimeClient\Types;
* Type representing an Anime object for display * Type representing an Anime object for display
*/ */
class FormItem extends AbstractType { class FormItem extends AbstractType {
/**
* @var string|int
*/
public string|int $id; public string|int $id;
/**
* @var string|int|null
*/
public string|int|null $mal_id; public string|int|null $mal_id;
public ?FormItemData $data; public ?FormItemData $data;

View File

@ -24,32 +24,17 @@ class FormItemData extends AbstractType {
public ?bool $private = FALSE; public ?bool $private = FALSE;
/** public ?int $progress = NULL;
* @var int
*/
public $progress;
/** public ?int $rating;
* @var int
*/
public $rating;
/** public ?int $ratingTwenty = NULL;
* @var int
*/
public $ratingTwenty;
/** public string|int $reconsumeCount;
* @var string|int
*/
public $reconsumeCount;
public bool $reconsuming = FALSE; public bool $reconsuming = FALSE;
/** public string $status;
* @var string
*/
public $status;
/** /**
* W3C Format Date string * W3C Format Date string

View File

@ -42,7 +42,7 @@ class HistoryItem extends AbstractType {
/** /**
* The kind of history event * The kind of history event
*/ */
public string $kind = ''; public ?string $kind = '';
/** /**
* When the item was last updated * When the item was last updated

View File

@ -20,60 +20,31 @@ namespace Aviat\AnimeClient\Types;
* Type representing an Anime object for display * Type representing an Anime object for display
*/ */
final class MangaListItem extends AbstractType { final class MangaListItem extends AbstractType {
/**
* @var string
*/
public $id;
/** public string $id;
* @var string
*/
public $mal_id;
/** public ?string $mal_id;
* @var array
*/ public array $chapters = [
public $chapters = [
'read' => 0, 'read' => 0,
'total' => 0, 'total' => 0,
]; ];
/** public array $volumes = [
* @var array
*/
public $volumes = [
'read' => '-', 'read' => '-',
'total' => 0, 'total' => 0,
]; ];
/** public object $manga;
* @var object
*/
public $manga;
/** public string $reading_status;
* @var string
*/
public $reading_status;
/** public ?string $notes;
* @var string
*/
public $notes;
/** public bool $rereading = false;
* @var bool
*/
public $rereading;
/** public ?int $reread;
* @var int
*/
public $reread;
/** public string|int|null $user_rating;
* @var int
*/
public $user_rating;
} }

View File

@ -20,43 +20,19 @@ namespace Aviat\AnimeClient\Types;
* Type representing the manga represented by the list item * Type representing the manga represented by the list item
*/ */
final class MangaListItemDetail extends AbstractType { final class MangaListItemDetail extends AbstractType {
/** public array $genres = [];
* @var array
*/
public $genres;
/** public string $id;
* @var string
*/
public $id;
/** public string $image;
* @var string
*/
public $image;
/** public string $slug;
* @var string
*/
public $slug;
/** public string $title;
* @var string
*/
public $title;
/** public array $titles;
* @var array
*/
public $titles;
/** public ?string $type;
* @var string
*/
public $type;
/** public string $url;
* @var string
*/
public $url;
} }

View File

@ -22,74 +22,32 @@ use Aviat\AnimeClient\API\Kitsu\Enum\MangaPublishingStatus;
* Type representing an Anime object for display * Type representing an Anime object for display
*/ */
final class MangaPage extends AbstractType { final class MangaPage extends AbstractType {
/**
* @var string|null
*/
public ?string $age_rating; public ?string $age_rating;
/**
* @var string|null
*/
public ?string $age_rating_guide; public ?string $age_rating_guide;
/**
* @var array
*/
public array $characters; public array $characters;
/**
* @var int|null
*/
public ?int $chapter_count; public ?int $chapter_count;
/**
* @var string|null
*/
public ?string $cover_image; public ?string $cover_image;
/**
* @var array
*/
public array $genres; public array $genres;
/**
* @var array
*/
public array $links; public array $links;
/**
* @var string
*/
public string $id; public string $id;
/**
* @var string
*/
public string $manga_type; public string $manga_type;
/**
* @var string
*/
public string $status = MangaPublishingStatus::FINISHED; public string $status = MangaPublishingStatus::FINISHED;
/**
* @var array
*/
public array $staff; public array $staff;
/**
* @var string
*/
public string $synopsis; public string $synopsis;
/**
* @var string
*/
public string $title; public string $title;
/**
* @var array
*/
public array $titles; public array $titles;
/** /**
@ -97,13 +55,7 @@ final class MangaPage extends AbstractType {
*/ */
public array $titles_more; public array $titles_more;
/**
* @var string
*/
public string $url; public string $url;
/**
* @var int|null
*/
public ?int $volume_count; public ?int $volume_count;
} }

View File

@ -66,13 +66,8 @@ class Container implements ContainerInterface {
* *
* @return mixed Entry. * @return mixed Entry.
*/ */
public function get($id): mixed public function get(string $id): mixed
{ {
if ( ! \is_string($id))
{
throw new ContainerException('Id must be a string');
}
if ($this->has($id)) if ($this->has($id))
{ {
// Return an object instance, if it already exists // Return an object instance, if it already exists
@ -94,18 +89,13 @@ class Container implements ContainerInterface {
* Get a new instance of the specified item * Get a new instance of the specified item
* *
* @param string $id - Identifier of the entry to look for. * @param string $id - Identifier of the entry to look for.
* @param array $args - Optional arguments for the factory callable * @param array|null $args - Optional arguments for the factory callable
* @throws NotFoundException - No entry was found for this identifier. * @throws NotFoundException - No entry was found for this identifier.
* @throws ContainerException - Error while retrieving the entry. * @throws ContainerException - Error while retrieving the entry.
* @return mixed * @return mixed
*/ */
public function getNew($id, array $args = NULL): mixed public function getNew(string $id, ?array $args = NULL): mixed
{ {
if ( ! \is_string($id))
{
throw new ContainerException('Id must be a string');
}
if ($this->has($id)) if ($this->has($id))
{ {
// By default, call a factory with the Container // By default, call a factory with the Container
@ -159,7 +149,7 @@ class Container implements ContainerInterface {
* @param string $id Identifier of the entry to look for. * @param string $id Identifier of the entry to look for.
* @return boolean * @return boolean
*/ */
public function has($id): bool public function has(string $id): bool
{ {
return array_key_exists($id, $this->container); return array_key_exists($id, $this->container);
} }

View File

@ -36,7 +36,7 @@ interface ContainerInterface {
* @throws Exception\ContainerException Error while retrieving the entry. * @throws Exception\ContainerException Error while retrieving the entry.
* @return mixed Entry. * @return mixed Entry.
*/ */
public function get($id); public function get(string $id): mixed;
/** /**
* Returns true if the container can return an entry for the given identifier. * Returns true if the container can return an entry for the given identifier.
@ -45,7 +45,7 @@ interface ContainerInterface {
* @param string $id Identifier of the entry to look for. * @param string $id Identifier of the entry to look for.
* @return boolean * @return boolean
*/ */
public function has($id): bool; public function has(string $id): bool;
/** /**
* Add a factory to the container * Add a factory to the container
@ -63,7 +63,7 @@ interface ContainerInterface {
* @param mixed $value * @param mixed $value
* @return ContainerInterface * @return ContainerInterface
*/ */
public function setInstance(string $id, $value): ContainerInterface; public function setInstance(string $id, mixed $value): ContainerInterface;
/** /**
* Get a new instance of the specified item * Get a new instance of the specified item
@ -71,7 +71,7 @@ interface ContainerInterface {
* @param string $id * @param string $id
* @return mixed * @return mixed
*/ */
public function getNew($id); public function getNew(string $id): mixed;
/** /**
* Determine whether a logger channel is registered * Determine whether a logger channel is registered

View File

@ -21,10 +21,9 @@ use Aviat\AnimeClient\Tests\AnimeClientTestCase;
use Aviat\Ion\Json; use Aviat\Ion\Json;
class AnimeListTransformerTest extends AnimeClientTestCase { class AnimeListTransformerTest extends AnimeClientTestCase {
protected $dir; protected string $dir;
protected $beforeTransform; protected array $beforeTransform;
protected $afterTransform; protected AnimeListTransformer $transformer;
protected $transformer;
public function setUp(): void { public function setUp(): void {
parent::setUp(); parent::setUp();
@ -36,13 +35,13 @@ class AnimeListTransformerTest extends AnimeClientTestCase {
$this->transformer = new AnimeListTransformer(); $this->transformer = new AnimeListTransformer();
} }
public function testTransform() public function testTransform(): void
{ {
$actual = $this->transformer->transform($this->beforeTransform); $actual = $this->transformer->transform($this->beforeTransform);
$this->assertMatchesSnapshot($actual); $this->assertMatchesSnapshot($actual);
} }
public function dataUntransform() public function dataUntransform(): array
{ {
return [[ return [[
'input' => [ 'input' => [
@ -85,8 +84,9 @@ class AnimeListTransformerTest extends AnimeClientTestCase {
/** /**
* @dataProvider dataUntransform * @dataProvider dataUntransform
* @param array $input
*/ */
public function testUntransform($input) public function testUntransform(array $input): void
{ {
$actual = $this->transformer->untransform($input); $actual = $this->transformer->untransform($input);
$this->assertMatchesSnapshot($actual); $this->assertMatchesSnapshot($actual);

View File

@ -0,0 +1,40 @@
<?php declare(strict_types=1);
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu to manage anime and manga watch lists
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Tests\API\Kitsu\Transformer;
use Aviat\AnimeClient\API\Kitsu\Transformer\CharacterTransformer;
use Aviat\AnimeClient\Tests\AnimeClientTestCase;
use Aviat\Ion\Json;
class CharacterTransformerTest extends AnimeClientTestCase {
protected array $beforeTransform;
protected string $dir;
public function setUp(): void {
parent::setUp();
$this->dir = AnimeClientTestCase::TEST_DATA_DIR . '/Kitsu';
$raw = Json::decodeFile("{$this->dir}/characterBeforeTransform.json");
$this->beforeTransform = $raw;
}
public function testTransform(): void
{
$actual = (new CharacterTransformer())->transform($this->beforeTransform);
$this->assertMatchesSnapshot($actual);
}
}

View File

@ -0,0 +1,47 @@
<?php declare(strict_types=1);
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu to manage anime and manga watch lists
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Tests\API\Kitsu\Transformer;
use Aviat\AnimeClient\API\Kitsu\Transformer\AnimeHistoryTransformer;
use Aviat\AnimeClient\API\Kitsu\Transformer\MangaHistoryTransformer;
use Aviat\AnimeClient\Tests\AnimeClientTestCase;
use Aviat\Ion\Json;
class HistoryTransformerTest extends AnimeClientTestCase {
protected array $beforeTransform;
protected string $dir;
public function setUp(): void {
parent::setUp();
$this->dir = AnimeClientTestCase::TEST_DATA_DIR . '/Kitsu';
$raw = Json::decodeFile("{$this->dir}/historyBeforeTransform.json");
$this->beforeTransform = $raw;
}
public function testAnimeTransform(): void
{
$actual = (new AnimeHistoryTransformer())->transform($this->beforeTransform);
$this->assertMatchesSnapshot($actual);
}
public function testMangaTransform(): void
{
$actual = (new MangaHistoryTransformer())->transform($this->beforeTransform);
$this->assertMatchesSnapshot($actual);
}
}

View File

@ -0,0 +1,40 @@
<?php declare(strict_types=1);
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu to manage anime and manga watch lists
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Tests\API\Kitsu\Transformer;
use Aviat\AnimeClient\API\Kitsu\Transformer\PersonTransformer;
use Aviat\AnimeClient\Tests\AnimeClientTestCase;
use Aviat\Ion\Json;
class PersonTransformerTest extends AnimeClientTestCase {
protected array $beforeTransform;
protected string $dir;
public function setUp(): void {
parent::setUp();
$this->dir = AnimeClientTestCase::TEST_DATA_DIR . '/Kitsu';
$raw = Json::decodeFile("{$this->dir}/personBeforeTransform.json");
$this->beforeTransform = $raw;
}
public function testTransform(): void
{
$actual = (new PersonTransformer())->transform($this->beforeTransform);
$this->assertMatchesSnapshot($actual);
}
}

View File

@ -0,0 +1,40 @@
<?php declare(strict_types=1);
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu to manage anime and manga watch lists
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Tests\API\Kitsu\Transformer;
use Aviat\AnimeClient\API\Kitsu\Transformer\UserTransformer;
use Aviat\AnimeClient\Tests\AnimeClientTestCase;
use Aviat\Ion\Json;
class UserTransformerTest extends AnimeClientTestCase {
protected array $beforeTransform;
protected string $dir;
public function setUp(): void {
parent::setUp();
$this->dir = AnimeClientTestCase::TEST_DATA_DIR . '/Kitsu';
$raw = Json::decodeFile("{$this->dir}/userBeforeTransform.json");
$this->beforeTransform = $raw;
}
public function testTransform(): void
{
$actual = (new UserTransformer())->transform($this->beforeTransform);
$this->assertMatchesSnapshot($actual);
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,20 @@
empty: false
about: 'Web Developer, Anime Fan, Reader of VNs, and web comics.'
avatar: images/avatars/2644.gif
favorites:
anime: { 933073: { __typename: Anime, id: '14212', slug: hataraku-saibou-tv, posterImage: { original: { url: 'https://media.kitsu.io/anime/poster_images/14212/original.jpg?1597697195', height: 1050, width: 750 }, views: [{ url: 'https://media.kitsu.io/anime/poster_images/14212/tiny.jpg?1597697195', height: 156, width: 110 }, { url: 'https://media.kitsu.io/anime/poster_images/14212/small.jpg?1597697195', height: 402, width: 284 }, { url: 'https://media.kitsu.io/anime/poster_images/14212/medium.jpg?1597697195', height: 554, width: 390 }, { url: 'https://media.kitsu.io/anime/poster_images/14212/large.jpg?1597697195', height: 780, width: 550 }] }, titles: { canonical: 'Hataraku Saibou', localized: { en: 'Cells at Work!', en_jp: 'Hataraku Saibou', ja_jp: はたらく細胞 } } }, 586217: { __typename: Anime, id: '323', slug: fate-stay-night, posterImage: { original: { url: 'https://media.kitsu.io/anime/poster_images/323/original.jpg?1597698066', height: 1074, width: 760 }, views: [{ url: 'https://media.kitsu.io/anime/poster_images/323/tiny.jpg?1597698066', height: 156, width: 110 }, { url: 'https://media.kitsu.io/anime/poster_images/323/small.jpg?1597698066', height: 402, width: 284 }, { url: 'https://media.kitsu.io/anime/poster_images/323/medium.jpg?1597698066', height: 554, width: 390 }, { url: 'https://media.kitsu.io/anime/poster_images/323/large.jpg?1597698066', height: 780, width: 550 }] }, titles: { canonical: 'Fate/stay night', localized: { en: 'Fate/stay night', en_jp: 'Fate/stay night', en_us: 'Fate/stay night', ja_jp: 'Fate/stay night' } } }, 607473: { __typename: Anime, id: '310', slug: tsukuyomi-moon-phase, posterImage: { original: { url: 'https://media.kitsu.io/anime/poster_images/310/original.jpg?1597690591', height: 320, width: 225 }, views: [{ url: 'https://media.kitsu.io/anime/poster_images/310/tiny.jpg?1597690591', height: 156, width: 110 }, { url: 'https://media.kitsu.io/anime/poster_images/310/small.jpg?1597690591', height: 402, width: 284 }, { url: 'https://media.kitsu.io/anime/poster_images/310/medium.jpg?1597690591', height: 554, width: 390 }, { url: 'https://media.kitsu.io/anime/poster_images/310/large.jpg?1597690591', height: 780, width: 550 }] }, titles: { canonical: 'Tsukuyomi: Moon Phase', localized: { en: 'Tsukuyomi: Moon Phase', en_jp: 'Tsukuyomi: Moon Phase', en_us: 'Tsukuyomi: Moon Phase', ja_jp: '月詠 MOON PHASE' } } }, 607472: { __typename: Anime, id: '5992', slug: carnival-phantasm, posterImage: { original: { url: 'https://media.kitsu.io/anime/poster_images/5992/original.jpg?1597697878', height: 693, width: 533 }, views: [{ url: 'https://media.kitsu.io/anime/poster_images/5992/tiny.jpg?1597697878', height: 156, width: 110 }, { url: 'https://media.kitsu.io/anime/poster_images/5992/small.jpg?1597697878', height: 402, width: 284 }, { url: 'https://media.kitsu.io/anime/poster_images/5992/medium.jpg?1597697878', height: 554, width: 390 }, { url: 'https://media.kitsu.io/anime/poster_images/5992/large.jpg?1597697878', height: 780, width: 550 }] }, titles: { canonical: 'Carnival Phantasm', localized: { en_jp: 'Carnival Phantasm', ja_jp: カーニバル・ファンタズム } } }, 636892: { __typename: Anime, id: '6062', slug: nichijou, posterImage: { original: { url: 'https://media.kitsu.io/anime/poster_images/6062/original.jpg?1597696783', height: 2292, width: 1610 }, views: [{ url: 'https://media.kitsu.io/anime/poster_images/6062/tiny.jpg?1597696783', height: 156, width: 110 }, { url: 'https://media.kitsu.io/anime/poster_images/6062/small.jpg?1597696783', height: 402, width: 284 }, { url: 'https://media.kitsu.io/anime/poster_images/6062/medium.jpg?1597696783', height: 554, width: 390 }, { url: 'https://media.kitsu.io/anime/poster_images/6062/large.jpg?1597696783', height: 780, width: 550 }] }, titles: { canonical: Nichijou, localized: { en: 'Nichijou - My Ordinary Life', en_jp: Nichijou, en_us: 'Nichijou - My Ordinary Life', ja_jp: 日常 } } } }
character: { 586219: { __typename: Character, id: '6553', slug: saber, image: { original: { url: 'https://media.kitsu.io/characters/images/6553/original.jpg?1483096805' } }, names: { alternatives: ['King of Knights'], canonical: Saber, canonicalLocale: null, localized: { en: Saber, ja_jp: セイバー } } }, 586218: { __typename: Character, id: '6556', slug: rin-tohsaka, image: { original: { url: 'https://media.kitsu.io/characters/images/6556/original.jpg?1483096805' } }, names: { alternatives: { }, canonical: 'Rin Toosaka', canonicalLocale: null, localized: { en: 'Rin Toosaka', ja_jp: '遠坂 凛' } } }, 611365: { __typename: Character, id: '32035', slug: nano-shinonome, image: { original: { url: 'https://media.kitsu.io/characters/images/32035/original.jpg?1483096805' } }, names: { alternatives: { }, canonical: 'Nano Shinonome', canonicalLocale: null, localized: { en: 'Nano Shinonome', ja_jp: '東雲 なの' } } }, 611364: { __typename: Character, id: '32034', slug: mio-naganohara, image: { original: { url: 'https://media.kitsu.io/characters/images/32034/original.jpg?1483096805' } }, names: { alternatives: { }, canonical: 'Mio Naganohara', canonicalLocale: null, localized: { en: 'Mio Naganohara', ja_jp: 長野原みお } } }, 636590: { __typename: Character, id: '31851', slug: aria-holmes-kanzaki, image: { original: { url: 'https://media.kitsu.io/characters/images/31851/original.jpg?1483096805' } }, names: { alternatives: ['Quadra Aria'], canonical: 'Aria Holmes Kanzaki', canonicalLocale: null, localized: { en: 'Aria Holmes Kanzaki', ja_jp: 神崎・H・アリア } } }, 636591: { __typename: Character, id: '25930', slug: taiga-aisaka, image: { original: { url: 'https://media.kitsu.io/characters/images/25930/original.jpg?1483096805' } }, names: { alternatives: ['Palmtop Tiger'], canonical: 'Taiga Aisaka', canonicalLocale: null, localized: { en: 'Taiga Aisaka', ja_jp: '逢坂 大河' } } }, 636593: { __typename: Character, id: '31625', slug: victorique-de-blois, image: { original: { url: 'https://media.kitsu.io/characters/images/31625/original.jpg?1483096805' } }, names: { alternatives: ['The Golden Fairy', 'Gray Wolf', 'Monstre Charmant'], canonical: 'Victorique de Blois', canonicalLocale: null, localized: { en: 'Victorique de Blois', ja_jp: ヴィクトリカ・ド・ブロワ } } } }
manga: { 636888: { __typename: Manga, id: '21733', slug: tonari-no-seki-kun, posterImage: { original: { url: 'https://media.kitsu.io/manga/poster_images/21733/original.jpg?1496845097', height: null, width: null }, views: [{ url: 'https://media.kitsu.io/manga/poster_images/21733/tiny.jpg?1496845097', height: null, width: null }, { url: 'https://media.kitsu.io/manga/poster_images/21733/small.jpg?1496845097', height: null, width: null }, { url: 'https://media.kitsu.io/manga/poster_images/21733/medium.jpg?1496845097', height: null, width: null }, { url: 'https://media.kitsu.io/manga/poster_images/21733/large.jpg?1496845097', height: null, width: null }] }, titles: { canonical: 'Tonari no Seki-kun', localized: { en: 'My Neighbour Seki', en_jp: 'Tonari no Seki-kun', en_us: 'My Neighbour Seki', ja_jp: となりの関くん } } } }
location: 'Michigan, USA'
name: timw4mail
slug: timw4mail
stats:
'Time spent watching anime:': '196 days, 5 hours, 25 minutes, and 17 seconds'
'Anime series watched:': '1,044'
'Anime episodes watched:': '14,943'
'Manga series read:': '49'
'Manga chapters read:': '2,678'
waifu:
label: Waifu
character: { id: '6553', slug: saber, image: { original: { name: original, url: 'https://media.kitsu.io/characters/images/6553/original.jpg?1483096805', width: null, height: null } }, names: { canonical: Saber, alternatives: ['King of Knights'], localized: { en: Saber, ja_jp: セイバー } } }
website: 'https://timshomepage.net'

View File

@ -16,9 +16,11 @@
namespace Aviat\AnimeClient\Tests; namespace Aviat\AnimeClient\Tests;
use Amp\Http\Client\Response;
use function Aviat\AnimeClient\arrayToToml; use function Aviat\AnimeClient\arrayToToml;
use function Aviat\AnimeClient\checkFolderPermissions;
use function Aviat\AnimeClient\clearCache;
use function Aviat\AnimeClient\colNotEmpty;
use function Aviat\AnimeClient\getLocalImg;
use function Aviat\AnimeClient\getResponse; use function Aviat\AnimeClient\getResponse;
use function Aviat\AnimeClient\isSequentialArray; use function Aviat\AnimeClient\isSequentialArray;
use function Aviat\AnimeClient\tomlToArray; use function Aviat\AnimeClient\tomlToArray;
@ -89,4 +91,46 @@ class AnimeClientTest extends AnimeClientTestCase
{ {
$this->assertNotEmpty(getResponse('https://example.com')); $this->assertNotEmpty(getResponse('https://example.com'));
} }
public function testCheckFolderPermissions(): void
{
$config = $this->container->get('config');
$actual = checkFolderPermissions($config);
$this->assertTrue(is_array($actual));
}
public function testGetLocalImageEmptyUrl(): void
{
$actual = getLocalImg('');
$this->assertEquals('images/placeholder.webp', $actual);
}
public function testGetLocalImageBadUrl(): void
{
$actual = getLocalImg('//foo.bar');
$this->assertEquals('images/placeholder.webp', $actual);
}
public function testColNotEmpty(): void
{
$hasEmptyCols = [[
'foo' => '',
], [
'foo' => '',
]];
$hasNonEmptyCols = [[
'foo' => 'bar',
], [
'foo' => 'baz',
]];
$this->assertEquals(false, colNotEmpty($hasEmptyCols, 'foo'));
$this->assertEquals(true, colNotEmpty($hasNonEmptyCols, 'foo'));
}
public function testClearCache(): void
{
$this->assertTrue(clearCache($this->container->get('cache')));
}
} }

View File

@ -16,6 +16,8 @@
namespace Aviat\AnimeClient\Tests; namespace Aviat\AnimeClient\Tests;
use Aviat\Ion\Di\ContainerAware;
use Aviat\Ion\Di\ContainerInterface;
use function Aviat\Ion\_dir; use function Aviat\Ion\_dir;
use Aviat\Ion\Json; use Aviat\Ion\Json;
@ -26,10 +28,16 @@ use Laminas\Diactoros\{
ServerRequestFactory ServerRequestFactory
}; };
use const Aviat\AnimeClient\{
SLUG_PATTERN,
DEFAULT_CONTROLLER,
};
/** /**
* Base class for TestCases * Base class for TestCases
*/ */
class AnimeClientTestCase extends TestCase { class AnimeClientTestCase extends TestCase {
use ContainerAware;
use MatchesSnapshots; use MatchesSnapshots;
// Test directory constants // Test directory constants
@ -38,17 +46,10 @@ class AnimeClientTestCase extends TestCase {
public const TEST_DATA_DIR = __DIR__ . '/test_data'; public const TEST_DATA_DIR = __DIR__ . '/test_data';
public const TEST_VIEW_DIR = __DIR__ . '/test_views'; public const TEST_VIEW_DIR = __DIR__ . '/test_views';
protected $container; protected ContainerInterface $container;
protected static $staticContainer;
protected static $session_handler;
public static function setUpBeforeClass(): void public static function setUpBeforeClass(): void
{ {
// Use mock session handler
//$session_handler = new TestSessionHandler();
//session_set_save_handler($session_handler, TRUE);
//self::$session_handler = $session_handler;
// Remove test cache files // Remove test cache files
$files = glob(_dir(self::TEST_DATA_DIR, 'cache', '*.json')); $files = glob(_dir(self::TEST_DATA_DIR, 'cache', '*.json'));
array_map('unlink', $files); array_map('unlink', $files);
@ -59,6 +60,7 @@ class AnimeClientTestCase extends TestCase {
parent::setUp(); parent::setUp();
$config_array = [ $config_array = [
'root' => self::ROOT_DIR,
'asset_path' => '/assets', 'asset_path' => '/assets',
'img_cache_path' => _dir(self::ROOT_DIR, 'public/images'), 'img_cache_path' => _dir(self::ROOT_DIR, 'public/images'),
'data_cache_path' => _dir(self::TEST_DATA_DIR, 'cache'), 'data_cache_path' => _dir(self::TEST_DATA_DIR, 'cache'),
@ -88,13 +90,11 @@ class AnimeClientTestCase extends TestCase {
'file' => ':memory:', 'file' => ':memory:',
] ]
], ],
'routes' => [ 'routes' => [ ],
],
]; ];
// Set up DI container // Set up DI container
$di = require _dir(self::ROOT_DIR, 'app', 'bootstrap.php'); $di = require self::ROOT_DIR . '/app/bootstrap.php';
$container = $di($config_array); $container = $di($config_array);
// Use mock session handler // Use mock session handler
@ -157,7 +157,7 @@ class AnimeClientTestCase extends TestCase {
* @param array $args * @param array $args
* @return mixed - the decoded data * @return mixed - the decoded data
*/ */
public function getMockFileData(...$args) public function getMockFileData(mixed ...$args): mixed
{ {
$rawData = $this->getMockFile(...$args); $rawData = $this->getMockFile(...$args);

View File

@ -27,8 +27,8 @@ class Command extends BaseCommand {
} }
class BaseCommandTest extends AnimeClientTestCase { class BaseCommandTest extends AnimeClientTestCase {
protected $base; protected Command $base;
protected $friend; protected Friend $friend;
public function setUp(): void { public function setUp(): void {
$this->base = new Command(new Console()); $this->base = new Command(new Console());

View File

@ -21,13 +21,14 @@ use Aviat\AnimeClient\Controller;
use Aviat\AnimeClient\Dispatcher; use Aviat\AnimeClient\Dispatcher;
use Aviat\AnimeClient\UrlGenerator; use Aviat\AnimeClient\UrlGenerator;
use Aviat\Ion\Config; use Aviat\Ion\Config;
use Aviat\Ion\Di\ContainerInterface;
use Monolog\Handler\TestHandler; use Monolog\Handler\TestHandler;
use Monolog\Logger; use Monolog\Logger;
class DispatcherTest extends AnimeClientTestCase { class DispatcherTest extends AnimeClientTestCase {
protected $container; protected ContainerInterface $container;
protected $router; protected $router;
protected $config; protected $config;
protected $urlGenerator; protected $urlGenerator;

View File

@ -0,0 +1,37 @@
<?php declare(strict_types=1);
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu to manage anime and manga watch lists
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Tests\Helper;
use Aviat\AnimeClient\Helper\Form as FormHelper;
use Aviat\AnimeClient\Tests\AnimeClientTestCase;
class FormHelperTest extends AnimeClientTestCase {
public function testFormHelper(): void
{
$helper = new FormHelper();
$helper->setContainer($this->container);
$actual = $helper('input', [
'type' => 'text',
'value' => 'foo',
'placeholder' => 'field',
'name' => 'test'
]);
$this->assertMatchesSnapshot($actual);
}
}

View File

@ -22,53 +22,55 @@ use Aviat\AnimeClient\Tests\AnimeClientTestCase;
class PictureHelperTest extends AnimeClientTestCase { class PictureHelperTest extends AnimeClientTestCase {
/** /**
* @dataProvider dataPictureCase * @dataProvider dataPictureCase
* @param array $params
*/ */
public function testPictureHelper($params, $expected = NULL) public function testPictureHelper(array $params): void
{ {
$helper = new PictureHelper(); $helper = new PictureHelper();
$helper->setContainer($this->container); $helper->setContainer($this->container);
$actual = $helper(...$params); $actual = $helper(...$params);
if ($expected === NULL) $this->assertMatchesSnapshot($actual);
{
$this->assertMatchesSnapshot($actual);
}
else
{
$this->assertEquals($expected, $actual);
}
} }
/** /**
* @dataProvider dataSimpleImageCase * @dataProvider dataSimpleImageCase
* @param string $ext
* @param bool $isSimple
* @param string $fallbackExt
*/ */
public function testSimpleImage(string $ext, bool $isSimple) public function testSimpleImage(string $ext, bool $isSimple, string $fallbackExt = 'jpg'): void
{ {
$helper = new PictureHelper(); $helper = new PictureHelper();
$helper->setContainer($this->container); $helper->setContainer($this->container);
$url = "https://example.com/image.{$ext}"; $url = "https://example.com/image.{$ext}";
$actual = $helper($url); $actual = $helper($url, $fallbackExt);
$actuallySimple = strpos($actual, '<picture') === FALSE; $actuallySimple = ! str_contains($actual, '<picture');
$this->assertEquals($isSimple, $actuallySimple); $this->assertEquals($isSimple, $actuallySimple);
} }
public function testSimpleImageByFallback() public function testSimpleImageByFallback(): void
{ {
$helper = new PictureHelper(); $helper = new PictureHelper();
$helper->setContainer($this->container); $helper->setContainer($this->container);
$actual = $helper("foo.svg", 'svg'); $actual = $helper("foo.svg", 'svg');
$this->assertTrue(strpos($actual, '<picture') === FALSE); $this->assertTrue(! str_contains($actual, '<picture'));
} }
public function dataPictureCase() public function dataPictureCase(): array
{ {
return [ return [
'Full AVIF URL' => [
'params' => [
'https://www.example.com/image.avif',
],
],
'Full webp URL' => [ 'Full webp URL' => [
'params' => [ 'params' => [
'https://www.example.com/image.webp', 'https://www.example.com/image.webp',
@ -112,16 +114,21 @@ class PictureHelperTest extends AnimeClientTestCase {
'params' => [ 'params' => [
'images/foo.jpg', 'images/foo.jpg',
'jpg', 'jpg',
[ 'x' => 1, 'y' => 1 ], [],
['width' => 200, 'height' => 200, 'alt' => 'should exist'], ['width' => 200, 'height' => 200, 'alt' => 'should exist'],
] ]
] ]
]; ];
} }
public function dataSimpleImageCase() public function dataSimpleImageCase(): array
{ {
return [ return [
'avif' => [
'ext' => 'avif',
'isSimple' => FALSE,
'fallback' => 'jpf'
],
'apng' => [ 'apng' => [
'ext' => 'apng', 'ext' => 'apng',
'isSimple' => FALSE, 'isSimple' => FALSE,

View File

@ -0,0 +1 @@
<input id="input" type="text" name="input" value="foo" />

View File

@ -0,0 +1 @@
<picture loading="lazy"><source srcset="https://www.example.com/image.avif" type="image/avif" /><source srcset="https://www.example.com/image.jpg" type="image/jpeg" /><img src="https://www.example.com/image.jpg" alt="" loading="lazy" /></picture>

View File

@ -55,6 +55,17 @@ class KitsuTest extends TestCase {
$this->assertEquals($expected, Kitsu::parseStreamingLinks($nodes)); $this->assertEquals($expected, Kitsu::parseStreamingLinks($nodes));
} }
public function testParseStreamingLinksNoHost(): void
{
$nodes = [[
'url' => '/link-fragment',
'dubs' => [],
'subs' => [],
]];
$this->assertEquals([], Kitsu::parseStreamingLinks($nodes));
}
public function testGetAiringStatusEmptyArguments(): void public function testGetAiringStatusEmptyArguments(): void
{ {
$this->assertEquals(AnimeAiringStatus::NOT_YET_AIRED, Kitsu::getAiringStatus()); $this->assertEquals(AnimeAiringStatus::NOT_YET_AIRED, Kitsu::getAiringStatus());
@ -123,7 +134,7 @@ class KitsuTest extends TestCase {
$this->assertEquals($expected, $actual); $this->assertEquals($expected, $actual);
} }
public function testFilterLocalizedTitles() public function testFilterLocalizedTitles(): void
{ {
$input = [ $input = [
'canonical' => 'foo', 'canonical' => 'foo',
@ -140,7 +151,7 @@ class KitsuTest extends TestCase {
$this->assertEquals(['Foo the Movie'], $actual); $this->assertEquals(['Foo the Movie'], $actual);
} }
public function testGetFilteredTitles() public function testGetFilteredTitles(): void
{ {
$input = [ $input = [
'canonical' => 'foo', 'canonical' => 'foo',

View File

@ -22,7 +22,7 @@ class RequirementsTest extends AnimeClientTestCase {
public function testPHPVersion(): void public function testPHPVersion(): void
{ {
$this->assertTrue(version_compare(PHP_VERSION, "7.4", "ge")); $this->assertTrue(version_compare(PHP_VERSION, "8", "ge"));
} }
public function testHasPDO(): void public function testHasPDO(): void

View File

@ -0,0 +1,52 @@
<?php declare(strict_types=1);
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu to manage anime and manga watch lists
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Tests\Types;
use Aviat\AnimeClient\Types\Config;
class ConfigTest extends ConfigTestCase {
public function setUp(): void
{
parent::setUp();
$this->testClass = Config::class;
}
public function testSetMethods(): void
{
$type = $this->testClass::from([
'anilist' => [],
'cache' => [],
'database' => [],
]);
$this->assertEquals(3, $type->count());
}
public function testOffsetUnset(): void
{
$type = $this->testClass::from([
'anilist' => [],
]);
$this->assertTrue($type->offsetExists('anilist'));
$type->offsetUnset('anilist');
$this->assertNotTrue($type->offsetExists('anilist'));
}
}

View File

@ -0,0 +1,72 @@
<?php declare(strict_types=1);
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu to manage anime and manga watch lists
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Tests\Types;
use Aviat\AnimeClient\Tests\AnimeClientTestCase;
use Aviat\AnimeClient\Types\UndefinedPropertyException;
abstract class ConfigTestCase extends AnimeClientTestCase {
public string $testClass;
public function testCheck(): void
{
$result = $this->testClass::check([]);
$this->assertEquals([], $result);
}
public function testSetUndefinedProperty(): void
{
$this->expectException(UndefinedPropertyException::class);
$this->testClass::from([
'foobar' => 'baz',
]);
}
public function testToString(): void
{
$actual = $this->testClass::from([])->__toString();
$this->assertMatchesSnapshot($actual);
}
public function testOffsetExists(): void
{
$actual = $this->testClass::from([
'anilist' => [],
])->offsetExists('anilist');
$this->assertTrue($actual);
}
public function testSetState(): void
{
$normal = $this->testClass::from([]);
$setState = $this->testClass::__set_state([]);
$this->assertEquals($normal, $setState);
}
public function testIsEmpty(): void
{
$type = $this->testClass::from([]);
$this->assertTrue($type->isEmpty());
}
public function testCount(): void
{
$type = $this->testClass::from([]);
$this->assertEquals(0, $type->count());
}
}

View File

@ -0,0 +1,3 @@
Aviat\AnimeClient\Types\Config Object
(
)

View File

@ -17,7 +17,7 @@ use Aviat\Ion\View\{HtmlView, HttpView, JsonView};
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
class MockErrorHandler { class MockErrorHandler {
public function addDataTable($name, array $values=[]) {} public function addDataTable(string $name, array $values=[]): void {}
} }
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
@ -128,12 +128,12 @@ class TestJsonView extends JsonView {
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
trait MockInjectionTrait { trait MockInjectionTrait {
public function __get($key) public function __get(string $key): mixed
{ {
return $this->$key; return $this->$key;
} }
public function __set($key, $value) public function __set(string $key, mixed $value)
{ {
$this->$key = $value; $this->$key = $value;
return $this; return $this;

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,536 @@
{
"data": {
"findProfileBySlug": {
"about": "Web Developer, Anime Fan, Reader of VNs, and web comics.",
"avatarImage": {
"original": {
"name": "original",
"url": "https://media.kitsu.io/users/avatars/2644/original.gif?1491510751",
"width": null,
"height": null
}
},
"bannerImage": {
"original": {
"name": "original",
"url": "https://media.kitsu.io/users/cover_images/2644/original.jpeg?1487201681",
"width": null,
"height": null
}
},
"birthday": "1990-03-09",
"id": "2644",
"location": "Michigan, USA",
"name": "timw4mail",
"proMessage": null,
"proTier": null,
"slug": "timw4mail",
"siteLinks": {
"nodes": [
{
"id": "5804",
"url": "https://timshomepage.net"
},
{
"id": "4149",
"url": "https://github.com/timw4mail"
},
{
"id": "4151",
"url": "https://twitter.com/timw4mail"
},
{
"id": "4150",
"url": "timw4mail#9933"
},
{
"id": "4152",
"url": "http://steamcommunity.com/id/timw4mail"
}
]
},
"favorites": {
"nodes": [
{
"id": "933073",
"item": {
"__typename": "Anime",
"id": "14212",
"slug": "hataraku-saibou-tv",
"posterImage": {
"original": {
"url": "https://media.kitsu.io/anime/poster_images/14212/original.jpg?1597697195",
"height": 1050,
"width": 750
},
"views": [
{
"url": "https://media.kitsu.io/anime/poster_images/14212/tiny.jpg?1597697195",
"height": 156,
"width": 110
},
{
"url": "https://media.kitsu.io/anime/poster_images/14212/small.jpg?1597697195",
"height": 402,
"width": 284
},
{
"url": "https://media.kitsu.io/anime/poster_images/14212/medium.jpg?1597697195",
"height": 554,
"width": 390
},
{
"url": "https://media.kitsu.io/anime/poster_images/14212/large.jpg?1597697195",
"height": 780,
"width": 550
}
]
},
"titles": {
"canonical": "Hataraku Saibou",
"localized": {
"en": "Cells at Work!",
"en_jp": "Hataraku Saibou",
"ja_jp": "はたらく細胞"
}
}
}
},
{
"id": "586217",
"item": {
"__typename": "Anime",
"id": "323",
"slug": "fate-stay-night",
"posterImage": {
"original": {
"url": "https://media.kitsu.io/anime/poster_images/323/original.jpg?1597698066",
"height": 1074,
"width": 760
},
"views": [
{
"url": "https://media.kitsu.io/anime/poster_images/323/tiny.jpg?1597698066",
"height": 156,
"width": 110
},
{
"url": "https://media.kitsu.io/anime/poster_images/323/small.jpg?1597698066",
"height": 402,
"width": 284
},
{
"url": "https://media.kitsu.io/anime/poster_images/323/medium.jpg?1597698066",
"height": 554,
"width": 390
},
{
"url": "https://media.kitsu.io/anime/poster_images/323/large.jpg?1597698066",
"height": 780,
"width": 550
}
]
},
"titles": {
"canonical": "Fate/stay night",
"localized": {
"en": "Fate/stay night",
"en_jp": "Fate/stay night",
"en_us": "Fate/stay night",
"ja_jp": "Fate/stay night"
}
}
}
},
{
"id": "586219",
"item": {
"__typename": "Character",
"id": "6553",
"slug": "saber",
"image": {
"original": {
"url": "https://media.kitsu.io/characters/images/6553/original.jpg?1483096805"
}
},
"names": {
"alternatives": [
"King of Knights"
],
"canonical": "Saber",
"canonicalLocale": null,
"localized": {
"en": "Saber",
"ja_jp": "セイバー"
}
}
}
},
{
"id": "586218",
"item": {
"__typename": "Character",
"id": "6556",
"slug": "rin-tohsaka",
"image": {
"original": {
"url": "https://media.kitsu.io/characters/images/6556/original.jpg?1483096805"
}
},
"names": {
"alternatives": [],
"canonical": "Rin Toosaka",
"canonicalLocale": null,
"localized": {
"en": "Rin Toosaka",
"ja_jp": "遠坂 凛"
}
}
}
},
{
"id": "611365",
"item": {
"__typename": "Character",
"id": "32035",
"slug": "nano-shinonome",
"image": {
"original": {
"url": "https://media.kitsu.io/characters/images/32035/original.jpg?1483096805"
}
},
"names": {
"alternatives": [],
"canonical": "Nano Shinonome",
"canonicalLocale": null,
"localized": {
"en": "Nano Shinonome",
"ja_jp": "東雲 なの"
}
}
}
},
{
"id": "611364",
"item": {
"__typename": "Character",
"id": "32034",
"slug": "mio-naganohara",
"image": {
"original": {
"url": "https://media.kitsu.io/characters/images/32034/original.jpg?1483096805"
}
},
"names": {
"alternatives": [],
"canonical": "Mio Naganohara",
"canonicalLocale": null,
"localized": {
"en": "Mio Naganohara",
"ja_jp": "長野原みお"
}
}
}
},
{
"id": "607473",
"item": {
"__typename": "Anime",
"id": "310",
"slug": "tsukuyomi-moon-phase",
"posterImage": {
"original": {
"url": "https://media.kitsu.io/anime/poster_images/310/original.jpg?1597690591",
"height": 320,
"width": 225
},
"views": [
{
"url": "https://media.kitsu.io/anime/poster_images/310/tiny.jpg?1597690591",
"height": 156,
"width": 110
},
{
"url": "https://media.kitsu.io/anime/poster_images/310/small.jpg?1597690591",
"height": 402,
"width": 284
},
{
"url": "https://media.kitsu.io/anime/poster_images/310/medium.jpg?1597690591",
"height": 554,
"width": 390
},
{
"url": "https://media.kitsu.io/anime/poster_images/310/large.jpg?1597690591",
"height": 780,
"width": 550
}
]
},
"titles": {
"canonical": "Tsukuyomi: Moon Phase",
"localized": {
"en": "Tsukuyomi: Moon Phase",
"en_jp": "Tsukuyomi: Moon Phase",
"en_us": "Tsukuyomi: Moon Phase",
"ja_jp": "月詠 MOON PHASE"
}
}
}
},
{
"id": "607472",
"item": {
"__typename": "Anime",
"id": "5992",
"slug": "carnival-phantasm",
"posterImage": {
"original": {
"url": "https://media.kitsu.io/anime/poster_images/5992/original.jpg?1597697878",
"height": 693,
"width": 533
},
"views": [
{
"url": "https://media.kitsu.io/anime/poster_images/5992/tiny.jpg?1597697878",
"height": 156,
"width": 110
},
{
"url": "https://media.kitsu.io/anime/poster_images/5992/small.jpg?1597697878",
"height": 402,
"width": 284
},
{
"url": "https://media.kitsu.io/anime/poster_images/5992/medium.jpg?1597697878",
"height": 554,
"width": 390
},
{
"url": "https://media.kitsu.io/anime/poster_images/5992/large.jpg?1597697878",
"height": 780,
"width": 550
}
]
},
"titles": {
"canonical": "Carnival Phantasm",
"localized": {
"en_jp": "Carnival Phantasm",
"ja_jp": "カーニバル・ファンタズム"
}
}
}
},
{
"id": "636590",
"item": {
"__typename": "Character",
"id": "31851",
"slug": "aria-holmes-kanzaki",
"image": {
"original": {
"url": "https://media.kitsu.io/characters/images/31851/original.jpg?1483096805"
}
},
"names": {
"alternatives": [
"Quadra Aria"
],
"canonical": "Aria Holmes Kanzaki",
"canonicalLocale": null,
"localized": {
"en": "Aria Holmes Kanzaki",
"ja_jp": "神崎・H・アリア"
}
}
}
},
{
"id": "636591",
"item": {
"__typename": "Character",
"id": "25930",
"slug": "taiga-aisaka",
"image": {
"original": {
"url": "https://media.kitsu.io/characters/images/25930/original.jpg?1483096805"
}
},
"names": {
"alternatives": [
"Palmtop Tiger"
],
"canonical": "Taiga Aisaka",
"canonicalLocale": null,
"localized": {
"en": "Taiga Aisaka",
"ja_jp": "逢坂 大河"
}
}
}
},
{
"id": "636593",
"item": {
"__typename": "Character",
"id": "31625",
"slug": "victorique-de-blois",
"image": {
"original": {
"url": "https://media.kitsu.io/characters/images/31625/original.jpg?1483096805"
}
},
"names": {
"alternatives": [
"The Golden Fairy",
"Gray Wolf",
"Monstre Charmant"
],
"canonical": "Victorique de Blois",
"canonicalLocale": null,
"localized": {
"en": "Victorique de Blois",
"ja_jp": "ヴィクトリカ・ド・ブロワ"
}
}
}
},
{
"id": "636888",
"item": {
"__typename": "Manga",
"id": "21733",
"slug": "tonari-no-seki-kun",
"posterImage": {
"original": {
"url": "https://media.kitsu.io/manga/poster_images/21733/original.jpg?1496845097",
"height": null,
"width": null
},
"views": [
{
"url": "https://media.kitsu.io/manga/poster_images/21733/tiny.jpg?1496845097",
"height": null,
"width": null
},
{
"url": "https://media.kitsu.io/manga/poster_images/21733/small.jpg?1496845097",
"height": null,
"width": null
},
{
"url": "https://media.kitsu.io/manga/poster_images/21733/medium.jpg?1496845097",
"height": null,
"width": null
},
{
"url": "https://media.kitsu.io/manga/poster_images/21733/large.jpg?1496845097",
"height": null,
"width": null
}
]
},
"titles": {
"canonical": "Tonari no Seki-kun",
"localized": {
"en": "My Neighbour Seki",
"en_jp": "Tonari no Seki-kun",
"en_us": "My Neighbour Seki",
"ja_jp": "となりの関くん"
}
}
}
},
{
"id": "636892",
"item": {
"__typename": "Anime",
"id": "6062",
"slug": "nichijou",
"posterImage": {
"original": {
"url": "https://media.kitsu.io/anime/poster_images/6062/original.jpg?1597696783",
"height": 2292,
"width": 1610
},
"views": [
{
"url": "https://media.kitsu.io/anime/poster_images/6062/tiny.jpg?1597696783",
"height": 156,
"width": 110
},
{
"url": "https://media.kitsu.io/anime/poster_images/6062/small.jpg?1597696783",
"height": 402,
"width": 284
},
{
"url": "https://media.kitsu.io/anime/poster_images/6062/medium.jpg?1597696783",
"height": 554,
"width": 390
},
{
"url": "https://media.kitsu.io/anime/poster_images/6062/large.jpg?1597696783",
"height": 780,
"width": 550
}
]
},
"titles": {
"canonical": "Nichijou",
"localized": {
"en": "Nichijou - My Ordinary Life",
"en_jp": "Nichijou",
"en_us": "Nichijou - My Ordinary Life",
"ja_jp": "日常"
}
}
}
}
]
},
"stats": {
"animeAmountConsumed": {
"completed": 893,
"id": "2161520",
"media": 1044,
"recalculatedAt": "2018-12-25",
"time": 16953917,
"units": 14943
},
"mangaAmountConsumed": {
"completed": 26,
"id": "841057",
"media": 49,
"recalculatedAt": "2018-12-20",
"units": 2678
}
},
"url": "https://kitsu/users/timw4mail",
"waifu": {
"id": "6553",
"slug": "saber",
"image": {
"original": {
"name": "original",
"url": "https://media.kitsu.io/characters/images/6553/original.jpg?1483096805",
"width": null,
"height": null
}
},
"names": {
"canonical": "Saber",
"alternatives": [
"King of Knights"
],
"localized": {
"en": "Saber",
"ja_jp": "セイバー"
}
}
},
"waifuOrHusbando": "Waifu"
}
}
}

View File

@ -23,6 +23,8 @@ use Monolog\Logger;
use Monolog\Handler\{TestHandler, NullHandler}; use Monolog\Handler\{TestHandler, NullHandler};
use Aviat\Ion\Di\ContainerInterface; use Aviat\Ion\Di\ContainerInterface;
use Aviat\Ion\Di\Exception\NotFoundException; use Aviat\Ion\Di\Exception\NotFoundException;
use Throwable;
use TypeError;
class FooTest { class FooTest {
@ -49,13 +51,11 @@ class ContainerTest extends IonTestCase {
return [ return [
'Bad index type: number' => [ 'Bad index type: number' => [
'id' => 42, 'id' => 42,
'exception' => ContainerException::class, 'exception' => TypeError::class,
'message' => 'Id must be a string'
], ],
'Bad index type: array' => [ 'Bad index type: array' => [
'id' => [], 'id' => [],
'exception' => ContainerException::class, 'exception' => TypeError::class,
'message' => 'Id must be a string'
], ],
'Non-existent id' => [ 'Non-existent id' => [
'id' => 'foo', 'id' => 'foo',
@ -68,7 +68,7 @@ class ContainerTest extends IonTestCase {
/** /**
* @dataProvider dataGetWithException * @dataProvider dataGetWithException
*/ */
public function testGetWithException($id, $exception, $message): void public function testGetWithException(mixed $id, $exception, ?string $message = NULL): void
{ {
try try
{ {
@ -79,15 +79,23 @@ class ContainerTest extends IonTestCase {
$this->assertInstanceOf($exception, $e); $this->assertInstanceOf($exception, $e);
$this->assertEquals($message, $e->getMessage()); $this->assertEquals($message, $e->getMessage());
} }
catch(Throwable $e)
{
$this->assertInstanceOf($exception, $e);
}
} }
/** /**
* @dataProvider dataGetWithException * @dataProvider dataGetWithException
*/ */
public function testGetNewWithException($id, $exception, $message): void public function testGetNewWithException(mixed $id, $exception, ?string $message = NULL): void
{ {
$this->expectException($exception); $this->expectException($exception);
$this->expectExceptionMessage($message); if ($message !== NULL)
{
$this->expectExceptionMessage($message);
}
$this->container->getNew($id); $this->container->getNew($id);
} }

View File

@ -4,11 +4,7 @@
*/ */
// Work around the silly timezone error // Work around the silly timezone error
$timezone = ini_get('date.timezone'); date_default_timezone_set('UTC');
if ($timezone === '' || $timezone === FALSE)
{
ini_set('date.timezone', 'GMT');
}
define('AC_TEST_ROOT_DIR', dirname(__DIR__) . '/'); define('AC_TEST_ROOT_DIR', dirname(__DIR__) . '/');
define('SRC_DIR', AC_TEST_ROOT_DIR . 'src/'); define('SRC_DIR', AC_TEST_ROOT_DIR . 'src/');