diff --git a/app/base/BaseController.php b/app/base/BaseController.php index 28adef0b..38dca981 100644 --- a/app/base/BaseController.php +++ b/app/base/BaseController.php @@ -54,6 +54,7 @@ class BaseController { public function __construct(Config &$config, Array $web) { $this->config = $config; + $this->base_data['config'] = $config; list($request, $response) = $web; $this->request = $request; @@ -70,6 +71,24 @@ class BaseController { $this->output(); } + /** + * Get a class member + * + * @param string $key + * @return object + */ + public function __get($key) + { + $allowed = ['request', 'response', 'config']; + + if (in_array($key, $allowed)) + { + return $this->$key; + } + + return NULL; + } + /** * Get the string output of a partial template * @@ -95,7 +114,7 @@ class BaseController { if ( ! is_file($template_path)) { - throw new Exception("Invalid template : {$path}"); + throw new InvalidArgumentException("Invalid template : {$path}"); } ob_start(); @@ -152,16 +171,9 @@ class BaseController { */ public function redirect($url, $code, $type="anime") { - $url = full_url($url, $type); + $url = $this->config->full_url($url, $type); - $codes = [ - 301 => 'Moved Permanently', - 302 => 'Found', - 303 => 'See Other' - ]; - - header("HTTP/1.1 {$code} {$codes[$code]}"); - header("Location: {$url}"); + $this->response->redirect->to($url, $code); } /** @@ -189,7 +201,7 @@ class BaseController { public function logout() { session_destroy(); - $this->response->redirect->seeOther(full_url('')); + $this->response->redirect->seeOther($this->config->full_url('')); } /** @@ -228,7 +240,7 @@ class BaseController { ) ) { - $this->response->redirect->afterPost(full_url('', $this->base_data['url_type'])); + $this->response->redirect->afterPost($this->config->full_url('', $this->base_data['url_type'])); return; } diff --git a/app/base/BaseModel.php b/app/base/BaseModel.php index a28e9c3c..5de557ce 100644 --- a/app/base/BaseModel.php +++ b/app/base/BaseModel.php @@ -73,7 +73,7 @@ class BaseModel { } else { - throw new Exception("Couldn't cache images because they couldn't be downloaded."); + throw new DomainException("Couldn't cache images because they couldn't be downloaded."); } // Resize the image diff --git a/app/base/Config.php b/app/base/Config.php index 93de5b35..c0e508a1 100644 --- a/app/base/Config.php +++ b/app/base/Config.php @@ -52,5 +52,77 @@ class Config { return NULL; } + + /** + * Get the base url for css/js/images + * + * @return string + */ + function asset_url(/*...*/) + { + $args = func_get_args(); + $base_url = rtrim($this->__get('asset_path'), '/'); + + array_unshift($args, $base_url); + + return implode("/", $args); + } + + /** + * Get the base url from the config + * + * @param string $type - (optional) The controller + * @return string + */ + function base_url($type="anime") + { + $config_path = trim($this->__get("{$type}_path"), "/"); + $config_host = $this->__get("{$type}_host"); + + // Set the appropriate HTTP host + $host = ($config_host !== '') ? $config_host : $_SERVER['HTTP_HOST']; + $path = ($config_path !== '') ? $config_path : ""; + + return implode("/", ['/', $host, $path]); + } + + /** + * 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") + { + $config_path = trim($this->__get("{$type}_path"), "/"); + $config_host = $this->__get("{$type}_host"); + $config_default_route = $this->__get("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 appropriate HTTP host + $host = ($config_host !== '') ? $config_host : $_SERVER['HTTP_HOST']; + + // Set the default view + if ($path === '') + { + $path .= trim($config_default_route, '/'); + if ($this->__get('default_to_list_view')) $path .= '/list'; + } + + // Set an leading folder + if ($config_path !== '') + { + $path = "{$config_path}/{$path}"; + } + + return "//{$host}/{$path}"; + } } // End of config.php \ No newline at end of file diff --git a/app/base/Router.php b/app/base/Router.php index 829b2c5d..beeb9804 100644 --- a/app/base/Router.php +++ b/app/base/Router.php @@ -64,7 +64,7 @@ class Router { { global $defaultHandler; - $raw_route = $this->request->server->get('REQUEST_URI'); + $raw_route = parse_url($this->request->server->get('REQUEST_URI'), \PHP_URL_PATH); $route_path = str_replace([$this->config->anime_path, $this->config->manga_path], '', $raw_route); $route_path = "/" . trim($route_path, '/'); @@ -108,14 +108,14 @@ class Router { $failure = $this->router->getFailedRoute(); $defaultHandler->addDataTable('failed_route', (array)$failure); - $controller_name = '\\AnimeClient\\BaseController'; + /*$controller_name = '\\AnimeClient\\BaseController'; $action_method = 'outputHTML'; $params = [ 'template' => '404', 'data' => [ 'title' => 'Page Not Found' ] - ]; + ];*/ } else { @@ -148,7 +148,7 @@ class Router { */ public function get_route_type() { - $route_type = ""; + $route_type = $this->config->default_list; $host = $this->request->server->get("HTTP_HOST"); $request_uri = $this->request->server->get('REQUEST_URI'); diff --git a/app/base/functions.php b/app/base/functions.php index ee84069e..fa9d81ce 100644 --- a/app/base/functions.php +++ b/app/base/functions.php @@ -38,87 +38,6 @@ function is_not_selected($a, $b) return ($a !== $b) ? 'selected' : ''; } -/** - * Get the base url for css/js/images - * - * @return string - */ -function asset_url(/*...*/) -{ - global $config; - - $args = func_get_args(); - $base_url = rtrim($config->asset_path, '/'); - - array_unshift($args, $base_url); - - return implode("/", $args); -} - -/** - * Get the base url from the config - * - * @param string $type - (optional) The controller - # @param object $config - (optional) Config - * @return string - */ -function base_url($type="anime", $config=NULL) -{ - if (is_null($config)) global $config; - - - $config_path = trim($config->{"{$type}_path"}, "/"); - $config_host = $config->{"{$type}_host"}; - - // Set the appropriate HTTP host - $host = ($config_host !== '') ? $config_host : $_SERVER['HTTP_HOST']; - $path = ($config_path !== '') ? $config_path : ""; - - return implode("/", ['/', $host, $path]); -} - -/** - * 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 - # @param object $config - (optional) Config - * @return string - */ -function full_url($path="", $type="anime", $config=NULL) -{ - if (is_null($config)) global $config; - - $config_path = trim($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 appropriate HTTP host - $host = ($config_host !== '') ? $config_host : $_SERVER['HTTP_HOST']; - - // Set the default view - if ($path === '') - { - $path .= trim($config_default_route, '/'); - if ($config->default_to_list_view) $path .= '/list'; - } - - // Set an leading folder - if ($config_path !== '') - { - $path = "{$config_path}/{$path}"; - } - - return "//{$host}/{$path}"; -} - /** * Get the last segment of the current url * @@ -131,4 +50,19 @@ function last_segment() return end($segments); } +/** + * Determine whether to show the sub-menu + * + * @return bool + */ +function is_view_page() +{ + $blacklist = ['edit', 'add', 'update', 'login', 'logout']; + $page_segments = explode("/", $_SERVER['REQUEST_URI']); + + $intersect = array_intersect($page_segments, $blacklist); + + return empty($intersect); +} + // End of functions.php \ No newline at end of file diff --git a/app/config/config.php b/app/config/config.php index dd505833..424e0534 100644 --- a/app/config/config.php +++ b/app/config/config.php @@ -32,6 +32,9 @@ $config = [ 'anime_path' => '', 'manga_path' => '', + // Which list should be the default? + 'default_list' => 'anime', // anime or manga + // Default pages for anime/manga 'default_anime_path' => '/watching', 'default_manga_path' => '/all', diff --git a/app/config/minify_js_groups.php b/app/config/minify_js_groups.php index 3df263a4..e747e40d 100644 --- a/app/config/minify_js_groups.php +++ b/app/config/minify_js_groups.php @@ -34,6 +34,12 @@ return [ 'show_message.js', 'anime_edit.js', 'manga_edit.js' + ], + 'collection' => [ + 'lib/jquery.min.js', + 'lib/jquery.throttle-debounce.js', + 'lib/jsrender.js', + 'collection.js' ] ]; diff --git a/app/config/routes.php b/app/config/routes.php index 3dac08b8..02c4241f 100644 --- a/app/config/routes.php +++ b/app/config/routes.php @@ -33,6 +33,10 @@ return [ 'code' => '301' ] ], + 'search' => [ + 'path' => '/search', + 'action' => ['search'], + ], 'all' => [ 'path' => '/all{/view}', 'action' => ['anime_list'], @@ -99,14 +103,36 @@ return [ 'view' => '[a-z_]+' ] ], + 'collection_add_form' => [ + 'path' => '/collection/add', + 'action' => ['collection_form'], + 'params' => [], + ], + 'collection_edit_form' => [ + 'path' => '/collection/edit/{id}', + 'action' => ['collection_form'], + 'tokens' => [ + 'id' => '[0-9]+' + ] + ], + 'collection_add' => [ + 'path' => '/collection/add', + 'action' => ['collection_add'], + 'verb' => 'post' + ], + 'collection_edit' => [ + 'path' => '/collection/edit', + 'action' => ['collection_edit'], + 'verb' => 'post' + ], 'collection' => [ - 'path' => '/collection{/view}', + 'path' => '/collection/view{/view}', 'action' => ['collection'], 'params' => [], 'tokens' => [ 'view' => '[a-z_]+' ] - ] + ], ], 'manga' => [ 'index' => [ diff --git a/app/controllers/AnimeController.php b/app/controllers/AnimeController.php index 81d24fee..7e8056a3 100644 --- a/app/controllers/AnimeController.php +++ b/app/controllers/AnimeController.php @@ -38,7 +38,7 @@ class AnimeController extends BaseController { 'On Hold' => '/on_hold{/view}', 'Dropped' => '/dropped{/view}', 'Completed' => '/completed{/view}', - 'Collection' => '/collection{/view}', + 'Collection' => '/collection/view{/view}', 'All' => '/all{/view}' ]; @@ -61,9 +61,21 @@ class AnimeController extends BaseController { 'url_type' => 'anime', 'other_type' => 'manga', 'nav_routes' => $this->nav_routes, + 'config' => $this->config, ]; } + /** + * Search for anime + * + * @return void + */ + public function search() + { + $query = $this->request->query->get('query'); + $this->outputJSON($this->model->search($query)); + } + /** * Show a portion, or all of the anime list * @@ -104,10 +116,66 @@ class AnimeController extends BaseController { $this->outputHTML('anime/' . $view_map[$view], [ 'title' => WHOSE . " Anime Collection", - 'sections' => $data + 'sections' => $data, + 'genres' => $this->collection_model->get_genre_list() ]); } + /** + * Show the anime collection add/edit form + * + * @param int $id + * @return void + */ + public function collection_form($id=NULL) + { + $action = (is_null($id)) ? "Add" : "Edit"; + + $this->outputHTML('anime/collection_' . strtolower($action), [ + 'action' => $action, + 'action_url' => $this->config->full_url("collection/" . strtolower($action)), + 'title' => WHOSE . " Anime Collection · {$action}", + 'media_items' => $this->collection_model->get_media_type_list(), + 'item' => ($action === "Edit") ? $this->collection_model->get($id) : [] + ]); + } + + /** + * Update a collection item + * + * @return void + */ + public function collection_edit() + { + $data = $this->request->post->get(); + if ( ! array_key_exists('hummingbird_id', $data)) + { + $this->redirect("collection/view", 303, "anime"); + } + + $this->collection_model->update($data); + + $this->redirect("collection/view", 303, "anime"); + } + + /** + * Add a collection item + * + * @return void + */ + public function collection_add() + { + $data = $this->request->post->get(); + if ( ! array_key_exists('id', $data)) + { + $this->redirect("collection/view", 303, "anime"); + } + + $this->collection_model->add($data); + + $this->redirect("collection/view", 303, "anime"); + } + /** * Update an anime item * @@ -115,7 +183,7 @@ class AnimeController extends BaseController { */ public function update() { - print_r($this->model->update($this->request->post->get())); + $this->outputJSON($this->model->update($this->request->post->get())); } } // End of AnimeController.php \ No newline at end of file diff --git a/app/controllers/MangaController.php b/app/controllers/MangaController.php index 846a6eb5..7f65dfe6 100644 --- a/app/controllers/MangaController.php +++ b/app/controllers/MangaController.php @@ -43,6 +43,7 @@ class MangaController extends BaseController { parent::__construct($config, $web); $this->model = new MangaModel($config); $this->base_data = [ + 'config' => $this->config, 'url_type' => 'manga', 'other_type' => 'anime', 'nav_routes' => $this->nav_routes diff --git a/app/models/AnimeCollectionModel.php b/app/models/AnimeCollectionModel.php index e350a835..9ec45571 100644 --- a/app/models/AnimeCollectionModel.php +++ b/app/models/AnimeCollectionModel.php @@ -42,6 +42,49 @@ class AnimeCollectionModel extends BaseDBModel { $this->json_import(); } + /** + * Get genres for anime collection items + * + * @param array $filter + * @return array + */ + public function get_genre_list($filter=[]) + { + $this->db->select('hummingbird_id, genre') + ->from('genre_anime_set_link gl') + ->join('genres g', 'g.id=gl.genre_id', 'left'); + + + if ( ! empty($filter)) $this->db->where_in('hummingbird_id', $filter); + + $query = $this->db->order_by('hummingbird_id') + ->order_by('genre') + ->get(); + + $output = []; + + foreach($query->fetchAll(\PDO::FETCH_ASSOC) as $row) + { + $id = $row['hummingbird_id']; + $genre = $row['genre']; + + // Empty genre names aren't useful + if (empty($genre)) continue; + + + if (array_key_exists($id, $output)) + { + array_push($output[$id], $genre); + } + else + { + $output[$id] = [$genre]; + } + } + + return $output; + } + /** * Get collection from the database, and organize by media type * @@ -68,6 +111,42 @@ class AnimeCollectionModel extends BaseDBModel { return $collection; } + /** + * Get list of media types + * + * @return array + */ + public function get_media_type_list() + { + $output = array(); + + $query = $this->db->select('id, type') + ->from('media') + ->get(); + + foreach($query->fetchAll(\PDO::FETCH_ASSOC) as $row) + { + $output[$row['id']] = $row['type']; + } + + return $output; + } + + /** + * Get item from collection for editing + * + * @param int $id + * @return array + */ + public function get_collection_entry($id) + { + $query = $this->db->from('anime_set') + ->where('hummingbird_id', (int) $id) + ->get(); + + return $query->fetch(\PDO::FETCH_ASSOC); + } + /** * Get full collection from the database * @@ -87,6 +166,67 @@ class AnimeCollectionModel extends BaseDBModel { return $query->fetchAll(\PDO::FETCH_ASSOC); } + /** + * Add an item to the anime collection + * + * @param array $data + * @return void + */ + public function add($data) + { + $anime = (object) $this->anime_model->get_anime($data['id']); + + $this->db->set([ + 'hummingbird_id' => $data['id'], + 'slug' => $anime->slug, + 'title' => $anime->title, + 'alternate_title' => $anime->alternate_title, + 'show_type' => $anime->show_type, + 'age_rating' => $anime->age_rating, + 'cover_image' => basename($this->get_cached_image($anime->cover_image, $anime->slug, 'anime')), + 'episode_count' => $anime->episode_count, + 'episode_length' => $anime->episode_length, + 'media_id' => $data['media_id'], + 'notes' => $data['notes'] + ])->insert('anime_set'); + + $this->update_genre($data['id']); + } + + /** + * Update a collection item + * + * @param array $data + * @return void + */ + public function update($data) + { + // If there's no id to update, don't update + if ( ! array_key_exists('hummingbird_id', $data)) return; + + $id = $data['hummingbird_id']; + unset($data['hummingbird_id']); + + $this->db->set($data) + ->where('hummingbird_id', $id) + ->update('anime_set'); + } + + /** + * Get the details of a collection item + * + * @param int $hummingbird_id + * @return array + */ + public function get($hummingbird_id) + { + $query = $this->db->from('anime_set') + ->where('hummingbird_id', $hummingbird_id) + ->get(); + + return $query->fetch(\PDO::FETCH_ASSOC); + } + /** * Import anime into collection from a json file * @@ -108,7 +248,7 @@ class AnimeCollectionModel extends BaseDBModel { 'alternate_title' => $item->alternate_title, 'show_type' => $item->show_type, 'age_rating' => $item->age_rating, - 'cover_image' => $this->get_cached_image($item->cover_image, $item->slug, 'anime'), + 'cover_image' => basename($this->get_cached_image($item->cover_image, $item->slug, 'anime')), 'episode_count' => $item->episode_count, 'episode_length' => $item->episode_length ])->insert('anime_set'); @@ -122,22 +262,67 @@ class AnimeCollectionModel extends BaseDBModel { } /** - * Update genre information + * Update genre information for selected anime * * @return void */ - private function update_genres() + private function update_genre($anime_id) + { + $genre_info = $this->get_genre_data(); + extract($genre_info); + + // Get api information + $anime = $this->anime_model->get_anime($anime_id); + + foreach($anime['genres'] as $genre) + { + // Add genres that don't currently exist + if ( ! in_array($genre['name'], $genres)) + { + $this->db->set('genre', $genre['name']) + ->insert('genres'); + + $genres[] = $genre['name']; + } + + // Update link table + // Get id of genre to put in link table + $flipped_genres = array_flip($genres); + + $insert_array = [ + 'hummingbird_id' => $anime['id'], + 'genre_id' => $flipped_genres[$genre['name']] + ]; + + if (array_key_exists($anime['id'], $links)) + { + if ( ! in_array($flipped_genres[$genre['name']], $links[$anime['id']])) + { + $this->db->set($insert_array)->insert('genre_anime_set_link'); + } + } + else + { + $this->db->set($insert_array)->insert('genre_anime_set_link'); + } + } + } + + /** + * Get list of existing genres + * + * @return array + */ + private function get_genre_data() { $genres = []; - $flipped_genres = []; - $links = []; // Get existing genres $query = $this->db->select('id, genre') ->from('genres') ->get(); - foreach($query->fetchAll(PDO::FETCH_ASSOC) as $genre) + foreach($query->fetchAll(\PDO::FETCH_ASSOC) as $genre) { $genres[$genre['id']] = $genre['genre']; } @@ -146,7 +331,7 @@ class AnimeCollectionModel extends BaseDBModel { $query = $this->db->select('hummingbird_id, genre_id') ->from('genre_anime_set_link') ->get(); - foreach($query->fetchAll(PDO::FETCH_ASSOC) as $link) + foreach($query->fetchAll(\PDO::FETCH_ASSOC) as $link) { if (array_key_exists($link['hummingbird_id'], $links)) { @@ -158,48 +343,25 @@ class AnimeCollectionModel extends BaseDBModel { } } + return [ + 'genres' => $genres, + 'links' => $links + ]; + } + + /** + * Update genre information for the entire collection + * + * @return void + */ + private function update_genres() + { // Get the anime collection $collection = $this->_get_collection(); foreach($collection as $anime) { // Get api information - $api = $this->anime_model->get_anime($anime['hummingbird_id']); - - - foreach($api['genres'] as $genre) - { - // Add genres that don't currently exist - if ( ! in_array($genre['name'], $genres)) - { - $this->db->set('genre', $genre['name']) - ->insert('genres'); - - $genres[] = $genre['name']; - } - - - // Update link table - - // Get id of genre to put in link table - $flipped_genres = array_flip($genres); - - $insert_array = [ - 'hummingbird_id' => $anime['hummingbird_id'], - 'genre_id' => $flipped_genres[$genre['name']] - ]; - - if (array_key_exists($anime['hummingbird_id'], $links)) - { - if ( ! in_array($flipped_genres[$genre['name']], $links[$anime['hummingbird_id']])) - { - $this->db->set($insert_array)->insert('genre_anime_set_link'); - } - } - else - { - $this->db->set($insert_array)->insert('genre_anime_set_link'); - } - } + $this->update_genre($anime['hummingbird_id']); } } } diff --git a/app/models/AnimeModel.php b/app/models/AnimeModel.php index a4bd5be3..e24a22c0 100644 --- a/app/models/AnimeModel.php +++ b/app/models/AnimeModel.php @@ -157,7 +157,7 @@ class AnimeModel extends BaseApiModel { if ($response->getStatusCode() != 200) { - throw new Exception($response->getEffectiveUrl()); + throw new RuntimeException($response->getEffectiveUrl()); } return $response->json(); @@ -192,7 +192,7 @@ class AnimeModel extends BaseApiModel { { if ( ! file_exists($cache_file)) { - throw new Exception($response->getEffectiveUrl()); + throw new DomainException($response->getEffectiveUrl()); } else { diff --git a/app/models/MangaModel.php b/app/models/MangaModel.php index ebef7243..b78da616 100644 --- a/app/models/MangaModel.php +++ b/app/models/MangaModel.php @@ -101,7 +101,7 @@ class MangaModel extends BaseApiModel { { if ( ! file_exists($cache_file)) { - throw new Exception($response->getEffectiveUrl()); + throw new DomainException($response->getEffectiveUrl()); } else { @@ -124,7 +124,7 @@ class MangaModel extends BaseApiModel { } // Bail out early if there isn't any manga data - if (empty($raw_data)) return []; + if ( ! array_key_exists('manga', $raw_data)) return []; $data = [ 'Reading' => [], @@ -174,8 +174,6 @@ class MangaModel extends BaseApiModel { } } - //file_put_contents(_dir($this->config->data_cache_path, "manga-processed.json"), json_encode($data, JSON_PRETTY_PRINT)); - return (array_key_exists($status, $data)) ? $data[$status] : $data; } diff --git a/app/views/anime/collection.php b/app/views/anime/collection.php index 28b40564..23b93e22 100644 --- a/app/views/anime/collection.php +++ b/app/views/anime/collection.php @@ -1,15 +1,22 @@
+ +[Add Item] + + +

There's nothing here!

+ $items): ?>

- - + + + [">Edit] +
+
\ No newline at end of file diff --git a/app/views/anime/collection_add.php b/app/views/anime/collection_add.php new file mode 100644 index 00000000..cbbaa396 --- /dev/null +++ b/app/views/anime/collection_add.php @@ -0,0 +1,38 @@ + +
+
+
+
Series
+
+
+
+
+
+ +
+
+ +
+ +
+
+ +
 
+
+ +
+
+
+
+ + + \ No newline at end of file diff --git a/app/views/anime/collection_edit.php b/app/views/anime/collection_edit.php index e69de29b..29411f01 100644 --- a/app/views/anime/collection_edit.php +++ b/app/views/anime/collection_edit.php @@ -0,0 +1,37 @@ + +
+
+
+

+

+ +
+
+ +
+ +
+
+ +
 
+
+ + + + +
+
+
+
+ + + \ No newline at end of file diff --git a/app/views/anime/collection_list.php b/app/views/anime/collection_list.php index 92471f92..ee4065bd 100644 --- a/app/views/anime/collection_list.php +++ b/app/views/anime/collection_list.php @@ -1,16 +1,25 @@
+ +[Add Item] + + +

There's nothing here!

+ $items): ?>

- + Alternate Title*/ ?> + + + @@ -20,24 +29,28 @@ + - + + + + + + */ ?> + + +
TitleAlternate TitleEpisode Count Episode Length Show Type Age Rating Notes 
[">Edit]

+
- - - \ No newline at end of file + \ No newline at end of file diff --git a/app/views/anime/cover.php b/app/views/anime/cover.php index e0ede9e5..71141395 100644 --- a/app/views/anime/cover.php +++ b/app/views/anime/cover.php @@ -1,4 +1,7 @@
+ +

There's nothing here!

+ $items): ?>

@@ -34,7 +37,8 @@
+
- + diff --git a/app/views/anime/list.php b/app/views/anime/list.php index 4a1c13ea..3067fcd0 100644 --- a/app/views/anime/list.php +++ b/app/views/anime/list.php @@ -1,4 +1,7 @@
+ +

There's nothing here!

+ $items): ?>

@@ -11,6 +14,7 @@ + @@ -27,10 +31,17 @@ +
Type Progress RatedGenres
Episodes: / + + + + +
+
- \ No newline at end of file + \ No newline at end of file diff --git a/app/views/header.php b/app/views/header.php index 5705f68f..415fdd44 100644 --- a/app/views/header.php +++ b/app/views/header.php @@ -3,24 +3,24 @@ <?= $title ?> - +

- "> + "> - ["> List] + ["> List] - [">Logout] + [">Logout] - ["> Login] + ["> Login]

@@ -28,14 +28,16 @@
\ No newline at end of file diff --git a/app/views/login.php b/app/views/login.php index 44f4ba22..2d59dadd 100644 --- a/app/views/login.php +++ b/app/views/login.php @@ -1,7 +1,7 @@
diff --git a/app/views/manga/cover.php b/app/views/manga/cover.php index 6cea8920..53f3541b 100644 --- a/app/views/manga/cover.php +++ b/app/views/manga/cover.php @@ -1,4 +1,7 @@
+ +

There's nothing here!

+ $items): ?>

