From 82c86b7b470fd5deee045f7c18ead6d76e3e6bbf Mon Sep 17 00:00:00 2001 From: Timothy J Warren Date: Fri, 31 Mar 2017 13:37:53 -0400 Subject: [PATCH] Update detail pages --- app/appConf/routes.php | 12 +- app/views/anime/details.php | 5 +- app/views/character.php | 2 +- app/views/manga/details.php | 2 +- public/css.php | 1 - public/css/base.css | 15 +- public/css/base.myth.css | 13 +- src/API/JsonAPI.php | 239 ++++++++++++++++--- src/API/Kitsu/Model.php | 18 +- src/AnimeClient.php | 3 +- src/Command/SyncKitsuWithMal.php | 83 ++----- src/Controller.php | 328 ++++++++++++++++++++++---- src/Controller/Index.php | 149 ++++++++++++ src/ControllerTrait.php | 384 ------------------------------- 14 files changed, 705 insertions(+), 549 deletions(-) create mode 100644 src/Controller/Index.php delete mode 100644 src/ControllerTrait.php diff --git a/app/appConf/routes.php b/app/appConf/routes.php index 21f92986..d8b81816 100644 --- a/app/appConf/routes.php +++ b/app/appConf/routes.php @@ -17,7 +17,7 @@ use const Aviat\AnimeClient\{ DEFAULT_CONTROLLER_METHOD, - DEFAULT_CONTROLLER_NAMESPACE + DEFAULT_CONTROLLER }; use Aviat\AnimeClient\AnimeClient; @@ -150,25 +150,25 @@ return [ 'cache_purge' => [ 'path' => '/cache_purge', 'action' => 'clearCache', - 'controller' => DEFAULT_CONTROLLER_NAMESPACE, + 'controller' => DEFAULT_CONTROLLER, 'verb' => 'get', ], 'login' => [ 'path' => '/login', 'action' => 'login', - 'controller' => DEFAULT_CONTROLLER_NAMESPACE, + 'controller' => DEFAULT_CONTROLLER, 'verb' => 'get', ], 'login.post' => [ 'path' => '/login', 'action' => 'loginAction', - 'controller' => DEFAULT_CONTROLLER_NAMESPACE, + 'controller' => DEFAULT_CONTROLLER, 'verb' => 'post', ], 'logout' => [ 'path' => '/logout', 'action' => 'logout', - 'controller' => DEFAULT_CONTROLLER_NAMESPACE, + 'controller' => DEFAULT_CONTROLLER, ], 'update' => [ 'path' => '/{controller}/update', @@ -204,7 +204,7 @@ return [ ], 'index_redirect' => [ 'path' => '/', - 'controller' => DEFAULT_CONTROLLER_NAMESPACE, + 'controller' => DEFAULT_CONTROLLER, 'action' => 'redirectToDefaultRoute', ], ]; \ No newline at end of file diff --git a/app/views/anime/details.php b/app/views/anime/details.php index 63232096..ea209ab0 100644 --- a/app/views/anime/details.php +++ b/app/views/anime/details.php @@ -1,4 +1,4 @@ -
+
@@ -74,9 +74,6 @@ - - - */ ?>
diff --git a/app/views/character.php b/app/views/character.php index 26ad4bc7..668a99ff 100644 --- a/app/views/character.php +++ b/app/views/character.php @@ -1,4 +1,4 @@ -
+
diff --git a/app/views/manga/details.php b/app/views/manga/details.php index 11790bfe..c883706e 100644 --- a/app/views/manga/details.php +++ b/app/views/manga/details.php @@ -1,4 +1,4 @@ -
+
<?= $data['title'] ?> cover image diff --git a/public/css.php b/public/css.php index 72870935..81e38ccc 100644 --- a/public/css.php +++ b/public/css.php @@ -14,7 +14,6 @@ * @link https://github.com/timw4mail/HummingBirdAnimeClient */ - namespace Aviat\EasyMin; require_once('./min.php'); diff --git a/public/css/base.css b/public/css/base.css index fe404c9e..de39c8b9 100644 --- a/public/css/base.css +++ b/public/css/base.css @@ -1250,13 +1250,16 @@ a:hover, a:active { .details { margin:15px auto 0 auto; margin: 1.5rem auto 0 auto; - max-width:930px; - max-width:93rem; padding:10px; padding:1rem; font-size:inherit; } +.details.fixed { + max-width:930px; + max-width:93rem; +} + .details .cover { display: block; width: 284px; @@ -1295,6 +1298,14 @@ a:hover, a:active { text-align:left; } +/* ---------------------------------------------------------------------------- + User page styles +-----------------------------------------------------------------------------*/ + +.small_character img { + max-width: 300px; +} + /* ---------------------------------------------------------------------------- Viewport-based styles -----------------------------------------------------------------------------*/ diff --git a/public/css/base.myth.css b/public/css/base.myth.css index 1179e421..679eb3df 100644 --- a/public/css/base.myth.css +++ b/public/css/base.myth.css @@ -510,11 +510,14 @@ a:hover, a:active { -----------------------------------------------------------------------------*/ .details { margin: 1.5rem auto 0 auto; - max-width:93rem; padding:1rem; font-size:inherit; } +.details.fixed { + max-width:93rem; +} + .details .cover { display: block; width: 284px; @@ -549,6 +552,14 @@ a:hover, a:active { text-align:left; } +/* ---------------------------------------------------------------------------- + User page styles +-----------------------------------------------------------------------------*/ +.small_character img { + max-width: 300px; +} + + /* ---------------------------------------------------------------------------- Viewport-based styles -----------------------------------------------------------------------------*/ diff --git a/src/API/JsonAPI.php b/src/API/JsonAPI.php index 7ca78704..105d377a 100644 --- a/src/API/JsonAPI.php +++ b/src/API/JsonAPI.php @@ -39,17 +39,212 @@ class JsonAPI { * @var array */ protected $data = []; - + + /** + * Inline all included data + * + * @param array $data - The raw JsonAPI response data + * @return data + */ + public static function organizeData(array $data): array + { + // relationships that have singular data + $singular = [ + 'waifu' + ]; + + // Reorganize included data + $included = static::organizeIncluded($data['included']); + + // Inline organized data + foreach($data['data'] as $i => $item) + { + if (array_key_exists('relationships', $item)) + { + foreach($item['relationships'] as $relType => $props) + { + + if (array_keys($props) === ['links']) + { + unset($data['data'][$i]['relationships'][$relType]); + + if (empty($data['data'][$i]['relationships'])) + { + unset($data['data'][$i]['relationships']); + } + + continue; + } + + if (array_key_exists('links', $props)) + { + unset($data['data'][$i]['relationships'][$relType]['links']); + } + + if (array_key_exists('data', $props)) + { + if (empty($props['data'])) + { + unset($data['data'][$i]['relationships'][$relType]['data']); + + if (empty($data['data'][$i]['relationships'][$relType])) + { + unset($data['data'][$i]['relationships'][$relType]); + } + + continue; + } + // Single data item + else if (array_key_exists('id', $props['data'])) + { + $idKey = $props['data']['id']; + $typeKey = $props['data']['type']; + $relationship =& $data['data'][$i]['relationships'][$relType]; + unset($relationship['data']); + + if (in_array($relType, $singular)) + { + $relationship = $included[$typeKey][$idKey]; + continue; + } + + if ($relType === $typeKey) + { + $relationship[$idKey] = $included[$typeKey][$idKey]; + continue; + } + + $relationship[$typeKey][$idKey] = $included[$typeKey][$idKey]; + } + // Multiple data items + else + { + foreach($props['data'] as $j => $datum) + { + $idKey = $props['data'][$j]['id']; + $typeKey = $props['data'][$j]['type']; + $relationship =& $data['data'][$i]['relationships'][$relType]; + + unset($relationship['data'][$j]); + + if (empty($relationship['data'])) + { + unset($relationship['data']); + } + + if ($relType === $typeKey) + { + $relationship[$idKey] = $included[$typeKey][$idKey]; + continue; + } + + $relationship[$typeKey][$idKey] = array_merge( + $included[$typeKey][$idKey], + $relationship[$typeKey][$idKey] ?? [] + ); + } + } + } + } + } + } + + return $data['data']; + } + + /** + * Restructure included data to make it simpler to inline + * + * @param array $included + * @return array + */ + public static function organizeIncluded(array $included): array + { + $organized = []; + + // First pass, create [ type => items[] ] structure + foreach($included as &$item) + { + $type = $item['type']; + $id = $item['id']; + $organized[$type] = $organized[$type] ?? []; + $newItem = []; + + foreach(['attributes', 'relationships'] as $key) + { + if (array_key_exists($key, $item)) + { + // Remove 'links' type relationships + if ($key === 'relationships') + { + foreach($item['relationships'] as $relType => $props) + { + if (array_keys($props) === ['links']) + { + unset($item['relationships'][$relType]); + if (empty($item['relationships'])) + { + continue 2; + } + } + } + } + + $newItem[$key] = $item[$key]; + } + } + + $organized[$type][$id] = $newItem; + } + + // Second pass, go through and fill missing relationships in the first pass + foreach($organized as $type => $items) + { + foreach($items as $id => $item) + { + if (array_key_exists('relationships', $item)) + { + foreach($item['relationships'] as $relType => $props) + { + if (array_key_exists('data', $props)) + { + if (array_key_exists($props['data']['id'], $organized[$props['data']['type']])) + { + $idKey = $props['data']['id']; + $typeKey = $props['data']['type']; + + + $relationship =& $organized[$type][$id]['relationships'][$relType]; + unset($relationship['links']); + unset($relationship['data']); + + if ($relType === $typeKey) + { + $relationship[$idKey] = $included[$typeKey][$idKey]; + continue; + } + + $relationship[$typeKey][$idKey] = $organized[$typeKey][$idKey]; + } + } + } + } + } + } + + return $organized; + } + public static function inlineRawIncludes(array &$data, string $key): array { foreach($data['data'] as $i => &$item) { $item[$key] = $data['included'][$i]; } - + return $data['data']; } - + /** * Take organized includes and inline them, where applicable * @@ -62,12 +257,12 @@ class JsonAPI { $inlined = [ $key => [] ]; - + foreach ($included[$key] as $itemId => $item) { // Duplicate the item for the output $inlined[$key][$itemId] = $item; - + foreach($item['relationships'] as $type => $ids) { $inlined[$key][$itemId]['relationships'][$type] = []; @@ -77,7 +272,7 @@ class JsonAPI { } } } - + return $inlined; } @@ -109,36 +304,16 @@ class JsonAPI { return $organized; } - + /** - * Reorganize 'included' data + * Reorganize 'included' data * * @param array $includes * @return array */ public static function lightlyOrganizeIncludes(array $includes): array { - $organized = []; - - foreach($includes as $item) - { - $type = $item['type']; - $id = $item['id']; - $organized[$type] = $organized[$type] ?? []; - $newItem = []; - - foreach(['attributes', 'relationships'] as $key) - { - if (array_key_exists($key, $item)) - { - $newItem[$key] = $item[$key]; - } - } - - $organized[$type][$id] = $newItem; - } - - return $organized; + return static::organizeIncluded($includes); } /** @@ -174,11 +349,11 @@ class JsonAPI { return $organized; } - + public static function fillRelationshipsFromIncludes(array $relationships, array $includes): array { $output = []; - + foreach ($relationships as $key => $block) { if (array_key_exists('data', $block) && is_array($block['data']) && ! empty($block['data'])) @@ -197,7 +372,7 @@ class JsonAPI { } } } - + return $output; } } \ No newline at end of file diff --git a/src/API/Kitsu/Model.php b/src/API/Kitsu/Model.php index 4e4ab2a8..cc7f80ea 100644 --- a/src/API/Kitsu/Model.php +++ b/src/API/Kitsu/Model.php @@ -164,7 +164,7 @@ class Model { $data = $this->getRequest('/characters', [ 'query' => [ 'filter' => [ - 'slug' => $slug + 'name' => $slug ], // 'include' => 'primaryMedia,castings' ] @@ -181,10 +181,17 @@ class Model { */ public function getUserData(string $username): array { - $userId = $this->getUserIdByUsername($username); - $data = $this->getRequest("/users/{$userId}", [ + // $userId = $this->getUserIdByUsername($username); + $data = $this->getRequest("/users", [ 'query' => [ - 'include' => 'waifu,pinnedPost,blocks,linkedAccounts,profileLinks,profileLinks.profileLinkSite,mediaFollows,userRoles' + 'filter' => [ + 'name' => $username, + ], + 'fields' => [ + // 'anime' => 'slug,name,canonicalTitle', + 'characters' => 'slug,name,image' + ], + 'include' => 'waifu,pinnedPost,blocks,linkedAccounts,profileLinks,profileLinks.profileLinkSite,mediaFollows,userRoles,favorites.item' ] ]); @@ -826,6 +833,9 @@ class Model { 'filter' => [ 'slug' => $slug ], + 'fields' => [ + 'characters' => 'slug,name,image' + ], 'include' => ($type === 'anime') ? 'genres,mappings,streamingLinks,animeCharacters.character' : 'genres,mappings,mangaCharacters.character,castings.character', diff --git a/src/AnimeClient.php b/src/AnimeClient.php index f1d07a15..f185e8a6 100644 --- a/src/AnimeClient.php +++ b/src/AnimeClient.php @@ -21,8 +21,9 @@ use Yosymfony\Toml\Toml; define('SRC_DIR', realpath(__DIR__)); const SESSION_SEGMENT = 'Aviat\AnimeClient\Auth'; +const DEFAULT_CONTROLLER = 'Aviat\AnimeClient\Controller\Index'; const DEFAULT_CONTROLLER_NAMESPACE = 'Aviat\AnimeClient\Controller'; -const DEFAULT_CONTROLLER = 'Aviat\AnimeClient\Controller\Anime'; +const DEFAULT_LIST_CONTROLLER = 'Aviat\AnimeClient\Controller\Anime'; const DEFAULT_CONTROLLER_METHOD = 'index'; const NOT_FOUND_METHOD = 'notFound'; const ERROR_MESSAGE_METHOD = 'errorPage'; diff --git a/src/Command/SyncKitsuWithMal.php b/src/Command/SyncKitsuWithMal.php index d2ba6081..706a4aaa 100644 --- a/src/Command/SyncKitsuWithMal.php +++ b/src/Command/SyncKitsuWithMal.php @@ -41,7 +41,7 @@ class SyncKitsuWithMal extends BaseCommand { * @var \Aviat\AnimeClient\API\Kitsu\Model */ protected $kitsuModel; - + /** * Model for making requests to MAL API * @var \Aviat\AnimeClient\API\MAL\Model @@ -82,7 +82,7 @@ class SyncKitsuWithMal extends BaseCommand { if ( ! empty($data['addToMAL'])) { $this->echoBox("Adding missing anime list items to MAL"); - $this->createMALAnimeListItems($data['addToMAL']); + $this->createMALListItems($data['addToMAL'], 'anime'); } $this->echoBox('Number of anime items that need to be added to Kitsu: ' . count($data['addToKitsu'])); @@ -90,7 +90,7 @@ class SyncKitsuWithMal extends BaseCommand { if ( ! empty($data['addToKitsu'])) { $this->echoBox("Adding missing anime list items to Kitsu"); - $this->createKitusAnimeListItems($data['addToKitsu']); + $this->createKitusListItems($data['addToKitsu'], 'anime'); } } @@ -109,7 +109,7 @@ class SyncKitsuWithMal extends BaseCommand { if ( ! empty($data['addToMAL'])) { $this->echoBox("Adding missing manga list items to MAL"); - $this->createMALMangaListItems($data['addToMAL']); + $this->createMALListItems($data['addToMAL'], 'manga'); } $this->echoBox('Number of manga items that need to be added to Kitsu: ' . count($data['addToKitsu'])); @@ -117,7 +117,7 @@ class SyncKitsuWithMal extends BaseCommand { if ( ! empty($data['addToKitsu'])) { $this->echoBox("Adding missing manga list items to Kitsu"); - $this->createKitsuMangaListItems($data['addToKitsu']); + $this->createKitsuListItems($data['addToKitsu'], 'manga'); } } @@ -177,6 +177,7 @@ class SyncKitsuWithMal extends BaseCommand { 'my_status' => $item['my_status'], 'status' => MangaReadingStatus::MAL_TO_KITSU[$item['my_status']], 'progress' => $item['my_read_chapters'], + 'volumes' => $item['my_read_volumes'], 'reconsuming' => (bool) $item['my_rereadingg'], /* 'reconsumeCount' => array_key_exists('times_rewatched', $item) ? $item['times_rewatched'] @@ -322,6 +323,8 @@ class SyncKitsuWithMal extends BaseCommand { $itemsToAddToMAL = []; $itemsToAddToKitsu = []; + $malUpdateItems = []; + $kitsuUpdateItems = []; $malIds = array_column($malList, 'id'); $kitsuMalIds = array_column($kitsuList, 'malId'); @@ -364,11 +367,13 @@ class SyncKitsuWithMal extends BaseCommand { return [ 'addToMAL' => $itemsToAddToMAL, - 'addToKitsu' => $itemsToAddToKitsu + 'updateMAL' => $malUpdateItems, + 'addToKitsu' => $itemsToAddToKitsu, + 'updateKitsu' => $kitsuUpdateItems ]; } - public function createKitsuMangaListItems($itemsToAdd) + public function createKitusAnimeListItems($itemsToAdd, $type = 'anime') { $requester = new ParallelAPIRequest(); foreach($itemsToAdd as $item) @@ -383,69 +388,17 @@ class SyncKitsuWithMal extends BaseCommand { $id = $itemsToAdd[$key]['id']; if ($response->getStatus() === 201) { - $this->echoBox("Successfully created Kitsu manga list item with id: {$id}"); + $this->echoBox("Successfully created Kitsu {$type} list item with id: {$id}"); } else { echo $response->getBody(); - $this->echoBox("Failed to create Kitsu manga list item with id: {$id}"); + $this->echoBox("Failed to create Kitsu {$type} list item with id: {$id}"); } } } - public function createMALMangaListItems($itemsToAdd) - { - $transformer = new MLT(); - $requester = new ParallelAPIRequest(); - - foreach($itemsToAdd as $item) - { - $data = $transformer->untransform($item); - $requester->addRequest($this->malModel->createFullListItem($data, 'manga')); - } - - $responses = $requester->makeRequests(); - - foreach($responses as $key => $response) - { - $id = $itemsToAdd[$key]['mal_id']; - if ($response->getBody() === 'Created') - { - $this->echoBox("Successfully created MAL manga list item with id: {$id}"); - } - else - { - $this->echoBox("Failed to create MAL manga list item with id: {$id}"); - } - } - } - - public function createKitusAnimeListItems($itemsToAdd) - { - $requester = new ParallelAPIRequest(); - foreach($itemsToAdd as $item) - { - $requester->addRequest($this->kitsuModel->createListItem($item)); - } - - $responses = $requester->makeRequests(); - - foreach($responses as $key => $response) - { - $id = $itemsToAdd[$key]['id']; - if ($response->getStatus() === 201) - { - $this->echoBox("Successfully created Kitsu anime list item with id: {$id}"); - } - else - { - echo $response->getBody(); - $this->echoBox("Failed to create Kitsu anime list item with id: {$id}"); - } - } - } - - public function createMALAnimeListItems($itemsToAdd) + public function createMALListItems($itemsToAdd, $type = 'anime') { $transformer = new ALT(); $requester = new ParallelAPIRequest(); @@ -453,7 +406,7 @@ class SyncKitsuWithMal extends BaseCommand { foreach($itemsToAdd as $item) { $data = $transformer->untransform($item); - $requester->addRequest($this->malModel->createFullListItem($data)); + $requester->addRequest($this->malModel->createFullListItem($data, $type)); } $responses = $requester->makeRequests(); @@ -463,11 +416,11 @@ class SyncKitsuWithMal extends BaseCommand { $id = $itemsToAdd[$key]['mal_id']; if ($response->getBody() === 'Created') { - $this->echoBox("Successfully created MAL anime list item with id: {$id}"); + $this->echoBox("Successfully created MAL {$type} list item with id: {$id}"); } else { - $this->echoBox("Failed to create MAL anime list item with id: {$id}"); + $this->echoBox("Failed to create MAL {$type} list item with id: {$id}"); } } } diff --git a/src/Controller.php b/src/Controller.php index 398011cb..21aea39b 100644 --- a/src/Controller.php +++ b/src/Controller.php @@ -31,7 +31,66 @@ use InvalidArgumentException; * @property Response object $response */ class Controller { - use ControllerTrait; + + use ContainerAware; + + /** + * Cache manager + * @var \Psr\Cache\CacheItemPoolInterface + */ + protected $cache; + + /** + * The global configuration object + * @var \Aviat\Ion\ConfigInterface $config + */ + public $config; + + /** + * Request object + * @var object $request + */ + protected $request; + + /** + * Response object + * @var object $response + */ + public $response; + + /** + * The api model for the current controller + * @var object + */ + protected $model; + + /** + * Url generation class + * @var UrlGenerator + */ + protected $urlGenerator; + + /** + * Aura url generator + * @var \Aura\Router\Generator + */ + protected $url; + + /** + * Session segment + * @var \Aura\Session\Segment + */ + protected $session; + + /** + * Common data to be sent to views + * @var array + */ + protected $baseData = [ + 'url_type' => 'anime', + 'other_type' => 'manga', + 'menu_name' => '' + ]; /** * Constructor @@ -73,81 +132,256 @@ class Controller { } /** - * Show the user profile page + * Redirect to the previous page * * @return void */ - public function me() + public function redirectToPrevious() { - $username = $this->config->get(['kitsu_username']); - $model = $this->container->get('kitsu-model'); - $data = $model->getUserData($username); - $included = JsonAPI::lightlyOrganizeIncludes($data['included']); - $relationships = JsonAPI::fillRelationshipsFromIncludes($data['data']['relationships'], $included); - $this->outputHTML('me', [ - 'title' => 'About' . $this->config->get('whose_list'), - 'attributes' => $data['data']['attributes'], - 'relationships' => $relationships, - 'included' => $included + $previous = $this->session->getFlash('previous'); + $this->redirect($previous, 303); + } + + /** + * Set the current url in the session as the target of a future redirect + * + * @param string|null $url + * @return void + */ + public function setSessionRedirect(string $url = NULL) + { + $serverParams = $this->request->getServerParams(); + + if ( ! array_key_exists('HTTP_REFERER', $serverParams)) + { + return; + } + + $util = $this->container->get('util'); + $doubleFormPage = $serverParams['HTTP_REFERER'] === $this->request->getUri(); + + // 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 page_segments + if ($doubleFormPage) + { + return; + } + + if (is_null($url)) + { + $url = $util->isViewPage() + ? $this->request->url->get() + : $serverParams['HTTP_REFERER']; + } + + $this->session->set('redirect_url', $url); + } + + /** + * Redirect to the url previously set in the session + * + * @return void + */ + public function sessionRedirect() + { + $target = $this->session->get('redirect_url'); + if (empty($target)) + { + $this->notFound(); + } + else + { + $this->redirect($target, 303); + $this->session->set('redirect_url', NULL); + } + } + + /** + * Get the string output of a partial template + * + * @param HtmlView $view + * @param string $template + * @param array $data + * @throws InvalidArgumentException + * @return string + */ + protected function loadPartial($view, string $template, array $data = []) + { + $router = $this->container->get('dispatcher'); + + if (isset($this->baseData)) + { + $data = array_merge($this->baseData, $data); + } + + $route = $router->getRoute(); + $data['route_path'] = $route ? $router->getRoute()->path : ''; + + + $templatePath = _dir($this->config->get('view_path'), "{$template}.php"); + + if ( ! is_file($templatePath)) + { + throw new InvalidArgumentException("Invalid template : {$template}"); + } + + return $view->renderTemplate($templatePath, (array)$data); + } + + /** + * Render a template with header and footer + * + * @param HtmlView $view + * @param string $template + * @param array $data + * @return void + */ + protected function renderFullPage($view, string $template, array $data) + { + $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'])); + } + + $view->appendOutput($this->loadPartial($view, $template, $data)); + $view->appendOutput($this->loadPartial($view, 'footer', $data)); + } + + /** + * 404 action + * + * @return void + */ + public function notFound( + string $title = 'Sorry, page not found', + string $message = 'Page Not Found' + ) + { + $this->outputHTML('404', [ + 'title' => $title, + 'message' => $message, + ], NULL, 404); + } + + /** + * Display a generic error page + * + * @param int $httpCode + * @param string $title + * @param string $message + * @param string $long_message + * @return void + */ + public function errorPage(int $httpCode, string $title, string $message, string $long_message = "") + { + $this->outputHTML('error', [ + 'title' => $title, + 'message' => $message, + 'long_message' => $long_message + ], NULL, $httpCode); + } + + /** + * Set a session flash variable to display a message on + * next page load + * + * @param string $message + * @param string $type + * @return void + */ + public function setFlashMessage(string $message, string $type = "info") + { + static $messages; + + if ( ! $messages) + { + $messages = []; + } + + $messages[] = [ + 'message_type' => $type, + 'message' => $message + ]; + + $this->session->setFlash('message', $messages); + } + + /** + * Helper for consistent page titles + * + * @param string ...$parts Title segements + * @return string + */ + public function formatTitle(string ...$parts) : string + { + return implode(' · ', $parts); + } + + /** + * Add a message box to the page + * + * @param HtmlView $view + * @param string $type + * @param string $message + * @return string + */ + protected function showMessage($view, string $type, string $message): string + { + return $this->loadPartial($view, 'message', [ + 'message_type' => $type, + 'message' => $message ]); } /** - * Show the login form + * Output a template to HTML, using the provided data * - * @param string $status + * @param string $template + * @param array $data + * @param HtmlView|null $view + * @param int $code * @return void */ - public function login(string $status = '') + protected function outputHTML(string $template, array $data = [], $view = NULL, int $code = 200) { - $message = ''; - - $view = new HtmlView($this->container); - - if ($status !== '') + if (is_null($view)) { - $message = $this->showMessage($view, 'error', $status); + $view = new HtmlView($this->container); } - // Set the redirect url - $this->setSessionRedirect(); - - $this->outputHTML('login', [ - 'title' => 'Api login', - 'message' => $message - ], $view); + $view->setStatusCode($code); + $this->renderFullPage($view, $template, $data); } /** - * Attempt login authentication + * Output a JSON Response * + * @param mixed $data + * @param int $code - the http status code * @return void */ - public function loginAction() + protected function outputJSON($data = 'Empty response', int $code = 200) { - $auth = $this->container->get('auth'); - $post = $this->request->getParsedBody(); - if ($auth->authenticate($post['password'])) - { - $this->sessionRedirect(); - return; - } - - $this->setFlashMessage('Invalid username or password.'); - $this->redirect($this->url->generate('login'), 303); + (new JsonView($this->container)) + ->setStatusCode($code) + ->setOutput($data) + ->send(); } /** - * Deauthorize the current user + * Redirect to the selected page * + * @param string $url + * @param int $code * @return void */ - public function logout() + protected function redirect(string $url, int $code) { - $auth = $this->container->get('auth'); - $auth->logout(); - - $this->redirectToDefaultRoute(); + $http = new HttpView($this->container); + $http->redirect($url, $code); } } // End of BaseController.php \ No newline at end of file diff --git a/src/Controller/Index.php b/src/Controller/Index.php new file mode 100644 index 00000000..9400e954 --- /dev/null +++ b/src/Controller/Index.php @@ -0,0 +1,149 @@ + + * @copyright 2015 - 2017 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 4.0 + * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient + */ + +namespace Aviat\AnimeClient\Controller; + +use Aviat\AnimeClient\Controller as BaseController; +use Aviat\AnimeClient\API\JsonAPI; +use Aviat\Ion\View\HtmlView; + +class Index extends BaseController { + + /** + * Purges the API cache + * + * @return void + */ + public function clearCache() + { + $this->cache->clear(); + $this->outputHTML('blank', [ + 'title' => 'Cache cleared' + ], NULL, 200); + } + + /** + * Show the login form + * + * @param string $status + * @return void + */ + public function login(string $status = '') + { + $message = ''; + + $view = new HtmlView($this->container); + + if ($status !== '') + { + $message = $this->showMessage($view, 'error', $status); + } + + // Set the redirect url + $this->setSessionRedirect(); + + $this->outputHTML('login', [ + 'title' => 'Api login', + 'message' => $message + ], $view); + } + + /** + * Attempt login authentication + * + * @return void + */ + public function loginAction() + { + $auth = $this->container->get('auth'); + $post = $this->request->getParsedBody(); + if ($auth->authenticate($post['password'])) + { + $this->sessionRedirect(); + return; + } + + $this->setFlashMessage('Invalid username or password.'); + $this->redirect($this->url->generate('login'), 303); + } + + /** + * Deauthorize the current user + * + * @return void + */ + public function logout() + { + $auth = $this->container->get('auth'); + $auth->logout(); + + $this->redirectToDefaultRoute(); + } + + /** + * Show the user profile page + * + * @return void + */ + public function me() + { + $username = $this->config->get(['kitsu_username']); + $model = $this->container->get('kitsu-model'); + $data = $model->getUserData($username); + $orgData = JsonAPI::organizeData($data); + $this->outputHTML('me', [ + 'title' => 'About' . $this->config->get('whose_list'), + 'data' => $orgData[0], + 'attributes' => $orgData[0]['attributes'], + 'relationships' => $orgData[0]['relationships'], + 'favorites' => $this->organizeFavorites($orgData[0]['relationships']['favorites']), + ]); + } + + /** + * Redirect to the default controller/url from an empty path + * + * @return void + */ + public function redirectToDefaultRoute() + { + $defaultType = $this->config->get(['routes', 'route_config', 'default_list']); + $this->redirect($this->urlGenerator->defaultUrl($defaultType), 303); + } + + private function organizeFavorites(array $rawfavorites): array + { + // return $rawfavorites; + $output = []; + + foreach($rawfavorites as $item) + { + $rank = $item['attributes']['favRank']; + foreach($item['relationships']['item'] as $key => $fav) + { + $output[$key] = $output[$key] ?? []; + foreach ($fav as $id => $data) + { + $output[$key][$rank] = $data['attributes']; + } + } + + ksort($output[$key]); + } + + return $output; + } +} \ No newline at end of file diff --git a/src/ControllerTrait.php b/src/ControllerTrait.php deleted file mode 100644 index 7f2f4310..00000000 --- a/src/ControllerTrait.php +++ /dev/null @@ -1,384 +0,0 @@ - - * @copyright 2015 - 2017 Timothy J. Warren - * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 - * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient - */ - -namespace Aviat\AnimeClient; - -use const Aviat\AnimeClient\SESSION_SEGMENT; - -use function Aviat\Ion\_dir; - -use Aviat\AnimeClient\API\JsonAPI; -use Aviat\Ion\Di\{ContainerAware, ContainerInterface}; -use Aviat\Ion\View\{HtmlView, HttpView, JsonView}; -use InvalidArgumentException; - -trait ControllerTrait { - - use ContainerAware; - - /** - * Cache manager - * @var \Psr\Cache\CacheItemPoolInterface - */ - protected $cache; - - /** - * The global configuration object - * @var \Aviat\Ion\ConfigInterface $config - */ - protected $config; - - /** - * Request object - * @var object $request - */ - protected $request; - - /** - * Response object - * @var object $response - */ - protected $response; - - /** - * The api model for the current controller - * @var object - */ - protected $model; - - /** - * Url generation class - * @var UrlGenerator - */ - protected $urlGenerator; - - /** - * Aura url generator - * @var \Aura\Router\Generator - */ - protected $url; - - /** - * Session segment - * @var \Aura\Session\Segment - */ - protected $session; - - /** - * Common data to be sent to views - * @var array - */ - protected $baseData = [ - 'url_type' => 'anime', - 'other_type' => 'manga', - 'menu_name' => '' - ]; - - /** - * Redirect to the default controller/url from an empty path - * - * @return void - */ - public function redirectToDefaultRoute() - { - $defaultType = $this->config->get(['routes', 'route_config', 'default_list']); - $this->redirect($this->urlGenerator->defaultUrl($defaultType), 303); - } - - /** - * Redirect to the previous page - * - * @return void - */ - public function redirectToPrevious() - { - $previous = $this->session->getFlash('previous'); - $this->redirect($previous, 303); - } - - /** - * Set the current url in the session as the target of a future redirect - * - * @param string|null $url - * @return void - */ - public function setSessionRedirect($url = NULL) - { - $serverParams = $this->request->getServerParams(); - - if ( ! array_key_exists('HTTP_REFERER', $serverParams)) - { - return; - } - - $util = $this->container->get('util'); - $doubleFormPage = $serverParams['HTTP_REFERER'] === $this->request->getUri(); - - // 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 page_segments - if ($doubleFormPage) - { - return; - } - - if (is_null($url)) - { - $url = $util->isViewPage() - ? $this->request->url->get() - : $serverParams['HTTP_REFERER']; - } - - $this->session->set('redirect_url', $url); - } - - /** - * Redirect to the url previously set in the session - * - * @return void - */ - public function sessionRedirect() - { - $target = $this->session->get('redirect_url'); - if (empty($target)) - { - $this->notFound(); - } - else - { - $this->redirect($target, 303); - $this->session->set('redirect_url', NULL); - } - } - - /** - * Get a class member - * - * @param string $key - * @return mixed - */ - public function __get(string $key) - { - $allowed = ['response', 'config']; - - if (in_array($key, $allowed)) - { - return $this->$key; - } - - return NULL; - } - - /** - * Get the string output of a partial template - * - * @param HtmlView $view - * @param string $template - * @param array $data - * @throws InvalidArgumentException - * @return string - */ - protected function loadPartial($view, $template, array $data = []) - { - $router = $this->container->get('dispatcher'); - - if (isset($this->baseData)) - { - $data = array_merge($this->baseData, $data); - } - - $route = $router->getRoute(); - $data['route_path'] = $route ? $router->getRoute()->path : ''; - - - $templatePath = _dir($this->config->get('view_path'), "{$template}.php"); - - if ( ! is_file($templatePath)) - { - throw new InvalidArgumentException("Invalid template : {$template}"); - } - - return $view->renderTemplate($templatePath, (array)$data); - } - - /** - * Render a template with header and footer - * - * @param HtmlView $view - * @param string $template - * @param array $data - * @return void - */ - protected function renderFullPage($view, $template, array $data) - { - $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'])); - } - - $view->appendOutput($this->loadPartial($view, $template, $data)); - $view->appendOutput($this->loadPartial($view, 'footer', $data)); - } - - /** - * 404 action - * - * @return void - */ - public function notFound( - string $title = 'Sorry, page not found', - string $message = 'Page Not Found' - ) - { - $this->outputHTML('404', [ - 'title' => $title, - 'message' => $message, - ], NULL, 404); - } - - /** - * Display a generic error page - * - * @param int $httpCode - * @param string $title - * @param string $message - * @param string $long_message - * @return void - */ - public function errorPage($httpCode, $title, $message, $long_message = "") - { - $this->outputHTML('error', [ - 'title' => $title, - 'message' => $message, - 'long_message' => $long_message - ], NULL, $httpCode); - } - - /** - * Set a session flash variable to display a message on - * next page load - * - * @param string $message - * @param string $type - * @return void - */ - public function setFlashMessage($message, $type = "info") - { - static $messages; - - if ( ! $messages) - { - $messages = []; - } - - $messages[] = [ - 'message_type' => $type, - 'message' => $message - ]; - - $this->session->setFlash('message', $messages); - } - - /** - * Purges the API cache - * - * @return void - */ - public function clearCache() - { - $this->cache->clear(); - $this->outputHTML('blank', [ - 'title' => 'Cache cleared' - ], NULL, 200); - } - - /** - * Helper for consistent page titles - * - * @param string ...$parts Title segements - * @return string - */ - public function formatTitle(string ...$parts) : string - { - return implode(' · ', $parts); - } - - /** - * Add a message box to the page - * - * @param HtmlView $view - * @param string $type - * @param string $message - * @return string - */ - protected function showMessage($view, $type, $message) - { - return $this->loadPartial($view, 'message', [ - 'message_type' => $type, - 'message' => $message - ]); - } - - /** - * Output a template to HTML, using the provided data - * - * @param string $template - * @param array $data - * @param HtmlView|null $view - * @param int $code - * @return void - */ - protected function outputHTML($template, array $data = [], $view = NULL, $code = 200) - { - if (is_null($view)) - { - $view = new HtmlView($this->container); - } - - $view->setStatusCode($code); - $this->renderFullPage($view, $template, $data); - } - - /** - * Output a JSON Response - * - * @param mixed $data - * @param int $code - the http status code - * @return void - */ - protected function outputJSON($data = 'Empty response', int $code = 200) - { - (new JsonView($this->container)) - ->setStatusCode($code) - ->setOutput($data) - ->send(); - } - - /** - * Redirect to the selected page - * - * @param string $url - * @param int $code - * @return void - */ - protected function redirect($url, $code) - { - $http = new HttpView($this->container); - $http->redirect($url, $code); - } -} \ No newline at end of file