Merge remote-tracking branch 'origin/develop'
timw4mail/HummingBirdAnimeClient/pipeline/head This commit looks good
Details
timw4mail/HummingBirdAnimeClient/pipeline/head This commit looks good
Details
This commit is contained in:
commit
73bbc569a7
|
@ -5,22 +5,12 @@ use PhpCsFixer\{Config, Finder};
|
|||
|
||||
$finder = Finder::create()
|
||||
->in([
|
||||
__DIR__,
|
||||
__DIR__ . '/app',
|
||||
__DIR__ . '/src',
|
||||
__DIR__ . '/tests',
|
||||
__DIR__ . '/tools',
|
||||
])
|
||||
->exclude([
|
||||
'apidocs',
|
||||
'build',
|
||||
'coverage',
|
||||
'frontEndSrc',
|
||||
'phinx',
|
||||
'public',
|
||||
'tools',
|
||||
'tmp',
|
||||
'vendor',
|
||||
'views',
|
||||
'templates',
|
||||
]);
|
||||
|
||||
return (new Config())
|
||||
|
@ -45,7 +35,7 @@ return (new Config())
|
|||
'blank_line_after_opening_tag' => false,
|
||||
'blank_line_before_statement' => [
|
||||
'statements' => [
|
||||
'case',
|
||||
// 'case',
|
||||
'continue',
|
||||
'declare',
|
||||
'default',
|
||||
|
@ -128,12 +118,12 @@ return (new Config())
|
|||
'noise_remaining_usages_exclude' => [],
|
||||
],
|
||||
'escape_implicit_backslashes' => [
|
||||
'double_quoted' => true,
|
||||
'heredoc_syntax' => true,
|
||||
'double_quoted' => false,
|
||||
'heredoc_syntax' => false,
|
||||
'single_quoted' => false,
|
||||
],
|
||||
'explicit_indirect_variable' => true,
|
||||
'explicit_string_variable' => true,
|
||||
'explicit_indirect_variable' => false,
|
||||
'explicit_string_variable' => false,
|
||||
'final_class' => false,
|
||||
'final_internal_class' => [
|
||||
'annotation_exclude' => ['@no-final'],
|
||||
|
@ -167,7 +157,7 @@ return (new Config())
|
|||
],
|
||||
'group_import' => true,
|
||||
'header_comment' => false, // false by default
|
||||
'heredoc_indentation' => ['indentation' => 'start_plus_one'],
|
||||
// 'heredoc_indentation' => ['indentation' => 'start_plus_one'],
|
||||
'heredoc_to_nowdoc' => true,
|
||||
'implode_call' => true,
|
||||
'include' => true,
|
||||
|
@ -232,8 +222,7 @@ return (new Config())
|
|||
'allow_unused_params' => true,
|
||||
'remove_inheritdoc' => false,
|
||||
],
|
||||
'no_trailing_comma_in_list_call' => true,
|
||||
'no_trailing_comma_in_singleline_array' => true,
|
||||
'no_trailing_comma_in_singleline' => true,
|
||||
'no_trailing_whitespace' => true,
|
||||
'no_trailing_whitespace_in_comment' => true,
|
||||
'no_trailing_whitespace_in_string' => true,
|
||||
|
@ -270,9 +259,16 @@ return (new Config())
|
|||
'ordered_class_elements' => [
|
||||
'order' => [
|
||||
'use_trait',
|
||||
'constant',
|
||||
'property',
|
||||
'method',
|
||||
'case',
|
||||
'constant_public',
|
||||
'constant_protected',
|
||||
'constant_private',
|
||||
'property_public',
|
||||
'property_protected',
|
||||
'property_private',
|
||||
'construct',
|
||||
'destruct',
|
||||
'magic',
|
||||
],
|
||||
'sort_algorithm' => 'none',
|
||||
],
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
# Changelog
|
||||
|
||||
## Version 5.2
|
||||
* Updated PHP requirement to 8
|
||||
* Updated to support PHP 8.1
|
||||
* Updated PHP requirement to 8.1
|
||||
* Updated to support PHP 8.2
|
||||
* Improve Anilist <-> Kitsu mappings to be more reliable
|
||||
|
||||
## Version 5.1
|
||||
* Added session check, so when coming back to a page, if the session is expired, the page will refresh.
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<?php
|
||||
|
||||
use Aviat\AnimeClient\Kitsu;
|
||||
use function Aviat\AnimeClient\friendlyTime;
|
||||
|
||||
?>
|
||||
<main class="details fixed">
|
||||
|
@ -38,14 +38,14 @@ use Aviat\AnimeClient\Kitsu;
|
|||
<?php if (( ! empty($data['episode_length'])) && $data['episode_count'] !== 1): ?>
|
||||
<tr>
|
||||
<td>Episode Length</td>
|
||||
<td><?= Kitsu::friendlyTime($data['episode_length']) ?></td>
|
||||
<td><?= friendlyTime($data['episode_length']) ?></td>
|
||||
</tr>
|
||||
<?php endif ?>
|
||||
|
||||
<?php if (isset($data['total_length'], $data['episode_count']) && $data['total_length'] > 0): ?>
|
||||
<tr>
|
||||
<td>Total Length</td>
|
||||
<td><?= Kitsu::friendlyTime($data['total_length']) ?></td>
|
||||
<td><?= friendlyTime($data['total_length']) ?></td>
|
||||
</tr>
|
||||
<?php endif ?>
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@ use Aviat\AnimeClient\Kitsu;
|
|||
<br />
|
||||
<hr />
|
||||
<div class="description">
|
||||
<p><?= str_replace("\n", '</p><p>', $data['description']) ?></p>
|
||||
<p><?= nl2br($data['description']) ?></p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
@ -8,6 +8,9 @@
|
|||
<?php foreach ($data['names'] as $name): ?>
|
||||
<h3><?= $name ?></h3>
|
||||
<?php endforeach ?>
|
||||
<?php if ( ! empty($data['birthday'])): ?>
|
||||
<h4><?= $data['birthday'] ?></h4>
|
||||
<?php endif ?>
|
||||
<br />
|
||||
<hr />
|
||||
<div class="description">
|
||||
|
|
|
@ -3,40 +3,58 @@ use Aviat\AnimeClient\Kitsu;
|
|||
?>
|
||||
<main class="user-page details">
|
||||
<h2 class="toph">
|
||||
About
|
||||
<?= $helper->a(
|
||||
"https://kitsu.io/users/{$data['slug']}",
|
||||
$data['name'], [
|
||||
'title' => 'View profile on Kitsu'
|
||||
"https://kitsu.io/users/{$data['slug']}",
|
||||
$data['name'], [
|
||||
'title' => 'View profile on Kitsu'
|
||||
])
|
||||
?>
|
||||
</h2>
|
||||
|
||||
<p><?= $escape->html($data['about']) ?></p>
|
||||
|
||||
<section class="flex flex-no-wrap">
|
||||
<aside class="info">
|
||||
<center>
|
||||
<?= $helper->img($data['avatar'], ['alt' => '']); ?>
|
||||
</center>
|
||||
<table class="media-details invisible">
|
||||
<tr>
|
||||
<?php if($data['avatar'] !== null): ?>
|
||||
<td><?= $helper->img($data['avatar'], ['alt' => '', 'width' => '225']); ?></td>
|
||||
<?php endif ?>
|
||||
<td><?= $escape->html($data['about']) ?></td>
|
||||
</tr>
|
||||
</table>
|
||||
<br />
|
||||
<table class="media-details">
|
||||
<?php foreach ([
|
||||
'joinDate' => 'Joined',
|
||||
'birthday' => 'Birthday',
|
||||
'gender' => 'Gender',
|
||||
'location' => 'Location'
|
||||
] as $key => $label): ?>
|
||||
<?php if ($data[$key] !== null): ?>
|
||||
<tr>
|
||||
<td>Location</td>
|
||||
<td><?= $data['location'] ?></td>
|
||||
<td><?= $label ?></td>
|
||||
<td><?= $data[$key] ?></td>
|
||||
</tr>
|
||||
<?php endif ?>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<?php if ($data['website'] !== null): ?>
|
||||
<tr>
|
||||
<td>Website</td>
|
||||
<td><?= $helper->a($data['website'], $data['website']) ?></td>
|
||||
</tr>
|
||||
<?php if ( ! empty($data['waifu'])): ?>
|
||||
<?php endif ?>
|
||||
|
||||
<?php if ($data['waifu']['character'] !== null): ?>
|
||||
<tr>
|
||||
<td><?= $escape->html($data['waifu']['label']) ?></td>
|
||||
<td>
|
||||
<?php
|
||||
$character = $data['waifu']['character'];
|
||||
echo $helper->a(
|
||||
$url->generate('character', ['slug' => $character['slug']]),
|
||||
$character['names']['canonical']
|
||||
echo $component->character(
|
||||
$character['names']['canonical'],
|
||||
$url->generate('character', ['slug' => $character['slug']]),
|
||||
$helper->img(Kitsu::getImage($character))
|
||||
);
|
||||
?>
|
||||
</td>
|
||||
|
@ -75,7 +93,7 @@ use Aviat\AnimeClient\Kitsu;
|
|||
$rendered[] = $component->character(
|
||||
$item['names']['canonical'],
|
||||
$url->generate('character', ['slug' => $item['slug']]),
|
||||
$helper->img($item['image']['original']['url'])
|
||||
$helper->img(Kitsu::getImage($item))
|
||||
);
|
||||
}
|
||||
else
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" colors="true" stopOnFailure="false" bootstrap="../tests/bootstrap.php" beStrictAboutTestsThatDoNotTestAnything="true" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd">
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" colors="true" stopOnFailure="false" bootstrap="../tests/bootstrap.php" beStrictAboutTestsThatDoNotTestAnything="true" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.1/phpunit.xsd">
|
||||
<coverage>
|
||||
<include>
|
||||
<directory suffix=".php">../src</directory>
|
||||
</include>
|
||||
<report>
|
||||
<clover outputFile="logs/clover.xml"/>
|
||||
<html outputDirectory="../coverage"/>
|
||||
|
@ -14,12 +11,12 @@
|
|||
<directory>../tests/AnimeClient</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Ion">
|
||||
<directory>../tests/Ion</directory>
|
||||
</testsuite>
|
||||
<directory>../tests/Ion</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<logging>
|
||||
<junit outputFile="logs/junit.xml"/>
|
||||
</logging>
|
||||
<logging>
|
||||
<junit outputFile="logs/junit.xml"/>
|
||||
</logging>
|
||||
<php>
|
||||
<server name="HTTP_USER_AGENT" value="Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:38.0) Gecko/20100101 Firefox/38.0"/>
|
||||
<server name="HTTP_HOST" value="localhost"/>
|
||||
|
@ -27,4 +24,9 @@
|
|||
<server name="REQUEST_URI" value="/"/>
|
||||
<server name="REQUEST_METHOD" value="GET"/>
|
||||
</php>
|
||||
<source>
|
||||
<include>
|
||||
<directory suffix=".php">../src</directory>
|
||||
</include>
|
||||
</source>
|
||||
</phpunit>
|
||||
|
|
|
@ -30,21 +30,20 @@
|
|||
"lock": false
|
||||
},
|
||||
"require": {
|
||||
"amphp/amp": "^2.5.0",
|
||||
"amphp/http-client": "^4.5.0",
|
||||
"aura/html": "^2.5.0",
|
||||
"aura/router": "^3.1.0",
|
||||
"aura/session": "^2.1.0",
|
||||
"aviat/banker": "^4.1.2",
|
||||
"aviat/query": "^4.0.0",
|
||||
"aviat/query": "^4.1.0",
|
||||
"ext-dom": "*",
|
||||
"ext-gd": "*",
|
||||
"ext-intl": "*",
|
||||
"ext-json": "*",
|
||||
"ext-mbstring": "*",
|
||||
"ext-pdo": "*",
|
||||
"laminas/laminas-diactoros": "^2.5.0",
|
||||
"laminas/laminas-httphandlerrunner": "^2.1.0",
|
||||
"laminas/laminas-diactoros": "^3.0.0",
|
||||
"laminas/laminas-httphandlerrunner": "^2.6.1",
|
||||
"maximebf/consolekit": "^1.0.3",
|
||||
"monolog/monolog": "^3.0.0",
|
||||
"php": ">= 8.1.0",
|
||||
|
@ -56,9 +55,9 @@
|
|||
},
|
||||
"require-dev": {
|
||||
"phpstan/phpstan": "^1.2.0",
|
||||
"phpunit/phpunit": "^9.5.0",
|
||||
"phpunit/phpunit": "^10.0.0",
|
||||
"roave/security-advisories": "dev-master",
|
||||
"spatie/phpunit-snapshot-assertions": "^4.1.0"
|
||||
"spatie/phpunit-snapshot-assertions": "^5.0.1"
|
||||
},
|
||||
"scripts": {
|
||||
"build:css": "cd public && npm run build:css && cd ..",
|
||||
|
|
|
@ -333,7 +333,8 @@ td.danger, td.danger:hover, td.danger:active {
|
|||
.borderless th,
|
||||
.invisible tr,
|
||||
.invisible td,
|
||||
.invisible th {
|
||||
.invisible th,
|
||||
table.invisible {
|
||||
box-shadow: none;
|
||||
border: 0;
|
||||
}
|
||||
|
@ -836,19 +837,11 @@ aside.info {
|
|||
max-width: 390px;
|
||||
}
|
||||
|
||||
/* .fixed aside.info + article {
|
||||
max-width: inherit;
|
||||
} */
|
||||
|
||||
aside picture, aside img {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* aside.info + article {
|
||||
max-width: 66%;
|
||||
} */
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
User page styles
|
||||
-----------------------------------------------------------------------------*/
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import _ from './anime-client.js'
|
||||
import { renderSearchResults } from './template-helpers.js'
|
||||
import { getNestedProperty, hasNestedProperty } from "./fns";
|
||||
|
||||
const search = (query, isCollection = false) => {
|
||||
// Show the loader
|
||||
|
@ -70,6 +71,14 @@ _.on('body.anime.list', 'click', '.plus-one', (e) => {
|
|||
}
|
||||
};
|
||||
|
||||
const displayMessage = (type, message) => {
|
||||
_.hide('#loading-shadow');
|
||||
_.showMessage(type, `${message} ${title}`);
|
||||
_.scrollToTop();
|
||||
}
|
||||
|
||||
const showError = () => displayMessage('error', 'Failed to update');
|
||||
|
||||
// If the episode count is 0, and incremented,
|
||||
// change status to currently watching
|
||||
if (isNaN(watchedCount) || watchedCount === 0) {
|
||||
|
@ -89,36 +98,31 @@ _.on('body.anime.list', 'click', '.plus-one', (e) => {
|
|||
dataType: 'json',
|
||||
type: 'POST',
|
||||
success: (res) => {
|
||||
const resData = JSON.parse(res);
|
||||
try {
|
||||
const resData = JSON.parse(res);
|
||||
|
||||
if (resData.error) {
|
||||
_.hide('#loading-shadow');
|
||||
_.showMessage('error', `Failed to update ${title}. `);
|
||||
_.scrollToTop();
|
||||
// Do a rough sanity check for weird errors
|
||||
let updatedProgress = getNestedProperty(resData, 'data.libraryEntry.update.libraryEntry.progress');
|
||||
if (hasNestedProperty(resData, 'error') || updatedProgress !== data.data.progress) {
|
||||
showError();
|
||||
return;
|
||||
}
|
||||
|
||||
return;
|
||||
// We've completed the series
|
||||
if (getNestedProperty(resData, 'data.libraryEntry.update.libraryEntry.status') === 'COMPLETED') {
|
||||
_.hide(parentSel);
|
||||
displayMessage('success', 'Completed')
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Just a normal update
|
||||
_.$('.completed_number', parentSel)[ 0 ].textContent = ++watchedCount;
|
||||
displayMessage('success', 'Updated');
|
||||
} catch (_) {
|
||||
showError();
|
||||
}
|
||||
|
||||
// We've completed the series
|
||||
if (resData.data.libraryEntry.update.libraryEntry.status === 'COMPLETED') {
|
||||
_.hide(parentSel);
|
||||
_.hide('#loading-shadow');
|
||||
_.showMessage('success', `Successfully completed ${title}`);
|
||||
_.scrollToTop();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
_.hide('#loading-shadow');
|
||||
|
||||
_.showMessage('success', `Successfully updated ${title}`);
|
||||
_.$('.completed_number', parentSel)[ 0 ].textContent = ++watchedCount;
|
||||
_.scrollToTop();
|
||||
},
|
||||
error: () => {
|
||||
_.hide('#loading-shadow');
|
||||
_.showMessage('error', `Failed to update ${title}. `);
|
||||
_.scrollToTop();
|
||||
}
|
||||
error: showError,
|
||||
});
|
||||
});
|
|
@ -0,0 +1,103 @@
|
|||
/**
|
||||
* Make sure properties are in an easily splittable format
|
||||
*
|
||||
* @private
|
||||
* @param {String} props
|
||||
* @param {String} [sep='.'] The default separator
|
||||
* @return {String}
|
||||
*/
|
||||
function _normalizeProperty(props, sep = '.') {
|
||||
// Since we split by period, and property lookup
|
||||
// is the same by dot or [], replace bracket lookups
|
||||
// with periods
|
||||
return props.replace(/\[(.*?)]/g, sep + '$1');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tell if a nested object has a given property (or array a given index)
|
||||
* given an object such as a.b.c.d = 5, hasNestedProperty(a, 'b.c.d') will return true.
|
||||
*
|
||||
* @param {Object} object the object to get the property from
|
||||
* @param {String} property the path to the property as a string
|
||||
* @returns {boolean} true when property in object, false otherwise
|
||||
*/
|
||||
export function hasNestedProperty(object, property) {
|
||||
if (object && typeof object === 'object') {
|
||||
if (typeof property === 'string' && property !== '') {
|
||||
property = _normalizeProperty(property);
|
||||
|
||||
let split = property.split('.');
|
||||
return split.reduce((obj, prop, idx, array) => {
|
||||
if (idx === array.length - 1) {
|
||||
return !!(obj && obj.hasOwnProperty(prop));
|
||||
}
|
||||
|
||||
return obj && obj[prop];
|
||||
}, object);
|
||||
} else if (typeof property === 'number') {
|
||||
return property in object;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value of a deeply nested property in an object
|
||||
*
|
||||
* @param {Object} object the object to get the property
|
||||
* @param {string} property the path to the property as a string
|
||||
* @param {string} [sep='.'] The default separator to split on
|
||||
* @return {*} the value of the property
|
||||
*/
|
||||
export function getNestedProperty(object, property, sep = '.') {
|
||||
if (isType('string', property) && property !== '') {
|
||||
// convert numbers to dot syntax
|
||||
property = _normalizeProperty(property, sep);
|
||||
const levels = property.split(sep);
|
||||
|
||||
try {
|
||||
return levels.reduce((obj, prop) => obj[prop], object);
|
||||
} catch (e) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reliably get the type of the value of a variable
|
||||
*
|
||||
* @param {*} x The variable to get the type of
|
||||
* @return {string} The name of the type
|
||||
*/
|
||||
export function getType(x) {
|
||||
// is it an array?
|
||||
if (Array.isArray(x)) {
|
||||
return 'array';
|
||||
}
|
||||
|
||||
// Use typeof for truthy primitives
|
||||
if (typeof x !== 'object') {
|
||||
return (typeof x).toLowerCase();
|
||||
}
|
||||
|
||||
const type = function () {
|
||||
return Object.prototype.toString.call(this).slice(8, -1);
|
||||
}
|
||||
|
||||
// Otherwise, strip the type out of the '[Object x]' toString value
|
||||
return type.call(x).toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the value matches the passed type name
|
||||
*
|
||||
* @param {string} type Javascript type name
|
||||
* @param {*} val The value to type check
|
||||
* @return {boolean}
|
||||
*/
|
||||
export function isType(type, val) {
|
||||
return getType(val) === String(type).toLowerCase();
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
import _ from './anime-client.js'
|
||||
import { renderSearchResults } from './template-helpers.js'
|
||||
import { getNestedProperty, hasNestedProperty } from "./fns";
|
||||
|
||||
const search = (query) => {
|
||||
_.show('.cssload-loader');
|
||||
|
@ -36,7 +37,7 @@ _.on('.manga.list', 'click', '.edit-buttons button', (e) => {
|
|||
let type = thisSel.classList.contains('plus-one-chapter') ? 'chapter' : 'volume';
|
||||
let completed = parseInt(_.$(`.${type}s_read`, parentSel)[ 0 ].textContent, 10) || 0;
|
||||
let total = parseInt(_.$(`.${type}_count`, parentSel)[ 0 ].textContent, 10);
|
||||
let mangaName = _.$('.name', parentSel)[ 0 ].textContent;
|
||||
let title = _.$('.name', parentSel)[ 0 ].textContent;
|
||||
|
||||
if (isNaN(completed)) {
|
||||
completed = 0;
|
||||
|
@ -45,12 +46,21 @@ _.on('.manga.list', 'click', '.edit-buttons button', (e) => {
|
|||
// Setup the update data
|
||||
let data = {
|
||||
id: parentSel.dataset.kitsuId,
|
||||
anilist_id: parentSel.dataset.anilistId,
|
||||
mal_id: parentSel.dataset.malId,
|
||||
data: {
|
||||
progress: completed
|
||||
}
|
||||
};
|
||||
|
||||
const displayMessage = (type, message) => {
|
||||
_.hide('#loading-shadow');
|
||||
_.showMessage(type, `${message} ${title}`);
|
||||
_.scrollToTop();
|
||||
}
|
||||
|
||||
const showError = () => displayMessage('error', 'Failed to update');
|
||||
|
||||
// If the episode count is 0, and incremented,
|
||||
// change status to currently reading
|
||||
if (isNaN(completed) || completed === 0) {
|
||||
|
@ -73,33 +83,32 @@ _.on('.manga.list', 'click', '.edit-buttons button', (e) => {
|
|||
type: 'POST',
|
||||
mimeType: 'application/json',
|
||||
success: (res) => {
|
||||
const resData = JSON.parse(res)
|
||||
if (resData.error) {
|
||||
_.hide('#loading-shadow');
|
||||
_.showMessage('error', `Failed to update ${mangaName}. `);
|
||||
_.scrollToTop();
|
||||
return;
|
||||
try {
|
||||
const resData = JSON.parse(res);
|
||||
|
||||
// Do a rough sanity check for weird errors
|
||||
let updatedProgress = getNestedProperty(resData, 'data.libraryEntry.update.libraryEntry.progress');
|
||||
if (hasNestedProperty(resData, 'error') || updatedProgress !== data.data.progress) {
|
||||
showError();
|
||||
return;
|
||||
}
|
||||
|
||||
// We've completed the series
|
||||
if (getNestedProperty(resData, 'data.libraryEntry.update.libraryEntry.status') === 'COMPLETED') {
|
||||
_.hide(parentSel);
|
||||
displayMessage('success', 'Completed')
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Just a normal update
|
||||
_.$(`.${type}s_read`, parentSel)[ 0 ].textContent = String(completed);
|
||||
displayMessage('success', 'Updated');
|
||||
|
||||
} catch (_) {
|
||||
showError();
|
||||
}
|
||||
|
||||
if (String(data.data.status).toUpperCase() === 'COMPLETED') {
|
||||
_.hide(parentSel);
|
||||
_.hide('#loading-shadow');
|
||||
_.showMessage('success', `Successfully completed ${mangaName}`);
|
||||
_.scrollToTop();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
_.hide('#loading-shadow');
|
||||
|
||||
_.$(`.${type}s_read`, parentSel)[ 0 ].textContent = String(completed);
|
||||
_.showMessage('success', `Successfully updated ${mangaName}`);
|
||||
_.scrollToTop();
|
||||
},
|
||||
error: () => {
|
||||
_.hide('#loading-shadow');
|
||||
_.showMessage('error', `Failed to update ${mangaName}`);
|
||||
_.scrollToTop();
|
||||
}
|
||||
error: showError,
|
||||
});
|
||||
});
|
|
@ -15,7 +15,7 @@
|
|||
"cssnano": "^5.0.1",
|
||||
"postcss": "^8.2.6",
|
||||
"postcss-import": "^15.0.0",
|
||||
"postcss-preset-env": "^7.8.2",
|
||||
"postcss-preset-env": "^8.0.1",
|
||||
"watch": "^1.0.2"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
module.exports = {
|
||||
const { config } = require("@swc/core/spack");
|
||||
|
||||
module.exports = config({
|
||||
entry: {
|
||||
'scripts.min': __dirname + '/js/index.js',
|
||||
'tables.min': __dirname + '/js/base/sort-tables.js',
|
||||
|
@ -8,12 +10,15 @@ module.exports = {
|
|||
},
|
||||
options: {
|
||||
jsc: {
|
||||
target: 'es3',
|
||||
loose: true,
|
||||
parser: {
|
||||
syntax: "ecmascript",
|
||||
jsx: false,
|
||||
},
|
||||
target: 'es2016',
|
||||
loose: false,
|
||||
},
|
||||
minify: true,
|
||||
module: {
|
||||
type: 'es6'
|
||||
}
|
||||
sourceMaps: false,
|
||||
isModule: true,
|
||||
}
|
||||
}
|
||||
});
|
File diff suppressed because it is too large
Load Diff
54
justfile
54
justfile
|
@ -2,13 +2,25 @@
|
|||
default:
|
||||
@just --list
|
||||
|
||||
# Runs rector, showing what changes will be make
|
||||
rector-dry-run:
|
||||
tools/vendor/bin/rector process --config=tools/rector.php --dry-run src
|
||||
# -------------------------------------------------------------------
|
||||
# Front-end stuff
|
||||
# -------------------------------------------------------------------
|
||||
|
||||
# Runs rector, and updates the files
|
||||
rector:
|
||||
tools/vendor/bin/rector process --config=tools/rector.php src
|
||||
# Builds/optimizes JS and CSS
|
||||
build:
|
||||
cd frontEndSrc && npm run build && cd ..
|
||||
|
||||
# Builds/optimizes CSS
|
||||
css:
|
||||
composer run-script build:css
|
||||
|
||||
# Builds/optimizes JS
|
||||
js:
|
||||
composer run-script build:js
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# Code Quality and Formatting
|
||||
# -------------------------------------------------------------------
|
||||
|
||||
# Check code formatting
|
||||
check-fmt:
|
||||
|
@ -18,6 +30,22 @@ check-fmt:
|
|||
fmt:
|
||||
tools/vendor/bin/php-cs-fixer fix --verbose
|
||||
|
||||
# Runs phpstan code check
|
||||
phpstan:
|
||||
composer run-script phpstan
|
||||
|
||||
# Runs rector, showing what changes will be make
|
||||
rector-dry-run:
|
||||
tools/vendor/bin/rector process --config=tools/rector.php --dry-run src tests
|
||||
|
||||
# Runs rector, and updates the source files
|
||||
rector:
|
||||
tools/vendor/bin/rector process --config=tools/rector.php src tests
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# Testing
|
||||
# -------------------------------------------------------------------
|
||||
|
||||
# Run tests
|
||||
test:
|
||||
composer run-script test
|
||||
|
@ -26,10 +54,14 @@ test:
|
|||
test-update:
|
||||
composer run-script test-update
|
||||
|
||||
# Update the per-file header comments
|
||||
update-headers:
|
||||
php tools/update_header_comments.php
|
||||
|
||||
# Run unit tests and generate test-coverage report
|
||||
coverage:
|
||||
composer run-script coverage
|
||||
composer run-script coverage
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# Misc
|
||||
# -------------------------------------------------------------------
|
||||
|
||||
# Update the per-file header comments
|
||||
update-headers:
|
||||
php tools/update_header_comments.php
|
|
@ -9,11 +9,12 @@ parameters:
|
|||
- ./console
|
||||
- index.php
|
||||
ignoreErrors:
|
||||
- "#Offset 'fields' does not exist on array#"
|
||||
- '#Function imagepalletetotruecolor not found#'
|
||||
- '#Unable to resolve the template type T#'
|
||||
- '#imagepalletetotruecolor not found#'
|
||||
- '#Call to an undefined method Aura\\\Html\\\HelperLocator::[a-zA-Z0-9_]+\(\)#'
|
||||
- '#Call to an undefined method Query\\QueryBuilderInterface::[a-zA-Z0-9_]+\(\)#'
|
||||
excludes_analyse:
|
||||
excludePaths:
|
||||
- src/Ion/Type/Stringy.php
|
||||
- tests/mocks.php
|
||||
- vendor
|
||||
# These are objects that basically can return anything
|
||||
|
|
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
File diff suppressed because one or more lines are too long
|
@ -1 +1 @@
|
|||
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);console.log("Comparing "+textA+" and "+textB);if(th.classList.contains("numeric")){var arrayA=textA.replace("episodes: ","").replace("-",0).split("/");var arrayB=textB.replace("episodes: ","").replace("-",0).split("/");if(arrayA.length>1){textA=parseInt(arrayA[0],10)/parseInt(arrayA[1],10);textB=parseInt(arrayB[0],10)/parseInt(arrayB[1],10)}else{textA=parseInt(arrayA[0],10);textB=parseInt(arrayB[0],10)}}else if(parseInt(textA,10)){textA=parseInt(textA,10);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=ths[i];th.classList.add("sorting");th.classList.add("testing");results.push(th.onclick=onClickEvent)}return results}}}();LightTableSorter.init();
|
||||
const LightTableSorter=(()=>{let th=null;let cellIndex=null;let order="";const text=row=>row.cells.item(cellIndex).textContent.toLowerCase();const sort=(a,b)=>{let textA=text(a);let textB=text(b);console.log("Comparing "+textA+" and "+textB);if(th.classList.contains("numeric")){let arrayA=textA.replace("episodes: ","").replace("-",0).split("/");let arrayB=textB.replace("episodes: ","").replace("-",0).split("/");if(arrayA.length>1){textA=parseInt(arrayA[0],10)/parseInt(arrayA[1],10);textB=parseInt(arrayB[0],10)/parseInt(arrayB[1],10)}else{textA=parseInt(arrayA[0],10);textB=parseInt(arrayB[0],10)}}else if(parseInt(textA,10)){textA=parseInt(textA,10);textB=parseInt(textB,10)}if(textA>textB)return 1;if(textA<textB)return -1;return 0};const toggle=()=>{const c=order!=="sorting-asc"?"sorting-asc":"sorting-desc";th.className=(th.className.replace(order,"")+" "+c).trim();return order=c};const reset=()=>{th.classList.remove("sorting-asc","sorting-desc");th.classList.add("sorting");return order=""};const onClickEvent=e=>{if(th&&cellIndex!==e.target.cellIndex)reset();th=e.target;if(th.nodeName.toLowerCase()==="th"){cellIndex=th.cellIndex;const tbody=th.offsetParent.getElementsByTagName("tbody")[0];let rows=Array.from(tbody.rows);if(rows){rows.sort(sort);if(order==="sorting-asc")rows.reverse();toggle();tbody.innerHtml="";rows.forEach(row=>{tbody.appendChild(row)})}}};return{init:()=>{let ths=document.getElementsByTagName("th");let results=[];for(let i=0,len=ths.length;i<len;i++){let th=ths[i];th.classList.add("sorting");th.classList.add("testing");results.push(th.onclick=onClickEvent)}return results}}})();LightTableSorter.init();
|
File diff suppressed because one or more lines are too long
|
@ -1,15 +0,0 @@
|
|||
{
|
||||
"name": "Anilist Schema",
|
||||
"schemaPath": "schema.graphql",
|
||||
"extensions": {
|
||||
"endpoints": {
|
||||
"Anilist": {
|
||||
"url": "https://graphql.anilist.co",
|
||||
"headers": {
|
||||
"user-agent": "JS GraphQL"
|
||||
},
|
||||
"introspect": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -189,7 +189,7 @@ final class Model
|
|||
*/
|
||||
public function deleteItem(FormItem $data, string $type): ?Request
|
||||
{
|
||||
$mediaId = $this->getMediaId((array)$data, $type);
|
||||
$mediaId = $this->getMediaId((array) $data, $type);
|
||||
if ($mediaId === NULL)
|
||||
{
|
||||
return NULL;
|
||||
|
@ -209,7 +209,7 @@ final class Model
|
|||
*/
|
||||
public function getListIdFromData(FormItem $data, string $type = 'ANIME'): ?string
|
||||
{
|
||||
$mediaId = $this->getMediaId((array)$data, $type);
|
||||
$mediaId = $this->getMediaId((array) $data, $type);
|
||||
if ($mediaId === NULL)
|
||||
{
|
||||
return NULL;
|
||||
|
@ -244,7 +244,7 @@ final class Model
|
|||
/**
|
||||
* Find the id to update by
|
||||
*/
|
||||
private function getMediaId (array $data, string $type = 'ANIME'): ?string
|
||||
private function getMediaId(array $data, string $type = 'ANIME'): ?string
|
||||
{
|
||||
if (isset($data['anilist_id']))
|
||||
{
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
schema: schema.graphql
|
||||
extensions:
|
||||
endpoints:
|
||||
Anilist:
|
||||
url: https://graphql.anilist.co
|
||||
headers:
|
||||
user-agent: JS GraphQL
|
||||
introspect: true
|
|
@ -1,15 +0,0 @@
|
|||
{
|
||||
"name": "Kitsu Schema",
|
||||
"schemaPath": "schema.graphql",
|
||||
"extensions": {
|
||||
"endpoints": {
|
||||
"Kitsu": {
|
||||
"url": "https://kitsu.io/api/graphql",
|
||||
"headers": {
|
||||
"user-agent": "JS GraphQL"
|
||||
},
|
||||
"introspect": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -52,7 +52,7 @@ final class Auth
|
|||
->getSegment(SESSION_SEGMENT);
|
||||
$this->model = $container->get('kitsu-model');
|
||||
|
||||
Event::on('::unauthorized::', [$this, 'reAuthenticate']);
|
||||
Event::on('::unauthorized::', $this->reAuthenticate(...));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -133,7 +133,7 @@ final class Auth
|
|||
/**
|
||||
* Save the new authentication information
|
||||
*/
|
||||
private function storeAuth(array|FALSE $auth): bool
|
||||
private function storeAuth(array|false $auth): bool
|
||||
{
|
||||
if (FALSE !== $auth)
|
||||
{
|
||||
|
|
|
@ -34,6 +34,7 @@ use Aviat\AnimeClient\API\{
|
|||
use Aviat\AnimeClient\Enum\MediaType;
|
||||
use Aviat\AnimeClient\Kitsu as K;
|
||||
use Aviat\AnimeClient\Types\{Anime, MangaPage};
|
||||
use Aviat\AnimeClient\Types\{AnimeListItem, MangaListItem};
|
||||
use Aviat\Ion\{
|
||||
Di\ContainerAware,
|
||||
Json
|
||||
|
@ -282,7 +283,7 @@ final class Model
|
|||
|
||||
if ($list === NULL)
|
||||
{
|
||||
$data = $this->getList(MediaType::ANIME, $status) ?? [];
|
||||
$data = $this->getList(MediaType::ANIME, $status);
|
||||
|
||||
// Bail out on no data
|
||||
if (empty($data))
|
||||
|
@ -319,7 +320,7 @@ final class Model
|
|||
/**
|
||||
* Get all the anime entries, that are organized for output to html
|
||||
*
|
||||
* @return array<string, mixed[]>
|
||||
* @return array<string, array>
|
||||
*/
|
||||
public function getFullOrganizedAnimeList(): array
|
||||
{
|
||||
|
@ -330,7 +331,7 @@ final class Model
|
|||
foreach ($statuses as $status)
|
||||
{
|
||||
$mappedStatus = AnimeWatchingStatus::KITSU_TO_TITLE[$status];
|
||||
$output[$mappedStatus] = $this->getAnimeList($status) ?? [];
|
||||
$output[$mappedStatus] = $this->getAnimeList($status);
|
||||
}
|
||||
|
||||
return $output;
|
||||
|
@ -412,7 +413,7 @@ final class Model
|
|||
|
||||
if ($list === NULL)
|
||||
{
|
||||
$data = $this->getList(MediaType::MANGA, $status) ?? [];
|
||||
$data = $this->getList(MediaType::MANGA, $status);
|
||||
|
||||
// Bail out on no data
|
||||
if (empty($data))
|
||||
|
@ -534,14 +535,14 @@ final class Model
|
|||
* Get the data for a specific list item, generally for editing
|
||||
*
|
||||
* @param string $listId - The unique identifier of that list item
|
||||
* @return mixed
|
||||
*/
|
||||
public function getListItem(string $listId)
|
||||
public function getListItem(string $listId): AnimeListItem|MangaListItem|array
|
||||
{
|
||||
$baseData = $this->listItem->get($listId);
|
||||
if ( ! isset($baseData['data']['findLibraryEntryById']))
|
||||
{
|
||||
return [];
|
||||
// We need to get the errors...
|
||||
return $baseData;
|
||||
}
|
||||
|
||||
return (new LibraryEntryTransformer())->transform($baseData['data']['findLibraryEntryById']);
|
||||
|
@ -566,7 +567,7 @@ final class Model
|
|||
// this way is much faster...
|
||||
foreach ($statuses as $status)
|
||||
{
|
||||
foreach ($this->getPages([$this, 'getThumbListPages'], strtoupper($type), $status) as $page)
|
||||
foreach ($this->getPages($this->getThumbListPages(...), strtoupper($type), $status) as $page)
|
||||
{
|
||||
$pages[] = $page;
|
||||
}
|
||||
|
@ -596,7 +597,7 @@ final class Model
|
|||
// this way is much faster...
|
||||
foreach ($statuses as $status)
|
||||
{
|
||||
foreach ($this->getPages([$this, 'getSyncPages'], strtoupper($type), $status) as $page)
|
||||
foreach ($this->getPages($this->getSyncPages(...), strtoupper($type), $status) as $page)
|
||||
{
|
||||
$pages[] = $page;
|
||||
}
|
||||
|
@ -626,7 +627,7 @@ final class Model
|
|||
{
|
||||
$pages = [];
|
||||
|
||||
foreach ($this->getPages([$this, 'getListPages'], strtoupper($type), strtoupper($status)) as $page)
|
||||
foreach ($this->getPages($this->getListPages(...), strtoupper($type), strtoupper($status)) as $page)
|
||||
{
|
||||
$pages[] = $page;
|
||||
}
|
||||
|
@ -786,7 +787,7 @@ final class Model
|
|||
}
|
||||
}
|
||||
|
||||
private function getUserId(): string
|
||||
protected function getUserId(): string
|
||||
{
|
||||
static $userId = NULL;
|
||||
|
||||
|
|
|
@ -67,9 +67,6 @@ trait MutationTrait
|
|||
|
||||
/**
|
||||
* Remove a list item
|
||||
*
|
||||
* @param FormItem $data
|
||||
* @return Request
|
||||
*/
|
||||
public function deleteItem(FormItem $data): Request
|
||||
{
|
||||
|
|
|
@ -19,7 +19,7 @@ query ($slug: String!) {
|
|||
}
|
||||
categories(first: 100) {
|
||||
nodes {
|
||||
title
|
||||
title(locales: "en")
|
||||
}
|
||||
}
|
||||
characters(first: 100) {
|
||||
|
@ -29,7 +29,7 @@ query ($slug: String!) {
|
|||
names {
|
||||
alternatives
|
||||
canonical
|
||||
localized
|
||||
localized(locales: "*")
|
||||
}
|
||||
image {
|
||||
original {
|
||||
|
@ -50,7 +50,7 @@ query ($slug: String!) {
|
|||
startCursor
|
||||
}
|
||||
}
|
||||
description
|
||||
description(locales: "en")
|
||||
startDate
|
||||
endDate
|
||||
episodeCount
|
||||
|
@ -87,7 +87,7 @@ query ($slug: String!) {
|
|||
names {
|
||||
alternatives
|
||||
canonical
|
||||
localized
|
||||
localized(locales: "*")
|
||||
}
|
||||
slug
|
||||
}
|
||||
|
@ -118,7 +118,7 @@ query ($slug: String!) {
|
|||
alternatives
|
||||
canonical
|
||||
canonicalLocale
|
||||
localized
|
||||
localized(locales: ["en", "en-t-ja", "ja", "ja-jp"])
|
||||
}
|
||||
totalLength
|
||||
youtubeTrailerVideoId
|
||||
|
|
|
@ -19,7 +19,7 @@ query ($id: ID!) {
|
|||
}
|
||||
categories(first: 100) {
|
||||
nodes {
|
||||
title
|
||||
title(locales: "en")
|
||||
}
|
||||
}
|
||||
characters(first: 100) {
|
||||
|
@ -29,7 +29,7 @@ query ($id: ID!) {
|
|||
names {
|
||||
alternatives
|
||||
canonical
|
||||
localized
|
||||
localized(locales: "*")
|
||||
}
|
||||
image {
|
||||
original {
|
||||
|
@ -50,7 +50,7 @@ query ($id: ID!) {
|
|||
startCursor
|
||||
}
|
||||
}
|
||||
description
|
||||
description(locales: "en")
|
||||
startDate
|
||||
endDate
|
||||
episodeCount
|
||||
|
@ -87,7 +87,7 @@ query ($id: ID!) {
|
|||
names {
|
||||
alternatives
|
||||
canonical
|
||||
localized
|
||||
localized(locales: "*")
|
||||
}
|
||||
slug
|
||||
}
|
||||
|
@ -118,7 +118,7 @@ query ($id: ID!) {
|
|||
alternatives
|
||||
canonical
|
||||
canonicalLocale
|
||||
localized
|
||||
localized(locales: ["en", "en-t-ja", "ja", "ja-jp"])
|
||||
}
|
||||
totalLength
|
||||
youtubeTrailerVideoId
|
||||
|
|
|
@ -6,12 +6,12 @@ query ($slug: String!) {
|
|||
url
|
||||
}
|
||||
}
|
||||
description
|
||||
description(locales: "en")
|
||||
names {
|
||||
alternatives
|
||||
canonical
|
||||
canonicalLocale
|
||||
localized
|
||||
localized(locales: "*")
|
||||
},
|
||||
media(first: 100) {
|
||||
nodes {
|
||||
|
@ -22,7 +22,7 @@ query ($slug: String!) {
|
|||
alternatives
|
||||
canonical
|
||||
canonicalLocale
|
||||
localized
|
||||
localized(locales: ["en", "en-t-ja", "ja", "ja-jp"])
|
||||
}
|
||||
posterImage {
|
||||
original {
|
||||
|
@ -41,7 +41,7 @@ query ($slug: String!) {
|
|||
type
|
||||
}
|
||||
role
|
||||
voices(first: 10) {
|
||||
voices(first: 10, locale:"*", sort:{direction:ASCENDING, on: UPDATED_AT}) {
|
||||
nodes {
|
||||
id
|
||||
locale
|
||||
|
@ -53,7 +53,7 @@ query ($slug: String!) {
|
|||
alternatives
|
||||
canonical
|
||||
canonicalLocale
|
||||
localized
|
||||
localized(locales: "*")
|
||||
}
|
||||
image {
|
||||
original {
|
||||
|
@ -70,4 +70,4 @@ query ($slug: String!) {
|
|||
}
|
||||
slug
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -57,7 +57,7 @@ query (
|
|||
type
|
||||
titles {
|
||||
canonical
|
||||
localized
|
||||
localized(locales: "*")
|
||||
alternatives
|
||||
}
|
||||
...on Anime {
|
||||
|
|
|
@ -16,7 +16,7 @@ query($id: ID!) {
|
|||
ageRating
|
||||
categories(first: 100) {
|
||||
nodes {
|
||||
title
|
||||
title(locales: "*")
|
||||
}
|
||||
}
|
||||
mappings(first: 10) {
|
||||
|
@ -41,7 +41,7 @@ query($id: ID!) {
|
|||
endDate
|
||||
titles {
|
||||
canonical
|
||||
localized
|
||||
localized(locales: "*")
|
||||
canonicalLocale
|
||||
}
|
||||
type
|
||||
|
|
|
@ -33,7 +33,7 @@ query ($slug: String!) {
|
|||
titles {
|
||||
alternatives
|
||||
canonical
|
||||
localized
|
||||
localized(locales: "*")
|
||||
}
|
||||
...on Anime {
|
||||
episodeCount
|
||||
|
|
|
@ -19,7 +19,7 @@ query ($slug: String!) {
|
|||
}
|
||||
categories(first: 100) {
|
||||
nodes {
|
||||
title
|
||||
title(locales: "en")
|
||||
}
|
||||
}
|
||||
chapterCount
|
||||
|
@ -51,7 +51,7 @@ query ($slug: String!) {
|
|||
startCursor
|
||||
}
|
||||
}
|
||||
description
|
||||
description(locales: "en")
|
||||
startDate
|
||||
endDate
|
||||
mappings(first: 10) {
|
||||
|
@ -98,7 +98,7 @@ query ($slug: String!) {
|
|||
names {
|
||||
alternatives
|
||||
canonical
|
||||
localized
|
||||
localized(locales: "*")
|
||||
}
|
||||
slug
|
||||
}
|
||||
|
@ -116,7 +116,7 @@ query ($slug: String!) {
|
|||
titles {
|
||||
canonical
|
||||
canonicalLocale
|
||||
localized
|
||||
localized(locales: ["en", "en-t-ja", "ja", "ja-jp"])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ query ($id: ID!) {
|
|||
}
|
||||
categories(first: 100) {
|
||||
nodes {
|
||||
title
|
||||
title(locales: "*")
|
||||
}
|
||||
}
|
||||
chapterCount
|
||||
|
@ -116,7 +116,7 @@ query ($id: ID!) {
|
|||
titles {
|
||||
canonical
|
||||
canonicalLocale
|
||||
localized
|
||||
localized(locales: "*")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
query ($slug: String!) {
|
||||
findPersonBySlug(slug: $slug) {
|
||||
id
|
||||
description
|
||||
description(locales: "en")
|
||||
birthday
|
||||
image {
|
||||
original {
|
||||
|
@ -20,7 +20,7 @@ query ($slug: String!) {
|
|||
names {
|
||||
alternatives
|
||||
canonical
|
||||
localized
|
||||
localized(locales: "*")
|
||||
}
|
||||
mediaStaff(first: 100) {
|
||||
nodes {
|
||||
|
@ -47,7 +47,7 @@ query ($slug: String!) {
|
|||
titles {
|
||||
alternatives
|
||||
canonical
|
||||
localized
|
||||
localized(locales: ["en", "en-t-ja", "ja", "ja-jp"])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -91,7 +91,7 @@ query ($slug: String!) {
|
|||
}
|
||||
titles {
|
||||
canonical
|
||||
localized
|
||||
localized(locales: ["en", "en-t-ja", "ja", "ja-jp"])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@ query ($type: MediaTypeEnum!) {
|
|||
}
|
||||
categories(first: 100) {
|
||||
nodes {
|
||||
title
|
||||
title(locales: "*")
|
||||
}
|
||||
}
|
||||
characters(first: 100) {
|
||||
|
@ -36,7 +36,7 @@ query ($type: MediaTypeEnum!) {
|
|||
names {
|
||||
alternatives
|
||||
canonical
|
||||
localized
|
||||
localized(locales: "*")
|
||||
}
|
||||
image {
|
||||
original {
|
||||
|
@ -90,7 +90,7 @@ query ($type: MediaTypeEnum!) {
|
|||
names {
|
||||
alternatives
|
||||
canonical
|
||||
localized
|
||||
localized(locales: "*")
|
||||
}
|
||||
slug
|
||||
}
|
||||
|
@ -108,7 +108,7 @@ query ($type: MediaTypeEnum!) {
|
|||
alternatives
|
||||
canonical
|
||||
canonicalLocale
|
||||
localized
|
||||
localized(locales: "*")
|
||||
}
|
||||
...on Anime {
|
||||
episodeCount
|
||||
|
|
|
@ -19,7 +19,7 @@ query ($query: String!) {
|
|||
slug
|
||||
titles {
|
||||
canonical
|
||||
localized
|
||||
localized(locales: ["en", "en-t-ja", "ja", "ja-jp"])
|
||||
alternatives
|
||||
}
|
||||
myLibraryEntry {
|
||||
|
|
|
@ -19,7 +19,7 @@ query ($query: String!) {
|
|||
slug
|
||||
titles {
|
||||
canonical
|
||||
localized
|
||||
localized(locales: ["en", "en-t-ja", "ja", "ja-jp"])
|
||||
alternatives
|
||||
}
|
||||
myLibraryEntry {
|
||||
|
|
|
@ -18,8 +18,10 @@ query ($slug: String!) {
|
|||
}
|
||||
}
|
||||
birthday
|
||||
createdAt
|
||||
id
|
||||
location
|
||||
gender
|
||||
name
|
||||
proMessage
|
||||
proTier
|
||||
|
@ -52,7 +54,7 @@ query ($slug: String!) {
|
|||
}
|
||||
titles {
|
||||
canonical
|
||||
localized
|
||||
localized(locales: ["en", "en-t-ja", "ja", "ja-jp"])
|
||||
}
|
||||
}
|
||||
...on Manga {
|
||||
|
@ -72,7 +74,7 @@ query ($slug: String!) {
|
|||
}
|
||||
titles {
|
||||
canonical
|
||||
localized
|
||||
localized(locales: ["en", "en-t-ja", "ja", "ja-jp"])
|
||||
}
|
||||
}
|
||||
...on Person {
|
||||
|
@ -88,11 +90,12 @@ query ($slug: String!) {
|
|||
width
|
||||
}
|
||||
}
|
||||
name,
|
||||
names {
|
||||
alternatives
|
||||
canonical
|
||||
canonicalLocale
|
||||
localized
|
||||
localized(locales: "*")
|
||||
},
|
||||
}
|
||||
...on Character {
|
||||
|
@ -107,12 +110,12 @@ query ($slug: String!) {
|
|||
height
|
||||
width
|
||||
}
|
||||
}
|
||||
},
|
||||
names {
|
||||
alternatives
|
||||
canonical
|
||||
canonicalLocale
|
||||
localized
|
||||
localized(locales: "*")
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -150,7 +153,7 @@ query ($slug: String!) {
|
|||
names {
|
||||
canonical
|
||||
alternatives
|
||||
localized
|
||||
localized(locales: "*")
|
||||
}
|
||||
}
|
||||
waifuOrHusbando
|
||||
|
|
|
@ -78,7 +78,7 @@ final class RequestBuilder extends APIRequestBuilder
|
|||
elseif ($url !== K::AUTH_URL && $sessionSegment->get('auth_token') !== NULL)
|
||||
{
|
||||
$token = $sessionSegment->get('auth_token');
|
||||
if ( ! (empty($token) || $cache->has(K::AUTH_TOKEN_CACHE_KEY)))
|
||||
if ( ! empty($token))
|
||||
{
|
||||
$cache->set(K::AUTH_TOKEN_CACHE_KEY, $token);
|
||||
}
|
||||
|
@ -239,43 +239,4 @@ final class RequestBuilder extends APIRequestBuilder
|
|||
'body' => $body,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a request
|
||||
*/
|
||||
private function request(string $type, string $url, array $options = []): array
|
||||
{
|
||||
$logger = $this->container->getLogger('kitsu-request');
|
||||
$response = $this->getResponse($type, $url, $options);
|
||||
$statusCode = $response->getStatus();
|
||||
|
||||
// Check for requests that are unauthorized
|
||||
if ($statusCode === 401 || $statusCode === 403)
|
||||
{
|
||||
Event::emit(EventType::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
$rawBody = wait($response->getBody()->buffer());
|
||||
|
||||
// Any other type of failed request
|
||||
if ($statusCode > 299 || $statusCode < 200)
|
||||
{
|
||||
if ($logger !== NULL)
|
||||
{
|
||||
$logger->warning('Non 2xx response for api call', (array) $response);
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return Json::decode($rawBody);
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
// dump($e);
|
||||
dump($rawBody);
|
||||
|
||||
exit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,8 +23,6 @@ trait RequestBuilderTrait
|
|||
|
||||
/**
|
||||
* Set the request builder object
|
||||
*
|
||||
* @return ListItem|Model|RequestBuilderTrait
|
||||
*/
|
||||
public function setRequestBuilder(RequestBuilder $requestBuilder): self
|
||||
{
|
||||
|
|
|
@ -36,7 +36,8 @@ final class AnimeTransformer extends AbstractTransformer
|
|||
$characters = [];
|
||||
$links = [];
|
||||
$staff = [];
|
||||
$genres = array_map(static fn ($genre) => $genre['title']['en'], $base['categories']['nodes']);
|
||||
$rawGenres = array_filter($base['categories']['nodes'], static fn ($c) => $c !== NULL);
|
||||
$genres = array_map(static fn ($genre) => $genre['title']['en'], $rawGenres);
|
||||
|
||||
sort($genres);
|
||||
|
||||
|
@ -56,7 +57,7 @@ final class AnimeTransformer extends AbstractTransformer
|
|||
|
||||
$details = $rawCharacter['character'];
|
||||
$characters[$type][$details['id']] = [
|
||||
'image' => $details['image']['original']['url'] ?? '',
|
||||
'image' => Kitsu::getImage($details),
|
||||
'name' => $details['names']['canonical'],
|
||||
'slug' => $details['slug'],
|
||||
];
|
||||
|
@ -100,7 +101,7 @@ final class AnimeTransformer extends AbstractTransformer
|
|||
$staff[$role][$person['id']] = [
|
||||
'id' => $person['id'],
|
||||
'name' => $name,
|
||||
'image' => $person['image']['original']['url'],
|
||||
'image' => Kitsu::getImage($person),
|
||||
'slug' => $person['slug'],
|
||||
];
|
||||
|
||||
|
|
|
@ -49,7 +49,7 @@ final class CharacterTransformer extends AbstractTransformer
|
|||
'castings' => $castings,
|
||||
'description' => $data['description']['en'],
|
||||
'id' => $data['id'],
|
||||
'image' => $data['image']['original']['url'] ?? 'images/placeholder.png',
|
||||
'image' => Kitsu::getImage($data),
|
||||
'media' => $media,
|
||||
'name' => $name,
|
||||
'names' => $names,
|
||||
|
@ -130,7 +130,7 @@ final class CharacterTransformer extends AbstractTransformer
|
|||
'person' => [
|
||||
'id' => $voice['person']['id'],
|
||||
'slug' => $voice['person']['slug'],
|
||||
'image' => $voice['person']['image']['original']['url'],
|
||||
'image' => Kitsu::getImage($voice['person']),
|
||||
'name' => $voice['person']['name'],
|
||||
],
|
||||
'series' => [],
|
||||
|
|
|
@ -54,10 +54,10 @@ final class MangaTransformer extends AbstractTransformer
|
|||
}
|
||||
|
||||
$details = $rawCharacter['character'];
|
||||
if (array_key_exists($details['id'], $characters[$type]))
|
||||
if (array_key_exists($details['id'], (array)$characters[$type]))
|
||||
{
|
||||
$characters[$type][$details['id']] = [
|
||||
'image' => $details['image']['original']['url'],
|
||||
'image' => Kitsu::getImage($details),
|
||||
'name' => $details['names']['canonical'],
|
||||
'slug' => $details['slug'],
|
||||
];
|
||||
|
@ -103,7 +103,7 @@ final class MangaTransformer extends AbstractTransformer
|
|||
'id' => $person['id'],
|
||||
'slug' => $person['slug'],
|
||||
'name' => $name,
|
||||
'image' => $person['image']['original']['url'],
|
||||
'image' => Kitsu::getImage($person),
|
||||
];
|
||||
|
||||
usort($staff[$role], static fn ($a, $b) => $a['name'] <=> $b['name']);
|
||||
|
|
|
@ -35,7 +35,8 @@ final class PersonTransformer extends AbstractTransformer
|
|||
return Person::from([
|
||||
'id' => $data['id'],
|
||||
'name' => $canonicalName,
|
||||
'image' => $data['image']['original']['url'],
|
||||
'birthday' => $data['birthday'],
|
||||
'image' => Kitsu::getImage($data),
|
||||
'names' => array_diff($data['names']['localized'], [$canonicalName]),
|
||||
'description' => $data['description']['en'] ?? '',
|
||||
'characters' => $orgData['characters'],
|
||||
|
@ -97,7 +98,12 @@ final class PersonTransformer extends AbstractTransformer
|
|||
{
|
||||
foreach ($data['voices']['nodes'] as $voicing)
|
||||
{
|
||||
$character = $voicing['mediaCharacter']['character'];
|
||||
if ($voicing === NULL)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$character = $voicing['mediaCharacter']['character'] ?? [];
|
||||
$charId = $character['id'];
|
||||
$rawMedia = $voicing['mediaCharacter']['media'];
|
||||
$role = strtolower($voicing['mediaCharacter']['role']);
|
||||
|
@ -123,7 +129,7 @@ final class PersonTransformer extends AbstractTransformer
|
|||
'character' => [
|
||||
'id' => $character['id'],
|
||||
'slug' => $character['slug'],
|
||||
'image' => $character['image']['original']['url'],
|
||||
'image' => Kitsu::getImage($character),
|
||||
'canonicalName' => $character['names']['canonical'],
|
||||
],
|
||||
'media' => [
|
||||
|
|
|
@ -14,11 +14,11 @@
|
|||
|
||||
namespace Aviat\AnimeClient\API\Kitsu\Transformer;
|
||||
|
||||
use Aviat\AnimeClient\Kitsu;
|
||||
|
||||
use Aviat\AnimeClient\Types\User;
|
||||
use Aviat\Ion\Transformer\AbstractTransformer;
|
||||
|
||||
use function Aviat\AnimeClient\{formatDate, friendlyTime, getDateDiff};
|
||||
|
||||
/**
|
||||
* Transform user profile data for display
|
||||
*
|
||||
|
@ -39,15 +39,22 @@ final class UserTransformer extends AbstractTransformer
|
|||
] : [];
|
||||
|
||||
return User::from([
|
||||
'about' => $base['about'],
|
||||
'avatar' => $base['avatarImage']['original']['url'],
|
||||
'about' => $base['about'] ?? '',
|
||||
'avatar' => $base['avatarImage']['original']['url'] ?? NULL,
|
||||
'birthday' => $base['birthday'] !== NULL
|
||||
? formatDate($base['birthday']) . ' (' .
|
||||
friendlyTime(getDateDiff($base['birthday']), 'year') . ')'
|
||||
: NULL,
|
||||
'joinDate' => formatDate($base['createdAt']) . ' (' .
|
||||
friendlyTime(getDateDiff($base['createdAt']), 'day') . ' ago)',
|
||||
'gender' => $base['gender'],
|
||||
'favorites' => $this->organizeFavorites($favorites),
|
||||
'location' => $base['location'],
|
||||
'name' => $base['name'],
|
||||
'slug' => $base['slug'],
|
||||
'stats' => $this->organizeStats($stats),
|
||||
'waifu' => $waifu,
|
||||
'website' => $base['siteLinks']['nodes'][0]['url'],
|
||||
'website' => $base['siteLinks']['nodes'][0]['url'] ?? NULL,
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -81,7 +88,7 @@ final class UserTransformer extends AbstractTransformer
|
|||
if (array_key_exists('animeAmountConsumed', $stats))
|
||||
{
|
||||
$animeStats = [
|
||||
'Time spent watching anime:' => Kitsu::friendlyTime($stats['animeAmountConsumed']['time']),
|
||||
'Time spent watching anime:' => friendlyTime($stats['animeAmountConsumed']['time']),
|
||||
'Anime series watched:' => number_format($stats['animeAmountConsumed']['media']),
|
||||
'Anime episodes watched:' => number_format($stats['animeAmountConsumed']['units']),
|
||||
];
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
schema: schema.graphql
|
||||
extensions:
|
||||
endpoints:
|
||||
Kitsu:
|
||||
url: https://kitsu.io/api/graphql
|
||||
headers:
|
||||
user-agent: JS GraphQL
|
||||
introspect: true
|
|
@ -35,7 +35,7 @@ final class ParallelAPIRequest
|
|||
/**
|
||||
* Add a request
|
||||
*/
|
||||
public function addRequest(string|Request $request, string|int|NULL $key = NULL): self
|
||||
public function addRequest(string|Request $request, string|int|null $key = NULL): self
|
||||
{
|
||||
if ($key !== NULL)
|
||||
{
|
||||
|
@ -56,7 +56,7 @@ final class ParallelAPIRequest
|
|||
*/
|
||||
public function addRequests(array $requests): self
|
||||
{
|
||||
array_walk($requests, [$this, 'addRequest']);
|
||||
array_walk($requests, $this->addRequest(...));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ namespace Aviat\AnimeClient;
|
|||
use Amp\Http\Client\{HttpClient, HttpClientBuilder, Request, Response};
|
||||
|
||||
use Aviat\Ion\{ConfigInterface, ImageBuilder};
|
||||
use DateTimeImmutable;
|
||||
use Psr\SimpleCache\CacheInterface;
|
||||
use Throwable;
|
||||
|
||||
|
@ -25,13 +26,17 @@ use Yosymfony\Toml\{Toml, TomlBuilder};
|
|||
use function Amp\Promise\wait;
|
||||
use function Aviat\Ion\_dir;
|
||||
|
||||
const SECONDS_IN_MINUTE = 60;
|
||||
const MINUTES_IN_HOUR = 60;
|
||||
const MINUTES_IN_DAY = 1440;
|
||||
const MINUTES_IN_YEAR = 525_600;
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
//! TOML Functions
|
||||
// ----------------------------------------------------------------------------
|
||||
/**
|
||||
* Load configuration options from .toml files
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
* @param string $path - Path to load config
|
||||
*/
|
||||
function loadConfig(string $path): array
|
||||
|
@ -72,8 +77,6 @@ function loadConfig(string $path): array
|
|||
|
||||
/**
|
||||
* Load config from one specific TOML file
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
function loadTomlFile(string $filename): array
|
||||
{
|
||||
|
@ -131,19 +134,6 @@ function tomlToArray(string $toml): array
|
|||
//! Misc Functions
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
if ( ! function_exists('array_is_list'))
|
||||
{
|
||||
/**
|
||||
* Polyfill for PHP 8
|
||||
*
|
||||
* @see https://www.php.net/manual/en/function.array-is-list
|
||||
*/
|
||||
function array_is_list(array $a): bool
|
||||
{
|
||||
return $a === [] || (array_keys($a) === range(0, count($a) - 1));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the array sequential, not associative?
|
||||
*/
|
||||
|
@ -256,8 +246,6 @@ function getLocalImg(string $kitsuUrl, bool $webp = TRUE): string
|
|||
|
||||
/**
|
||||
* Create a transparent placeholder image
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
function createPlaceholderImage(string $path, int $width = 200, int $height = 200, string $text = 'Image Unavailable'): bool
|
||||
{
|
||||
|
@ -303,15 +291,13 @@ function clearCache(CacheInterface $cache): bool
|
|||
|
||||
$cleared = $cache->clear();
|
||||
|
||||
$saved = (empty($userData)) ? TRUE : $cache->setMultiple($userData);
|
||||
$saved = empty($userData) || $cache->setMultiple($userData);
|
||||
|
||||
return $cleared && $saved;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a PHP code template as a string
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
function renderTemplate(string $path, array $data): string
|
||||
{
|
||||
|
@ -322,3 +308,87 @@ function renderTemplate(string $path, array $data): string
|
|||
|
||||
return (is_string($rawOutput)) ? $rawOutput : '';
|
||||
}
|
||||
|
||||
function formatDate(string $date): string
|
||||
{
|
||||
$date = new DateTimeImmutable($date);
|
||||
|
||||
return $date->format('F d, Y');
|
||||
}
|
||||
|
||||
function getDateDiff(string $date): int
|
||||
{
|
||||
$now = new DateTimeImmutable();
|
||||
$then = new DateTimeImmutable($date);
|
||||
|
||||
$interval = $now->diff($then, TRUE);
|
||||
|
||||
$years = $interval->y * SECONDS_IN_MINUTE * MINUTES_IN_YEAR;
|
||||
$days = $interval->d * SECONDS_IN_MINUTE * MINUTES_IN_DAY;
|
||||
$hours = $interval->h * SECONDS_IN_MINUTE * MINUTES_IN_HOUR;
|
||||
$minutes = $interval->i * SECONDS_IN_MINUTE;
|
||||
$seconds = $interval->s;
|
||||
|
||||
return $years + $days + $hours + $minutes + $seconds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a time in seconds to a more human-readable format
|
||||
*/
|
||||
function friendlyTime(int $seconds, string $minUnit = 'second'): string
|
||||
{
|
||||
// All the seconds left
|
||||
$remSeconds = $seconds % SECONDS_IN_MINUTE;
|
||||
$minutes = ($seconds - $remSeconds) / SECONDS_IN_MINUTE;
|
||||
|
||||
// Minutes short of a year
|
||||
$years = (int) floor($minutes / MINUTES_IN_YEAR);
|
||||
$minutes %= MINUTES_IN_YEAR;
|
||||
|
||||
// Minutes short of a day
|
||||
$extraMinutes = $minutes % MINUTES_IN_DAY;
|
||||
$days = ($minutes - $extraMinutes) / MINUTES_IN_DAY;
|
||||
|
||||
// Minutes short of an hour
|
||||
$remMinutes = $extraMinutes % MINUTES_IN_HOUR;
|
||||
$hours = ($extraMinutes - $remMinutes) / MINUTES_IN_HOUR;
|
||||
|
||||
$parts = [];
|
||||
|
||||
foreach ([
|
||||
'year' => $years,
|
||||
'day' => $days,
|
||||
'hour' => $hours,
|
||||
'minute' => $remMinutes,
|
||||
'second' => $remSeconds,
|
||||
] as $label => $value)
|
||||
{
|
||||
if ($value === 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($value > 1)
|
||||
{
|
||||
$label .= 's';
|
||||
}
|
||||
|
||||
$parts[] = "{$value} {$label}";
|
||||
|
||||
if ($label === $minUnit || $label === $minUnit . 's')
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$last = array_pop($parts);
|
||||
|
||||
if (empty($parts))
|
||||
{
|
||||
return $last ?? '';
|
||||
}
|
||||
|
||||
return (count($parts) > 1)
|
||||
? implode(', ', $parts) . ", and {$last}"
|
||||
: "{$parts[0]}, {$last}";
|
||||
}
|
||||
|
|
|
@ -44,7 +44,7 @@ abstract class BaseCommand extends Command
|
|||
/**
|
||||
* Echo text in a box
|
||||
*/
|
||||
public function echoBox(string|array $message, string|int|NULL $fgColor = NULL, string|int|NULL $bgColor = NULL): void
|
||||
public function echoBox(string|array $message, string|int|null $fgColor = NULL, string|int|null $bgColor = NULL): void
|
||||
{
|
||||
if (is_array($message))
|
||||
{
|
||||
|
@ -131,7 +131,7 @@ abstract class BaseCommand extends Command
|
|||
return $this->_di($configArray, $APP_DIR);
|
||||
}
|
||||
|
||||
private function _line(string $message, int|string|NULL $fgColor = NULL, int|string|NULL $bgColor = NULL): void
|
||||
private function _line(string $message, int|string|null $fgColor = NULL, int|string|null $bgColor = NULL): void
|
||||
{
|
||||
if ($fgColor !== NULL)
|
||||
{
|
||||
|
|
|
@ -98,21 +98,23 @@ final class SyncLists extends BaseCommand
|
|||
if ( ! $anilistEnabled)
|
||||
{
|
||||
$this->echoErrorBox('Anlist API is not enabled. Can not sync.');
|
||||
return false;
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
// Authentication is required to update Kitsu
|
||||
$isKitsuAuthenticated = $this->container->get('auth')->isAuthenticated();
|
||||
if ( !$isKitsuAuthenticated)
|
||||
if ( ! $isKitsuAuthenticated)
|
||||
{
|
||||
$this->echoErrorBox('Kitsu is not authenticated. Kitsu list can not be updated.');
|
||||
return false;
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
$this->anilistModel = $this->container->get('anilist-model');
|
||||
$this->kitsuModel = $this->container->get('kitsu-model');
|
||||
|
||||
return true;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -148,7 +150,7 @@ final class SyncLists extends BaseCommand
|
|||
*/
|
||||
protected function fetch(string $type): array
|
||||
{
|
||||
$this->echo("Fetching $type List Data");
|
||||
$this->echo("Fetching {$type} List Data");
|
||||
$progress = new Widgets\ProgressBar($this->getConsole(), 2, 50, FALSE);
|
||||
|
||||
$anilist = $this->fetchAnilist($type);
|
||||
|
|
|
@ -42,6 +42,11 @@ class Controller
|
|||
{
|
||||
use ContainerAware;
|
||||
|
||||
/**
|
||||
* The global configuration object
|
||||
*/
|
||||
public ConfigInterface $config;
|
||||
|
||||
/**
|
||||
* The authentication object
|
||||
*/
|
||||
|
@ -52,11 +57,6 @@ class Controller
|
|||
*/
|
||||
protected CacheInterface $cache;
|
||||
|
||||
/**
|
||||
* The global configuration object
|
||||
*/
|
||||
public ConfigInterface $config;
|
||||
|
||||
/**
|
||||
* Request object
|
||||
*/
|
||||
|
@ -120,197 +120,194 @@ class Controller
|
|||
Event::on(EventType::RESET_CACHE_KEY, fn (string $key) => $this->cache->delete($key));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current url in the session as the target of a future redirect
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
* @throws ContainerException
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
public function setSessionRedirect(?string $url = NULL): void
|
||||
{
|
||||
$serverParams = $this->request->getServerParams();
|
||||
/**
|
||||
* Set the current url in the session as the target of a future redirect
|
||||
*
|
||||
* @throws ContainerException
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
#[\PHPUnit\Framework\Attributes\CodeCoverageIgnore]
|
||||
public function setSessionRedirect(?string $url = NULL): void
|
||||
{
|
||||
$serverParams = $this->request->getServerParams();
|
||||
|
||||
if ( ! array_key_exists('HTTP_REFERER', $serverParams))
|
||||
{
|
||||
return;
|
||||
}
|
||||
if ( ! array_key_exists('HTTP_REFERER', $serverParams))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
$util = $this->container->get('util');
|
||||
$doubleFormPage = $serverParams['HTTP_REFERER'] === $this->request->getUri();
|
||||
$isLoginPage = str_contains($serverParams['HTTP_REFERER'], 'login');
|
||||
$util = $this->container->get('util');
|
||||
$doubleFormPage = $serverParams['HTTP_REFERER'] === $this->request->getUri();
|
||||
$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,
|
||||
// and the previous page is also a form type
|
||||
if ($doubleFormPage || $isLoginPage)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// Don't attempt to set the redirect url if
|
||||
// the page is one of the form type pages,
|
||||
// and the previous page is also a form type
|
||||
if ($doubleFormPage || $isLoginPage)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (NULL === $url)
|
||||
{
|
||||
$url = $util->isViewPage()
|
||||
? (string) $this->request->getUri()
|
||||
: $serverParams['HTTP_REFERER'];
|
||||
}
|
||||
if (NULL === $url)
|
||||
{
|
||||
$url = $util->isViewPage()
|
||||
? (string) $this->request->getUri()
|
||||
: $serverParams['HTTP_REFERER'];
|
||||
}
|
||||
|
||||
$this->session->set('redirect_url', $url);
|
||||
}
|
||||
$this->session->set('redirect_url', $url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect to the url previously set in the session
|
||||
*
|
||||
* If one is not set, redirect to default url
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function sessionRedirect(): void
|
||||
{
|
||||
$target = $this->session->get('redirect_url') ?? '/';
|
||||
/**
|
||||
* Redirect to the url previously set in the session
|
||||
*
|
||||
* If one is not set, redirect to default url
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
#[\PHPUnit\Framework\Attributes\CodeCoverageIgnore]
|
||||
public function sessionRedirect(): void
|
||||
{
|
||||
$target = $this->session->get('redirect_url') ?? '/';
|
||||
|
||||
$this->redirect($target, 303);
|
||||
$this->session->set('redirect_url', NULL);
|
||||
}
|
||||
$this->redirect($target, 303);
|
||||
$this->session->set('redirect_url', NULL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current user is authenticated, else error and exit
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
protected function checkAuth(): void
|
||||
{
|
||||
if ( ! $this->auth->isAuthenticated())
|
||||
{
|
||||
$this->errorPage(
|
||||
403,
|
||||
'Forbidden',
|
||||
'You must <a href="/login">log in</a> to perform this action.'
|
||||
);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Check if the current user is authenticated, else error and exit
|
||||
*/
|
||||
#[\PHPUnit\Framework\Attributes\CodeCoverageIgnore]
|
||||
protected function checkAuth(): void
|
||||
{
|
||||
if ( ! $this->auth->isAuthenticated())
|
||||
{
|
||||
$this->errorPage(
|
||||
403,
|
||||
'Forbidden',
|
||||
'You must <a href="/login">log in</a> to perform this action.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the string output of a partial template
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
protected function loadPartial(HtmlView $view, string $template, array $data = []): string
|
||||
{
|
||||
$router = $this->container->get('dispatcher');
|
||||
/**
|
||||
* Get the string output of a partial template
|
||||
*/
|
||||
#[\PHPUnit\Framework\Attributes\CodeCoverageIgnore]
|
||||
protected function loadPartial(HtmlView $view, string $template, array $data = []): string
|
||||
{
|
||||
$router = $this->container->get('dispatcher');
|
||||
|
||||
if (isset($this->baseData))
|
||||
{
|
||||
$data = array_merge($this->baseData, $data);
|
||||
}
|
||||
if (isset($this->baseData))
|
||||
{
|
||||
$data = array_merge($this->baseData, $data);
|
||||
}
|
||||
|
||||
$route = $router->getRoute();
|
||||
$data['route_path'] = $route !== FALSE ? $route->path : '';
|
||||
$route = $router->getRoute();
|
||||
$data['route_path'] = $route !== FALSE ? $route->path : '';
|
||||
|
||||
$templatePath = _dir($this->config->get('view_path'), "{$template}.php");
|
||||
$templatePath = _dir($this->config->get('view_path'), "{$template}.php");
|
||||
|
||||
if ( ! is_file($templatePath))
|
||||
{
|
||||
throw new InvalidArgumentException("Invalid template : {$template}");
|
||||
}
|
||||
if ( ! is_file($templatePath))
|
||||
{
|
||||
throw new InvalidArgumentException("Invalid template : {$template}");
|
||||
}
|
||||
|
||||
return $view->renderTemplate($templatePath, $data);
|
||||
}
|
||||
return $view->renderTemplate($templatePath, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a template with header and footer
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
protected function renderFullPage(HtmlView $view, string $template, array $data): HtmlView
|
||||
{
|
||||
$csp = [
|
||||
"default-src 'self' media.kitsu.io kitsu-production-media.s3.us-west-002.backblazeb2.com",
|
||||
"object-src 'none'",
|
||||
"child-src 'self' *.youtube.com polyfill.io",
|
||||
];
|
||||
/**
|
||||
* Render a template with header and footer
|
||||
*/
|
||||
#[\PHPUnit\Framework\Attributes\CodeCoverageIgnore]
|
||||
protected function renderFullPage(HtmlView $view, string $template, array $data): HtmlView
|
||||
{
|
||||
$csp = [
|
||||
"default-src 'self' media.kitsu.io kitsu-production-media.s3.us-west-002.backblazeb2.com",
|
||||
"object-src 'none'",
|
||||
"child-src 'self' *.youtube.com polyfill.io",
|
||||
];
|
||||
|
||||
$view->addHeader('Content-Security-Policy', implode('; ', $csp));
|
||||
$view->appendOutput($this->loadPartial($view, 'header', $data));
|
||||
$view->addHeader('Content-Security-Policy', implode('; ', $csp));
|
||||
$view->appendOutput($this->loadPartial($view, 'header', $data));
|
||||
|
||||
if (array_key_exists('message', $data) && is_array($data['message']))
|
||||
{
|
||||
$view->appendOutput($this->loadPartial($view, 'message', $data['message']));
|
||||
}
|
||||
if (array_key_exists('message', $data) && is_array($data['message']))
|
||||
{
|
||||
$view->appendOutput($this->loadPartial($view, 'message', $data['message']));
|
||||
}
|
||||
|
||||
$view->appendOutput($this->loadPartial($view, $template, $data));
|
||||
$view->appendOutput($this->loadPartial($view, 'footer', $data));
|
||||
$view->appendOutput($this->loadPartial($view, $template, $data));
|
||||
$view->appendOutput($this->loadPartial($view, 'footer', $data));
|
||||
|
||||
return $view;
|
||||
}
|
||||
return $view;
|
||||
}
|
||||
|
||||
/**
|
||||
* 404 action
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function notFound(
|
||||
string $title = 'Sorry, page not found',
|
||||
string $message = 'Page Not Found'
|
||||
): void {
|
||||
$this->outputHTML('404', [
|
||||
'title' => $title,
|
||||
'message' => $message,
|
||||
], NULL, 404);
|
||||
/**
|
||||
* 404 action
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
#[\PHPUnit\Framework\Attributes\CodeCoverageIgnore]
|
||||
public function notFound(
|
||||
string $title = 'Sorry, page not found',
|
||||
string $message = 'Page Not Found'
|
||||
): never {
|
||||
$this->outputHTML('404', [
|
||||
'title' => $title,
|
||||
'message' => $message,
|
||||
], NULL, 404);
|
||||
|
||||
exit();
|
||||
}
|
||||
exit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a generic error page
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function errorPage(int $httpCode, string $title, string $message, string $longMessage = ''): void
|
||||
{
|
||||
$this->outputHTML('error', [
|
||||
'title' => $title,
|
||||
'message' => $message,
|
||||
'long_message' => $longMessage,
|
||||
], NULL, $httpCode);
|
||||
}
|
||||
/**
|
||||
* Display a generic error page
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
#[\PHPUnit\Framework\Attributes\CodeCoverageIgnore]
|
||||
public function errorPage(int $httpCode, string $title, string $message, string $longMessage = ''): void
|
||||
{
|
||||
$this->outputHTML('error', [
|
||||
'title' => $title,
|
||||
'message' => $message,
|
||||
'long_message' => $longMessage,
|
||||
], NULL, $httpCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect to the default controller/url from an empty path
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function redirectToDefaultRoute(): void
|
||||
{
|
||||
$defaultType = $this->config->get('default_list');
|
||||
$this->redirect($this->urlGenerator->defaultUrl($defaultType), 303);
|
||||
}
|
||||
/**
|
||||
* Redirect to the default controller/url from an empty path
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
#[\PHPUnit\Framework\Attributes\CodeCoverageIgnore]
|
||||
public function redirectToDefaultRoute(): void
|
||||
{
|
||||
$defaultType = $this->config->get('default_list');
|
||||
$this->redirect($this->urlGenerator->defaultUrl($defaultType), 303);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a session flash variable to display a message on
|
||||
* next page load
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function setFlashMessage(string $message, string $type = 'info'): void
|
||||
{
|
||||
static $messages;
|
||||
/**
|
||||
* Set a session flash variable to display a message on
|
||||
* next page load
|
||||
*/
|
||||
#[\PHPUnit\Framework\Attributes\CodeCoverageIgnore]
|
||||
public function setFlashMessage(string $message, string $type = 'info'): void
|
||||
{
|
||||
static $messages;
|
||||
|
||||
if ( ! $messages)
|
||||
{
|
||||
$messages = [];
|
||||
}
|
||||
if ( ! $messages)
|
||||
{
|
||||
$messages = [];
|
||||
}
|
||||
|
||||
$messages[] = [
|
||||
'message_type' => $type,
|
||||
'message' => $message,
|
||||
];
|
||||
$messages[] = [
|
||||
'message_type' => $type,
|
||||
'message' => $message,
|
||||
];
|
||||
|
||||
$this->session->setFlash('message', $messages);
|
||||
}
|
||||
$this->session->setFlash('message', $messages);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for consistent page titles
|
||||
|
@ -322,63 +319,62 @@ class Controller
|
|||
return implode(' · ', $parts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a message box to the page
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
protected function showMessage(HtmlView $view, string $type, string $message): string
|
||||
{
|
||||
return $this->loadPartial($view, 'message', [
|
||||
'message_type' => $type,
|
||||
'message' => $message,
|
||||
]);
|
||||
}
|
||||
/**
|
||||
* Add a message box to the page
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
#[\PHPUnit\Framework\Attributes\CodeCoverageIgnore]
|
||||
protected function showMessage(HtmlView $view, string $type, string $message): string
|
||||
{
|
||||
return $this->loadPartial($view, 'message', [
|
||||
'message_type' => $type,
|
||||
'message' => $message,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Output a template to HTML, using the provided data
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
protected function outputHTML(string $template, array $data = [], ?HtmlView $view = NULL, int $code = 200): void
|
||||
{
|
||||
if (NULL === $view)
|
||||
{
|
||||
$view = new HtmlView($this->container);
|
||||
}
|
||||
/**
|
||||
* Output a template to HTML, using the provided data
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
#[\PHPUnit\Framework\Attributes\CodeCoverageIgnore]
|
||||
protected function outputHTML(string $template, array $data = [], ?HtmlView $view = NULL, int $code = 200): void
|
||||
{
|
||||
if (NULL === $view)
|
||||
{
|
||||
$view = new HtmlView($this->container);
|
||||
}
|
||||
|
||||
$view->setStatusCode($code);
|
||||
$this->renderFullPage($view, $template, $data)->send();
|
||||
}
|
||||
$view->setStatusCode($code);
|
||||
$this->renderFullPage($view, $template, $data)->send();
|
||||
}
|
||||
|
||||
/**
|
||||
* Output a JSON Response
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
* @param int $code - the http status code
|
||||
* @throws DoubleRenderException
|
||||
*/
|
||||
protected function outputJSON(mixed $data, int $code): void
|
||||
{
|
||||
JsonView::new()
|
||||
->setOutput($data)
|
||||
->setStatusCode($code)
|
||||
->send();
|
||||
}
|
||||
/**
|
||||
* Output a JSON Response
|
||||
*
|
||||
* @param int $code - the http status code
|
||||
* @throws DoubleRenderException
|
||||
*/
|
||||
#[\PHPUnit\Framework\Attributes\CodeCoverageIgnore]
|
||||
protected function outputJSON(mixed $data, int $code): void
|
||||
{
|
||||
JsonView::new()
|
||||
->setOutput($data)
|
||||
->setStatusCode($code)
|
||||
->send();
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect to the selected page
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
protected function redirect(string $url, int $code): void
|
||||
{
|
||||
HttpView::new()
|
||||
->redirect($url, $code)
|
||||
->send();
|
||||
}
|
||||
/**
|
||||
* Redirect to the selected page
|
||||
*/
|
||||
#[\PHPUnit\Framework\Attributes\CodeCoverageIgnore]
|
||||
protected function redirect(string $url, int $code): void
|
||||
{
|
||||
HttpView::new()
|
||||
->redirect($url, $code)
|
||||
->send();
|
||||
}
|
||||
}
|
||||
|
||||
// End of BaseController.php
|
||||
|
|
|
@ -21,8 +21,7 @@ use Aviat\AnimeClient\API\Mapping\AnimeWatchingStatus;
|
|||
use Aviat\AnimeClient\Controller as BaseController;
|
||||
use Aviat\AnimeClient\Model\Anime as AnimeModel;
|
||||
use Aviat\AnimeClient\Types\FormItem;
|
||||
use Aviat\Ion\Attribute\Controller;
|
||||
use Aviat\Ion\Attribute\Route;
|
||||
use Aviat\Ion\Attribute\{Controller, Route};
|
||||
use Aviat\Ion\Di\ContainerInterface;
|
||||
use Aviat\Ion\Di\Exception\{ContainerException, NotFoundException};
|
||||
use Aviat\Ion\Json;
|
||||
|
@ -307,8 +306,6 @@ final class Anime extends BaseController
|
|||
'Anime not found',
|
||||
'Anime Not Found'
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->outputHTML('anime/details', [
|
||||
|
@ -346,8 +343,6 @@ final class Anime extends BaseController
|
|||
'Anime not found',
|
||||
'Anime Not Found'
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->outputHTML('anime/details', [
|
||||
|
|
|
@ -20,11 +20,9 @@ use Aviat\AnimeClient\Model\{
|
|||
Anime as AnimeModel,
|
||||
AnimeCollection as AnimeCollectionModel
|
||||
};
|
||||
use Aviat\Ion\Attribute\Controller;
|
||||
use Aviat\Ion\Attribute\Route;
|
||||
use Aviat\Ion\Attribute\{Controller, Route};
|
||||
use Aviat\Ion\Di\ContainerInterface;
|
||||
use Aviat\Ion\Di\Exception\{ContainerException, NotFoundException};
|
||||
use Aviat\Ion\Json;
|
||||
use Aviat\Ion\Exception\DoubleRenderException;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
@ -114,7 +112,6 @@ final class AnimeCollection extends BaseController
|
|||
/**
|
||||
* Show the anime collection add/edit form
|
||||
*
|
||||
* @param int|null $id
|
||||
* @throws ContainerException
|
||||
* @throws InvalidArgumentException
|
||||
* @throws NotFoundException
|
||||
|
|
|
@ -18,8 +18,7 @@ use Aviat\AnimeClient\API\Kitsu\Model;
|
|||
use Aviat\AnimeClient\API\Kitsu\Transformer\CharacterTransformer;
|
||||
use Aviat\AnimeClient\Controller as BaseController;
|
||||
|
||||
use Aviat\Ion\Attribute\Controller;
|
||||
use Aviat\Ion\Attribute\Route;
|
||||
use Aviat\Ion\Attribute\{Controller, Route};
|
||||
use Aviat\Ion\Di\ContainerInterface;
|
||||
use Aviat\Ion\Di\Exception\{ContainerException, NotFoundException};
|
||||
|
||||
|
@ -60,8 +59,6 @@ final class Character extends BaseController
|
|||
),
|
||||
'Character Not Found'
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$data = (new CharacterTransformer())->transform($rawData)->toArray();
|
||||
|
|
|
@ -14,9 +14,8 @@
|
|||
|
||||
namespace Aviat\AnimeClient\Controller;
|
||||
|
||||
use Aviat\Ion\Attribute\Controller;
|
||||
use Aviat\Ion\Attribute\Route;
|
||||
use Aviat\AnimeClient\{Controller as BaseController, Model};
|
||||
use Aviat\Ion\Attribute\{Controller, Route};
|
||||
use Aviat\Ion\Di\ContainerInterface;
|
||||
use Aviat\Ion\Di\Exception\{ContainerException, NotFoundException};
|
||||
|
||||
|
|
|
@ -15,8 +15,7 @@
|
|||
namespace Aviat\AnimeClient\Controller;
|
||||
|
||||
use Aviat\AnimeClient\Controller as BaseController;
|
||||
use Aviat\Ion\Attribute\Controller;
|
||||
use Aviat\Ion\Attribute\Route;
|
||||
use Aviat\Ion\Attribute\{Controller, Route};
|
||||
use Throwable;
|
||||
use function Amp\Promise\wait;
|
||||
use function Aviat\AnimeClient\{createPlaceholderImage, getResponse};
|
||||
|
@ -97,7 +96,7 @@ final class Images extends BaseController
|
|||
|
||||
$kitsuUrl .= $imageType['kitsuUrl'];
|
||||
$width = $imageType['width'];
|
||||
$height = $imageType['height'];
|
||||
$height = $imageType['height'] ?? 225;
|
||||
$filePrefix = "{$baseSavePath}/{$type}/{$id}";
|
||||
|
||||
$response = getResponse($kitsuUrl);
|
||||
|
@ -121,11 +120,11 @@ final class Images extends BaseController
|
|||
|
||||
if ($display)
|
||||
{
|
||||
$this->getPlaceholder("{$baseSavePath}/{$type}", $width, $height);
|
||||
$this->getPlaceholder("{$baseSavePath}/{$type}", $width ?? 225, $height);
|
||||
}
|
||||
else
|
||||
{
|
||||
createPlaceholderImage("{$baseSavePath}/{$type}", $width, $height);
|
||||
createPlaceholderImage("{$baseSavePath}/{$type}", $width ?? 225, $height);
|
||||
}
|
||||
|
||||
return;
|
||||
|
@ -133,7 +132,13 @@ final class Images extends BaseController
|
|||
|
||||
$data = wait($response->getBody()->buffer());
|
||||
|
||||
[$origWidth] = getimagesizefromstring($data);
|
||||
$size = getimagesizefromstring($data);
|
||||
if ($size === FALSE)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
[$origWidth] = $size;
|
||||
$gdImg = imagecreatefromstring($data);
|
||||
if ($gdImg === FALSE)
|
||||
{
|
||||
|
@ -183,15 +188,15 @@ final class Images extends BaseController
|
|||
/**
|
||||
* Get a placeholder for a missing image
|
||||
*/
|
||||
private function getPlaceholder(string $path, ?int $width = 200, ?int $height = NULL): void
|
||||
private function getPlaceholder(string $path, ?int $width = NULL, ?int $height = NULL): void
|
||||
{
|
||||
$height ??= $width;
|
||||
$height ??= $width ?? 200;
|
||||
|
||||
$filename = $path . '/placeholder.png';
|
||||
|
||||
if ( ! file_exists($path . '/placeholder.png'))
|
||||
{
|
||||
createPlaceholderImage($path, $width, $height);
|
||||
createPlaceholderImage($path, $width ?? 200, $height);
|
||||
}
|
||||
|
||||
header('Content-Type: image/png');
|
||||
|
|
|
@ -14,21 +14,16 @@
|
|||
|
||||
namespace Aviat\AnimeClient\Controller;
|
||||
|
||||
use Aura\Router\Exception\RouteNotFound;
|
||||
use Aviat\AnimeClient\API\Kitsu\Transformer\MangaListTransformer;
|
||||
use Aviat\AnimeClient\API\Mapping\MangaReadingStatus;
|
||||
use Aviat\AnimeClient\Controller as BaseController;
|
||||
use Aviat\AnimeClient\Model\Manga as MangaModel;
|
||||
use Aviat\AnimeClient\Types\FormItem;
|
||||
use Aviat\Ion\Attribute\Controller;
|
||||
use Aviat\Ion\Attribute\Route;
|
||||
use Aviat\Ion\Attribute\{Controller, Route};
|
||||
use Aviat\Ion\Di\ContainerInterface;
|
||||
use Aviat\Ion\Di\Exception\{ContainerException, NotFoundException};
|
||||
use Aviat\Ion\Json;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Controller for manga list
|
||||
*/
|
||||
|
@ -282,8 +277,6 @@ final class Manga extends BaseController
|
|||
'Manga not found',
|
||||
'Manga Not Found'
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->outputHTML('manga/details', [
|
||||
|
@ -311,8 +304,6 @@ final class Manga extends BaseController
|
|||
'Manga not found',
|
||||
'Manga Not Found'
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->outputHTML('manga/details', [
|
||||
|
|
|
@ -15,12 +15,10 @@
|
|||
namespace Aviat\AnimeClient\Controller;
|
||||
|
||||
use Aviat\AnimeClient\API\Kitsu\Model;
|
||||
use Aviat\AnimeClient\API\Kitsu\Transformer\CharacterTransformer;
|
||||
use Aviat\AnimeClient\API\Kitsu\Transformer\PersonTransformer;
|
||||
use Aviat\AnimeClient\API\Kitsu\Transformer\{CharacterTransformer, PersonTransformer};
|
||||
use Aviat\AnimeClient\Controller as BaseController;
|
||||
use Aviat\AnimeClient\Enum\EventType;
|
||||
use Aviat\Ion\Attribute\DefaultController;
|
||||
use Aviat\Ion\Attribute\Route;
|
||||
use Aviat\Ion\Attribute\{DefaultController, Route};
|
||||
use Aviat\Ion\Di\ContainerInterface;
|
||||
use Aviat\Ion\Event;
|
||||
use Aviat\Ion\View\HtmlView;
|
||||
|
@ -103,11 +101,7 @@ final class Misc extends BaseController
|
|||
}
|
||||
|
||||
$this->setFlashMessage('Invalid username or password.');
|
||||
|
||||
$redirectUrl = $this->url->generate('login');
|
||||
$redirectUrl = ($redirectUrl !== FALSE) ? $redirectUrl : '';
|
||||
|
||||
$this->redirect($redirectUrl, 303);
|
||||
$this->redirect($this->url->generate('login'), 303);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -147,8 +141,6 @@ final class Misc extends BaseController
|
|||
),
|
||||
'Character Not Found'
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$data = (new CharacterTransformer())->transform($rawData)->toArray();
|
||||
|
@ -180,8 +172,6 @@ final class Misc extends BaseController
|
|||
),
|
||||
'Person Not Found'
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->outputHTML('person/details', [
|
||||
|
|
|
@ -18,8 +18,7 @@ use Aviat\AnimeClient\API\Kitsu\Model;
|
|||
use Aviat\AnimeClient\API\Kitsu\Transformer\PersonTransformer;
|
||||
use Aviat\AnimeClient\Controller as BaseController;
|
||||
|
||||
use Aviat\Ion\Attribute\Controller;
|
||||
use Aviat\Ion\Attribute\Route;
|
||||
use Aviat\Ion\Attribute\{Controller, Route};
|
||||
use Aviat\Ion\Di\ContainerInterface;
|
||||
use Aviat\Ion\Di\Exception\{ContainerException, NotFoundException};
|
||||
|
||||
|
@ -61,8 +60,6 @@ final class People extends BaseController
|
|||
),
|
||||
'Person Not Found'
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->outputHTML('person/details', [
|
||||
|
|
|
@ -18,8 +18,7 @@ use Aura\Router\Exception\RouteNotFound;
|
|||
use Aviat\AnimeClient\API\Anilist\Model as AnilistModel;
|
||||
use Aviat\AnimeClient\Controller as BaseController;
|
||||
use Aviat\AnimeClient\Model\Settings as SettingsModel;
|
||||
use Aviat\Ion\Attribute\Controller;
|
||||
use Aviat\Ion\Attribute\Route;
|
||||
use Aviat\Ion\Attribute\{Controller, Route};
|
||||
use Aviat\Ion\Di\ContainerInterface;
|
||||
use Aviat\Ion\Di\Exception\{ContainerException, NotFoundException};
|
||||
|
||||
|
@ -87,10 +86,7 @@ final class Settings extends BaseController
|
|||
? $this->setFlashMessage('Saved config settings.', 'success')
|
||||
: $this->setFlashMessage('Failed to save config file.', 'error');
|
||||
|
||||
$redirectUrl = $this->url->generate('settings');
|
||||
$redirectUrl = ($redirectUrl !== FALSE) ? $redirectUrl : '';
|
||||
|
||||
$this->redirect($redirectUrl, 303);
|
||||
$this->redirect($this->url->generate('settings'), 303);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -153,9 +149,6 @@ final class Settings extends BaseController
|
|||
? $this->setFlashMessage('Linked Anilist Account', 'success')
|
||||
: $this->setFlashMessage('Error Linking Anilist Account', 'error');
|
||||
|
||||
$redirectUrl = $this->url->generate('settings');
|
||||
$redirectUrl = ($redirectUrl !== FALSE) ? $redirectUrl : '';
|
||||
|
||||
$this->redirect($redirectUrl, 303);
|
||||
$this->redirect($this->url->generate('settings'), 303);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,8 +18,7 @@ use Aviat\AnimeClient\API\Kitsu\Model;
|
|||
use Aviat\AnimeClient\API\Kitsu\Transformer\UserTransformer;
|
||||
use Aviat\AnimeClient\Controller as BaseController;
|
||||
|
||||
use Aviat\Ion\Attribute\Controller;
|
||||
use Aviat\Ion\Attribute\Route;
|
||||
use Aviat\Ion\Attribute\{Controller, Route};
|
||||
use Aviat\Ion\Di\ContainerInterface;
|
||||
use Aviat\Ion\Di\Exception\{ContainerException, NotFoundException};
|
||||
|
||||
|
@ -70,6 +69,11 @@ final class User extends BaseController
|
|||
: $username;
|
||||
|
||||
$rawData = $this->kitsuModel->getUserData($username);
|
||||
if ($rawData['data']['findProfileBySlug'] === NULL)
|
||||
{
|
||||
$this->notFound('Sorry, user not found', "The user '{$username}' does not seem to exist.");
|
||||
}
|
||||
|
||||
$data = (new UserTransformer())->transform($rawData)->toArray();
|
||||
|
||||
$this->outputHTML('user/details', [
|
||||
|
|
|
@ -69,6 +69,44 @@ final class Dispatcher extends RoutingBase
|
|||
$this->outputRoutes = $this->setupRoutes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the current route
|
||||
*
|
||||
* @throws ReflectionException
|
||||
*/
|
||||
public function __invoke(?object $route = NULL): void
|
||||
{
|
||||
$logger = $this->container->getLogger();
|
||||
|
||||
if ($route === NULL)
|
||||
{
|
||||
$route = $this->getRoute();
|
||||
|
||||
$logger?->info('Dispatcher - Route invoke arguments');
|
||||
$logger?->info(print_r($route, TRUE));
|
||||
}
|
||||
|
||||
if ( ! $route)
|
||||
{
|
||||
// If not route was matched, return an appropriate http
|
||||
// error message
|
||||
$errorRoute = $this->getErrorParams();
|
||||
$controllerName = DEFAULT_CONTROLLER;
|
||||
$actionMethod = $errorRoute['action_method'];
|
||||
$params = $errorRoute['params'];
|
||||
$this->call($controllerName, $actionMethod, $params);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$parsed = $this->processRoute(new Friend($route));
|
||||
$controllerName = $parsed['controller_name'];
|
||||
$actionMethod = $parsed['action_method'];
|
||||
$params = $parsed['params'];
|
||||
|
||||
$this->call($controllerName, $actionMethod, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current route object, if one matches
|
||||
*/
|
||||
|
@ -100,47 +138,6 @@ final class Dispatcher extends RoutingBase
|
|||
return $this->outputRoutes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the current route
|
||||
*
|
||||
* @throws ReflectionException
|
||||
*/
|
||||
public function __invoke(?object $route = NULL): void
|
||||
{
|
||||
$logger = $this->container->getLogger();
|
||||
|
||||
if ($route === NULL)
|
||||
{
|
||||
$route = $this->getRoute();
|
||||
|
||||
if ($logger !== NULL)
|
||||
{
|
||||
$logger->info('Dispatcher - Route invoke arguments');
|
||||
$logger->info(print_r($route, TRUE));
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! $route)
|
||||
{
|
||||
// If not route was matched, return an appropriate http
|
||||
// error message
|
||||
$errorRoute = $this->getErrorParams();
|
||||
$controllerName = DEFAULT_CONTROLLER;
|
||||
$actionMethod = $errorRoute['action_method'];
|
||||
$params = $errorRoute['params'];
|
||||
$this->call($controllerName, $actionMethod, $params);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$parsed = $this->processRoute(new Friend($route));
|
||||
$controllerName = $parsed['controller_name'];
|
||||
$actionMethod = $parsed['action_method'];
|
||||
$params = $parsed['params'];
|
||||
|
||||
$this->call($controllerName, $actionMethod, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse out the arguments for the appropriate controller for
|
||||
* the current route
|
||||
|
@ -183,10 +180,7 @@ final class Dispatcher extends RoutingBase
|
|||
}
|
||||
|
||||
$logger = $this->container->getLogger();
|
||||
if ($logger !== NULL)
|
||||
{
|
||||
$logger->info(Json::encode($params));
|
||||
}
|
||||
$logger?->info(Json::encode($params));
|
||||
|
||||
return [
|
||||
'controller_name' => $controllerName,
|
||||
|
@ -208,10 +202,7 @@ final class Dispatcher extends RoutingBase
|
|||
$controller = reset($segments);
|
||||
|
||||
$logger = $this->container->getLogger();
|
||||
if ($logger !== NULL)
|
||||
{
|
||||
$logger->info('Controller: ' . $controller);
|
||||
}
|
||||
$logger?->info('Controller: ' . $controller);
|
||||
|
||||
if (empty($controller))
|
||||
{
|
||||
|
@ -224,7 +215,7 @@ final class Dispatcher extends RoutingBase
|
|||
/**
|
||||
* Get the list of controllers in the default namespace
|
||||
*
|
||||
* @return mixed[]
|
||||
* @return array
|
||||
*/
|
||||
public function getControllerList(): array
|
||||
{
|
||||
|
@ -300,7 +291,6 @@ final class Dispatcher extends RoutingBase
|
|||
/**
|
||||
* Get the appropriate params for the error page
|
||||
* passed on the failed route
|
||||
* @return mixed[][]
|
||||
*/
|
||||
protected function getErrorParams(): array
|
||||
{
|
||||
|
@ -317,14 +307,15 @@ final class Dispatcher extends RoutingBase
|
|||
|
||||
$params = [];
|
||||
|
||||
switch ($failure->failedRule) {
|
||||
switch ($failure?->failedRule)
|
||||
{
|
||||
case Rule\Allows::class:
|
||||
$params = [
|
||||
'http_code' => 405,
|
||||
'title' => '405 Method Not Allowed',
|
||||
'message' => 'Invalid HTTP Verb',
|
||||
];
|
||||
break;
|
||||
break;
|
||||
|
||||
case Rule\Accepts::class:
|
||||
$params = [
|
||||
|
@ -332,12 +323,12 @@ final class Dispatcher extends RoutingBase
|
|||
'title' => '406 Not Acceptable',
|
||||
'message' => 'Unacceptable content type',
|
||||
];
|
||||
break;
|
||||
break;
|
||||
|
||||
default:
|
||||
// Fall back to a 404 message
|
||||
$actionMethod = NOT_FOUND_METHOD;
|
||||
break;
|
||||
break;
|
||||
}
|
||||
|
||||
return [
|
||||
|
@ -348,8 +339,6 @@ final class Dispatcher extends RoutingBase
|
|||
|
||||
/**
|
||||
* Select controller based on the current url, and apply its relevant routes
|
||||
*
|
||||
* @return mixed[]
|
||||
*/
|
||||
protected function setupRoutes(): array
|
||||
{
|
||||
|
|
|
@ -83,16 +83,16 @@ final class FormGenerator
|
|||
];
|
||||
$params['strict'] = TRUE;
|
||||
unset($params['attribs']['id']);
|
||||
break;
|
||||
break;
|
||||
|
||||
case 'string':
|
||||
$params['type'] = 'text';
|
||||
break;
|
||||
break;
|
||||
|
||||
case 'select':
|
||||
$params['type'] = 'select';
|
||||
$params['options'] = array_flip($form['options']);
|
||||
break;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
|
|
|
@ -31,10 +31,6 @@ final class Kitsu
|
|||
public const ANIME_HISTORY_LIST_CACHE_KEY = 'kitsu-anime-history-list';
|
||||
public const MANGA_HISTORY_LIST_CACHE_KEY = 'kitsu-manga-history-list';
|
||||
public const GRAPHQL_ENDPOINT = 'https://kitsu.io/api/graphql';
|
||||
public const SECONDS_IN_MINUTE = 60;
|
||||
public const MINUTES_IN_HOUR = 60;
|
||||
public const MINUTES_IN_DAY = 1440;
|
||||
public const MINUTES_IN_YEAR = 525_600;
|
||||
|
||||
/**
|
||||
* Determine whether an anime is airing, finished airing, or has not yet aired
|
||||
|
@ -72,18 +68,18 @@ final class Kitsu
|
|||
}
|
||||
|
||||
$monthMap = [
|
||||
'01' => 'Jan',
|
||||
'02' => 'Feb',
|
||||
'03' => 'Mar',
|
||||
'04' => 'Apr',
|
||||
'01' => 'January',
|
||||
'02' => 'February',
|
||||
'03' => 'March',
|
||||
'04' => 'April',
|
||||
'05' => 'May',
|
||||
'06' => 'Jun',
|
||||
'07' => 'Jul',
|
||||
'08' => 'Aug',
|
||||
'09' => 'Sep',
|
||||
'10' => 'Oct',
|
||||
'11' => 'Nov',
|
||||
'12' => 'Dec',
|
||||
'06' => 'June',
|
||||
'07' => 'July',
|
||||
'08' => 'August',
|
||||
'09' => 'September',
|
||||
'10' => 'October',
|
||||
'11' => 'November',
|
||||
'12' => 'December',
|
||||
];
|
||||
|
||||
[$startYear, $startMonth, $startDay] = explode('-', $startDate);
|
||||
|
@ -305,7 +301,16 @@ final class Kitsu
|
|||
{
|
||||
// Really don't care about languages that aren't english
|
||||
// or Japanese for titles
|
||||
if ( ! in_array($locale, ['en', 'en_us', 'en_jp', 'ja_jp'], TRUE))
|
||||
if ( ! in_array($locale, [
|
||||
'en',
|
||||
'en-jp',
|
||||
'en-us',
|
||||
'en_jp',
|
||||
'en_us',
|
||||
'ja-jp',
|
||||
'ja_jp',
|
||||
'jp',
|
||||
], TRUE))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
@ -326,15 +331,29 @@ final class Kitsu
|
|||
/**
|
||||
* Get the url of the posterImage from Kitsu, with fallbacks
|
||||
*/
|
||||
public static function getPosterImage(array $base, int $size = 1): string
|
||||
public static function getPosterImage(array $base, int $sizeId = 1): string
|
||||
{
|
||||
$rawUrl = $base['posterImage']['views'][$size]['url']
|
||||
$rawUrl = $base['posterImage']['views'][$sizeId]['url']
|
||||
?? $base['posterImage']['original']['url']
|
||||
?? '/public/images/placeholder.png';
|
||||
|
||||
$parts = explode('?', $rawUrl);
|
||||
|
||||
return (empty($parts)) ? $rawUrl : $parts[0];
|
||||
return $parts[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the url of the image from Kitsu, with fallbacks
|
||||
*/
|
||||
public static function getImage(array $base, int $sizeId = 1): string
|
||||
{
|
||||
$rawUrl = $base['image']['original']['url']
|
||||
?? $base['image']['views'][$sizeId]['url']
|
||||
?? '/public/images/placeholder.png';
|
||||
|
||||
$parts = explode('?', $rawUrl);
|
||||
|
||||
return $parts[0];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -418,62 +437,6 @@ final class Kitsu
|
|||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a time in seconds to a more human-readable format
|
||||
*/
|
||||
public static function friendlyTime(int $seconds): string
|
||||
{
|
||||
// All the seconds left
|
||||
$remSeconds = $seconds % self::SECONDS_IN_MINUTE;
|
||||
$minutes = ($seconds - $remSeconds) / self::SECONDS_IN_MINUTE;
|
||||
|
||||
// Minutes short of a year
|
||||
$years = (int) floor($minutes / self::MINUTES_IN_YEAR);
|
||||
$minutes %= self::MINUTES_IN_YEAR;
|
||||
|
||||
// Minutes short of a day
|
||||
$extraMinutes = $minutes % self::MINUTES_IN_DAY;
|
||||
$days = ($minutes - $extraMinutes) / self::MINUTES_IN_DAY;
|
||||
|
||||
// Minutes short of an hour
|
||||
$remMinutes = $extraMinutes % self::MINUTES_IN_HOUR;
|
||||
$hours = ($extraMinutes - $remMinutes) / self::MINUTES_IN_HOUR;
|
||||
|
||||
$parts = [];
|
||||
|
||||
foreach ([
|
||||
'year' => $years,
|
||||
'day' => $days,
|
||||
'hour' => $hours,
|
||||
'minute' => $remMinutes,
|
||||
'second' => $remSeconds,
|
||||
] as $label => $value)
|
||||
{
|
||||
if ($value === 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($value > 1)
|
||||
{
|
||||
$label .= 's';
|
||||
}
|
||||
|
||||
$parts[] = "{$value} {$label}";
|
||||
}
|
||||
|
||||
$last = array_pop($parts);
|
||||
|
||||
if (empty($parts))
|
||||
{
|
||||
return $last ?? '';
|
||||
}
|
||||
|
||||
return (count($parts) > 1)
|
||||
? implode(', ', $parts) . ", and {$last}"
|
||||
: "{$parts[0]}, {$last}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if an alternate title is unique enough to list
|
||||
*/
|
||||
|
@ -486,7 +449,7 @@ final class Kitsu
|
|||
|
||||
foreach ($existingTitles as $existing)
|
||||
{
|
||||
$isSubset = mb_substr_count($existing, $title) > 0;
|
||||
$isSubset = mb_substr_count(mb_strtolower($existing), mb_strtolower($title)) > 0;
|
||||
$diff = levenshtein(mb_strtolower($existing), mb_strtolower($title));
|
||||
|
||||
if ($diff <= 4 || $isSubset || mb_strlen($title) > 45 || mb_strlen($existing) > 50)
|
||||
|
|
|
@ -36,6 +36,19 @@ final class MenuGenerator extends UrlGenerator
|
|||
*/
|
||||
protected ServerRequestInterface $request;
|
||||
|
||||
/**
|
||||
* MenuGenerator constructor.
|
||||
*
|
||||
* @throws ContainerException
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
private function __construct(ContainerInterface $container)
|
||||
{
|
||||
parent::__construct($container);
|
||||
$this->helper = $container->get('html-helper');
|
||||
$this->request = $container->get('request');
|
||||
}
|
||||
|
||||
public static function new(ContainerInterface $container): self
|
||||
{
|
||||
return new self($container);
|
||||
|
@ -80,19 +93,6 @@ final class MenuGenerator extends UrlGenerator
|
|||
return (string) $this->helper->ul();
|
||||
}
|
||||
|
||||
/**
|
||||
* MenuGenerator constructor.
|
||||
*
|
||||
* @throws ContainerException
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
private function __construct(ContainerInterface $container)
|
||||
{
|
||||
parent::__construct($container);
|
||||
$this->helper = $container->get('html-helper');
|
||||
$this->request = $container->get('request');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the full menu structure from the config files
|
||||
*
|
||||
|
|
|
@ -91,7 +91,7 @@ final class AnimeCollection extends Collection
|
|||
$genres = $this->getGenreList();
|
||||
$media = $this->getMediaList();
|
||||
|
||||
if ($rows === FALSE)
|
||||
if (empty($rows))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
@ -133,7 +133,7 @@ final class AnimeCollection extends Collection
|
|||
->get();
|
||||
|
||||
$rows = $query->fetchAll(PDO::FETCH_ASSOC);
|
||||
if ($rows === FALSE)
|
||||
if (empty($rows))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
@ -349,7 +349,7 @@ final class AnimeCollection extends Collection
|
|||
->get()
|
||||
->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($mediaRows === FALSE)
|
||||
if (empty($mediaRows))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
@ -411,7 +411,7 @@ final class AnimeCollection extends Collection
|
|||
->get();
|
||||
|
||||
$rows = $query->fetchAll(PDO::FETCH_ASSOC);
|
||||
if ($rows === FALSE)
|
||||
if (empty($rows))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
@ -479,7 +479,7 @@ final class AnimeCollection extends Collection
|
|||
->get();
|
||||
|
||||
$rows = $query->fetchAll(PDO::FETCH_ASSOC);
|
||||
if ($rows === FALSE)
|
||||
if (empty($rows))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
@ -659,7 +659,7 @@ final class AnimeCollection extends Collection
|
|||
->get();
|
||||
|
||||
$rows = $query->fetchAll(PDO::FETCH_ASSOC);
|
||||
if ($rows === FALSE)
|
||||
if (empty($rows))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
@ -691,7 +691,7 @@ final class AnimeCollection extends Collection
|
|||
->get();
|
||||
|
||||
$rows = $query->fetchAll(PDO::FETCH_ASSOC);
|
||||
if ($rows === FALSE)
|
||||
if (empty($rows))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
@ -737,7 +737,7 @@ final class AnimeCollection extends Collection
|
|||
|
||||
// Add genres associated with each item
|
||||
$rows = $query->fetchAll(PDO::FETCH_ASSOC);
|
||||
if ($rows === FALSE)
|
||||
if (empty($rows))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
|
|
@ -74,7 +74,7 @@ trait MediaTrait
|
|||
* Get information about a specific list item
|
||||
* for editing/updating that item
|
||||
*/
|
||||
public function getItem(string $itemId): AnimeListItem|MangaListItem
|
||||
public function getItem(string $itemId): AnimeListItem|MangaListItem|array
|
||||
{
|
||||
return $this->kitsuModel->getListItem($itemId);
|
||||
}
|
||||
|
|
|
@ -95,14 +95,7 @@ final class Settings
|
|||
}
|
||||
}
|
||||
|
||||
if (array_key_exists($key, $values) && is_scalar($values[$key]))
|
||||
{
|
||||
$value['value'] = $values[$key];
|
||||
}
|
||||
else
|
||||
{
|
||||
$value['value'] = $value['default'] ?? '';
|
||||
}
|
||||
$value['value'] = array_key_exists($key, $values) && is_scalar($values[$key]) ? $values[$key] : $value['default'] ?? '';
|
||||
|
||||
foreach (['readonly', 'disabled'] as $flag)
|
||||
{
|
||||
|
|
|
@ -20,37 +20,6 @@ use Stringable;
|
|||
|
||||
abstract class AbstractType implements ArrayAccess, Countable, Stringable
|
||||
{
|
||||
/**
|
||||
* Populate values for un-serializing data
|
||||
*/
|
||||
public static function __set_state(mixed $properties): self
|
||||
{
|
||||
return new static($properties);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the shape of the object, and return the array equivalent
|
||||
*/
|
||||
final public static function check(array $data = []): ?array
|
||||
{
|
||||
$currentClass = static::class;
|
||||
|
||||
if (get_parent_class($currentClass) !== FALSE)
|
||||
{
|
||||
return static::class::from($data)->toArray();
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Static constructor
|
||||
*/
|
||||
final public static function from(mixed $data): static
|
||||
{
|
||||
return new static($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the properties by using the constructor
|
||||
*/
|
||||
|
@ -73,6 +42,14 @@ abstract class AbstractType implements ArrayAccess, Countable, Stringable
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate values for un-serializing data
|
||||
*/
|
||||
public static function __set_state(mixed $properties): self
|
||||
{
|
||||
return new static($properties);
|
||||
}
|
||||
|
||||
/**
|
||||
* See if a property is set
|
||||
*/
|
||||
|
@ -123,6 +100,29 @@ abstract class AbstractType implements ArrayAccess, Countable, Stringable
|
|||
return print_r($this, TRUE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the shape of the object, and return the array equivalent
|
||||
*/
|
||||
final public static function check(array $data = []): ?array
|
||||
{
|
||||
$currentClass = static::class;
|
||||
|
||||
if (get_parent_class($currentClass) !== FALSE)
|
||||
{
|
||||
return static::class::from($data)->toArray();
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Static constructor
|
||||
*/
|
||||
final public static function from(mixed $data): static
|
||||
{
|
||||
return new static($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementing ArrayAccess
|
||||
*/
|
||||
|
@ -201,27 +201,25 @@ abstract class AbstractType implements ArrayAccess, Countable, Stringable
|
|||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
final protected function fromObject(mixed $parent = NULL): float|NULL|bool|int|array|string
|
||||
{
|
||||
$object = $parent ?? $this;
|
||||
#[\PHPUnit\Framework\Attributes\CodeCoverageIgnore]
|
||||
final protected function fromObject(mixed $parent = NULL): float|null|bool|int|array|string
|
||||
{
|
||||
$object = $parent ?? $this;
|
||||
|
||||
if (is_scalar($object) || $object === NULL)
|
||||
{
|
||||
return $object;
|
||||
}
|
||||
if (is_scalar($object) || $object === NULL)
|
||||
{
|
||||
return $object;
|
||||
}
|
||||
|
||||
$output = [];
|
||||
$output = [];
|
||||
|
||||
foreach ($object as $key => $value)
|
||||
{
|
||||
$output[$key] = (is_scalar($value) || empty($value))
|
||||
? $value
|
||||
: $this->fromObject((array) $value);
|
||||
}
|
||||
foreach ($object as $key => $value)
|
||||
{
|
||||
$output[$key] = (is_scalar($value) || empty($value))
|
||||
? $value
|
||||
: $this->fromObject((array) $value);
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
return $output;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ final class Person extends AbstractType
|
|||
{
|
||||
public string $id;
|
||||
public ?string $name;
|
||||
public ?string $birthday;
|
||||
public string $image;
|
||||
public array $names = [];
|
||||
public ?string $description;
|
||||
|
|
|
@ -21,11 +21,14 @@ final class User extends AbstractType
|
|||
{
|
||||
public ?string $about;
|
||||
public ?string $avatar;
|
||||
public ?string $birthday;
|
||||
public string $joinDate;
|
||||
public ?string $gender;
|
||||
public ?array $favorites;
|
||||
public ?string $location;
|
||||
public ?string $name;
|
||||
public ?string $slug;
|
||||
public ?array $stats;
|
||||
public ?array $waifu;
|
||||
public array $waifu;
|
||||
public ?string $website;
|
||||
}
|
||||
|
|
|
@ -17,6 +17,9 @@ namespace Aviat\Ion\Attribute;
|
|||
use Attribute;
|
||||
|
||||
#[Attribute(Attribute::TARGET_CLASS)]
|
||||
class Controller {
|
||||
public function __construct(public string $prefix = '') {}
|
||||
}
|
||||
class Controller
|
||||
{
|
||||
public function __construct(public string $prefix = '')
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,4 +17,6 @@ namespace Aviat\Ion\Attribute;
|
|||
use Attribute;
|
||||
|
||||
#[Attribute(Attribute::TARGET_CLASS)]
|
||||
class DefaultController {}
|
||||
class DefaultController
|
||||
{
|
||||
}
|
||||
|
|
|
@ -17,16 +17,15 @@ namespace Aviat\Ion\Attribute;
|
|||
use Attribute;
|
||||
|
||||
#[Attribute(Attribute::TARGET_FUNCTION | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
|
||||
class Route {
|
||||
public const GET = 'get';
|
||||
public const POST = 'post';
|
||||
class Route
|
||||
{
|
||||
final public const GET = 'get';
|
||||
final public const POST = 'post';
|
||||
|
||||
public function __construct(
|
||||
public string $name,
|
||||
public string $path,
|
||||
public string $verb = self::GET,
|
||||
)
|
||||
{
|
||||
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,8 +39,8 @@ class Container implements ContainerInterface
|
|||
*/
|
||||
public function __construct(
|
||||
/**
|
||||
* Array of container Generator functions
|
||||
*/
|
||||
* Array of container Generator functions
|
||||
*/
|
||||
protected array $container = []
|
||||
) {
|
||||
$this->loggers = [];
|
||||
|
|
|
@ -25,7 +25,10 @@ class Json
|
|||
/**
|
||||
* Encode data in json format
|
||||
*
|
||||
* @throws JsonException
|
||||
* @param mixed $data
|
||||
* @param int $options
|
||||
* @param int<1, max> $depth
|
||||
* @return string
|
||||
*/
|
||||
public static function encode(mixed $data, int $options = 0, int $depth = 512): string
|
||||
{
|
||||
|
@ -54,7 +57,11 @@ class Json
|
|||
/**
|
||||
* Decode data from json
|
||||
*
|
||||
* @throws JsonException
|
||||
* @param string|null $json
|
||||
* @param bool $assoc
|
||||
* @param int<1, max> $depth
|
||||
* @param int $options
|
||||
* @return mixed
|
||||
*/
|
||||
public static function decode(?string $json, bool $assoc = TRUE, int $depth = 512, int $options = 0): mixed
|
||||
{
|
||||
|
@ -74,7 +81,11 @@ class Json
|
|||
/**
|
||||
* Decode json data loaded from the passed filename
|
||||
*
|
||||
* @throws JsonException
|
||||
* @param string $filename
|
||||
* @param bool $assoc
|
||||
* @param int<1, max> $depth
|
||||
* @param int $options
|
||||
* @return mixed
|
||||
*/
|
||||
public static function decodeFile(string $filename, bool $assoc = TRUE, int $depth = 512, int $options = 0): mixed
|
||||
{
|
||||
|
|
|
@ -33,7 +33,7 @@ abstract class AbstractTransformer implements TransformerInterface
|
|||
{
|
||||
$list = (array) $collection;
|
||||
|
||||
return array_map([$this, 'transform'], $list);
|
||||
return array_map($this->transform(...), $list);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -65,14 +65,6 @@ class ArrayType
|
|||
'pop' => 'array_pop',
|
||||
];
|
||||
|
||||
/**
|
||||
* Create an ArrayType wrapper class from an array
|
||||
*/
|
||||
public static function from(array $arr): ArrayType
|
||||
{
|
||||
return new ArrayType($arr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an ArrayType wrapper class
|
||||
*/
|
||||
|
@ -108,6 +100,14 @@ class ArrayType
|
|||
throw new InvalidArgumentException("Method '{$method}' does not exist");
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an ArrayType wrapper class from an array
|
||||
*/
|
||||
public static function from(array $arr): ArrayType
|
||||
{
|
||||
return new ArrayType($arr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the passed key exist in the current array?
|
||||
*/
|
||||
|
@ -156,7 +156,7 @@ class ArrayType
|
|||
/**
|
||||
* Find an array key by its associated value
|
||||
*/
|
||||
public function search(mixed $value, bool $strict = TRUE): int|string|FALSE|null
|
||||
public function search(mixed $value, bool $strict = TRUE): int|string|false|null
|
||||
{
|
||||
return array_search($value, $this->arr, $strict);
|
||||
}
|
||||
|
@ -172,7 +172,7 @@ class ArrayType
|
|||
/**
|
||||
* Return the array, or a key
|
||||
*/
|
||||
public function &get(string|int|NULL $key = NULL): mixed
|
||||
public function &get(string|int|null $key = NULL): mixed
|
||||
{
|
||||
$value = NULL;
|
||||
if ($key === NULL)
|
||||
|
|
|
@ -24,9 +24,9 @@ final class StringType extends Stringy
|
|||
/**
|
||||
* Alias for `create` static constructor
|
||||
*/
|
||||
public static function from(string $str): self
|
||||
public static function from(string $str = '', ?string $encoding = NULL): self
|
||||
{
|
||||
return self::create($str);
|
||||
return self::create($str, $encoding);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -34,11 +34,11 @@ class HtmlView extends HttpView
|
|||
/**
|
||||
* Create the Html View
|
||||
*/
|
||||
public function __construct(ContainerInterface $container)
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$this->setContainer($container);
|
||||
$this->setContainer(func_get_arg(0));
|
||||
$this->response = new HtmlResponse('');
|
||||
}
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ use InvalidArgumentException;
|
|||
|
||||
use Laminas\Diactoros\Response;
|
||||
use Laminas\HttpHandlerRunner\Emitter\SapiEmitter;
|
||||
use PHPUnit\Framework\Attributes\CodeCoverageIgnore;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Stringable;
|
||||
|
||||
|
@ -168,10 +169,10 @@ class HttpView implements HttpViewInterface, Stringable
|
|||
/**
|
||||
* Send the appropriate response
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
* @throws DoubleRenderException
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
#[CodeCoverageIgnore]
|
||||
protected function output(): void
|
||||
{
|
||||
if ($this->hasRendered)
|
||||
|
|
|
@ -40,8 +40,10 @@ final class APIRequestBuilderTest extends TestCase
|
|||
$this->builder->setLogger(new NullLogger());
|
||||
}
|
||||
|
||||
public function testGzipRequest(): void
|
||||
public function testGzipRequest(): never
|
||||
{
|
||||
$this->markTestSkipped('Need new test API');
|
||||
|
||||
$request = $this->builder->newRequest('GET', 'gzip')
|
||||
->getFullRequest();
|
||||
$response = getResponse($request);
|
||||
|
@ -49,15 +51,19 @@ final class APIRequestBuilderTest extends TestCase
|
|||
$this->assertTrue($body['gzipped']);
|
||||
}
|
||||
|
||||
public function testInvalidRequestMethod(): void
|
||||
public function testInvalidRequestMethod(): never
|
||||
{
|
||||
$this->markTestSkipped('Need new test API');
|
||||
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
$this->builder->newRequest('FOO', 'gzip')
|
||||
->getFullRequest();
|
||||
}
|
||||
|
||||
public function testRequestWithBasicAuth(): void
|
||||
public function testRequestWithBasicAuth(): never
|
||||
{
|
||||
$this->markTestSkipped('Need new test API');
|
||||
|
||||
$request = $this->builder->newRequest('GET', 'headers')
|
||||
->setBasicAuth('username', 'password')
|
||||
->getFullRequest();
|
||||
|
@ -68,8 +74,10 @@ final class APIRequestBuilderTest extends TestCase
|
|||
$this->assertSame('Basic dXNlcm5hbWU6cGFzc3dvcmQ=', $body['headers']['Authorization']);
|
||||
}
|
||||
|
||||
public function testRequestWithQueryString(): void
|
||||
public function testRequestWithQueryString(): never
|
||||
{
|
||||
$this->markTestSkipped('Need new test API');
|
||||
|
||||
$query = [
|
||||
'foo' => 'bar',
|
||||
'bar' => [
|
||||
|
@ -96,8 +104,10 @@ final class APIRequestBuilderTest extends TestCase
|
|||
$this->assertSame($expected, $body['args']);
|
||||
}
|
||||
|
||||
public function testFormValueRequest(): void
|
||||
public function testFormValueRequest(): never
|
||||
{
|
||||
$this->markTestSkipped('Need new test API');
|
||||
|
||||
$formValues = [
|
||||
'bar' => 'foo',
|
||||
'foo' => 'bar',
|
||||
|
@ -113,8 +123,10 @@ final class APIRequestBuilderTest extends TestCase
|
|||
$this->assertSame($formValues, $body['form']);
|
||||
}
|
||||
|
||||
public function testFullUrlRequest(): void
|
||||
public function testFullUrlRequest(): never
|
||||
{
|
||||
$this->markTestSkipped('Need new test API');
|
||||
|
||||
$data = [
|
||||
'foo' => [
|
||||
'bar' => 1,
|
||||
|
|
|
@ -38,7 +38,7 @@ final class AnimeListTransformerTest extends AnimeClientTestCase
|
|||
$this->transformer = new AnimeListTransformer();
|
||||
}
|
||||
|
||||
public function testTransform(): void
|
||||
public function testTransform(): never
|
||||
{
|
||||
$this->markTestSkipped('Old test data');
|
||||
|
||||
|
@ -46,11 +46,11 @@ final class AnimeListTransformerTest extends AnimeClientTestCase
|
|||
$this->assertMatchesSnapshot($actual);
|
||||
}
|
||||
|
||||
public function dataUntransform(): array
|
||||
public static function dataUntransform(): array
|
||||
{
|
||||
return [[
|
||||
'input' => [
|
||||
'id' => 14047981,
|
||||
'id' => 14_047_981,
|
||||
'watching_status' => 'current',
|
||||
'user_rating' => 8,
|
||||
'episodes_watched' => 38,
|
||||
|
@ -60,7 +60,7 @@ final class AnimeListTransformerTest extends AnimeClientTestCase
|
|||
],
|
||||
], [
|
||||
'input' => [
|
||||
'id' => 14047981,
|
||||
'id' => 14_047_981,
|
||||
'mal_id' => '12345',
|
||||
'watching_status' => 'current',
|
||||
'user_rating' => 8,
|
||||
|
@ -73,7 +73,7 @@ final class AnimeListTransformerTest extends AnimeClientTestCase
|
|||
],
|
||||
], [
|
||||
'input' => [
|
||||
'id' => 14047983,
|
||||
'id' => 14_047_983,
|
||||
'mal_id' => '12347',
|
||||
'watching_status' => 'current',
|
||||
'user_rating' => 0,
|
||||
|
@ -87,12 +87,10 @@ final class AnimeListTransformerTest extends AnimeClientTestCase
|
|||
]];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider dataUntransform
|
||||
*/
|
||||
public function testUntransform(array $input): void
|
||||
{
|
||||
$actual = $this->transformer->untransform($input);
|
||||
$this->assertMatchesSnapshot($actual);
|
||||
}
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('dataUntransform')]
|
||||
public function testUntransform(array $input): void
|
||||
{
|
||||
$actual = $this->transformer->untransform($input);
|
||||
$this->assertMatchesSnapshot($actual);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,7 +37,7 @@ final class AnimeTransformerTest extends AnimeClientTestCase
|
|||
$this->transformer = new AnimeTransformer();
|
||||
}
|
||||
|
||||
public function testTransform()
|
||||
public function testTransform(): never
|
||||
{
|
||||
$this->markTestSkipped('May fail on CI');
|
||||
$actual = $this->transformer->transform($this->beforeTransform);
|
||||
|
|
|
@ -35,7 +35,7 @@ final class CharacterTransformerTest extends AnimeClientTestCase
|
|||
$this->beforeTransform = $raw;
|
||||
}
|
||||
|
||||
public function testTransform(): void
|
||||
public function testTransform(): never
|
||||
{
|
||||
$this->markTestSkipped('Fails on CI');
|
||||
$actual = (new CharacterTransformer())->transform($this->beforeTransform);
|
||||
|
|
|
@ -35,7 +35,7 @@ final class HistoryTransformerTest extends AnimeClientTestCase
|
|||
$this->beforeTransform = $raw;
|
||||
}
|
||||
|
||||
public function testAnimeTransform(): void
|
||||
public function testAnimeTransform(): never
|
||||
{
|
||||
$this->markTestSkipped('Old test data');
|
||||
|
||||
|
|
|
@ -35,7 +35,7 @@ final class PersonTransformerTest extends AnimeClientTestCase
|
|||
$this->beforeTransform = $raw;
|
||||
}
|
||||
|
||||
public function testTransform(): void
|
||||
public function testTransform(): never
|
||||
{
|
||||
$this->markTestSkipped('Fails on CI');
|
||||
$actual = (new PersonTransformer())->transform($this->beforeTransform);
|
||||
|
|
|
@ -38,6 +38,10 @@ final class UserTransformerTest extends AnimeClientTestCase
|
|||
public function testTransform(): void
|
||||
{
|
||||
$actual = (new UserTransformer())->transform($this->beforeTransform);
|
||||
|
||||
// Unset the time value that will change every day, so the test is consistent
|
||||
$actual->joinDate = '';
|
||||
|
||||
$this->assertMatchesSnapshot($actual);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ id: '20286'
|
|||
manga_type: MANGA
|
||||
status: Completed
|
||||
staff:
|
||||
'Story & Art': [{ id: '8712', slug: ruri-miyahara, name: 'Ruri Miyahara', image: 'https://media.kitsu.io/people/images/8712/original.jpg?1533271952' }]
|
||||
'Story & Art': [{ id: '8712', slug: ruri-miyahara, name: 'Ruri Miyahara', image: 'https://media.kitsu.io/people/images/8712/original.jpg' }]
|
||||
synopsis: "Usa, a high-school student aspiring to begin a bachelor lifestyle, moves into a new apartment only to discover that he not only shares a room with a perverted roommate that has an obsession for underaged girls, but also that another girl, Ritsu, a love-at-first-sight, is living in the same building as well!\r\n(Source: Kirei Cake)"
|
||||
title: 'Bokura wa Minna Kawai-sou'
|
||||
titles:
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -15,11 +15,17 @@
|
|||
namespace Aviat\AnimeClient\Tests;
|
||||
|
||||
use DateTime;
|
||||
use function Aviat\AnimeClient\{arrayToToml, checkFolderPermissions, clearCache, colNotEmpty, getLocalImg, getResponse, isSequentialArray, tomlToArray};
|
||||
use PHPUnit\Framework\Attributes\IgnoreFunctionForCodeCoverage;
|
||||
use function Aviat\AnimeClient\{arrayToToml, checkFolderPermissions, clearCache, colNotEmpty, friendlyTime, getLocalImg, getResponse, isSequentialArray, tomlToArray};
|
||||
use const Aviat\AnimeClient\{MINUTES_IN_DAY, MINUTES_IN_HOUR, MINUTES_IN_YEAR, SECONDS_IN_MINUTE};
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
#[IgnoreFunctionForCodeCoverage('Aviat\AnimeClient\loadConfig')]
|
||||
#[IgnoreFunctionForCodeCoverage('Aviat\AnimeClient\createPlaceholderImage')]
|
||||
#[IgnoreFunctionForCodeCoverage('Aviat\AnimeClient\renderTemplate')]
|
||||
#[IgnoreFunctionForCodeCoverage('Aviat\AnimeClient\getLocalImg')]
|
||||
final class AnimeClientTest extends AnimeClientTestCase
|
||||
{
|
||||
public function testArrayToToml(): void
|
||||
|
@ -128,4 +134,33 @@ final class AnimeClientTest extends AnimeClientTestCase
|
|||
{
|
||||
$this->assertTrue(clearCache($this->container->get('cache')));
|
||||
}
|
||||
|
||||
public static function getFriendlyTime(): array
|
||||
{
|
||||
$SECONDS_IN_DAY = SECONDS_IN_MINUTE * MINUTES_IN_DAY;
|
||||
$SECONDS_IN_HOUR = SECONDS_IN_MINUTE * MINUTES_IN_HOUR;
|
||||
$SECONDS_IN_YEAR = SECONDS_IN_MINUTE * MINUTES_IN_YEAR;
|
||||
|
||||
return [[
|
||||
'seconds' => $SECONDS_IN_YEAR,
|
||||
'expected' => '1 year',
|
||||
], [
|
||||
'seconds' => $SECONDS_IN_HOUR,
|
||||
'expected' => '1 hour',
|
||||
], [
|
||||
'seconds' => (2 * $SECONDS_IN_YEAR) + 30,
|
||||
'expected' => '2 years, 30 seconds',
|
||||
], [
|
||||
'seconds' => (5 * $SECONDS_IN_YEAR) + (3 * $SECONDS_IN_DAY) + (17 * SECONDS_IN_MINUTE),
|
||||
'expected' => '5 years, 3 days, and 17 minutes',
|
||||
]];
|
||||
}
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('getFriendlyTime')]
|
||||
public function testGetFriendlyTime(int $seconds, string $expected): void
|
||||
{
|
||||
$actual = friendlyTime($seconds);
|
||||
|
||||
$this->assertSame($expected, $actual);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,10 +40,10 @@ class AnimeClientTestCase extends TestCase
|
|||
use MatchesSnapshots;
|
||||
|
||||
// Test directory constants
|
||||
public const ROOT_DIR = AC_TEST_ROOT_DIR;
|
||||
public const SRC_DIR = SRC_DIR;
|
||||
public const TEST_DATA_DIR = __DIR__ . '/test_data';
|
||||
public const TEST_VIEW_DIR = __DIR__ . '/test_views';
|
||||
final public const ROOT_DIR = AC_TEST_ROOT_DIR;
|
||||
final public const SRC_DIR = SRC_DIR;
|
||||
final public const TEST_DATA_DIR = __DIR__ . '/test_data';
|
||||
final public const TEST_VIEW_DIR = __DIR__ . '/test_views';
|
||||
|
||||
protected ContainerInterface $container;
|
||||
|
||||
|
@ -97,7 +97,7 @@ class AnimeClientTestCase extends TestCase
|
|||
$container = $di($config_array);
|
||||
|
||||
// Use mock session handler
|
||||
$container->set('session-handler', static function () {
|
||||
$container->set('session-handler', static function (): TestSessionHandler {
|
||||
$session_handler = new TestSessionHandler();
|
||||
session_set_save_handler($session_handler, TRUE);
|
||||
|
||||
|
@ -123,7 +123,7 @@ class AnimeClientTestCase extends TestCase
|
|||
];
|
||||
|
||||
$request = call_user_func_array(
|
||||
[ServerRequestFactory::class, 'fromGlobals'],
|
||||
ServerRequestFactory::fromGlobals(...),
|
||||
array_values(array_merge($default, $supers)),
|
||||
);
|
||||
$this->container->setInstance('request', $request);
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue