diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d602057..f20fc455 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## Version 5.1 +* Added session check, so when coming back to a page, if the session is expired, the page will refresh. +* Updated logging config so that much fewer, much smaller files are generated. +* Updated Kitsu integration to use GraphQL API, reducing a lot of internal complexity. + ## Version 5 * Updated PHP requirement to 7.4 * Added anime watching history view diff --git a/RoboFile.php b/RoboFile.php index bb2958ef..59003dfb 100644 --- a/RoboFile.php +++ b/RoboFile.php @@ -227,7 +227,7 @@ class RoboFile extends Tasks { { $this->lint(); - $this->_run(['phpunit']); + $this->_run(['vendor/bin/phpunit']); } /** diff --git a/app/appConf/routes.php b/app/appConf/routes.php index 3f7c62ab..48f3a357 100644 --- a/app/appConf/routes.php +++ b/app/appConf/routes.php @@ -28,6 +28,17 @@ use const Aviat\AnimeClient\{ // Maps paths to controllers and methods // ------------------------------------------------------------------------- $routes = [ + // --------------------------------------------------------------------- + // AJAX Routes + // --------------------------------------------------------------------- + 'cache_purge' => [ + 'path' => '/cache_purge', + 'action' => 'clearCache', + ], + 'heartbeat' => [ + 'path' => '/heartbeat', + 'action' => 'heartbeat', + ], // --------------------------------------------------------------------- // Anime List Routes // --------------------------------------------------------------------- @@ -175,9 +186,9 @@ $routes = [ ] ], 'person' => [ - 'path' => '/people/{id}', + 'path' => '/people/{slug}', 'tokens' => [ - 'id' => SLUG_PATTERN + 'slug' => SLUG_PATTERN, ] ], 'default_user_info' => [ @@ -215,10 +226,6 @@ $routes = [ 'file' => '[a-z0-9\-]+\.[a-z]{3,4}' ] ], - 'cache_purge' => [ - 'path' => '/cache_purge', - 'action' => 'clearCache', - ], 'settings' => [ 'path' => '/settings', ], diff --git a/app/bootstrap.php b/app/bootstrap.php index 34068f1b..149c1c2a 100644 --- a/app/bootstrap.php +++ b/app/bootstrap.php @@ -10,7 +10,7 @@ * @author Timothy J. Warren * @copyright 2015 - 2020 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 5 + * @version 5.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ @@ -19,20 +19,24 @@ namespace Aviat\AnimeClient; use Aura\Html\HelperLocatorFactory; use Aura\Router\RouterContainer; use Aura\Session\SessionFactory; -use Aviat\AnimeClient\API\{ - Anilist, - Kitsu, - Kitsu\KitsuRequestBuilder -}; +use Aviat\AnimeClient\API\{Anilist, Kitsu}; +use Aviat\AnimeClient\Component; use Aviat\AnimeClient\Model; use Aviat\Banker\Teller; use Aviat\Ion\Config; use Aviat\Ion\Di\Container; use Aviat\Ion\Di\ContainerInterface; -use Psr\SimpleCache\CacheInterface; -use Laminas\Diactoros\{Response, ServerRequestFactory}; +use Laminas\Diactoros\ServerRequestFactory; +use Monolog\Formatter\JsonFormatter; use Monolog\Handler\RotatingFileHandler; use Monolog\Logger; +use Psr\SimpleCache\CacheInterface; + +if ( ! defined('APP_DIR')) +{ + define('APP_DIR', __DIR__); + define('TEMPLATE_DIR', APP_DIR . '/templates'); +} // ----------------------------------------------------------------------------- // Setup DI container @@ -45,17 +49,18 @@ return static function (array $configArray = []): Container { // ------------------------------------------------------------------------- $appLogger = new Logger('animeclient'); - $appLogger->pushHandler(new RotatingFileHandler(__DIR__ . '/logs/app.log', Logger::NOTICE)); - - $anilistRequestLogger = new Logger('anilist-request'); - $anilistRequestLogger->pushHandler(new RotatingFileHandler(__DIR__ . '/logs/anilist_request.log', Logger::NOTICE)); - - $kitsuRequestLogger = new Logger('kitsu-request'); - $kitsuRequestLogger->pushHandler(new RotatingFileHandler(__DIR__ . '/logs/kitsu_request.log', Logger::NOTICE)); - + $appLogger->pushHandler(new RotatingFileHandler(__DIR__ . '/logs/app.log', 2, Logger::WARNING)); $container->setLogger($appLogger); - $container->setLogger($anilistRequestLogger, 'anilist-request'); - $container->setLogger($kitsuRequestLogger, 'kitsu-request'); + + foreach (['anilist-request', 'kitsu-request', 'kitsu-graphql'] as $channel) + { + $logger = new Logger($channel); + $handler = new RotatingFileHandler(__DIR__ . "/logs/{$channel}.log", 2, Logger::WARNING); + $handler->setFormatter(new JsonFormatter()); + $logger->pushHandler($handler); + + $container->setLogger($logger, $channel); + } // ------------------------------------------------------------------------- // Injected Objects @@ -74,29 +79,52 @@ return static function (array $configArray = []): Container { // Create Aura Router Object $container->set('aura-router', fn() => new RouterContainer); - // Create Html helper Object + // Create Html helpers $container->set('html-helper', static function(ContainerInterface $container) { $htmlHelper = (new HelperLocatorFactory)->newInstance(); - $htmlHelper->set('menu', static function() use ($container) { - $menuHelper = new Helper\Menu(); - $menuHelper->setContainer($container); - return $menuHelper; - }); - $htmlHelper->set('field', static function() use ($container) { - $formHelper = new Helper\Form(); - $formHelper->setContainer($container); - return $formHelper; - }); - $htmlHelper->set('picture', static function() use ($container) { - $pictureHelper = new Helper\Picture(); - $pictureHelper->setContainer($container); - return $pictureHelper; - }); + $helpers = [ + 'menu' => Helper\Menu::class, + 'field' => Helper\Form::class, + 'picture' => Helper\Picture::class, + ]; + + foreach ($helpers as $name => $class) + { + $htmlHelper->set($name, static function() use ($class, $container) { + $helper = new $class; + $helper->setContainer($container); + return $helper; + }); + } return $htmlHelper; }); - // Create Request/Response Objects + // Create Component helpers + $container->set('component-helper', static function (ContainerInterface $container) { + $helper = (new HelperLocatorFactory)->newInstance(); + $components = [ + 'animeCover' => Component\AnimeCover::class, + 'mangaCover' => Component\MangaCover::class, + 'character' => Component\Character::class, + 'media' => Component\Media::class, + 'tabs' => Component\Tabs::class, + 'verticalTabs' => Component\VerticalTabs::class, + ]; + + foreach ($components as $name => $componentClass) + { + $helper->set($name, static function () use ($container, $componentClass) { + $helper = new $componentClass; + $helper->setContainer($container); + return $helper; + }); + } + + return $helper; + }); + + // Create Request Object $container->set('request', fn () => ServerRequestFactory::fromGlobals( $_SERVER, $_GET, @@ -104,7 +132,6 @@ return static function (array $configArray = []): Container { $_COOKIE, $_FILES )); - $container->set('response', fn () => new Response); // Create session Object $container->set('session', fn () => (new SessionFactory())->newInstance($_COOKIE)); @@ -114,7 +141,7 @@ return static function (array $configArray = []): Container { // Models $container->set('kitsu-model', static function(ContainerInterface $container): Kitsu\Model { - $requestBuilder = new KitsuRequestBuilder($container); + $requestBuilder = new Kitsu\RequestBuilder($container); $requestBuilder->setLogger($container->getLogger('kitsu-request')); $listItem = new Kitsu\ListItem(); @@ -130,7 +157,7 @@ return static function (array $configArray = []): Container { return $model; }); $container->set('anilist-model', static function(ContainerInterface $container): Anilist\Model { - $requestBuilder = new Anilist\AnilistRequestBuilder(); + $requestBuilder = new Anilist\RequestBuilder($container); $requestBuilder->setLogger($container->getLogger('anilist-request')); $listItem = new Anilist\ListItem(); diff --git a/app/views/anime/cover-item.php b/app/templates/anime-cover.php similarity index 92% rename from app/views/anime/cover-item.php rename to app/templates/anime-cover.php index af680967..aef5ec2b 100644 --- a/app/views/anime/cover-item.php +++ b/app/templates/anime-cover.php @@ -1,7 +1,7 @@
isAuthenticated()): ?> @@ -31,13 +31,13 @@ 0): ?>
-
Rewatched once
+
Rewatched once
-
Rewatched twice
+
Rewatched twice
-
Rewatched thrice
+
Rewatched thrice
-
Rewatched times
+
Rewatched times
diff --git a/app/templates/character.php b/app/templates/character.php new file mode 100644 index 00000000..78e00795 --- /dev/null +++ b/app/templates/character.php @@ -0,0 +1,6 @@ +
+
+ +
+ +
\ No newline at end of file diff --git a/app/templates/manga-cover.php b/app/templates/manga-cover.php new file mode 100644 index 00000000..feb646bc --- /dev/null +++ b/app/templates/manga-cover.php @@ -0,0 +1,74 @@ +
+ isAuthenticated()): ?> + + + picture("images/manga/{$item['manga']['id']}.webp") ?> + +
+ isAuthenticated()): ?> +
+ + + Edit + + +
+ +
+
+
Rating: / 10
+
+ + +
+ + + + + +
+ + + 0): ?> +
+ +
Reread once
+ +
Reread twice
+ +
Reread thrice
+ +
Reread times
+ +
+ + +
+
+ Chapters: / + +
+ +
*/ ?> +
+ Volumes: +
+
+
+
\ No newline at end of file diff --git a/app/templates/media.php b/app/templates/media.php new file mode 100644 index 00000000..3cf7f25e --- /dev/null +++ b/app/templates/media.php @@ -0,0 +1,12 @@ + \ No newline at end of file diff --git a/app/templates/single-tab.php b/app/templates/single-tab.php new file mode 100644 index 00000000..0b2ceb53 --- /dev/null +++ b/app/templates/single-tab.php @@ -0,0 +1,5 @@ +
+ $tabData): ?> + + +
\ No newline at end of file diff --git a/app/templates/tabs.php b/app/templates/tabs.php new file mode 100644 index 00000000..c7f70b91 --- /dev/null +++ b/app/templates/tabs.php @@ -0,0 +1,32 @@ +
+ $tabData): ?> + + + + /> + + + +
+ + +
+ +
+ + +
+ + + +
\ No newline at end of file diff --git a/app/templates/vertical-tabs.php b/app/templates/vertical-tabs.php new file mode 100644 index 00000000..a36a2bb1 --- /dev/null +++ b/app/templates/vertical-tabs.php @@ -0,0 +1,25 @@ +
+ + $tabData): ?> + +
+ + /> + +
+ +
+
+ + +
\ No newline at end of file diff --git a/app/views/anime/cover.php b/app/views/anime/cover.php index 9c872607..96cdf359 100644 --- a/app/views/anime/cover.php +++ b/app/views/anime/cover.php @@ -20,7 +20,7 @@
isAuthenticated()) continue; ?> - + animeCover($item) ?>
diff --git a/app/views/anime/details.php b/app/views/anime/details.php index 9e72b539..f7d4b313 100644 --- a/app/views/anime/details.php +++ b/app/views/anime/details.php @@ -1,6 +1,11 @@ - +
-
+
-

