Merge remote-tracking branch 'origin/develop'

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

8
Jenkinsfile vendored
View File

@ -18,7 +18,8 @@ pipeline {
}
}
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'
}
}
@ -30,14 +31,15 @@ pipeline {
}
}
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'
}
}
stage('Code Cleanliness') {
agent any
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(
failOnError: false,
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
*
* 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
* @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
* @version 4.0
* @link https://github.com/timw4mail/HummingBirdAnimeClient
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
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
// ----------------------------------------------------------------------------
$APP_DIR = realpath(__DIR__ . '/../');
$ROOT_DIR = realpath("{$APP_DIR}/../");
$APP_DIR = dirname(__DIR__);
$ROOT_DIR = dirname($APP_DIR);
$tomlConfig = loadConfig(__DIR__);
return array_merge($tomlConfig, [
'root' => $ROOT_DIR,
'asset_dir' => "{$ROOT_DIR}/public",
'base_config_dir' => __DIR__,
'config_dir' => "{$APP_DIR}/config",

View File

@ -2,15 +2,15 @@
/**
* 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
* @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
* @version 4.0
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/

View File

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

View File

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

View File

@ -5,13 +5,13 @@ namespace Aviat\AnimeClient;
$whose = $config->get('whose_list') . "'s ";
$lastSegment = $urlGenerator->lastSegment();
$extraSegment = $lastSegment === 'list' ? '/list' : '';
$hasAnime = stripos($GLOBALS['_SERVER']['REQUEST_URI'], 'anime') !== FALSE;
$hasManga = stripos($GLOBALS['_SERVER']['REQUEST_URI'], 'manga') !== FALSE;
$hasAnime = str_contains($GLOBALS['_SERVER']['REQUEST_URI'], 'anime');
$hasManga = str_contains($GLOBALS['_SERVER']['REQUEST_URI'], 'manga');
?>
<div id="main-nav" class="flex flex-align-end flex-wrap">
<span class="flex-no-wrap grow-1">
<?php if(strpos($route_path, 'collection') === FALSE): ?>
<?php if( ! str_contains($route_path, 'collection')): ?>
<?= $helper->a(
$urlGenerator->defaultUrl($url_type),
$whose . ucfirst($url_type) . ' List',

View File

@ -2,6 +2,7 @@
declare(strict_types=1);
$file_patterns = [
'app/appConf/*.php',
'app/bootstrap.php',
'migrations/*.php',
'src/**/*.php',
@ -16,7 +17,7 @@ if ( ! function_exists('glob_recursive'))
{
// 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);
@ -57,17 +58,21 @@ function get_text_to_replace(array $tokens): string
return $output;
}
function get_tokens($source): array
function get_tokens(string $source): array
{
return token_get_all($source);
}
function replace_files(array $files, $template)
function replace_files(array $files, string $template): void
{
print_r($files);
foreach ($files as $file)
{
$source = file_get_contents($file);
if ($source === FALSE)
{
continue;
}
if (stripos($source, 'namespace') === FALSE)
{

View File

@ -43,26 +43,25 @@
"aviat/query": "^3.0.0",
"danielstjules/stringy": "^3.1.0",
"ext-dom": "*",
"ext-iconv": "*",
"ext-intl": "*",
"ext-json": "*",
"ext-gd": "*",
"ext-pdo": "*",
"filp/whoops": "^2.1",
"laminas/laminas-diactoros": "^2.5.0",
"laminas/laminas-httphandlerrunner": "^1.1.0",
"maximebf/consolekit": "^1.0.3",
"monolog/monolog": "^2.0.2",
"php": "^8.0.0",
"php": ">= 8.0.0",
"psr/container": "^1.0.0",
"psr/http-message": "^1.0.1",
"psr/log": "^1.1.3",
"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"
},
"require-dev": {
"consolidation/robo": "^2.0.0",
"pdepend/pdepend": "^2.",
"phploc/phploc": "^7.0.0",
"phpmd/phpmd": "^2.8.2",

View File

@ -26,7 +26,7 @@ try
'sync:lists' => Command\SyncLists::class
]))->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;
}
export const AnimeClient = {
const AnimeClient = {
/**
* Placeholder function
*/
@ -18,8 +18,8 @@ export const AnimeClient = {
* DOM selector
*
* @param {string} selector - The dom selector string
* @param {object} [context]
* @return {[HTMLElement]} - array of dom elements
* @param {Element} [context]
* @return array of dom elements
*/
$(selector, context = null) {
if (typeof selector !== 'string') {
@ -60,7 +60,7 @@ export const AnimeClient = {
/**
* 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}
*/
hide (sel) {
@ -77,7 +77,7 @@ export const AnimeClient = {
/**
* 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}
*/
show (sel) {
@ -116,9 +116,9 @@ export const AnimeClient = {
/**
* 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
* @return {HTMLElement|null} - the parent element
* @return {Element|null} - the parent element
*/
closestParent (current, parentSelector) {
if (Element.prototype.closest !== undefined) {
@ -204,9 +204,9 @@ function delegateEvent(sel, target, 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|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
* @return {void}
*/

View File

@ -71,7 +71,7 @@ _.on('body.anime.list', 'click', '.plus-one', (e) => {
success: (res) => {
const resData = JSON.parse(res);
if (resData.errors) {
if (resData.error) {
_.hide('#loading-shadow');
_.showMessage('error', `Failed to update ${title}. `);
_.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
*
* @param event
* @param {MouseEvent} event
* @return void
*/
function hide (event) {
@ -26,7 +26,7 @@ function hide (event) {
/**
* Confirm deletion of an item
*
* @param event
* @param {MouseEvent} event
* @return void
*/
function confirmDelete (event) {
@ -52,7 +52,7 @@ function clearAPICache () {
/**
* Scroll to the accordion/vertical tab section just opened
*
* @param event
* @param {InputEvent} event
* @return void
*/
function scrollToSection (event) {
@ -70,7 +70,7 @@ function scrollToSection (event) {
/**
* Filter an anime or manga list
*
* @param event
* @param {InputEvent} event
* @return void
*/
function filterMedia (event) {

View File

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

View File

@ -72,14 +72,22 @@ _.on('.manga.list', 'click', '.edit-buttons button', (e) => {
dataType: 'json',
type: 'POST',
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') {
_.hide(parentSel);
}
_.hide('#loading-shadow');
_.$(`.${type}s_read`, parentSel)[ 0 ].textContent = completed;
_.$(`.${type}s_read`, parentSel)[ 0 ].textContent = String(completed);
_.showMessage('success', `Successfully updated ${mangaName}`);
_.scrollToTop();
},

View File

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

View File

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

View File

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

View File

@ -3,19 +3,19 @@
"scripts": {
"build": "npm run build:css && npm run build: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:js": "watch 'npm run build:js' ./js",
"watch": "concurrently \"npm:watch:css\" \"npm:watch:js\" --kill-others"
},
"devDependencies": {
"@ampproject/rollup-plugin-closure-compiler": "^0.25.2",
"concurrently": "^5.1.0",
"cssnano": "^4.1.10",
"postcss": "^7.0.27",
"postcss-import": "^12.0.1",
"@swc/cli": "^0.1.39",
"@swc/core": "^1.2.54",
"concurrently": "^6.0.2",
"cssnano": "^5.0.1",
"postcss": "^8.2.6",
"postcss-import": "^14.0.0",
"postcss-preset-env": "^6.7.0",
"rollup": "^2.4.0",
"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;
use Aviat\AnimeClient\Types\Config as ConfigType;
use Whoops\Handler\PrettyPageHandler;
use Whoops\Run;
use Tracy\Debugger;
use function Aviat\Ion\_dir;
setlocale(LC_CTYPE, 'en_US');
@ -27,12 +25,9 @@ setlocale(LC_CTYPE, 'en_US');
// Load composer autoloader
require_once __DIR__ . '/vendor/autoload.php';
if (file_exists('.is-dev'))
{
$whoops = new Run;
$whoops->pushHandler(new PrettyPageHandler);
$whoops->register();
}
Debugger::$strictMode = true;
Debugger::$showBar = false;
Debugger::enable(Debugger::DEVELOPMENT, __DIR__ . '/app/logs');
// Define base directories
$APP_DIR = _dir(__DIR__, 'app');

View File

@ -4,6 +4,7 @@ parameters:
inferPrivatePropertyTypeFromConstructor: true
level: 8
paths:
- app/appConf
- src
- ./console
- 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=
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
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();

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -61,14 +61,16 @@ abstract class BaseCommand extends Command {
if ($fgColor !== NULL)
{
$fgColor = (string)$fgColor;
$fgColor = (int)$fgColor;
}
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);
// create the box
@ -142,13 +144,16 @@ abstract class BaseCommand extends Command {
{
if ($fgColor !== NULL)
{
$fgColor = (string)$fgColor;
$fgColor = (int)$fgColor;
}
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);
$this->getConsole()->writeln($message);
}

View File

@ -16,6 +16,7 @@
namespace Aviat\AnimeClient\Command;
use Aviat\Ion\JsonException;
use ConsoleKit\Widgets;
use Aviat\AnimeClient\API\{
@ -287,9 +288,17 @@ final class SyncLists extends BaseCommand {
// This uses a static so I don't have to fetch this list twice for a count
if ($list[$type] === NULL)
{
try
{
$list[$type] = $this->anilistModel->getSyncList(strtoupper($type));
}
catch (JsonException)
{
$this->echoErrorBox('Anlist API exception. Can not sync.');
die();
}
}
return $list[$type];
}
@ -354,7 +363,7 @@ final class SyncLists extends BaseCommand {
'progress' => $listItem['progress'],
// Comparision is done on 1-10 scale,
// Kitsu returns 1-20 scale.
'rating' => $listItem['rating'] / 2,
'rating' => (int) $listItem['rating'] / 2,
'reconsumeCount' => $listItem['reconsumeCount'],
'reconsuming' => $listItem['reconsuming'],
'status' => strtolower($listItem['status']),
@ -404,7 +413,7 @@ final class SyncLists extends BaseCommand {
$malIds = array_keys($anilistList);
$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
foreach($missingMalIds as $mid)
@ -599,7 +608,7 @@ final class SyncLists extends BaseCommand {
$kitsuItem['data']['ratingTwenty'] !== 0
)
{
$update['data']['ratingTwenty'] = $kitsuItem['data']['ratingTwenty'];
$update['data']['ratingTwenty'] = $kitsuItem['data']['rating'];
$return['updateType'][] = Enum\API::ANILIST;
}
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
// Kitsu expects a rating from 1-20
'rating' => (((int)$anilistItem['data']['rating']) > 0)
? $anilistItem['data']['rating'] / 5
? (int) $anilistItem['data']['rating'] / 5
: 0,
'reconsumeCount' => $anilistItem['data']['reconsumeCount'],
'reconsuming' => $anilistItem['data']['reconsuming'],
@ -738,7 +747,7 @@ final class SyncLists extends BaseCommand {
$responseData = Json::decode($response);
$id = $itemsToUpdate[$key]['id'];
$mal_id = $itemsToUpdate[$key]['mal_id'];
$mal_id = $itemsToUpdate[$key]['mal_id'] ?? NULL;
if ( ! array_key_exists('errors', $responseData))
{
$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
*
* @codeCoverageIgnore
* @param string|NULL $url
* @throws ContainerException
* @throws NotFoundException
@ -141,7 +142,7 @@ class Controller {
$util = $this->container->get('util');
$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
// the page is one of the form type pages,
@ -166,6 +167,7 @@ class Controller {
*
* If one is not set, redirect to default url
*
* @codeCoverageIgnore
* @throws InvalidArgumentException
* @return void
*/
@ -179,6 +181,7 @@ class Controller {
/**
* Check if the current user is authenticated, else error and exit
* @codeCoverageIgnore
*/
protected function checkAuth(): void
{
@ -195,12 +198,10 @@ class Controller {
/**
* Get the string output of a partial template
*
* @codeCoverageIgnore
* @param HtmlView $view
* @param string $template
* @param array $data
* @throws InvalidArgumentException
* @throws ContainerException
* @throws NotFoundException
* @return string
*/
protected function loadPartial(HtmlView $view, string $template, array $data = []): string
@ -229,19 +230,18 @@ class Controller {
/**
* Render a template with header and footer
*
* @codeCoverageIgnore
* @param HtmlView $view
* @param string $template
* @param array $data
* @return HtmlView
* @throws ContainerException
* @throws NotFoundException
*/
protected function renderFullPage(HtmlView $view, string $template, array $data): HtmlView
{
$csp = [
"default-src 'self'",
"object-src 'none'",
'frame-src *.youtube.com',
"child-src 'self' *.youtube.com polyfill.io",
];
$view->addHeader('Content-Security-Policy', implode('; ', $csp));
@ -261,11 +261,10 @@ class Controller {
/**
* 404 action
*
* @codeCoverageIgnore
* @param string $title
* @param string $message
* @throws InvalidArgumentException
* @throws ContainerException
* @throws NotFoundException
* @return void
*/
public function notFound(
@ -283,13 +282,12 @@ class Controller {
/**
* Display a generic error page
*
* @codeCoverageIgnore
* @param int $httpCode
* @param string $title
* @param string $message
* @param string $longMessage
* @throws InvalidArgumentException
* @throws ContainerException
* @throws NotFoundException
* @return 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
*
* @codeCoverageIgnore
* @throws InvalidArgumentException
* @return void
*/
@ -317,6 +316,7 @@ class Controller {
* Set a session flash variable to display a message on
* next page load
*
* @codeCoverageIgnore
* @param string $message
* @param string $type
* @return void
@ -352,12 +352,11 @@ class Controller {
/**
* Add a message box to the page
*
* @codeCoverageIgnore
* @param HtmlView $view
* @param string $type
* @param string $message
* @throws InvalidArgumentException
* @throws ContainerException
* @throws NotFoundException
* @return 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
*
* @codeCoverageIgnore
* @param string $template
* @param array $data
* @param HtmlView|NULL $view
* @param int $code
* @throws InvalidArgumentException
* @throws ContainerException
* @throws NotFoundException
* @return void
*/
protected function outputHTML(string $template, array $data = [], $view = NULL, int $code = 200): void
@ -394,6 +392,7 @@ class Controller {
/**
* Output a JSON Response
*
* @codeCoverageIgnore
* @param mixed $data
* @param int $code - the http status code
* @throws DoubleRenderException
@ -410,6 +409,7 @@ class Controller {
/**
* Redirect to the selected page
*
* @codeCoverageIgnore
* @param string $url
* @param int $code
* @return void

View File

@ -251,7 +251,7 @@ final class Anime extends BaseController {
{
$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());
}
@ -302,8 +302,6 @@ final class Anime extends BaseController {
* View details of an anime
*
* @param string $id
* @throws ContainerException
* @throws NotFoundException
* @throws InvalidArgumentException
* @return void
*/

View File

@ -64,8 +64,6 @@ final class Manga extends Controller {
*
* @param string $status
* @param string $view
* @throws ContainerException
* @throws NotFoundException
* @throws InvalidArgumentException
* @return void
*/
@ -251,7 +249,7 @@ final class Manga extends Controller {
{
$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());
}
@ -298,8 +296,6 @@ final class Manga extends Controller {
* View details of an manga
*
* @param string $id
* @throws ContainerException
* @throws NotFoundException
* @throws InvalidArgumentException
* @throws Throwable
* @return void
@ -331,8 +327,6 @@ final class Manga extends Controller {
/**
* View details of a random manga
*
* @throws ContainerException
* @throws NotFoundException
* @throws InvalidArgumentException
* @throws Throwable
* @return void

View File

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

View File

@ -25,22 +25,6 @@ final class Picture {
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 = [
'gif',
'jpeg',
@ -68,12 +52,12 @@ final class Picture {
// If it is a placeholder image, make the
// fallback a png, not a jpg
if (strpos($uri, 'placeholder') !== FALSE)
if (str_contains($uri, 'placeholder'))
{
$fallbackExt = 'png';
}
if (strpos($uri, '//') === FALSE)
if ( ! str_contains($uri, '//'))
{
$uri = $urlGenerator->assetUrl($uri);
}
@ -82,22 +66,34 @@ final class Picture {
$ext = array_pop($urlParts);
$path = implode('.', $urlParts);
$mime = array_key_exists($ext, static::MIME_MAP)
? static::MIME_MAP[$ext]
: 'image/jpeg';
$mime = match ($ext) {
'avif' => 'image/avif',
'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)
? static::MIME_MAP[$fallbackExt]
: 'image/jpeg';
$fallbackMime = match ($fallbackExt) {
'gif' => 'image/gif',
'png' => 'image/png',
default => 'image/jpeg',
};
// For image types that are well-established, just return a
// simple <img /> element instead
if (
$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
: $picAttrs;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -66,13 +66,8 @@ class Container implements ContainerInterface {
*
* @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))
{
// Return an object instance, if it already exists
@ -94,18 +89,13 @@ class Container implements ContainerInterface {
* Get a new instance of the specified item
*
* @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 ContainerException - Error while retrieving the entry.
* @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))
{
// 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.
* @return boolean
*/
public function has($id): bool
public function has(string $id): bool
{
return array_key_exists($id, $this->container);
}

View File

@ -36,7 +36,7 @@ interface ContainerInterface {
* @throws Exception\ContainerException Error while retrieving the 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.
@ -45,7 +45,7 @@ interface ContainerInterface {
* @param string $id Identifier of the entry to look for.
* @return boolean
*/
public function has($id): bool;
public function has(string $id): bool;
/**
* Add a factory to the container
@ -63,7 +63,7 @@ interface ContainerInterface {
* @param mixed $value
* @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
@ -71,7 +71,7 @@ interface ContainerInterface {
* @param string $id
* @return mixed
*/
public function getNew($id);
public function getNew(string $id): mixed;
/**
* Determine whether a logger channel is registered

View File

@ -21,10 +21,9 @@ use Aviat\AnimeClient\Tests\AnimeClientTestCase;
use Aviat\Ion\Json;
class AnimeListTransformerTest extends AnimeClientTestCase {
protected $dir;
protected $beforeTransform;
protected $afterTransform;
protected $transformer;
protected string $dir;
protected array $beforeTransform;
protected AnimeListTransformer $transformer;
public function setUp(): void {
parent::setUp();
@ -36,13 +35,13 @@ class AnimeListTransformerTest extends AnimeClientTestCase {
$this->transformer = new AnimeListTransformer();
}
public function testTransform()
public function testTransform(): void
{
$actual = $this->transformer->transform($this->beforeTransform);
$this->assertMatchesSnapshot($actual);
}
public function dataUntransform()
public function dataUntransform(): array
{
return [[
'input' => [
@ -85,8 +84,9 @@ class AnimeListTransformerTest extends AnimeClientTestCase {
/**
* @dataProvider dataUntransform
* @param array $input
*/
public function testUntransform($input)
public function testUntransform(array $input): void
{
$actual = $this->transformer->untransform($input);
$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;
use Amp\Http\Client\Response;
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\isSequentialArray;
use function Aviat\AnimeClient\tomlToArray;
@ -89,4 +91,46 @@ class AnimeClientTest extends AnimeClientTestCase
{
$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;
use Aviat\Ion\Di\ContainerAware;
use Aviat\Ion\Di\ContainerInterface;
use function Aviat\Ion\_dir;
use Aviat\Ion\Json;
@ -26,10 +28,16 @@ use Laminas\Diactoros\{
ServerRequestFactory
};
use const Aviat\AnimeClient\{
SLUG_PATTERN,
DEFAULT_CONTROLLER,
};
/**
* Base class for TestCases
*/
class AnimeClientTestCase extends TestCase {
use ContainerAware;
use MatchesSnapshots;
// Test directory constants
@ -38,17 +46,10 @@ class AnimeClientTestCase extends TestCase {
public const TEST_DATA_DIR = __DIR__ . '/test_data';
public const TEST_VIEW_DIR = __DIR__ . '/test_views';
protected $container;
protected static $staticContainer;
protected static $session_handler;
protected ContainerInterface $container;
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
$files = glob(_dir(self::TEST_DATA_DIR, 'cache', '*.json'));
array_map('unlink', $files);
@ -59,6 +60,7 @@ class AnimeClientTestCase extends TestCase {
parent::setUp();
$config_array = [
'root' => self::ROOT_DIR,
'asset_path' => '/assets',
'img_cache_path' => _dir(self::ROOT_DIR, 'public/images'),
'data_cache_path' => _dir(self::TEST_DATA_DIR, 'cache'),
@ -88,13 +90,11 @@ class AnimeClientTestCase extends TestCase {
'file' => ':memory:',
]
],
'routes' => [
],
'routes' => [ ],
];
// 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);
// Use mock session handler
@ -157,7 +157,7 @@ class AnimeClientTestCase extends TestCase {
* @param array $args
* @return mixed - the decoded data
*/
public function getMockFileData(...$args)
public function getMockFileData(mixed ...$args): mixed
{
$rawData = $this->getMockFile(...$args);

View File

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

View File

@ -21,13 +21,14 @@ use Aviat\AnimeClient\Controller;
use Aviat\AnimeClient\Dispatcher;
use Aviat\AnimeClient\UrlGenerator;
use Aviat\Ion\Config;
use Aviat\Ion\Di\ContainerInterface;
use Monolog\Handler\TestHandler;
use Monolog\Logger;
class DispatcherTest extends AnimeClientTestCase {
protected $container;
protected ContainerInterface $container;
protected $router;
protected $config;
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 {
/**
* @dataProvider dataPictureCase
* @param array $params
*/
public function testPictureHelper($params, $expected = NULL)
public function testPictureHelper(array $params): void
{
$helper = new PictureHelper();
$helper->setContainer($this->container);
$actual = $helper(...$params);
if ($expected === NULL)
{
$this->assertMatchesSnapshot($actual);
}
else
{
$this->assertEquals($expected, $actual);
}
}
/**
* @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->setContainer($this->container);
$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);
}
public function testSimpleImageByFallback()
public function testSimpleImageByFallback(): void
{
$helper = new PictureHelper();
$helper->setContainer($this->container);
$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 [
'Full AVIF URL' => [
'params' => [
'https://www.example.com/image.avif',
],
],
'Full webp URL' => [
'params' => [
'https://www.example.com/image.webp',
@ -112,16 +114,21 @@ class PictureHelperTest extends AnimeClientTestCase {
'params' => [
'images/foo.jpg',
'jpg',
[ 'x' => 1, 'y' => 1 ],
[],
['width' => 200, 'height' => 200, 'alt' => 'should exist'],
]
]
];
}
public function dataSimpleImageCase()
public function dataSimpleImageCase(): array
{
return [
'avif' => [
'ext' => 'avif',
'isSimple' => FALSE,
'fallback' => 'jpf'
],
'apng' => [
'ext' => 'apng',
'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));
}
public function testParseStreamingLinksNoHost(): void
{
$nodes = [[
'url' => '/link-fragment',
'dubs' => [],
'subs' => [],
]];
$this->assertEquals([], Kitsu::parseStreamingLinks($nodes));
}
public function testGetAiringStatusEmptyArguments(): void
{
$this->assertEquals(AnimeAiringStatus::NOT_YET_AIRED, Kitsu::getAiringStatus());
@ -123,7 +134,7 @@ class KitsuTest extends TestCase {
$this->assertEquals($expected, $actual);
}
public function testFilterLocalizedTitles()
public function testFilterLocalizedTitles(): void
{
$input = [
'canonical' => 'foo',
@ -140,7 +151,7 @@ class KitsuTest extends TestCase {
$this->assertEquals(['Foo the Movie'], $actual);
}
public function testGetFilteredTitles()
public function testGetFilteredTitles(): void
{
$input = [
'canonical' => 'foo',

View File

@ -22,7 +22,7 @@ class RequirementsTest extends AnimeClientTestCase {
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

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 {
public function addDataTable($name, array $values=[]) {}
public function addDataTable(string $name, array $values=[]): void {}
}
// -----------------------------------------------------------------------------
@ -128,12 +128,12 @@ class TestJsonView extends JsonView {
// -----------------------------------------------------------------------------
trait MockInjectionTrait {
public function __get($key)
public function __get(string $key): mixed
{
return $this->$key;
}
public function __set($key, $value)
public function __set(string $key, mixed $value)
{
$this->$key = $value;
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 Aviat\Ion\Di\ContainerInterface;
use Aviat\Ion\Di\Exception\NotFoundException;
use Throwable;
use TypeError;
class FooTest {
@ -49,13 +51,11 @@ class ContainerTest extends IonTestCase {
return [
'Bad index type: number' => [
'id' => 42,
'exception' => ContainerException::class,
'message' => 'Id must be a string'
'exception' => TypeError::class,
],
'Bad index type: array' => [
'id' => [],
'exception' => ContainerException::class,
'message' => 'Id must be a string'
'exception' => TypeError::class,
],
'Non-existent id' => [
'id' => 'foo',
@ -68,7 +68,7 @@ class ContainerTest extends IonTestCase {
/**
* @dataProvider dataGetWithException
*/
public function testGetWithException($id, $exception, $message): void
public function testGetWithException(mixed $id, $exception, ?string $message = NULL): void
{
try
{
@ -79,15 +79,23 @@ class ContainerTest extends IonTestCase {
$this->assertInstanceOf($exception, $e);
$this->assertEquals($message, $e->getMessage());
}
catch(Throwable $e)
{
$this->assertInstanceOf($exception, $e);
}
}
/**
* @dataProvider dataGetWithException
*/
public function testGetNewWithException($id, $exception, $message): void
public function testGetNewWithException(mixed $id, $exception, ?string $message = NULL): void
{
$this->expectException($exception);
if ($message !== NULL)
{
$this->expectExceptionMessage($message);
}
$this->container->getNew($id);
}

View File

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