Merge remote-tracking branch 'origin/develop'
All checks were successful
timw4mail/HummingBirdAnimeClient/pipeline/head This commit looks good
All checks were successful
timw4mail/HummingBirdAnimeClient/pipeline/head This commit looks good
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.
|
||||
|
0
app/logs/.gitkeep
Normal file → Executable file
0
app/logs/.gitkeep
Normal file → Executable file
@ -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,
|
||||
});
|
||||
});
|
103
frontEndSrc/js/fns.js
Normal file
103
frontEndSrc/js/fns.js
Normal file
@ -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
|
||||
|
2
public/css/auto.min.css
vendored
2
public/css/auto.min.css
vendored
File diff suppressed because one or more lines are too long
2
public/css/dark.min.css
vendored
2
public/css/dark.min.css
vendored
File diff suppressed because one or more lines are too long
2
public/css/light.min.css
vendored
2
public/css/light.min.css
vendored
File diff suppressed because one or more lines are too long
39
public/js/scripts.min.js
vendored
39
public/js/scripts.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
public/js/tables.min.js
vendored
2
public/js/tables.min.js
vendored
@ -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']))
|
||||
{
|
||||
|
8
src/AnimeClient/API/Anilist/graphql.config.yml
Normal file
8
src/AnimeClient/API/Anilist/graphql.config.yml
Normal file
@ -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']),
|
||||
];
|
||||
|
8
src/AnimeClient/API/Kitsu/graphql.config.yml
Normal file
8
src/AnimeClient/API/Kitsu/graphql.config.yml
Normal file
@ -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
Block a user