+

@@ -99,81 +131,69 @@
-

Trailer

- +

Trailer

+
0): ?> -
-

Characters

+
+

Characters

-
- - $list): ?> - /> - -
- $char): ?> - - - - -
- - -
-
+ tabs('character-types', $data['characters'], static function ($characterList, $role) + use ($component, $url, $helper) { + $rendered = []; + foreach ($characterList as $id => $character): + if (empty($character['image']['original'])) + { + continue; + } + $rendered[] = $component->character( + $character['name'], + $url->generate('character', ['slug' => $character['slug']]), + $helper->picture("images/characters/{$id}.webp"), + (strtolower($role) !== 'main') ? 'small-character' : 'character' + ); + endforeach; + + return implode('', array_map('mb_trim', $rendered)); + }) ?> +
0): ?> -
-

Staff

+
+

Staff

-
- - $people): ?> -
- /> - -
- $person): ?> - - -
-
- - -
-
+ verticalTabs('staff-role', $data['staff'], static function ($staffList) + use ($component, $url, $helper) { + $rendered = []; + foreach ($staffList as $id => $person): + if (empty($person['image']['original'])) + { + continue; + } + $rendered[] = $component->character( + $person['name'], + $url->generate('person', ['slug' => $person['slug']]), + $helper->picture(getLocalImg($person['image']['original'] ?? NULL)), + 'character small-person', + ); + endforeach; + + return implode('', array_map('mb_trim', $rendered)); + }) ?> +
\ No newline at end of file diff --git a/app/views/anime/edit.php b/app/views/anime/edit.php index 500f777d..6d9a8ff2 100644 --- a/app/views/anime/edit.php +++ b/app/views/anime/edit.php @@ -32,7 +32,7 @@ diff --git a/app/views/anime/list.php b/app/views/anime/list.php index 1c2d482b..17373e3b 100644 --- a/app/views/anime/list.php +++ b/app/views/anime/list.php @@ -1,4 +1,4 @@ - +
isAuthenticated()): ?> Add Item @@ -15,7 +15,7 @@