@@ -43,7 +46,8 @@
+
- + \ No newline at end of file diff --git a/app/views/manga/list.php b/app/views/manga/list.php index dda12c62..89cddde7 100644 --- a/app/views/manga/list.php +++ b/app/views/manga/list.php @@ -1,4 +1,7 @@
+ +

There's nothing here!

+ $items): ?>

@@ -29,5 +32,6 @@
+
- \ No newline at end of file + \ No newline at end of file diff --git a/public/css/base.css b/public/css/base.css index 24b2e0f6..4107aa1f 100644 --- a/public/css/base.css +++ b/public/css/base.css @@ -1,3 +1,7 @@ +template { + display: none; +} + body { margin: 0.5em; } @@ -35,8 +39,22 @@ tbody > tr:nth-child(odd) { align-items: flex-end; } +.flex-align-space-around { + -webkit-align-content: space-around; + -ms-flex-line-pack: distribute; + align-content: space-around; +} + .flex-justify-space-around { - jusify-content: space-around; + -webkit-justify-content: space-around; + -ms-flex-pack: distribute; + justify-content: space-around; +} + +.flex-self-center { + -webkit-align-self: center; + -ms-flex-item-align: center; + align-self: center; } .flex { @@ -49,6 +67,10 @@ tbody > tr:nth-child(odd) { font-size: 1.6rem; } +.align_center { + text-align: center; +} + .align_left { text-align: left; } @@ -57,22 +79,6 @@ tbody > tr:nth-child(odd) { text-align: right; } -.round_all { - border-radius: 0.5em; -} - -.round_top { - border-radius: 0; - border-top-right-radius: 0.5em; - border-top-left-radius: 0.5em; -} - -.round_bottom { - border-radius: 0; - border-bottom-right-radius: 0.5em; - border-bottom-left-radius: 0.5em; -} - .media-wrap { text-align: center; margin: 0 auto; diff --git a/public/css/base.myth.css b/public/css/base.myth.css index 227db035..8c8165d5 100644 --- a/public/css/base.myth.css +++ b/public/css/base.myth.css @@ -3,12 +3,11 @@ --title-overlay: rgba(0, 0, 0, 0.45); --text-color: #ffffff; --normal-padding: 0.25em; - --radius: 0.5em; } -body { - margin: 0.5em; -} +template {display:none} + +body {margin: 0.5em;} table { width:85%; @@ -23,36 +22,18 @@ tbody > tr:nth-child(odd) { .flex-wrap {flex-wrap: wrap} .flex-no-wrap {flex-wrap: nowrap} .flex-align-end {align-items: flex-end} -.flex-justify-space-around {jusify-content: space-around} +.flex-align-space-around {align-content: space-around} +.flex-justify-space-around {justify-content: space-around} +.flex-self-center {align-self:center} .flex {display: flex} .small-font { font-size:1.6rem; } -.align_left { - text-align:left; -} - -.align_right { - text-align:right; -} - -.round_all { - border-radius:var(--radius); -} - -.round_top { - border-radius: 0; - border-top-right-radius:var(--radius); - border-top-left-radius:var(--radius); -} - -.round_bottom { - border-radius: 0; - border-bottom-right-radius:var(--radius); - border-bottom-left-radius:var(--radius); -} +.align_center {text-align:center} +.align_left {text-align:left;} +.align_right {text-align:right;} .media-wrap { text-align:center; diff --git a/public/js.php b/public/js.php index 8a297ff2..88fb22e8 100644 --- a/public/js.php +++ b/public/js.php @@ -40,7 +40,7 @@ function get_files() foreach($groups[$_GET['g']] as $file) { $new_file = realpath($js_root.$file); - $js .= file_get_contents($new_file); + $js .= file_get_contents($new_file) . "\n\n"; } return $js; @@ -57,14 +57,32 @@ function get_files() */ function google_min($new_file) { + $options = [ + 'output_info' => 'compiled_code', + 'output_format' => 'json', + 'compilation_level' => 'SIMPLE_OPTIMIZATIONS', + 'js_code' => urlencode($new_file), + 'warning_level' => 'QUIET', + 'language' => 'ECMASCRIPT5' + ]; + + $option_pairs = []; + foreach($options as $key => $value) + { + $option_pairs[] = "{$key}={$value}"; + } + $option_string = implode("&", $option_pairs); + //Get a much-minified version from Google's closure compiler $ch = curl_init('http://closure-compiler.appspot.com/compile'); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_POST, 1); - curl_setopt($ch, CURLOPT_POSTFIELDS, 'output_info=compiled_code&output_format=text&compilation_level=SIMPLE_OPTIMIZATIONS&js_code=' . urlencode($new_file)); - $output = curl_exec($ch); + curl_setopt($ch, CURLOPT_POSTFIELDS, $option_string); + $json = curl_exec($ch); curl_close($ch); - return $output; + + $obj = json_decode($json); + return $obj->compiledCode; } // -------------------------------------------------------------------------- @@ -146,7 +164,12 @@ if($last_modified === $requested_time) // -------------------------------------------------------------------------- //Determine what to do: rebuild cache, send files as is, or send cache. -if($cache_modified < $last_modified) +// If debug is set, just concatenate +if(isset($_GET['debug'])) +{ + $js = get_files(); +} +else if($cache_modified < $last_modified) { $js = google_min(get_files()); @@ -156,11 +179,6 @@ if($cache_modified < $last_modified) die("Cache file was not created. Make sure you have the correct folder permissions."); } } -// If debug is set, just concatenate -else if(isset($_GET['debug'])) -{ - $js = get_files(); -} // Otherwise, send the cached file else { diff --git a/public/js/anime_edit.js b/public/js/anime_edit.js index e90f41d2..6d6935ea 100644 --- a/public/js/anime_edit.js +++ b/public/js/anime_edit.js @@ -9,6 +9,7 @@ $(".media button.plus_one").on("click", function(e) { e.stopPropagation(); + var self = this; var this_sel = $(this); var parent_sel = $(this).closest("article"); var self = this; @@ -41,7 +42,7 @@ $.post(BASE_URL + 'update', data, function(res) { if (res.status === 'completed') { - this_sel.parent('article').hide(); + $(self).closest('article').hide(); } add_message('success', "Sucessfully updated " + title); diff --git a/public/js/collection.js b/public/js/collection.js new file mode 100644 index 00000000..ed40a344 --- /dev/null +++ b/public/js/collection.js @@ -0,0 +1,17 @@ +(function($, undefined) { + + function search(query, callback) + { + $.get(BASE_URL + 'search', {'query':query}, callback); + } + + $("#search").on('keypress', $.throttle(250, function(e) { + var query = encodeURIComponent($(this).val()); + search(query, function(res) { + var template = $.templates("#show_list"); + var html = template.render(res); + $('#series_list').html(html); + }); + })); + +}(jQuery)); \ No newline at end of file diff --git a/public/js/lib/jquery.throttle-debounce.js b/public/js/lib/jquery.throttle-debounce.js new file mode 100644 index 00000000..f39717c1 --- /dev/null +++ b/public/js/lib/jquery.throttle-debounce.js @@ -0,0 +1,252 @@ +/*! + * jQuery throttle / debounce - v1.1 - 3/7/2010 + * http://benalman.com/projects/jquery-throttle-debounce-plugin/ + * + * Copyright (c) 2010 "Cowboy" Ben Alman + * Dual licensed under the MIT and GPL licenses. + * http://benalman.com/about/license/ + */ + +// Script: jQuery throttle / debounce: Sometimes, less is more! +// +// *Version: 1.1, Last updated: 3/7/2010* +// +// Project Home - http://benalman.com/projects/jquery-throttle-debounce-plugin/ +// GitHub - http://github.com/cowboy/jquery-throttle-debounce/ +// Source - http://github.com/cowboy/jquery-throttle-debounce/raw/master/jquery.ba-throttle-debounce.js +// (Minified) - http://github.com/cowboy/jquery-throttle-debounce/raw/master/jquery.ba-throttle-debounce.min.js (0.7kb) +// +// About: License +// +// Copyright (c) 2010 "Cowboy" Ben Alman, +// Dual licensed under the MIT and GPL licenses. +// http://benalman.com/about/license/ +// +// About: Examples +// +// These working examples, complete with fully commented code, illustrate a few +// ways in which this plugin can be used. +// +// Throttle - http://benalman.com/code/projects/jquery-throttle-debounce/examples/throttle/ +// Debounce - http://benalman.com/code/projects/jquery-throttle-debounce/examples/debounce/ +// +// About: Support and Testing +// +// Information about what version or versions of jQuery this plugin has been +// tested with, what browsers it has been tested in, and where the unit tests +// reside (so you can test it yourself). +// +// jQuery Versions - none, 1.3.2, 1.4.2 +// Browsers Tested - Internet Explorer 6-8, Firefox 2-3.6, Safari 3-4, Chrome 4-5, Opera 9.6-10.1. +// Unit Tests - http://benalman.com/code/projects/jquery-throttle-debounce/unit/ +// +// About: Release History +// +// 1.1 - (3/7/2010) Fixed a bug in where trailing callbacks +// executed later than they should. Reworked a fair amount of internal +// logic as well. +// 1.0 - (3/6/2010) Initial release as a stand-alone project. Migrated over +// from jquery-misc repo v0.4 to jquery-throttle repo v1.0, added the +// no_trailing throttle parameter and debounce functionality. +// +// Topic: Note for non-jQuery users +// +// jQuery isn't actually required for this plugin, because nothing internal +// uses any jQuery methods or properties. jQuery is just used as a namespace +// under which these methods can exist. +// +// Since jQuery isn't actually required for this plugin, if jQuery doesn't exist +// when this plugin is loaded, the method described below will be created in +// the `Cowboy` namespace. Usage will be exactly the same, but instead of +// $.method() or jQuery.method(), you'll need to use Cowboy.method(). + +(function(window,undefined){ + '$:nomunge'; // Used by YUI compressor. + + // Since jQuery really isn't required for this plugin, use `jQuery` as the + // namespace only if it already exists, otherwise use the `Cowboy` namespace, + // creating it if necessary. + var $ = window.jQuery || window.Cowboy || ( window.Cowboy = {} ), + + // Internal method reference. + jq_throttle; + + // Method: jQuery.throttle + // + // Throttle execution of a function. Especially useful for rate limiting + // execution of handlers on events like resize and scroll. If you want to + // rate-limit execution of a function to a single time, see the + // method. + // + // In this visualization, | is a throttled-function call and X is the actual + // callback execution: + // + // > Throttled with `no_trailing` specified as false or unspecified: + // > ||||||||||||||||||||||||| (pause) ||||||||||||||||||||||||| + // > X X X X X X X X X X X X + // > + // > Throttled with `no_trailing` specified as true: + // > ||||||||||||||||||||||||| (pause) ||||||||||||||||||||||||| + // > X X X X X X X X X X + // + // Usage: + // + // > var throttled = jQuery.throttle( delay, [ no_trailing, ] callback ); + // > + // > jQuery('selector').bind( 'someevent', throttled ); + // > jQuery('selector').unbind( 'someevent', throttled ); + // + // This also works in jQuery 1.4+: + // + // > jQuery('selector').bind( 'someevent', jQuery.throttle( delay, [ no_trailing, ] callback ) ); + // > jQuery('selector').unbind( 'someevent', callback ); + // + // Arguments: + // + // delay - (Number) A zero-or-greater delay in milliseconds. For event + // callbacks, values around 100 or 250 (or even higher) are most useful. + // no_trailing - (Boolean) Optional, defaults to false. If no_trailing is + // true, callback will only execute every `delay` milliseconds while the + // throttled-function is being called. If no_trailing is false or + // unspecified, callback will be executed one final time after the last + // throttled-function call. (After the throttled-function has not been + // called for `delay` milliseconds, the internal counter is reset) + // callback - (Function) A function to be executed after delay milliseconds. + // The `this` context and all arguments are passed through, as-is, to + // `callback` when the throttled-function is executed. + // + // Returns: + // + // (Function) A new, throttled, function. + + $.throttle = jq_throttle = function( delay, no_trailing, callback, debounce_mode ) { + // After wrapper has stopped being called, this timeout ensures that + // `callback` is executed at the proper times in `throttle` and `end` + // debounce modes. + var timeout_id, + + // Keep track of the last time `callback` was executed. + last_exec = 0; + + // `no_trailing` defaults to falsy. + if ( typeof no_trailing !== 'boolean' ) { + debounce_mode = callback; + callback = no_trailing; + no_trailing = undefined; + } + + // The `wrapper` function encapsulates all of the throttling / debouncing + // functionality and when executed will limit the rate at which `callback` + // is executed. + function wrapper() { + var that = this, + elapsed = +new Date() - last_exec, + args = arguments; + + // Execute `callback` and update the `last_exec` timestamp. + function exec() { + last_exec = +new Date(); + callback.apply( that, args ); + }; + + // If `debounce_mode` is true (at_begin) this is used to clear the flag + // to allow future `callback` executions. + function clear() { + timeout_id = undefined; + }; + + if ( debounce_mode && !timeout_id ) { + // Since `wrapper` is being called for the first time and + // `debounce_mode` is true (at_begin), execute `callback`. + exec(); + } + + // Clear any existing timeout. + timeout_id && clearTimeout( timeout_id ); + + if ( debounce_mode === undefined && elapsed > delay ) { + // In throttle mode, if `delay` time has been exceeded, execute + // `callback`. + exec(); + + } else if ( no_trailing !== true ) { + // In trailing throttle mode, since `delay` time has not been + // exceeded, schedule `callback` to execute `delay` ms after most + // recent execution. + // + // If `debounce_mode` is true (at_begin), schedule `clear` to execute + // after `delay` ms. + // + // If `debounce_mode` is false (at end), schedule `callback` to + // execute after `delay` ms. + timeout_id = setTimeout( debounce_mode ? clear : exec, debounce_mode === undefined ? delay - elapsed : delay ); + } + }; + + // Set the guid of `wrapper` function to the same of original callback, so + // it can be removed in jQuery 1.4+ .unbind or .die by using the original + // callback as a reference. + if ( $.guid ) { + wrapper.guid = callback.guid = callback.guid || $.guid++; + } + + // Return the wrapper function. + return wrapper; + }; + + // Method: jQuery.debounce + // + // Debounce execution of a function. Debouncing, unlike throttling, + // guarantees that a function is only executed a single time, either at the + // very beginning of a series of calls, or at the very end. If you want to + // simply rate-limit execution of a function, see the + // method. + // + // In this visualization, | is a debounced-function call and X is the actual + // callback execution: + // + // > Debounced with `at_begin` specified as false or unspecified: + // > ||||||||||||||||||||||||| (pause) ||||||||||||||||||||||||| + // > X X + // > + // > Debounced with `at_begin` specified as true: + // > ||||||||||||||||||||||||| (pause) ||||||||||||||||||||||||| + // > X X + // + // Usage: + // + // > var debounced = jQuery.debounce( delay, [ at_begin, ] callback ); + // > + // > jQuery('selector').bind( 'someevent', debounced ); + // > jQuery('selector').unbind( 'someevent', debounced ); + // + // This also works in jQuery 1.4+: + // + // > jQuery('selector').bind( 'someevent', jQuery.debounce( delay, [ at_begin, ] callback ) ); + // > jQuery('selector').unbind( 'someevent', callback ); + // + // Arguments: + // + // delay - (Number) A zero-or-greater delay in milliseconds. For event + // callbacks, values around 100 or 250 (or even higher) are most useful. + // at_begin - (Boolean) Optional, defaults to false. If at_begin is false or + // unspecified, callback will only be executed `delay` milliseconds after + // the last debounced-function call. If at_begin is true, callback will be + // executed only at the first debounced-function call. (After the + // throttled-function has not been called for `delay` milliseconds, the + // internal counter is reset) + // callback - (Function) A function to be executed after delay milliseconds. + // The `this` context and all arguments are passed through, as-is, to + // `callback` when the debounced-function is executed. + // + // Returns: + // + // (Function) A new, debounced, function. + + $.debounce = function( delay, at_begin, callback ) { + return callback === undefined + ? jq_throttle( delay, at_begin, false ) + : jq_throttle( delay, callback, at_begin !== false ); + }; + +})(this); diff --git a/public/js/lib/jsrender.js b/public/js/lib/jsrender.js new file mode 100644 index 00000000..67ec428f --- /dev/null +++ b/public/js/lib/jsrender.js @@ -0,0 +1,1958 @@ +/*! JsRender v1.0.0-beta: http://www.jsviews.com/#jsrender +informal pre V1.0 commit counter: 64pre*/ +/* + * Optimized version of jQuery Templates, for rendering to string. + * Does not require jQuery, or HTML DOM + * Integrates with JsViews (http://www.jsviews.com/#jsviews) + * + * Copyright 2015, Boris Moore + * Released under the MIT License. + */ + +//jshint -W018, -W041 + +(function (factory) { + if (typeof define === 'function' && define.amd) { + // Loading from AMD script loader. Register as an anonymous module. + define(factory); + } else if (typeof exports === 'object') { + // CommonJS + var jsrender = module.exports = factory(true, require("fs")); // jsrender = jsviews.views + + jsrender.renderFile = jsrender.__express = function(filepath, data, callback) { // Support for rendering templates from file + // system in Node.js Node, and for Express template engine integration, using app.engine('html', jsrender.__express); + var html = jsrender.templates("@" + filepath).render(data); + if (callback) { + callback(null, html); + } + return html; + }; + } else { + // Browser using plain