({$item['alternate_title']})" : ""; ?> -
= $name ?>
-({$item['alternate_title']})" : ""; ?> -
= $name ?>
+({$item['alternate_title']})" : ""; ?> +
diff --git a/app/base/BaseApiModel.php b/app/base/BaseApiModel.php index 38934cfa..6458fdb0 100644 --- a/app/base/BaseApiModel.php +++ b/app/base/BaseApiModel.php @@ -1,4 +1,7 @@ get_route(); $data['route_path'] = ($route) ? $router->get_route()->path : ""; - $path = _dir(APP_DIR, 'views', "{$template}.php"); + $defaultHandler->addDataTable('Template Data', $data); - if ( ! is_file($path)) + $template_path = _dir(APP_DIR, 'views', "{$template}.php"); + + if ( ! is_file($template_path)) { throw new Exception("Invalid template : {$path}"); + die(); } ob_start(); extract($data); include _dir(APP_DIR, 'views', 'header.php'); - include $path; + include $template_path; + include _dir(APP_DIR, 'views', 'footer.php'); $buffer = ob_get_contents(); ob_end_clean(); @@ -55,7 +62,7 @@ class BaseController { /** * Output json with the proper content type * - * @param mixed data + * @param mixed $data * @return void */ public function outputJSON($data) @@ -68,5 +75,27 @@ class BaseController { header("Content-type: application/json"); echo $data; } + + /** + * Redirect to the selected page + * + * @param string $url + * @param int $code + * @return void + */ + public function redirect($url, $code, $type="anime") + { + $url = full_url($url, $type); + + $codes = [ + 301 => 'Moved Permanently', + 302 => 'Found', + 303 => 'See Other' + ]; + + header("HTTP/1.1 {$code} {$codes[$code]}"); + header("Location: {$url}"); + die(); + } } // End of BaseController.php \ No newline at end of file diff --git a/app/base/BaseDBModel.php b/app/base/BaseDBModel.php index a4480090..174eae35 100644 --- a/app/base/BaseDBModel.php +++ b/app/base/BaseDBModel.php @@ -1,4 +1,7 @@ 3) return $api_path; $cached_image = "{$series_slug}.{$ext}"; $cached_path = "{$this->config->img_cache_path}/{$type}/{$cached_image}"; diff --git a/app/base/Router.php b/app/base/Router.php index a1c41940..b7704c6b 100644 --- a/app/base/Router.php +++ b/app/base/Router.php @@ -1,4 +1,7 @@ params['controller']; $action_method = $route->params['action']; $params = (isset($route->params['params'])) ? $route->params['params'] : []; + + if ( ! empty($route->tokens)) + { + foreach($route->tokens as $key => $v) + { + if (array_key_exists($key, $route->params)) + { + $params[$key] = $route->params[$key]; + } + } + } } $controller = new $controller_name(); // Run the appropriate controller method + $defaultHandler->addDataTable('controller_args', $params); call_user_func_array([$controller, $action_method], $params); } @@ -123,7 +138,20 @@ class Router { { $path = $route['path']; unset($route['path']); - $this->router->add($name, $path)->addValues($route); + + if ( ! array_key_exists('tokens', $route)) + { + $this->router->add($name, $path)->addValues($route); + } + else + { + $tokens = $route['tokens']; + unset($route['tokens']); + + $this->router->add($name, $path) + ->addValues($route) + ->addTokens($tokens); + } } } } diff --git a/app/base/functions.php b/app/base/functions.php index eded0de8..bc326bd6 100644 --- a/app/base/functions.php +++ b/app/base/functions.php @@ -17,25 +17,50 @@ function is_selected($a, $b) } /** - * Generate full url path from the route path based on config + * Inverse of selected helper function * - * @param string $path - The route path - * @param [string] $host - The controller (anime or manga), defaults to anime + * @param string $a - First item to compare + * @param string $b - Second item to compare * @return string */ -function full_url($path, $type="anime") +function is_not_selected($a, $b) +{ + return ($a !== $b) ? 'selected' : ''; +} + +/** + * Generate full url path from the route path based on config + * + * @param string $path - (optional) The route path + * @param string $type - (optional) The controller (anime or manga), defaults to anime + * @return string + */ +function full_url($path="", $type="anime") { global $config; $config_path = $config->{"{$type}_path"}; $config_host = $config->{"{$type}_host"}; + $config_default_route = $config->{"default_{$type}_path"}; // Remove beginning/trailing slashes $config_path = trim($config_path, '/'); $path = trim($path, '/'); + // Remove any optional parameters from the route + $path = preg_replace('`{/.*?}`i', '', $path); + + // Set the default view + if ($path === '') + { + $path .= trim($config_default_route, '/'); + if ($config->default_to_list_view) $path .= '/list'; + } + + // Set the appropriate HTTP host $host = ($config_host !== '') ? $config_host : $_SERVER['HTTP_HOST']; + // Set an leading folder if ($config_path !== '') { $path = "{$config_path}/{$path}"; @@ -44,4 +69,16 @@ function full_url($path, $type="anime") return "//{$host}/{$path}"; } +/** + * Get the last segment of the current url + * + * @return string + */ +function last_segment() +{ + $path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); + $segments = explode('/', $path); + return end($segments); +} + // End of functions.php \ No newline at end of file diff --git a/app/config/config.php b/app/config/config.php index 748300d6..2d4f409c 100644 --- a/app/config/config.php +++ b/app/config/config.php @@ -3,6 +3,10 @@ return (object)[ // Username for feeds 'hummingbird_username' => 'timw4mail', + // Included config files + 'routes' => require _dir(CONF_DIR, 'routes.php'), + 'database' => require _dir(CONF_DIR, 'database.php'), + // ---------------------------------------------------------------------------- // Routing // @@ -15,11 +19,14 @@ return (object)[ 'anime_path' => '', 'manga_path' => '', + // Default pages for anime/manga + 'default_anime_path' => '/watching', + 'default_manga_path' => '/all', + + // Default to list view? + 'default_to_list_view' => FALSE, + // Cache paths 'data_cache_path' => _dir(APP_DIR, 'cache'), 'img_cache_path' => _dir(ROOT_DIR, 'public/images'), - - // Included config files - 'routes' => require _dir(CONF_DIR, 'routes.php'), - 'database' => require _dir(CONF_DIR, 'database.php'), ]; \ No newline at end of file diff --git a/app/config/routes.php b/app/config/routes.php index f2d9916b..e164aa80 100644 --- a/app/config/routes.php +++ b/app/config/routes.php @@ -2,120 +2,178 @@ return [ 'anime' => [ + 'index' => [ + 'path' => '/', + 'controller' => 'AnimeController', + 'action' => 'redirect', + 'params' => [ + 'url' => '', // Determined by config + 'code' => '301' + ] + ], 'all' => [ - 'path' => '/all', + 'path' => '/all{/view}', 'controller' => 'AnimeController', 'action' => 'anime_list', 'params' => [ 'type' => 'all', - 'title' => WHOSE . " Anime List · All" + 'title' => WHOSE . " Anime List · All" + ], + 'tokens' => [ + 'view' => '[a-z_]+' ] ], - 'index' => [ - 'path' => '/', + 'watching' => [ + 'path' => '/watching{/view}', 'controller' => 'AnimeController', 'action' => 'anime_list', 'params' => [ 'type' => 'currently-watching', - 'title' => WHOSE . " Anime List · Watching" + 'title' => WHOSE . " Anime List · Watching" + ], + 'tokens' => [ + 'view' => '[a-z_]+' ] ], 'plan_to_watch' => [ - 'path' => '/plan_to_watch', + 'path' => '/plan_to_watch{/view}', 'controller' => 'AnimeController', 'action' => 'anime_list', 'params' => [ 'type' => 'plan-to-watch', - 'title' => WHOSE . " Anime List · Plan to Watch" + 'title' => WHOSE . " Anime List · Plan to Watch" + ], + 'tokens' => [ + 'view' => '[a-z_]+' ] ], 'on_hold' => [ - 'path' => '/on_hold', + 'path' => '/on_hold{/view}', 'controller' => 'AnimeController', 'action' => 'anime_list', 'params' => [ 'type' => 'on-hold', - 'title' => WHOSE . " Anime List · On Hold" + 'title' => WHOSE . " Anime List · On Hold" + ], + 'tokens' => [ + 'view' => '[a-z_]+' ] ], 'dropped' => [ - 'path' => '/dropped', + 'path' => '/dropped{/view}', 'controller' => 'AnimeController', 'action' => 'anime_list', 'params' => [ 'type' => 'dropped', - 'title' => WHOSE . " Anime List · Dropped" + 'title' => WHOSE . " Anime List · Dropped" + ], + 'tokens' => [ + 'view' => '[a-z_]+' ] ], 'completed' => [ - 'path' => '/completed', + 'path' => '/completed{/view}', 'controller' => 'AnimeController', 'action' => 'anime_list', 'params' => [ 'type' => 'completed', - 'title' => WHOSE . " Anime List · Completed" + 'title' => WHOSE . " Anime List · Completed" + ], + 'tokens' => [ + 'view' => '[a-z_]+' ] ], 'collection' => [ - 'path' => '/collection', + 'path' => '/collection{/view}', 'controller' => 'AnimeController', 'action' => 'collection', - 'params' => [] + 'params' => [], + 'tokens' => [ + 'view' => '[a-z_]+' + ] ] ], 'manga' => [ + 'index' => [ + 'path' => '/', + 'controller' => 'MangaController', + 'action' => 'redirect', + 'params' => [ + 'url' => '', // Determined by config + 'code' => '301', + 'type' => 'manga' + ] + ], 'all' => [ - 'path' => '/all', + 'path' => '/all{/view}', 'controller' => 'MangaController', 'action' => 'manga_list', 'params' => [ 'type' => 'all', 'title' => WHOSE . " Manga List · All" + ], + 'tokens' => [ + 'view' => '[a-z_]+' ] ], - 'index' => [ - 'path' => '/', + 'reading' => [ + 'path' => '/reading{/view}', 'controller' => 'MangaController', 'action' => 'manga_list', 'params' => [ 'type' => 'Reading', 'title' => WHOSE . " Manga List · Reading" + ], + 'tokens' => [ + 'view' => '[a-z_]+' ] ], 'plan_to_read' => [ - 'path' => '/plan_to_read', + 'path' => '/plan_to_read{/view}', 'controller' => 'MangaController', 'action' => 'manga_list', 'params' => [ 'type' => 'Plan to Read', 'title' => WHOSE . " Manga List · Plan to Read" + ], + 'tokens' => [ + 'view' => '[a-z_]+' ] ], 'on_hold' => [ - 'path' => '/on_hold', + 'path' => '/on_hold{/view}', 'controller' => 'MangaController', 'action' => 'manga_list', 'params' => [ 'type' => 'On Hold', 'title' => WHOSE . " Manga List · On Hold" + ], + 'tokens' => [ + 'view' => '[a-z_]+' ] ], 'dropped' => [ - 'path' => '/dropped', + 'path' => '/dropped{/view}', 'controller' => 'MangaController', 'action' => 'manga_list', 'params' => [ 'type' => 'Dropped', 'title' => WHOSE . " Manga List · Dropped" + ], + 'tokens' => [ + 'view' => '[a-z_]+' ] ], 'completed' => [ - 'path' => '/completed', + 'path' => '/completed{/view}', 'controller' => 'MangaController', 'action' => 'manga_list', 'params' => [ 'type' => 'Completed', 'title' => WHOSE . " Manga List · Completed" + ], + 'tokens' => [ + 'view' => '[a-z_]+' ] ], ] diff --git a/app/controllers/AnimeController.php b/app/controllers/AnimeController.php index 334d9238..7573e279 100644 --- a/app/controllers/AnimeController.php +++ b/app/controllers/AnimeController.php @@ -1,4 +1,7 @@ '/', - 'Plan to Watch' => '/plan_to_watch', - 'On Hold' => '/on_hold', - 'Dropped' => '/dropped', - 'Completed' => '/completed', - 'Collection' => '/collection', - 'All' => '/all' + 'Watching' => '/watching{/view}', + 'Plan to Watch' => '/plan_to_watch{/view}', + 'On Hold' => '/on_hold{/view}', + 'Dropped' => '/dropped{/view}', + 'Completed' => '/completed{/view}', + 'Collection' => '/collection{/view}', + 'All' => '/all{/view}' ]; /** @@ -39,6 +48,12 @@ class AnimeController extends BaseController { parent::__construct(); $this->model = new AnimeModel(); $this->collection_model = new AnimeCollectionModel(); + + $this->base_data = [ + 'url_type' => 'anime', + 'other_type' => 'manga', + 'nav_routes' => $this->nav_routes, + ]; } /** @@ -48,17 +63,21 @@ class AnimeController extends BaseController { * @param string $title - The title of the page * @return void */ - public function anime_list($type, $title) + public function anime_list($type, $title, $view) { + $view_map = [ + '' => 'cover', + 'list' => 'list' + ]; + $data = ($type != 'all') ? $this->model->get_list($type) : $this->model->get_all_lists(); - $this->outputHTML('anime/list', [ + $this->outputHTML('anime/' . $view_map[$view], array_merge($this->base_data, [ 'title' => $title, - 'nav_routes' => $this->nav_routes, 'sections' => $data - ]); + ])); } /** @@ -66,15 +85,19 @@ class AnimeController extends BaseController { * * @return void */ - public function collection() + public function collection($view) { + $view_map = [ + '' => 'collection', + 'list' => 'collection_list' + ]; + $data = $this->collection_model->get_collection(); - $this->outputHTML('anime/collection', [ + $this->outputHTML('anime/' . $view_map[$view], array_merge($this->base_data, [ 'title' => WHOSE . " Anime Collection", - 'nav_routes' => $this->nav_routes, 'sections' => $data - ]); + ])); } } // End of AnimeController.php \ No newline at end of file diff --git a/app/controllers/MangaController.php b/app/controllers/MangaController.php index 991936f2..8039fd07 100644 --- a/app/controllers/MangaController.php +++ b/app/controllers/MangaController.php @@ -1,56 +1,67 @@ - '/', - 'Plan to Read' => '/plan_to_read', - 'On Hold' => '/on_hold', - 'Dropped' => '/dropped', - 'Completed' => '/completed', - 'All' => '/all' - ]; - - /** - * Constructor - */ - public function __construct() - { - parent::__construct(); - $this->model = new MangaModel(); - } - - /** - * Get a section of the manga list - * - * @param string $status - * @param string $title - * @return void - */ - public function manga_list($status, $title) - { - $data = ($status !== 'all') - ? [$status => $this->model->get_list($status)] - : $this->model->get_all_lists(); - - $this->outputHTML('manga/list', [ - 'title' => $title, - 'nav_routes' => $this->nav_routes, - 'sections' => $data - ]); - } -} + '/reading{/view}', + 'Plan to Read' => '/plan_to_read{/view}', + 'On Hold' => '/on_hold{/view}', + 'Dropped' => '/dropped{/view}', + 'Completed' => '/completed{/view}', + 'All' => '/all{/view}' + ]; + + /** + * Constructor + */ + public function __construct() + { + parent::__construct(); + $this->model = new MangaModel(); + } + + /** + * Get a section of the manga list + * + * @param string $status + * @param string $title + * @param string $view + * @return void + */ + public function manga_list($status, $title, $view) + { + $view_map = [ + '' => 'cover', + 'list' => 'list' + ]; + + $data = ($status !== 'all') + ? [$status => $this->model->get_list($status)] + : $this->model->get_all_lists(); + + $this->outputHTML('manga/' . $view_map[$view], [ + 'url_type' => 'manga', + 'other_type' => 'anime', + 'title' => $title, + 'nav_routes' => $this->nav_routes, + 'sections' => $data + ]); + } +} // End of MangaController.php \ No newline at end of file diff --git a/app/models/AnimeCollectionModel.php b/app/models/AnimeCollectionModel.php index 2627044b..061ac8d9 100644 --- a/app/models/AnimeCollectionModel.php +++ b/app/models/AnimeCollectionModel.php @@ -1,4 +1,7 @@ _get_list(); - - foreach ($data as $key => &$val) - { - $this->sort_by_name($val); - } - - return $data; - } - - /** - * Get a category out of the full list - * - * @param string $status - * @return array - */ - public function get_list($status) - { - $data = $this->_get_list($status); - - $this->sort_by_name($data); - - return $data; - } - - /** - * Massage the list of manga entries into something more usable - * - * @param string $status - * @return array - */ - private function _get_list($status="all") - { - global $defaultHandler; - - $cache_file = _dir($this->config->data_cache_path, 'manga.json'); - - $config = [ - 'query' => [ - 'user_id' => $this->config->hummingbird_username - ], - 'allow_redirects' => false - ]; - - $response = $this->client->get($this->_url('/manga_library_entries'), $config); - - $defaultHandler->addDataTable('response', (array)$response); - - if ($response->getStatusCode() != 200) - { - if ( ! file_exists($cache_file)) - { - throw new Exception($response->getEffectiveUrl()); - } - else - { - $raw_data = json_decode(file_get_contents($cache_file), TRUE); - } - } - else - { - // Reorganize data to be more usable - $raw_data = $response->json(); - - // Cache data in case of downtime - file_put_contents($cache_file, json_encode($raw_data)); - } - - $data = [ - 'Reading' => [], - 'Plan to Read' => [], - 'On Hold' => [], - 'Dropped' => [], - 'Completed' => [], - ]; - $manga_data = []; - - // Massage the two lists into one - foreach($raw_data['manga'] as $manga) - { - $manga_data[$manga['id']] = $manga; - } - - // Filter data by status - foreach($raw_data['manga_library_entries'] as &$entry) - { - $entry['manga'] = $manga_data[$entry['manga_id']]; - - // Cache poster images - $entry['manga']['poster_image'] = $this->get_cached_image($entry['manga']['poster_image'], $entry['manga_id'], 'manga'); - - switch($entry['status']) - { - case "Plan to Read": - $data['Plan to Read'][] = $entry; - break; - - case "Dropped": - $data['Dropped'][] = $entry; - break; - - case "On Hold": - $data['On Hold'][] = $entry; - break; - - case "Currently Reading": - $data['Reading'][] = $entry; - break; - - case "Completed": - default: - $data['Completed'][] = $entry; - break; - } - } - - return (array_key_exists($status, $data)) ? $data[$status] : $data; - } - - /** - * Sort the manga entries by their title - * - * @param array $array - * @return void - */ - private function sort_by_name(&$array) - { - $sort = array(); - - foreach($array as $key => $item) - { - $sort[$key] = $item['manga']['romaji_title']; - } - - array_multisort($sort, SORT_ASC, $array); - } -} +_get_list(); + + foreach ($data as $key => &$val) + { + $this->sort_by_name($val); + } + + return $data; + } + + /** + * Get a category out of the full list + * + * @param string $status + * @return array + */ + public function get_list($status) + { + $data = $this->_get_list($status); + + $this->sort_by_name($data); + + return $data; + } + + /** + * Massage the list of manga entries into something more usable + * + * @param string $status + * @return array + */ + private function _get_list($status="all") + { + global $defaultHandler; + + $cache_file = _dir($this->config->data_cache_path, 'manga.json'); + + $config = [ + 'query' => [ + 'user_id' => $this->config->hummingbird_username + ], + 'allow_redirects' => false + ]; + + $response = $this->client->get($this->_url('/manga_library_entries'), $config); + + $defaultHandler->addDataTable('response', (array)$response); + + if ($response->getStatusCode() != 200) + { + if ( ! file_exists($cache_file)) + { + throw new Exception($response->getEffectiveUrl()); + } + else + { + $raw_data = json_decode(file_get_contents($cache_file), TRUE); + } + } + else + { + // Reorganize data to be more usable + $raw_data = $response->json(); + + // Cache data in case of downtime + file_put_contents($cache_file, json_encode($raw_data)); + } + + $data = [ + 'Reading' => [], + 'Plan to Read' => [], + 'On Hold' => [], + 'Dropped' => [], + 'Completed' => [], + ]; + $manga_data = []; + + // Massage the two lists into one + foreach($raw_data['manga'] as $manga) + { + $manga_data[$manga['id']] = $manga; + } + + // Filter data by status + foreach($raw_data['manga_library_entries'] as &$entry) + { + $entry['manga'] = $manga_data[$entry['manga_id']]; + + // Cache poster images + $entry['manga']['poster_image'] = $this->get_cached_image($entry['manga']['poster_image'], $entry['manga_id'], 'manga'); + + switch($entry['status']) + { + case "Plan to Read": + $data['Plan to Read'][] = $entry; + break; + + case "Dropped": + $data['Dropped'][] = $entry; + break; + + case "On Hold": + $data['On Hold'][] = $entry; + break; + + case "Currently Reading": + $data['Reading'][] = $entry; + break; + + case "Completed": + default: + $data['Completed'][] = $entry; + break; + } + } + + return (array_key_exists($status, $data)) ? $data[$status] : $data; + } + + /** + * Sort the manga entries by their title + * + * @param array $array + * @return void + */ + private function sort_by_name(&$array) + { + $sort = array(); + + foreach($array as $key => $item) + { + $sort[$key] = $item['manga']['romaji_title']; + } + + array_multisort($sort, SORT_ASC, $array); + } +} // End of MangaModel.php \ No newline at end of file diff --git a/app/views/anime/collection.php b/app/views/anime/collection.php index 9e9cf143..8faec9e6 100644 --- a/app/views/anime/collection.php +++ b/app/views/anime/collection.php @@ -1,32 +1,27 @@ -
-