There's nothing here!

@@ -31,7 +31,6 @@ - @@ -103,10 +102,6 @@ - diff --git a/app/views/character/details.php b/app/views/character/details.php index e079621a..842da727 100644 --- a/app/views/character/details.php +++ b/app/views/character/details.php @@ -1,7 +1,7 @@
@@ -33,63 +33,20 @@ use Aviat\AnimeClient\API\Kitsu;

Media

-
- - - -
- $anime): ?> - - -
- + tabs('character-media', $data['media'], static function ($media, $mediaType) use ($url, $component, $helper) { + $rendered = []; + foreach ($media as $id => $item) + { + $rendered[] = $component->media( + array_merge([$item['title']], $item['titles']), + $url->generate("{$mediaType}.details", ['id' => $item['slug']]), + $helper->picture("images/{$mediaType}/{$item['id']}.webp") + ); + } - - - - -
- $manga): ?> - - -
- -
+ return implode('', array_map('mb_trim', $rendered)); + }, 'media-wrap content') ?>
@@ -158,66 +115,47 @@ use Aviat\AnimeClient\API\Kitsu;

Voice Actors

-
- + tabs('character-vas', $vas, static function ($casting) use ($url, $component, $helper) { + $castings = []; + foreach ($casting as $id => $c): + $person = $component->character( + $c['person']['name'], + $url->generate('person', ['slug' => $c['person']['slug']]), + $helper->picture(getLocalImg($c['person']['image'])) + ); + $medias = array_map(fn ($series) => $component->media( + array_merge([$series['title']], $series['titles']), + $url->generate('anime.details', ['id' => $series['slug']]), + $helper->picture(getLocalImg($series['posterImage'], TRUE)) + ), $c['series']); + $media = implode('', array_map('mb_trim', $medias)); - $casting): ?> - type="radio" id="character-va" - name="character-vas" - /> - -
-
Rated Attributes NotesGenres

html($item['notes']) ?>

- genres) ?> - genres) ?> -
- - - - - $c): ?> - - - - - -
Cast MemberSeries
- - -
- - - -
-
- - - - + $castings[] = << + {$person} + +
+ {$media} +
+ + +HTML; + endforeach; + + $languages = implode('', array_map('mb_trim', $castings)); + + return << + + + Cast Member + Series + + + {$languages} + +HTML; + }, 'content') ?> diff --git a/app/views/collection/add.php b/app/views/collection/add.php index 7a8bca88..aa625fa5 100644 --- a/app/views/collection/add.php +++ b/app/views/collection/add.php @@ -19,7 +19,7 @@ - + diff --git a/app/views/collection/cover.php b/app/views/collection/cover.php index 0d4c1345..924d03e1 100644 --- a/app/views/collection/cover.php +++ b/app/views/collection/cover.php @@ -1,3 +1,4 @@ +
isAuthenticated()): ?> Add Item @@ -8,30 +9,20 @@

-
- - $items): ?> - - -
-
- - - -
-
- - - - - -
-
- - - -
-
-
+ tabs('collection-tab', $sections, static function ($items) use ($auth, $collection_type, $helper, $url, $component) { + $rendered = []; + foreach ($items as $item) + { + $rendered[] = renderTemplate(__DIR__ . '/cover-item.php', [ + 'auth' => $auth, + 'collection_type' => $collection_type, + 'helper' => $helper, + 'item' => $item, + 'url' => $url, + ]); + } + + return implode('', array_map('mb_trim', $rendered)); + }, 'media-wrap', true) ?>
diff --git a/app/views/collection/edit.php b/app/views/collection/edit.php index c3b8f2b0..65129799 100644 --- a/app/views/collection/edit.php +++ b/app/views/collection/edit.php @@ -1,3 +1,4 @@ + isAuthenticated()): ?>

Edit Anime Collection Item

@@ -24,7 +25,7 @@ - + diff --git a/app/views/collection/list-all.php b/app/views/collection/list-all.php deleted file mode 100644 index b14db2b1..00000000 --- a/app/views/collection/list-all.php +++ /dev/null @@ -1,44 +0,0 @@ - - -
- - - - isAuthenticated()): ?> - - - - - - - - - - - - - generate($collection_type . '.collection.edit.get', ['id' => $item['hummingbird_id']]); ?> - - isAuthenticated()): ?> - - - - - - - - - - - - - -
 TitleMediaEpisode CountEpisode LengthShow TypeAge RatingNotesGenres
- Edit - - - - - ' . $item['alternate_title'] . '' : '' ?> - 1) ? $item['episode_count'] : '-' ?>
-
\ No newline at end of file diff --git a/app/views/collection/list-item.php b/app/views/collection/list-item.php index 9c8ac967..4fd1191a 100644 --- a/app/views/collection/list-item.php +++ b/app/views/collection/list-item.php @@ -11,6 +11,9 @@ ' . $item['alternate_title'] . '' : '' ?> + + + 1) ? $item['episode_count'] : '-' ?> diff --git a/app/views/collection/list.php b/app/views/collection/list.php index 092d307c..ec3fd5e8 100644 --- a/app/views/collection/list.php +++ b/app/views/collection/list.php @@ -1,4 +1,4 @@ - +
isAuthenticated()): ?> Add Item @@ -9,38 +9,48 @@

- -
- $items): ?> - - - -
- - - - isAuthenticated()): ?> - - - - - - - - - - - - - - -
 TitleEpisode CountEpisode LengthShow TypeAge RatingNotesGenres
-
- - - - -
+ tabs('collection-tab', $sections, static function ($items, $section) use ($auth, $helper, $url, $collection_type) { + $hasNotes = colNotEmpty($items, 'notes'); + $hasMedia = $section === 'All'; + $firstTh = ($auth->isAuthenticated()) ? ' ' : ''; + $mediaTh = ($hasMedia) ? 'Media' : ''; + $noteTh = ($hasNotes) ? 'Notes' : ''; + + $rendered = []; + foreach ($items as $item) + { + $rendered[] = renderTemplate(__DIR__ . '/list-item.php', [ + 'auth' => $auth, + 'collection_type' => $collection_type, + 'hasMedia' => $hasMedia, + 'hasNotes' => $hasNotes, + 'helper' => $helper, + 'item' => $item, + 'url' => $url, + ]); + } + $rows = implode('', array_map('mb_trim', $rendered)); + + return << + + + {$firstTh} + Title + {$mediaTh} + Episode Count + Episode Length + Show Type + Age Rating + {$noteTh} + Genres + + + {$rows} + +HTML; + + }) ?>
\ No newline at end of file diff --git a/app/views/collection/_media-select-list.php b/app/views/collection/media-select-list.php similarity index 100% rename from app/views/collection/_media-select-list.php rename to app/views/collection/media-select-list.php diff --git a/app/views/header.php b/app/views/header.php index 25828f09..930bba74 100644 --- a/app/views/header.php +++ b/app/views/header.php @@ -39,5 +39,4 @@ } } ?> - \ No newline at end of file diff --git a/app/views/manga/cover.php b/app/views/manga/cover.php index 7d3edbae..8d4822c7 100644 --- a/app/views/manga/cover.php +++ b/app/views/manga/cover.php @@ -19,80 +19,7 @@

html($name) ?>

-
- isAuthenticated()): ?> - - - picture("images/manga/{$item['manga']['id']}.webp") ?> - -
- isAuthenticated()): ?> -
- - - Edit - - -
- -
-
-
Rating: / 10
-
- - -
- - - - - -
- - - 0): ?> -
- -
Reread once
- -
Reread twice
- -
Reread thrice
- -
Reread times
- -
- - -
-
- Chapters: / - -
- -
*/ ?> -
- Volumes: -
-
-
-
+ mangaCover($item, $name) ?>
diff --git a/app/views/manga/details.php b/app/views/manga/details.php index 877a929f..0df57deb 100644 --- a/app/views/manga/details.php +++ b/app/views/manga/details.php @@ -7,17 +7,45 @@ - - + + + + + + + + + + + + + + + + + + + 0): ?> + + + + + + - @@ -70,9 +69,6 @@ - diff --git a/app/views/person/character-mapping.php b/app/views/person/character-mapping.php deleted file mode 100644 index 077d0c2b..00000000 --- a/app/views/person/character-mapping.php +++ /dev/null @@ -1,67 +0,0 @@ - -

Voice Acting Roles

-
- - $characterList): ?> - type="radio" name="character-type-tabs" id="character-type-" /> - -
-
Manga TypePublishing Status
Manga Type
Volume Count
Chapter Count
Age Rating +
External Links + $externalUrl): ?> +
+ +
Genres @@ -29,8 +57,8 @@
-

- +

+

@@ -44,61 +72,34 @@ 0): ?>

Characters

-
- - $list): ?> - /> - -
- $char): ?> - - - - -
- - -
+ tabs('manga-characters', $data['characters'], static function($list, $role) use ($component, $helper, $url) { + $rendered = []; + foreach ($list as $id => $char) + { + $rendered[] = $component->character( + $char['name'], + $url->generate('character', ['slug' => $char['slug']]), + $helper->picture("images/characters/{$id}.webp"), + ($role !== 'main') ? 'small-character' : 'character' + ); + } + + return implode('', array_map('mb_trim', $rendered)); + }) ?> 0): ?>

Staff

-
- - $people): ?> -
- /> - -
- $person): ?> - - -
-
- - -
+ verticalTabs('manga-staff', $data['staff'], + fn($people) => implode('', array_map( + fn ($person) => $component->character( + $person['name'], + $url->generate('person', ['slug' => $person['slug']]), + $helper->picture("images/people/{$person['id']}.webp") + ), + $people + )) + ) ?> \ No newline at end of file diff --git a/app/views/manga/list.php b/app/views/manga/list.php index 02aa879d..ffcb53f7 100644 --- a/app/views/manga/list.php +++ b/app/views/manga/list.php @@ -25,7 +25,6 @@
# of Volumes Attributes TypeGenres
- -
- - - - - $character): ?> - - - - - -
CharacterSeries
- - -
- $series): ?> - - -
-
- - - - diff --git a/app/views/person/details.php b/app/views/person/details.php index 98c6282d..fc868e22 100644 --- a/app/views/person/details.php +++ b/app/views/person/details.php @@ -1,7 +1,5 @@
@@ -10,12 +8,21 @@ use Aviat\AnimeClient\API\Kitsu;

+ +

+ +
+
+
+

', $data['description']) ?>

+

Castings

+
$entries): ?> @@ -24,31 +31,17 @@ use Aviat\AnimeClient\API\Kitsu; type="radio" name="staff-roles" id="staff-role" /> $casting): ?> - - +

-
+
$series): ?> - + + media( + $series['titles'], + $url->generate("{$mediaType}.details", ['id' => $series['slug']]), + $helper->picture("images/{$type}/{$sid}.webp") + ) ?>
@@ -59,9 +52,53 @@ use Aviat\AnimeClient\API\Kitsu;
- +
- +

Voice Acting Roles

+ tabs('voice-acting-roles', $data['characters'], static function ($characterList) use ($component, $helper, $url) { + $voiceRoles = []; + foreach ($characterList as $cid => $item): + $character = $component->character( + $item['character']['canonicalName'], + $url->generate('character', ['slug' => $item['character']['slug']]), + $helper->picture(getLocalImg($item['character']['image']['original'] ?? null)) + ); + $medias = []; + foreach ($item['media'] as $sid => $series) + { + $medias[] = $component->media( + $series['titles'], + $url->generate('anime.details', ['id' => $series['slug']]), + $helper->picture("images/anime/{$sid}.webp") + ); + } + $media = implode('', array_map('mb_trim', $medias)); + + $voiceRoles[] = << + {$character} + +
{$media}
+ + +HTML; + endforeach; + + $roles = implode('', array_map('mb_trim', $voiceRoles)); + + return << + + + Character + Series + + + {$roles} + +HTML; + + }) ?>
diff --git a/app/views/settings/settings.php b/app/views/settings/settings.php index b51319aa..16fef5f0 100644 --- a/app/views/settings/settings.php +++ b/app/views/settings/settings.php @@ -28,9 +28,7 @@ $nestedPrefix = 'config'; />
- -
checkAuth(); ?>

Not Authorized.

@@ -43,11 +41,15 @@ $nestedPrefix = 'config';

Linked to Anilist. Your access token will expire around

+ a( - $url->generate('anilist-redirect'), - 'Update Access Token' - ) ?> + $url->generate('anilist-redirect'), + 'Update Access Token', + ['class' => 'bracketed user-btn'] + ) ?> + +
diff --git a/app/views/user/details.php b/app/views/user/details.php index f47bb675..bca971a1 100644 --- a/app/views/user/details.php +++ b/app/views/user/details.php @@ -1,5 +1,5 @@

@@ -36,7 +36,7 @@ use Aviat\AnimeClient\API\Kitsu; $character = $data['waifu']['character']; echo $helper->a( $url->generate('character', ['slug' => $character['slug']]), - $character['canonicalName'] + $character['names']['canonical'] ); ?> @@ -57,79 +57,43 @@ use Aviat\AnimeClient\API\Kitsu;

Favorites

-
- - - /> - -
- $char): ?> - - - - -
- - - - /> - -
- - - -
- - - - /> - -
- - - -
- - -
+ tabs('user-favorites', $data['favorites'], static function ($items, $type) use ($component, $helper, $url) { + $rendered = []; + if ($type === 'character') + { + uasort($items, fn ($a, $b) => $a['names']['canonical'] <=> $b['names']['canonical']); + } + else + { + uasort($items, fn ($a, $b) => $a['titles']['canonical'] <=> $b['titles']['canonical']); + } + + foreach ($items as $id => $item) + { + if ($type === 'character') + { + $rendered[] = $component->character( + $item['names']['canonical'], + $url->generate('character', ['slug' => $item['slug']]), + $helper->picture("images/characters/{$item['id']}.webp") + ); + } + else + { + $rendered[] = $component->media( + array_merge( + [$item['titles']['canonical']], + Kitsu::getFilteredTitles($item['titles']), + ), + $url->generate("{$type}.details", ['id' => $item['slug']]), + $helper->picture("images/{$type}/{$item['id']}.webp"), + ); + } + } + + return implode('', array_map('mb_trim', $rendered)); + + }, 'content full-width media-wrap') ?>
diff --git a/build/header_comment.txt b/build/header_comment.txt index 73dc0735..e780e729 100644 --- a/build/header_comment.txt +++ b/build/header_comment.txt @@ -9,7 +9,7 @@ * @author Timothy J. Warren * @copyright 2015 - 2020 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 5 + * @version 5.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ diff --git a/composer.json b/composer.json index cea18548..46b5f80a 100644 --- a/composer.json +++ b/composer.json @@ -34,7 +34,8 @@ } }, "require": { - "amphp/http-client": "^4.2.2", + "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", @@ -43,6 +44,7 @@ "danielstjules/stringy": "^3.1.0", "ext-dom": "*", "ext-iconv": "*", + "ext-intl": "*", "ext-json": "*", "ext-gd": "*", "ext-pdo": "*", @@ -65,7 +67,7 @@ "phpstan/phpstan": "^0.12.19", "phpunit/phpunit": "^8.5.2", "roave/security-advisories": "dev-master", - "robmorgan/phinx": "^0.10.6", + "robmorgan/phinx": "^0.12.4", "sebastian/phpcpd": "^4.1.0", "spatie/phpunit-snapshot-assertions": "^4.1.0", "squizlabs/php_codesniffer": "^3.5.4", diff --git a/console b/console index a0999077..e499c9b1 100755 --- a/console +++ b/console @@ -9,6 +9,9 @@ use ConsoleKit\Console; $_SERVER['HTTP_HOST'] = 'localhost'; +define('APP_DIR', __DIR__ . '/app'); +define('TEMPLATE_DIR', APP_DIR . '/templates'); + // ----------------------------------------------------------------------------- // Start console script // ----------------------------------------------------------------------------- diff --git a/frontEndSrc/css/src/components.css b/frontEndSrc/css/src/components.css index e50deaaf..f0181df2 100644 --- a/frontEndSrc/css/src/components.css +++ b/frontEndSrc/css/src/components.css @@ -163,7 +163,7 @@ CSS Tabs /* text-align: center; */ } -.tabs .content { +.tabs .content, .single-tab { display: none; max-height: 950px; border: 1px solid #e5e5e5; @@ -175,7 +175,14 @@ CSS Tabs overflow: auto; } -.tabs .content.full-height { +.single-tab { + display: block; + border: 1px solid #e5e5e5; + box-shadow: 0 48px 80px -32px rgba(0, 0, 0, 0.3); + margin-top: 1.5em; +} + +.tabs .content.full-height, .single-tab.full-height { max-height: none; } diff --git a/frontEndSrc/css/src/dark-override.css b/frontEndSrc/css/src/dark-override.css index 890b2e12..46dd8d67 100644 --- a/frontEndSrc/css/src/dark-override.css +++ b/frontEndSrc/css/src/dark-override.css @@ -147,7 +147,8 @@ button:active { .tabs > [type="radio"]:checked + label, .tabs > [type="radio"]:checked + label + .content, .vertical-tabs [type="radio"]:checked + label, -.vertical-tabs [type="radio"]:checked ~ .content { +.vertical-tabs [type="radio"]:checked ~ .content, +.single-tab { /* border-color: #333; */ border: 0; background: #666; diff --git a/frontEndSrc/css/src/general.css b/frontEndSrc/css/src/general.css index 587e0582..29efb1f5 100644 --- a/frontEndSrc/css/src/general.css +++ b/frontEndSrc/css/src/general.css @@ -94,6 +94,7 @@ a:hover, a:active { iframe { display: block; margin: 0 auto; + border: 0; } /* ----------------------------------------------------------------------------- diff --git a/frontEndSrc/js/anime-client.js b/frontEndSrc/js/anime-client.js index 04869dba..da05b7c5 100644 --- a/frontEndSrc/js/anime-client.js +++ b/frontEndSrc/js/anime-client.js @@ -261,7 +261,7 @@ function ajaxSerialize(data) { * * @param {string} url - the url to request * @param {Object} config - the configuration object - * @return {void} + * @return {XMLHttpRequest} */ AnimeClient.ajax = (url, config) => { // Set some sane defaults @@ -322,6 +322,8 @@ AnimeClient.ajax = (url, config) => { } else { request.send(config.data); } + + return request }; /** @@ -330,6 +332,7 @@ AnimeClient.ajax = (url, config) => { * @param {string} url * @param {object|function} data * @param {function} [callback] + * @return {XMLHttpRequest} */ AnimeClient.get = (url, data, callback = null) => { if (callback === null) { diff --git a/frontEndSrc/js/anime.js b/frontEndSrc/js/anime.js index cdf1ac7e..c0a7fad3 100644 --- a/frontEndSrc/js/anime.js +++ b/frontEndSrc/js/anime.js @@ -6,25 +6,31 @@ const search = (query) => { _.show('.cssload-loader'); // Do the api search - _.get(_.url('/anime-collection/search'), { query }, (searchResults, status) => { + return _.get(_.url('/anime-collection/search'), { query }, (searchResults, status) => { searchResults = JSON.parse(searchResults); // Hide the loader _.hide('.cssload-loader'); // Show the results - _.$('#series-list')[ 0 ].innerHTML = renderAnimeSearchResults(searchResults.data); + _.$('#series-list')[ 0 ].innerHTML = renderAnimeSearchResults(searchResults); }); }; if (_.hasElement('.anime #search')) { + let prevRequest = null; + _.on('#search', 'input', _.throttle(250, (e) => { const query = encodeURIComponent(e.target.value); if (query === '') { return; } - search(query); + if (prevRequest !== null) { + prevRequest.abort(); + } + + prevRequest = search(query); })); } @@ -47,12 +53,12 @@ _.on('body.anime.list', 'click', '.plus-one', (e) => { // If the episode count is 0, and incremented, // change status to currently watching if (isNaN(watchedCount) || watchedCount === 0) { - data.data.status = 'current'; + data.data.status = 'CURRENT'; } // If you increment at the last episode, mark as completed if ((!isNaN(watchedCount)) && (watchedCount + 1) === totalCount) { - data.data.status = 'completed'; + data.data.status = 'COMPLETED'; } _.show('#loading-shadow'); @@ -72,7 +78,7 @@ _.on('body.anime.list', 'click', '.plus-one', (e) => { return; } - if (resData.data.attributes.status === 'completed') { + if (resData.data.libraryEntry.update.libraryEntry.status === 'COMPLETED') { _.hide(parentSel); } diff --git a/frontEndSrc/js/index.js b/frontEndSrc/js/index.js index 6cad73e4..9fd34d36 100644 --- a/frontEndSrc/js/index.js +++ b/frontEndSrc/js/index.js @@ -1,4 +1,5 @@ import './anon.js'; +import './session-check.js'; import './anime.js'; import './manga.js'; diff --git a/frontEndSrc/js/manga.js b/frontEndSrc/js/manga.js index 6af89f93..cb8ad8a7 100644 --- a/frontEndSrc/js/manga.js +++ b/frontEndSrc/js/manga.js @@ -3,21 +3,27 @@ import { renderMangaSearchResults } from './template-helpers.js' const search = (query) => { _.show('.cssload-loader'); - _.get(_.url('/manga/search'), { query }, (searchResults, status) => { + return _.get(_.url('/manga/search'), { query }, (searchResults, status) => { searchResults = JSON.parse(searchResults); _.hide('.cssload-loader'); - _.$('#series-list')[ 0 ].innerHTML = renderMangaSearchResults(searchResults.data); + _.$('#series-list')[ 0 ].innerHTML = renderMangaSearchResults(searchResults); }); }; if (_.hasElement('.manga #search')) { + let prevRequest = null + _.on('#search', 'input', _.throttle(250, (e) => { let query = encodeURIComponent(e.target.value); if (query === '') { return; } - search(query); + if (prevRequest !== null) { + prevRequest.abort(); + } + + prevRequest = search(query); })); } @@ -48,12 +54,12 @@ _.on('.manga.list', 'click', '.edit-buttons button', (e) => { // If the episode count is 0, and incremented, // change status to currently reading if (isNaN(completed) || completed === 0) { - data.data.status = 'current'; + data.data.status = 'CURRENT'; } // If you increment at the last chapter, mark as completed if ((!isNaN(completed)) && (completed + 1) === total) { - data.data.status = 'completed'; + data.data.status = 'COMPLETED'; } // Update the total count @@ -67,7 +73,7 @@ _.on('.manga.list', 'click', '.edit-buttons button', (e) => { type: 'POST', mimeType: 'application/json', success: () => { - if (data.data.status === 'completed') { + if (String(data.data.status).toUpperCase() === 'COMPLETED') { _.hide(parentSel); } diff --git a/frontEndSrc/js/session-check.js b/frontEndSrc/js/session-check.js new file mode 100644 index 00000000..6777ed5e --- /dev/null +++ b/frontEndSrc/js/session-check.js @@ -0,0 +1,41 @@ +import _ from './anime-client.js'; + +(() => { + // Var is intentional + var hidden = null; + var visibilityChange = null; + + if (typeof document.hidden !== "undefined") { + hidden = "hidden"; + visibilityChange = "visibilitychange"; + } else if (typeof document.msHidden !== "undefined") { + hidden = "msHidden"; + visibilityChange = "msvisibilitychange"; + } else if (typeof document.webkitHidden !== "undefined") { + hidden = "webkitHidden"; + visibilityChange = "webkitvisibilitychange"; + } + + function handleVisibilityChange() { + // Check the user's session to see if they are currently logged-in + // when the page becomes visible + if ( ! document[hidden]) { + _.get('/heartbeat', (beat) => { + const status = JSON.parse(beat) + + // If the session is expired, immediately reload so that + // you can't attempt to do an action that requires authentication + if (status.hasAuth !== true) { + document.removeEventListener(visibilityChange, handleVisibilityChange, false); + location.reload(); + } + }); + } + } + + if (hidden === null) { + console.info('Page visibility API not supported, JS session check will not work'); + } else { + document.addEventListener(visibilityChange, handleVisibilityChange, false); + } +})(); \ No newline at end of file diff --git a/frontEndSrc/js/template-helpers.js b/frontEndSrc/js/template-helpers.js index caf054b3..14ee6650 100644 --- a/frontEndSrc/js/template-helpers.js +++ b/frontEndSrc/js/template-helpers.js @@ -10,20 +10,19 @@ _.on('main', 'change', '.big-check', (e) => { export function renderAnimeSearchResults (data) { const results = []; - data.forEach(x => { - const item = x.attributes; + data.forEach(item => { const titles = item.titles.join('
'); results.push(`