Compare commits

...

123 Commits
master ... v2.1

Author SHA1 Message Date
Timothy Warren c59288b5f9 Merge pull request #13 from timw4mail/develop
Sync with dev
2016-01-11 15:44:57 -05:00
Timothy Warren 6ff4ee2746 Change logger methods to be inline with interface, fix Manga Model tests 2016-01-11 15:31:53 -05:00
Timothy Warren 9b03f102f3 Update whoops to 2.0 2016-01-11 14:57:43 -05:00
Timothy Warren 816a309f18 Remove errorhandler, and replace with logger 2016-01-11 14:39:53 -05:00
Timothy Warren b85ddb9464 Update collection to use flash messages and more intelligent redirects 2016-01-11 13:33:56 -05:00
Timothy Warren 3918ce4eb7 Add more test coverage, and update build.xml 2016-01-11 10:42:34 -05:00
Timothy Warren 3bd5c7d218 Further refine Dispatcher 2016-01-08 16:39:18 -05:00
Timothy Warren 9c73ab928c Refactor Dispatcher 2016-01-08 15:54:21 -05:00
Timothy Warren 016e0988e9 Fix line endings in view classes 2016-01-08 15:53:50 -05:00
Timothy Warren 97dfa89b6d Actually fix view tests 2016-01-08 11:40:24 -05:00
Timothy Warren f5549934fe Fix view tests 2016-01-08 11:19:56 -05:00
Timothy Warren 27ac7e8063 Fix http verb for update route, add correct http codes for http errors 2016-01-07 20:48:18 -05:00
Timothy Warren 275b0eea40 Update codebase to use new Json class 2016-01-07 13:45:43 -05:00
Timothy Warren fa4940f22d Add missing classes 2016-01-06 17:08:44 -05:00
Timothy Warren bfe46fbbd1 Fix PHP 5.5 build 2016-01-06 17:06:30 -05:00
Timothy Warren 38faaebb5f Remove unnamespaced constants, and improve some tests 2016-01-06 15:44:40 -05:00
Timothy Warren 3c124456d0 Simplify routing 2016-01-06 11:08:56 -05:00
Timothy Warren b4489311c9 Fix testss 2016-01-05 10:05:14 -05:00
Timothy Warren 7a9fe42d83 Merge pull request #12 from timw4mail/scrutinizer-patch-2
Scrutinizer Auto-Fixes
2016-01-05 10:02:18 -05:00
Scrutinizer Auto-Fixer dd0e15137a Scrutinizer Auto-Fixes
This commit consists of patches automatically generated for this project on https://scrutinizer-ci.com
2016-01-05 14:55:08 +00:00
Timothy Warren 205c7ac76d Update header comments, add start of manga editing functionality 2016-01-04 16:58:33 -05:00
Timothy Warren 6455541210 Merge pull request #11 from timw4mail/scrutinizer-patch-1
Scrutinizer Auto-Fixes
2016-01-04 11:16:15 -05:00
Scrutinizer Auto-Fixer 7cdd79a116 Scrutinizer Auto-Fixes
This commit consists of patches automatically generated for this project on https://scrutinizer-ci.com
2016-01-04 16:15:25 +00:00
Timothy Warren 6e4e8edd9d Add full edit form to anime list 2016-01-04 10:53:03 -05:00
Timothy Warren 1251486aa3 Update composer.json 2015-12-16 10:12:31 -05:00
Timothy Warren 1b8ed53afb Start of delete functionality for anime collection 2015-12-15 15:55:30 -05:00
Timothy Warren 5e048977a3 Merge pull request #10 from timw4mail/scrutinizer-patch-1
Scrutinizer Auto-Fixes
2015-12-09 15:13:24 -05:00
Scrutinizer Auto-Fixer 355aff2951 Scrutinizer Auto-Fixes
This commit consists of patches automatically generated for this project on https://scrutinizer-ci.com
2015-12-09 19:59:54 +00:00
Timothy Warren 3c41682d73 Some more minor code-style fixes 2015-12-09 14:54:11 -05:00
Timothy Warren 77709c068a Some code style fixes 2015-12-08 16:39:49 -05:00
Timothy Warren d48cafa54d update travis build file 2015-12-08 14:58:43 -05:00
Timothy Warren f386766841 Fix collection functionality 2015-12-08 14:52:59 -05:00
Timothy Warren 52397bbd61 Update README and composer 2015-11-18 16:03:40 -05:00
Timothy Warren 99b429433c Skip erroring tests on travis 2015-11-18 10:58:12 -05:00
Timothy Warren c23c63cb15 Fix some minor formating issues 2015-11-18 10:54:06 -05:00
Timothy Warren fff46421bf Update minor documention issues 2015-11-18 10:48:05 -05:00
Timothy Warren 61f1963db8 Try mocking out get_cached_image method 2015-11-18 10:41:00 -05:00
Timothy Warren 253f191113 More test coverage 2015-11-18 10:31:42 -05:00
Timothy Warren 6622014bd1 Improve some test coverage 2015-11-17 16:45:41 -05:00
Timothy Warren bd6b5e2b54 Remove loose functions file 2015-11-16 19:30:04 -05:00
Timothy Warren 4ed6200d9f Fix manga list updating 2015-11-16 15:57:37 -05:00
Timothy Warren c009a96a15 Merge pull request #9 from timw4mail/scrutinizer-patch-1
Scrutinizer Auto-Fixes
2015-11-16 11:40:26 -05:00
Timothy Warren ae88282b13 Update header comments 2015-11-16 11:40:01 -05:00
Scrutinizer Auto-Fixer 38b2f34527 Scrutinizer Auto-Fixes
This commit consists of patches automatically generated for this project on https://scrutinizer-ci.com
2015-11-16 15:33:30 +00:00
Timothy Warren afced2339a Poor style progress update commit 2015-11-13 16:31:01 -05:00
Timothy Warren 83cd815750 Update some config and metadata 2015-11-13 11:34:30 -05:00
Timothy Warren ca2e72d3f0 Update 404 view 2015-11-13 11:33:47 -05:00
Timothy Warren 3c4ba096c6 Make updating of anime list work 2015-11-13 11:33:27 -05:00
Timothy Warren 1891aafef5 Update js minifier to be more robust, with better error handling 2015-11-13 11:32:12 -05:00
Timothy Warren aee2fa7120 Fix various code style nuances 2015-11-11 15:28:51 -05:00
Timothy Warren c55f91a79f Fix some sonarqube issues 2015-11-11 14:53:09 -05:00
Timothy Warren 8f95bfe7e0 Merge pull request #8 from timw4mail/scrutinizer-patch-1
Scrutinizer Auto-Fixes
2015-11-09 15:55:54 -05:00
Scrutinizer Auto-Fixer db33f46547 Scrutinizer Auto-Fixes
This commit consists of patches automatically generated for this project on https://scrutinizer-ci.com
2015-11-09 20:08:08 +00:00
Timothy Warren 68cb36b193 Update config and header for new auth class 2015-11-09 11:50:24 -05:00
Timothy Warren ff28b40c9e Fix ArrayType class 2015-11-09 11:49:51 -05:00
Timothy Warren e6b4fe59a3 More quality fixes 2015-11-09 11:10:15 -05:00
Timothy Warren 0cd30e811d No coverage for scrutinizer 2015-11-05 11:30:51 -05:00
Timothy Warren d3541da789 Fix some more code style issues 2015-11-05 11:26:03 -05:00
Timothy Warren cdb4406e14 Some more style fixes 2015-11-05 10:41:46 -05:00
Timothy Warren 6ef8caca00 Some code style fixes 2015-11-04 16:53:22 -05:00
Timothy Warren beb127c06c Some minor refactoring 2015-11-04 16:36:54 -05:00
Timothy Warren 9b38242f9d Merge pull request #7 from timw4mail/scrutinizer-patch-1
Scrutinizer Auto-Fixes
2015-11-04 16:33:18 -05:00
Scrutinizer Auto-Fixer d63d33d245 Scrutinizer Auto-Fixes
This commit consists of patches automatically generated for this project on https://scrutinizer-ci.com
2015-11-04 21:31:03 +00:00
Timothy Warren 30f18106cb Update metadata and build information files 2015-11-04 16:12:46 -05:00
Timothy Warren fdd0da8d93 Fix tests broken by missing fix to Anime Collection Model 2015-10-21 15:46:50 -04:00
Timothy Warren 4ad6178bf0 More test coverage 2015-10-21 15:43:51 -04:00
Timothy Warren 32d20a9234 Fix issue where cache file doesn't exist, add tests for Menu Helper 2015-10-21 11:57:58 -04:00
Timothy Warren def424da72 Fix default redirect and tests 2015-10-20 16:41:51 -04:00
Timothy Warren c99d4ee53d Fix the rest of the menu urls 2015-10-20 15:59:51 -04:00
Timothy Warren 935076cc63 Remove another vistigal controller method 2015-10-19 15:19:02 -04:00
Timothy Warren 766fad6bb2 Remove risky tests, update .gitignore 2015-10-19 15:13:18 -04:00
Timothy Warren 672b781425 Remove some vestigal methods from base controller 2015-10-19 13:58:59 -04:00
Timothy Warren 95ecc9e9b8 Fix spacing style 2015-10-19 13:26:50 -04:00
Timothy Warren 8625f20b74 Fix tests for PHP 5.5 2015-10-19 13:02:10 -04:00
Timothy Warren 944a0c9c2a More test coverage 2015-10-19 12:50:46 -04:00
Timothy Warren f22015635e Scrutinizer fixes 2015-10-16 12:53:55 -04:00
Timothy Warren 23122964d2 Better testing for ArrayType and Config classes 2015-10-15 22:00:09 -04:00
Timothy Warren 67080a098f Add partial test for config delete 2015-10-15 10:23:00 -04:00
Timothy Warren 5bf46e0840 Fix origin value in API Model tests 2015-10-15 09:49:38 -04:00
Timothy Warren 454679626c Fix html view test for PHP < 7 2015-10-15 09:28:10 -04:00
Timothy Warren e2e27c2311 More test coverage 2015-10-15 09:25:30 -04:00
Timothy Warren 3cf753a707 Update lots of docblocks 2015-10-14 09:20:52 -04:00
Timothy Warren 9ed18ce131 Fix documentation issues 2015-10-12 14:27:20 -04:00
Timothy Warren 6b1f39525d Remove some dead code 2015-10-12 14:11:00 -04:00
Timothy Warren 725915060b Merge pull request #6 from timw4mail/scrutinizer-patch-1
Scrutinizer Auto-Fixes
2015-10-12 11:00:09 -04:00
Scrutinizer Auto-Fixer 2b6b8bff43 Scrutinizer Auto-Fixes
This commit consists of patches automatically generated for this project on https://scrutinizer-ci.com
2015-10-10 02:35:39 +00:00
Timothy Warren f49e4fe3d8 Rearrange some namespaces and add more docblocks 2015-10-09 22:29:59 -04:00
Timothy Warren 9cf958b1ed Merge pull request #5 from timw4mail/scrutinizer-patch-1
Scrutinizer Auto-Fixes
2015-10-09 15:04:55 -04:00
Scrutinizer Auto-Fixer 25981afeef Scrutinizer Auto-Fixes
This commit consists of patches automatically generated for this project on https://scrutinizer-ci.com
2015-10-09 18:55:15 +00:00
Timothy Warren 3de32f52af Basic Menu generation 2015-10-09 14:34:55 -04:00
Timothy Warren 15707167f1 More scrutinizer fixes 2015-10-06 13:38:59 -04:00
Timothy Warren c86d72f3e2 Merge pull request #4 from timw4mail/scrutinizer-patch-1
Scrutinizer Auto-Fixes
2015-10-06 13:37:48 -04:00
Scrutinizer Auto-Fixer edd393793a Scrutinizer Auto-Fixes
This commit consists of patches automatically generated for this project on https://scrutinizer-ci.com
2015-10-06 17:35:42 +00:00
Timothy Warren f9b3d64eca Fix more scrutinizer issues 2015-10-06 12:15:19 -04:00
Timothy Warren 83f1a9afd9 Fix failing test 2015-10-06 11:41:21 -04:00
Timothy Warren d53524ed86 Code style improvements 2015-10-06 11:38:20 -04:00
Timothy Warren aedabd6eda Scrutinizer fixes 2015-10-06 10:44:33 -04:00
Timothy Warren af95ac941f Merge pull request #3 from timw4mail/scrutinizer-patch-1
Spacing and docblock fixes
2015-10-06 10:28:33 -04:00
Scrutinizer Auto-Fixer 7c5a73e73b Scrutinizer Auto-Fixes
This commit consists of patches automatically generated for this project on https://scrutinizer-ci.com
2015-10-06 14:24:48 +00:00
Timothy Warren 5e5434d057 Miscellaneous updates, prep for menu generator 2015-10-05 16:54:25 -04:00
Timothy Warren 9f123822b1 Update Router 2015-10-01 16:30:46 -04:00
Timothy Warren 651b9c4483 Update some meta files 2015-10-01 16:21:09 -04:00
Timothy Warren e71e76dbc6 fix test 2015-10-01 16:07:40 -04:00
Timothy Warren 7917c39065 Lots of miscellaneous improvements 2015-10-01 16:02:51 -04:00
Timothy Warren e8a9982f9a Fix views to match transformed data 2015-10-01 16:01:23 -04:00
Timothy Warren e634a22134 Fix broken test 2015-09-28 15:11:45 -04:00
Timothy Warren a7ae1ac3a6 Use Anime transformer class 2015-09-28 14:41:45 -04:00
Timothy Warren 5624d9b44e Transformers and Enums 2015-09-25 13:41:12 -04:00
Timothy Warren 92d9124bb7 Update manga model to use Zipper transformer 2015-09-21 09:48:15 -04:00
Timothy Warren 8fb6dae119 More tests for Ion 2015-09-18 22:55:40 -04:00
Timothy Warren 5f6119c86b Fix failing tests for PHP < 5.6 2015-09-18 13:06:22 -04:00
Timothy Warren 602759b471 Decouple and generalise 2015-09-17 23:11:18 -04:00
Timothy Warren c788cf5d87 Start of refactoring routing to be more convention based 2015-09-16 12:25:35 -04:00
Timothy Warren 9193938dee More namespace refactoring 2015-09-15 13:19:29 -04:00
Timothy Warren b1c6039630 Namespace refactoring 2015-09-14 19:54:34 -04:00
Timothy Warren 67799fcdfa fix a few variable changes, remove old code from app folder 2015-09-14 16:14:02 -04:00
Timothy Warren dfe0b3a6cf Pass the tests! 2015-09-14 15:49:20 -04:00
Timothy Warren cee211621c Some progress toward better structure through refactoring 2015-09-14 10:54:50 -04:00
Timothy Warren 8904054212 Update default config, add phpci config file 2015-07-20 16:13:00 -04:00
Timothy Warren 6450a76351 Update readme with new instructions for collection 2015-07-06 14:35:24 -04:00
Timothy Warren 54371bf76a Miscellaneous rework, and adding/editing of collection items when logged in 2015-07-02 14:04:04 -04:00
Timothy Warren c33575e3d8 More dependency injection, and code coverage 2015-06-30 13:03:20 -04:00
Timothy Warren ef4e9b3e85 Merge pull request #1 from timw4mail/scrutinizer-patch-1
Scrutinizer Auto-Fixes
2015-06-29 10:36:28 -04:00
168 changed files with 16988 additions and 2599 deletions

20
.editorconfig Normal file
View File

@ -0,0 +1,20 @@
# EditorConfig is awesome: http://EditorConfig.org
# top-most EditorConfig file
root = true
# Unix-style newlines with a newline ending every file
[*]
end_of_line = lf
insert_final_newline = false
charset = utf-8
indent_style = tab
trim_trailing_whitespace = true
[*.{cpp,c,h,hpp,cxx}]
insert_final_newline = true
# Yaml files
[*.{yml,yaml}]
indent_style = space
indent_size = 4

31
.gitignore vendored
View File

@ -1,10 +1,21 @@
vendor
app/cache/*
public/images/*
public/js/cache/*
composer.lock
*.sqlite
*.db
*.sqlite3
docs/*
coverage/*
.codelite
*.phprj
*.workspace
vendor
app/cache/*
app/logs/*
public/images/*
public/js/cache/*
composer.lock
*.sqlite
*.db
*.sqlite3
docs/*
coverage/*
tests/test_data/sessions/*
build/coverage/*
build/logs/*
build/pdepend/*
build/phpdox/*
cache.properties
tests/test_data/cache/*

View File

@ -4,7 +4,6 @@ install:
- composer install
php:
- 5.4
- 5.5
- 5.6
- 7
@ -13,8 +12,12 @@ php:
script:
- mkdir -p build/logs
- phpunit --coverage-clover=coverage.clover
- phpunit -c build
after_script:
- wget https://scrutinizer-ci.com/ocular.phar
- php ocular.phar code-coverage:upload --format=php-clover coverage.clover
- php ocular.phar code-coverage:upload --format=php-clover build/logs/coverage.clover
matrix:
allow_failures:
- php: nightly

View File

@ -2,9 +2,11 @@
A self-hosted client that allows custom formatting of data from the hummingbird api
[![Build Status](https://travis-ci.org/timw4mail/HummingBirdAnimeClient.svg)](https://travis-ci.org/timw4mail/HummingBirdAnimeClient)
[![Build Status](https://jenkins.timshomepage.net/buildStatus/icon?job=animeclient)](https://jenkins.timshomepage.net/job/animeclient/)
[![Build Status](https://travis-ci.org/timw4mail/HummingBirdAnimeClient.svg?branch=master)](https://travis-ci.org/timw4mail/HummingBirdAnimeClient)
[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/timw4mail/HummingBirdAnimeClient/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/timw4mail/HummingBirdAnimeClient/?branch=master)
[[Hosted Example](https://anime.timshomepage.net)]
[[Hosted Example](https://list.timshomepage.net)]
## Features
@ -13,33 +15,32 @@ A self-hosted client that allows custom formatting of data from the hummingbird
* Plan to Watch
* On Hold
* Dropped
* Completed
* All of the above
* Completed
* Combined View
* Manga List views (Each with list and cover views):
* Reading
* Plan to Read
* On Hold
* Dropped
* Completed
* All of the above
* Combined View
* Anime collection view (segmented by media type):
* Cover Images
* Table List
### Requirements
* PHP 5.4+
* PHP 5.5+
* PDO SQLite (For collection tab)
* GD
### Installation
1. Install dependencies via composer: `composer install`
2. Change the `WHOSE` constant declaration in `index.php` to your name
3. Configure settings in `app/config/config.php` to your liking
4. Create the following directories if they don't exist, and make sure they are world writable
1. Install via composer: `composer create-project timw4mail/hummingbird-anime-client`
2. Configure settings in `app/config/config.php` to your liking
3. Create the following directories if they don't exist, and make sure they are world writable
* app/cache
* public/images/manga
* public/images/anime
@ -48,8 +49,11 @@ A self-hosted client that allows custom formatting of data from the hummingbird
#### Anime Collection Additional Installation
* Run `php /vendor/bin/phinx migrate -e development` to create the database tables
* For importing anime:
1. Login
2. Use the form to select your media
3. Save &amp; Repeat as needed
* For bulk importing anime:
1. Find the anime you are looking for on the hummingbird search api page: `https://hummingbird.me/api/v1/search/anime?query=`
2. Create an `import.json` file in the root of the app, with an array of objects from the search page that you want to import
3. Go to the anime collection tab, and the import will be run

View File

@ -1,81 +0,0 @@
<?php
/**
* Base API Model
*/
namespace AnimeClient;
use \GuzzleHttp\Client;
use \GuzzleHttp\Cookie\CookieJar;
/**
* Base model for api interaction
*/
class BaseApiModel extends BaseModel {
/**
* Base url for making api requests
* @var string
*/
protected $base_url = '';
/**
* The Guzzle http client object
* @var object
*/
protected $client;
/**
* Cookie jar object for api requests
* @var object
*/
protected $cookieJar;
/**
* Constructor
*/
public function __construct()
{
parent::__construct();
$this->cookieJar = new CookieJar();
$this->client = new Client([
'base_url' => $this->base_url,
'defaults' => [
'cookies' => $this->cookieJar,
'headers' => [
'User-Agent' => $_SERVER['HTTP_USER_AGENT'],
'Accept-Encoding' => 'application/json'
],
'timeout' => 5,
'connect_timeout' => 5
]
]);
}
/**
* Attempt login via the api
*
* @codeCoverageIgnore
* @param string $username
* @param string $password
* @return bool
*/
public function authenticate($username, $password)
{
$result = $this->client->post('https://hummingbird.me/api/v1/users/authenticate', [
'body' => [
'username' => $username,
'password' => $password
]
]);
if ($result->getStatusCode() === 201)
{
$_SESSION['hummingbird_anime_token'] = $result->json();
return TRUE;
}
return FALSE;
}
}
// End of BaseApiModel.php

View File

@ -1,254 +0,0 @@
<?php
/**
* Base Controller
*/
namespace AnimeClient;
use Aura\Web\WebFactory;
/**
* Base class for controllers, defines output methods
*/
class BaseController {
/**
* The global configuration object
* @var object $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;
/**
* Common data to be sent to views
* @var array
*/
protected $base_data = [];
/**
* Constructor
*/
public function __construct(Config $config, Array $web)
{
$this->config = $config;
list($request, $response) = $web;
$this->request = $request;
$this->response = $response;
}
public function __destruct()
{
$this->output();
}
/**
* Get the string output of a partial template
*
* @param string $template
* @param array|object $data
* @return string
*/
public function load_partial($template, $data=[])
{
if (isset($this->base_data))
{
$data = array_merge($this->base_data, $data);
}
global $router, $defaultHandler;
$route = $router->get_route();
$data['route_path'] = ($route) ? $router->get_route()->path : "";
$defaultHandler->addDataTable('Template Data', $data);
$template_path = _dir(APP_DIR, 'views', "{$template}.php");
if ( ! is_file($template_path))
{
throw new Exception("Invalid template : {$path}");
}
ob_start();
extract($data);
include _dir(APP_DIR, 'views', 'header.php');
include $template_path;
include _dir(APP_DIR, 'views', 'footer.php');
$buffer = ob_get_contents();
ob_end_clean();
return $buffer;
}
/**
* Output a template to HTML, using the provided data
*
* @param string $template
* @param array|object $data
* @return void
*/
public function outputHTML($template, $data=[])
{
$buffer = $this->load_partial($template, $data);
$this->response->content->setType('text/html');
$this->response->content->set($buffer);
}
/**
* Output json with the proper content type
*
* @param mixed $data
* @return void
*/
public function outputJSON($data)
{
if ( ! is_string($data))
{
$data = json_encode($data);
}
$this->response->content->setType('application/json');
$this->response->content->set($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}");
}
/**
* Add a message box to the page
*
* @param string $type
* @param string $message
* @return string
*/
public function show_message($type, $message)
{
return $this->load_partial('message', [
'stat_class' => $type,
'message' => $message
]);
}
/**
* Clear the api session
*
* @return void
*/
public function logout()
{
session_destroy();
$this->response->redirect->seeOther(full_url(''));
}
/**
* Show the login form
*
* @param string $status
* @return void
*/
public function login($status="")
{
$message = "";
if ($status != "")
{
$message = $this->show_message('error', $status);
}
$this->outputHTML('login', [
'title' => 'Api login',
'message' => $message
]);
}
/**
* Attempt to log in with the api
*
* @return void
*/
public function login_action()
{
if (
$this->model->authenticate(
$this->config->hummingbird_username,
$this->request->post->get('password')
)
)
{
$this->response->redirect->afterPost(full_url('', $this->base_data['url_type']));
return;
}
$this->login("Invalid username or password.");
}
/**
* Send the appropriate response
*
* @return void
*/
private function output()
{
// send status
@header($this->response->status->get(), true, $this->response->status->getCode());
// headers
foreach($this->response->headers->get() as $label => $value)
{
@header("{$label}: {$value}");
}
// cookies
foreach($this->response->cookies->get() as $name => $cookie)
{
@setcookie(
$name,
$cookie['value'],
$cookie['expire'],
$cookie['path'],
$cookie['domain'],
$cookie['secure'],
$cookie['httponly']
);
}
// send the actual response
echo $this->response->content->get();
}
}
// End of BaseController.php

View File

@ -1,32 +0,0 @@
<?php
/**
* Base DB model
*/
namespace AnimeClient;
/**
* Base model for database interaction
*/
class BaseDBModel extends BaseModel {
/**
* The query builder object
* @var object $db
*/
protected $db;
/**
* The database connection information array
* @var array $db_config
*/
protected $db_config;
/**
* Constructor
*/
public function __construct()
{
parent::__construct();
$this->db_config = $this->config->database;
}
}
// End of BaseDBModel.php

View File

@ -1,56 +0,0 @@
<?php
namespace AnimeClient;
/**
* Wrapper for configuration values
*/
class Config {
/**
* Config object
*
* @var array
*/
protected $config = [];
/**
* Constructor
*
* @param array $config_files
*/
public function __construct(Array $config_files=[])
{
// @codeCoverageIgnoreStart
if (empty($config_files))
{
require_once _dir(CONF_DIR, 'config.php'); // $config
require_once _dir(CONF_DIR, 'base_config.php'); // $base_config
}
else // @codeCoverageIgnoreEnd
{
$config = $config_files['config'];
$base_config = $config_files['base_config'];
}
$this->config = array_merge($config, $base_config);
}
/**
* Getter for config values
*
* @param string $key
* @return mixed
*/
public function __get($key)
{
if (isset($this->config[$key]))
{
return $this->config[$key];
}
return NULL;
}
}
// End of config.php

View File

@ -1,175 +0,0 @@
<?php
/**
* Routing logic
*/
namespace AnimeClient;
/**
* Basic routing/ dispatch
*/
class Router {
/**
* The route-matching object
* @var object $router
*/
protected $router;
/**
* The global configuration object
* @var object $config
*/
protected $config;
/**
* Array containing request and response objects
* @var array $web
*/
protected $web;
/**
* Constructor
*
* @param
*/
public function __construct(Config $config, \Aura\Router\Router $router, \Aura\Web\Request $request, \Aura\Web\Response $response)
{
$this->config = $config;
$this->router = $router;
$this->web = [$request, $response];
$this->_setup_routes();
}
/**
* Get the current route object, if one matches
*
* @return object
*/
public function get_route()
{
global $defaultHandler;
$raw_route = $_SERVER['REQUEST_URI'];
$route_path = str_replace([$this->config->anime_path, $this->config->manga_path], '', $raw_route);
$route_path = "/" . trim($route_path, '/');
$defaultHandler->addDataTable('Route Info', [
'route_path' => $route_path
]);
$route = $this->router->match($route_path, $_SERVER);
return $route;
}
/**
* Handle the current route
*
* @param [object] $route
* @return void
*/
public function dispatch($route = NULL)
{
global $defaultHandler;
if (is_null($route))
{
$route = $this->get_route();
}
if ( ! $route)
{
$failure = $this->router->getFailedRoute();
$defaultHandler->addDataTable('failed_route', (array)$failure);
$controller_name = 'BaseController';
$action_method = 'outputHTML';
$params = [
'template' => '404',
'data' => [
'title' => 'Page Not Found'
]
];
}
else
{
list($controller_name, $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($this->config, $this->web);
// Run the appropriate controller method
$defaultHandler->addDataTable('controller_args', $params);
call_user_func_array([$controller, $action_method], $params);
}
/**
* Select controller based on the current url, and apply its relevent routes
*
* @return void
*/
private function _setup_routes()
{
$route_map = [
'anime' => '\\AnimeClient\\AnimeController',
'manga' => '\\AnimeClient\\MangaController',
];
$route_type = "anime";
if ($this->config->manga_host !== "" && strpos($_SERVER['HTTP_HOST'], $this->config->manga_host) !== FALSE)
{
$route_type = "manga";
}
else if ($this->config->manga_path !== "" && strpos($_SERVER['REQUEST_URI'], $this->config->manga_path) !== FALSE)
{
$route_type = "manga";
}
$routes = $this->config->routes;
// Add routes
foreach(['common', $route_type] as $key)
{
foreach($routes[$key] as $name => &$route)
{
$path = $route['path'];
unset($route['path']);
// Prepend the controller to the route parameters
array_unshift($route['action'], $route_map[$route_type]);
// Select the appropriate router method based on the http verb
$add = (array_key_exists('verb', $route)) ? "add" . ucfirst(strtolower($route['verb'])) : "addGet";
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);
}
}
}
}
}
// End of Router.php

View File

@ -1,134 +0,0 @@
<?php
/**
* Global functions
*/
/**
* Check if the user is currently logged in
*
* @return bool
*/
function is_logged_in()
{
return array_key_exists('hummingbird_anime_token', $_SESSION);
}
/**
* HTML selection helper function
*
* @param string $a - First item to compare
* @param string $b - Second item to compare
* @return string
*/
function is_selected($a, $b)
{
return ($a === $b) ? 'selected' : '';
}
/**
* Inverse of selected helper function
*
* @param string $a - First item to compare
* @param string $b - Second item to compare
* @return string
*/
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
*
* @return string
*/
function last_segment()
{
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$segments = explode('/', $path);
return end($segments);
}
// End of functions.php

View File

@ -1,43 +0,0 @@
<?php
/**
* Functions that need to be included before config
*/
/**
* Joins paths together. Variadic to take an
* arbitrary number of arguments
*
* @return string
*/
function _dir()
{
return implode(DIRECTORY_SEPARATOR, func_get_args());
}
/**
* Set up autoloaders
*
* @codeCoverageIgnore
* @return void
*/
function _setup_autoloaders()
{
require _dir(ROOT_DIR, '/vendor/autoload.php');
spl_autoload_register(function ($class) {
$class_parts = explode('\\', $class);
$class = end($class_parts);
$dirs = ["base", "controllers", "models"];
foreach($dirs as $dir)
{
$file = _dir(APP_DIR, $dir, "{$class}.php");
if (file_exists($file))
{
require_once $file;
return;
}
}
});
}

View File

@ -1,57 +1,94 @@
<?php
namespace AnimeClient;
/**
* Bootstrap / Dependency Injection
*/
namespace Aviat\AnimeClient;
use \Whoops\Handler\PrettyPageHandler;
use \Whoops\Handler\JsonResponseHandler;
use \Aura\Web\WebFactory;
use \Aura\Router\RouterFactory;
use \GuzzleHttp\Client;
use \GuzzleHttp\Cookie\CookieJar;
use Aura\Html\HelperLocatorFactory;
use Aura\Web\WebFactory;
use Aura\Router\RouterFactory;
use Aura\Session\SessionFactory;
use Monolog\Logger;
use Monolog\Handler\RotatingFileHandler;
use Monolog\Handler\BrowserConsoleHandler;
use Aviat\Ion\Di\Container;
use Aviat\AnimeClient\Auth\HummingbirdAuth;
use Aviat\AnimeClient\Model;
// -----------------------------------------------------------------------------
// Setup error handling
// Setup DI container
// -----------------------------------------------------------------------------
$whoops = new \Whoops\Run();
return function(array $config_array = []) {
$container = new Container();
// Set up default handler for general errors
$defaultHandler = new PrettyPageHandler();
$whoops->pushHandler($defaultHandler);
// -------------------------------------------------------------------------
// Logging
// -------------------------------------------------------------------------
// Set up json handler for ajax errors
$jsonHandler = new JsonResponseHandler();
$jsonHandler->onlyForAjaxRequests(true);
$whoops->pushHandler($jsonHandler);
$app_logger = new Logger('animeclient');
$app_logger->pushHandler(new RotatingFileHandler(__DIR__ . '/logs/app.log', Logger::NOTICE));
$container->setLogger($app_logger, 'default');
$whoops->register();
// -------------------------------------------------------------------------
// Injected Objects
// -------------------------------------------------------------------------
// -----------------------------------------------------------------------------
// Injected Objects
// -----------------------------------------------------------------------------
// Create Config Object
$config = new Config($config_array);
$container->set('config', $config);
// Create Config Object
$config = new Config();
require _dir(BASE_DIR, '/functions.php');
// Create Aura Router Object
$aura_router = (new RouterFactory())->newInstance();
$container->set('aura-router', $aura_router);
// Create Aura Router Object
$router_factory = new RouterFactory();
$aura_router = $router_factory->newInstance();
// Create Html helper Object
$html_helper = (new HelperLocatorFactory)->newInstance();
$html_helper->set('menu', function() use ($container) {
$menu_helper = new Helper\Menu();
$menu_helper->setContainer($container);
return $menu_helper;
});
$container->set('html-helper', $html_helper);
// Create Request/Response Objects
$web_factory = new WebFactory([
'_GET' => $_GET,
'_POST' => $_POST,
'_COOKIE' => $_COOKIE,
'_SERVER' => $_SERVER,
'_FILES' => $_FILES
]);
$request = $web_factory->newRequest();
$response = $web_factory->newResponse();
// Create Request/Response Objects
$web_factory = new WebFactory([
'_GET' => $_GET,
'_POST' => $_POST,
'_COOKIE' => $_COOKIE,
'_SERVER' => $_SERVER,
'_FILES' => $_FILES
]);
$container->set('request', $web_factory->newRequest());
$container->set('response', $web_factory->newResponse());
// -----------------------------------------------------------------------------
// Router
// -----------------------------------------------------------------------------
$router = new Router($config, $aura_router, $request, $response);
$router->dispatch();
// Create session Object
$session = (new SessionFactory())->newInstance($_COOKIE);
$container->set('session', $session);
$container->set('url-generator', new UrlGenerator($container));
// Miscellaneous helper methods
$anime_client = new AnimeClient();
$anime_client->setContainer($container);
$container->set('anime-client', $anime_client);
// Models
$container->set('api-model', new Model\API($container));
$container->set('anime-model', new Model\Anime($container));
$container->set('manga-model', new Model\Manga($container));
$container->set('anime-collection-model', new Model\AnimeCollection($container));
$container->set('auth', new HummingbirdAuth($container));
// -------------------------------------------------------------------------
// Dispatcher
// -------------------------------------------------------------------------
$container->set('dispatcher', new Dispatcher($container));
return $container;
};
// End of bootstrap.php

View File

@ -1,15 +1,34 @@
<?php
/**
* Hummingbird Anime Client
*
* An API client for Hummingbird to manage anime and manga watch lists
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren
* @copyright Copyright (c) 2015 - 2016
* @link https://github.com/timw4mail/HummingBirdAnimeClient
* @license MIT
*/
// ----------------------------------------------------------------------------
// Lower level configuration
//
// You shouldn't generally need to change anything below this line
// ----------------------------------------------------------------------------
$APP_DIR = realpath(__DIR__ . '/../');
$ROOT_DIR = realpath("{$APP_DIR}/../");
$base_config = [
// Template file path
'view_path' => "{$APP_DIR}/views",
// Cache paths
'data_cache_path' => _dir(APP_DIR, 'cache'),
'img_cache_path' => _dir(ROOT_DIR, 'public/images'),
'data_cache_path' => "{$APP_DIR}/cache",
'img_cache_path' => "{$ROOT_DIR}/public/images",
// Included config files
'routes' => require _dir(CONF_DIR, 'routes.php'),
'database' => require _dir(CONF_DIR, 'database.php'),
'database' => require 'database.php',
'menus' => require 'menus.php',
'routes' => require 'routes.php',
];

View File

@ -1,40 +1,37 @@
<?php
/**
* Hummingbird Anime Client
*
* An API client for Hummingbird to manage anime and manga watch lists
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren
* @copyright Copyright (c) 2015 - 2016
* @link https://github.com/timw4mail/HummingBirdAnimeClient
* @license MIT
*/
$config = [
// ----------------------------------------------------------------------------
// Username for anime and manga lists
// ----------------------------------------------------------------------------
'hummingbird_username' => 'timw4mail',
// ----------------------------------------------------------------------------
// Whose list is it?
// ----------------------------------------------------------------------------
'whose_list' => 'Tim',
// ----------------------------------------------------------------------------
// General config
// ----------------------------------------------------------------------------
// do you wish to show the anime collection tab?
// do you wish to show the anime collection?
'show_anime_collection' => TRUE,
// path to public directory
'asset_path' => '//' . $_SERVER['HTTP_HOST'] . '/public',
// do you wish to show the manga collection?
'show_manga_collection' => FALSE,
// path to public directory on the server
'asset_dir' => __DIR__ . '/../../public',
// ----------------------------------------------------------------------------
// Routing
//
// Route by path, or route by domain. To route by path, set the _host suffixed
// options to an empty string. To route by host, set the _path suffixed options
// to an empty string
// ----------------------------------------------------------------------------
'anime_host' => 'anime.timshomepage.net',
'manga_host' => 'manga.timshomepage.net',
'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,
'asset_dir' => realpath(__DIR__ . '/../../public'),
];

View File

@ -1,4 +1,16 @@
<?php
/**
* Hummingbird Anime Client
*
* An API client for Hummingbird to manage anime and manga watch lists
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren
* @copyright Copyright (c) 2015 - 2016
* @link https://github.com/timw4mail/HummingBirdAnimeClient
* @license MIT
*/
return [
'collection' => [
'type' => 'sqlite',

37
app/config/menus.php Normal file
View File

@ -0,0 +1,37 @@
<?php
/**
* Hummingbird Anime Client
*
* An API client for Hummingbird to manage anime and manga watch lists
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren
* @copyright Copyright (c) 2015 - 2016
* @link https://github.com/timw4mail/HummingBirdAnimeClient
* @license MIT
*/
return [
'anime_list' => [
'route_prefix' => '/anime',
'items' => [
'watching' => '/watching',
'plan_to_watch' => '/plan_to_watch',
'on_hold' => '/on_hold',
'dropped' => '/dropped',
'completed' => '/completed',
'all' => '/all'
]
],
'manga_list' => [
'route_prefix' => '/manga',
'items' => [
'reading' => '/reading',
'plan_to_read' => '/plan_to_read',
'on_hold' => '/on_hold',
'dropped' => '/dropped',
'completed' => '/completed',
'all' => '/all'
]
]
];

View File

@ -1,21 +1,20 @@
<?php
/**
* Easy Min
* Hummingbird Anime Client
*
* Simple minification for better website performance
* An API client for Hummingbird to manage anime and manga watch lists
*
* @author Timothy J. Warren
* @copyright Copyright (c) 2012
* @link https://github.com/aviat4ion/Easy-Min
* @license http://philsturgeon.co.uk/code/dbad-license
* @package HummingbirdAnimeClient
* @author Timothy J. Warren
* @copyright Copyright (c) 2015 - 2016
* @link https://github.com/timw4mail/HummingBirdAnimeClient
* @license MIT
*/
// --------------------------------------------------------------------------
/* $config = */require 'config.php';
$config = (object)$config;
// Should we use myth to preprocess?
$use_myth = FALSE;
@ -27,7 +26,7 @@ $use_myth = FALSE;
| The folder where css files exist, in relation to the document root
|
*/
$css_root = $config->asset_dir. '/css/';
$css_root = $config['asset_dir'] . '/css/';
/*
|--------------------------------------------------------------------------
@ -57,4 +56,4 @@ $path_to = '';
| The folder where javascript files exist, in relation to the document root
|
*/
$js_root = $config->asset_dir. '/js/';
$js_root = $config['asset_dir'] . '/js/';

View File

@ -1,13 +1,14 @@
<?php
/**
* Easy Min
* Hummingbird Anime Client
*
* Simple minification for better website performance
* An API client for Hummingbird to manage anime and manga watch lists
*
* @author Timothy J. Warren
* @copyright Copyright (c) 2012
* @link https://github.com/aviat4ion/Easy-Min
* @license http://philsturgeon.co.uk/code/dbad-license
* @package HummingbirdAnimeClient
* @author Timothy J. Warren
* @copyright Copyright (c) 2015 - 2016
* @link https://github.com/timw4mail/HummingBirdAnimeClient
* @license MIT
*/
// --------------------------------------------------------------------------

View File

@ -1,13 +1,14 @@
<?php
/**
* Easy Min
* Hummingbird Anime Client
*
* Simple minification for better website performance
* An API client for Hummingbird to manage anime and manga watch lists
*
* @author Timothy J. Warren
* @copyright Copyright (c) 2012
* @link https://github.com/aviat4ion/Easy-Min
* @license http://philsturgeon.co.uk/code/dbad-license
* @package HummingbirdAnimeClient
* @author Timothy J. Warren
* @copyright Copyright (c) 2015 - 2016
* @link https://github.com/timw4mail/HummingBirdAnimeClient
* @license MIT
*/
// --------------------------------------------------------------------------
@ -34,6 +35,26 @@ return [
'show_message.js',
'anime_edit.js',
'manga_edit.js'
],
'table_edit' => [
'lib/jquery.min.js',
'lib/table_sorter/jquery.tablesorter.min.js',
'sort_tables.js',
'show_message.js',
'anime_edit.js',
'manga_edit.js'
],
'anime_collection' => [
'lib/jquery.min.js',
'lib/jquery.throttle-debounce.js',
'lib/jsrender.js',
'anime_collection.js'
],
'manga_collection' => [
'lib/jquery.min.js',
'lib/jquery.throttle-debounce.js',
'lib/jsrender.js',
'manga_collection.js'
]
];

View File

@ -1,188 +1,150 @@
<?php
/**
* Hummingbird Anime Client
*
* An API client for Hummingbird to manage anime and manga watch lists
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren
* @copyright Copyright (c) 2015 - 2016
* @link https://github.com/timw4mail/HummingBirdAnimeClient
* @license MIT
*/
use Aviat\AnimeClient\AnimeClient;
return [
// Routes on all controllers
'common' => [
'update' => [
'path' => '/update',
'action' => ['update'],
'verb' => 'post'
],
'login_form' => [
'path' => '/login',
'action' => ['login'],
// -------------------------------------------------------------------------
// Routing options
//
// Specify default paths and views
// -------------------------------------------------------------------------
'route_config' => [
// Subfolder prefix for url, if in a subdirectory of the web root
'subfolder_prefix' => '',
// Path to public directory, where images/css/javascript are located,
// appended to the url
'asset_path' => '/public',
// Which list should be the default?
'default_list' => 'anime', // anime or manga
// Default pages for anime/manga
'default_anime_list_path' => "watching", // watching|plan_to_watch|on_hold|dropped|completed|all
'default_manga_list_path' => "reading", // reading|plan_to_read|on_hold|dropped|completed|all
// Default view type (cover_view/list_view)
'default_view_type' => 'cover_view',
],
// -------------------------------------------------------------------------
// Routing Config
//
// Maps paths to controlers and methods
// -------------------------------------------------------------------------
'routes' => [
// ---------------------------------------------------------------------
// Anime List Routes
// ---------------------------------------------------------------------
'anime_add_form' => [
'path' => '/anime/add',
'action' => 'add_form',
'verb' => 'get'
],
'login_action' => [
'path' => '/login',
'action' => ['login_action'],
'anime_add' => [
'path' => '/anime/add',
'action' => 'add',
'verb' => 'post'
],
'logout' => [
'path' => '/logout',
'action' => ['logout']
// ---------------------------------------------------------------------
// Anime Collection Routes
// ---------------------------------------------------------------------
'collection_search' => [
'path' => '/collection/search',
'action' => 'search'
],
],
// Routes on anime controller
'anime' => [
'index' => [
'path' => '/',
'action' => ['redirect'],
'params' => [
'url' => '', // Determined by config
'code' => '301'
]
'collection_add_form' => [
'path' => '/collection/add',
'action' => 'form',
'params' => [],
],
'all' => [
'path' => '/all{/view}',
'action' => ['anime_list'],
'params' => [
'type' => 'all',
'title' => WHOSE . " Anime List &middot; All"
],
'collection_edit_form' => [
'path' => '/collection/edit/{id}',
'action' => 'form',
'tokens' => [
'view' => '[a-z_]+'
'id' => '[0-9]+'
]
],
'watching' => [
'path' => '/watching{/view}',
'action' => ['anime_list'],
'params' => [
'type' => 'currently-watching',
'title' => WHOSE . " Anime List &middot; Watching"
],
'tokens' => [
'view' => '[a-z_]+'
]
'collection_add' => [
'path' => '/collection/add',
'action' => 'add',
'verb' => 'post'
],
'plan_to_watch' => [
'path' => '/plan_to_watch{/view}',
'action' => ['anime_list'],
'params' => [
'type' => 'plan-to-watch',
'title' => WHOSE . " Anime List &middot; Plan to Watch"
],
'tokens' => [
'view' => '[a-z_]+'
]
],
'on_hold' => [
'path' => '/on_hold{/view}',
'action' => ['anime_list'],
'params' => [
'type' => 'on-hold',
'title' => WHOSE . " Anime List &middot; On Hold"
],
'tokens' => [
'view' => '[a-z_]+'
]
],
'dropped' => [
'path' => '/dropped{/view}',
'action' => ['anime_list'],
'params' => [
'type' => 'dropped',
'title' => WHOSE . " Anime List &middot; Dropped"
],
'tokens' => [
'view' => '[a-z_]+'
]
],
'completed' => [
'path' => '/completed{/view}',
'action' => ['anime_list'],
'params' => [
'type' => 'completed',
'title' => WHOSE . " Anime List &middot; Completed"
],
'tokens' => [
'view' => '[a-z_]+'
]
'collection_edit' => [
'path' => '/collection/edit',
'action' => 'edit',
'verb' => 'post'
],
'collection' => [
'path' => '/collection{/view}',
'action' => ['collection'],
'path' => '/collection/view{/view}',
'action' => 'index',
'params' => [],
'tokens' => [
'view' => '[a-z_]+'
]
]
],
'manga' => [
'index' => [
],
// ---------------------------------------------------------------------
// Default / Shared routes
// ---------------------------------------------------------------------
'login' => [
'path' => '/{controller}/login',
'action' => 'login',
],
'login_post' => [
'path' => '/{controller}/login',
'action' => 'login_action',
'verb' => 'post'
],
'logout' => [
'path' => '/{controller}/logout',
'action' => 'logout'
],
'update' => [
'path' => '/{controller}/update',
'action' => 'update',
'verb' => 'post',
'tokens' => [
'controller' => '[a-z_]+'
]
],
'update_form' => [
'path' => '/{controller}/update_form',
'action' => 'form_update',
'verb' => 'post',
'tokens' => [
'controller' => '[a-z_]+'
]
],
'edit' => [
'path' => '/{controller}/edit/{id}/{status}',
'action' => 'edit',
'tokens' => [
'id' => '[0-9a-z_]+',
'status' => '[a-zA-z\- ]+',
]
],
'list' => [
'path' => '/{controller}/{type}{/view}',
'action' => AnimeClient::DEFAULT_CONTROLLER_METHOD,
'tokens' => [
'type' => '[a-z_]+',
'view' => '[a-z_]+'
]
],
'index_redirect' => [
'path' => '/',
'action' => ['redirect'],
'params' => [
'url' => '', // Determined by config
'code' => '301',
'type' => 'manga'
]
'controller' => AnimeClient::DEFAULT_CONTROLLER_NAMESPACE,
'action' => 'redirect_to_default'
],
'all' => [
'path' => '/all{/view}',
'action' => ['manga_list'],
'params' => [
'type' => 'all',
'title' => WHOSE . " Manga List &middot; All"
],
'tokens' => [
'view' => '[a-z_]+'
]
],
'reading' => [
'path' => '/reading{/view}',
'action' => ['manga_list'],
'params' => [
'type' => 'Reading',
'title' => WHOSE . " Manga List &middot; Reading"
],
'tokens' => [
'view' => '[a-z_]+'
]
],
'plan_to_read' => [
'path' => '/plan_to_read{/view}',
'action' => ['manga_list'],
'params' => [
'type' => 'Plan to Read',
'title' => WHOSE . " Manga List &middot; Plan to Read"
],
'tokens' => [
'view' => '[a-z_]+'
]
],
'on_hold' => [
'path' => '/on_hold{/view}',
'action' => ['manga_list'],
'params' => [
'type' => 'On Hold',
'title' => WHOSE . " Manga List &middot; On Hold"
],
'tokens' => [
'view' => '[a-z_]+'
]
],
'dropped' => [
'path' => '/dropped{/view}',
'action' => ['manga_list'],
'params' => [
'type' => 'Dropped',
'title' => WHOSE . " Manga List &middot; Dropped"
],
'tokens' => [
'view' => '[a-z_]+'
]
],
'completed' => [
'path' => '/completed{/view}',
'action' => ['manga_list'],
'params' => [
'type' => 'Completed',
'title' => WHOSE . " Manga List &middot; Completed"
],
'tokens' => [
'view' => '[a-z_]+'
]
]
]
];

View File

@ -1,121 +0,0 @@
<?php
/**
* Anime Controller
*/
namespace AnimeClient;
/**
* Controller for Anime-related pages
*/
class AnimeController extends BaseController {
/**
* The anime list model
* @var object $model
*/
protected $model;
/**
* The anime collection model
* @var object $collection_model
*/
private $collection_model;
/**
* Data to ve sent to all routes in this controller
* @var array $base_data
*/
protected $base_data;
/**
* Route mapping for main navigation
* @var array $nav_routes
*/
private $nav_routes = [
'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}'
];
/**
* Constructor
*/
public function __construct(Config $config, Array $web)
{
parent::__construct($config, $web);
if ($this->config->show_anime_collection === FALSE)
{
unset($this->nav_routes['Collection']);
}
$this->model = new AnimeModel();
$this->collection_model = new AnimeCollectionModel();
$this->base_data = [
'message' => '',
'url_type' => 'anime',
'other_type' => 'manga',
'nav_routes' => $this->nav_routes,
];
}
/**
* Show a portion, or all of the anime list
*
* @param string $type - The section of the list
* @param string $title - The title of the page
* @return void
*/
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/' . $view_map[$view], [
'title' => $title,
'sections' => $data
]);
}
/**
* Show the anime collection page
*
* @return void
*/
public function collection($view)
{
$view_map = [
'' => 'collection',
'list' => 'collection_list'
];
$data = $this->collection_model->get_collection();
$this->outputHTML('anime/' . $view_map[$view], [
'title' => WHOSE . " Anime Collection",
'sections' => $data
]);
}
/**
* Update an anime item
*
* @return bool
*/
public function update()
{
print_r($this->model->update($this->request->post->get()));
}
}
// End of AnimeController.php

View File

@ -1,87 +0,0 @@
<?php
/**
* Manga Controller
*/
namespace AnimeClient;
/**
* Controller for manga list
*/
class MangaController extends BaseController {
/**
* The manga model
* @var object $model
*/
protected $model;
/**
* Data to ve sent to all routes in this controller
* @var array $base_data
*/
protected $base_data;
/**
* Route mapping for main navigation
* @var array $nav_routes
*/
private $nav_routes = [
'Reading' => '/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(Config $config, Array $web)
{
parent::__construct($config, $web);
$this->model = new MangaModel();
$this->base_data = [
'url_type' => 'manga',
'other_type' => 'anime',
'nav_routes' => $this->nav_routes
];
}
/**
* Update an anime item
*
* @return bool
*/
public function update()
{
$this->outputJSON($this->model->update($this->request->post->get()));
}
/**
* 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], [
'title' => $title,
'sections' => $data
]);
}
}
// End of MangaController.php

View File

@ -1,206 +0,0 @@
<?php
/**
* Anime Collection DB Model
*/
namespace AnimeClient;
/**
* Model for getting anime collection data
*/
class AnimeCollectionModel extends BaseDBModel {
/**
* Anime API Model
* @var object $anime_model
*/
private $anime_model;
/**
* Whether the database is valid for querying
* @var bool
*/
private $valid_database = FALSE;
/**
* Constructor
*/
public function __construct()
{
parent::__construct();
$this->db = \Query($this->db_config['collection']);
$this->anime_model = new AnimeModel();
// Is database valid? If not, set a flag so the
// app can be run without a valid database
$db_file = file_get_contents($this->db_config['collection']['file']);
$this->valid_database = (strpos($db_file, 'SQLite format 3') === 0);
// Do an import if an import file exists
$this->json_import();
}
/**
* Get collection from the database, and organize by media type
*
* @return array
*/
public function get_collection()
{
$raw_collection = $this->_get_collection();
$collection = [];
foreach($raw_collection as $row)
{
if (array_key_exists($row['media'], $collection))
{
$collection[$row['media']][] = $row;
}
else
{
$collection[$row['media']] = [$row];
}
}
return $collection;
}
/**
* Get full collection from the database
*
* @return array
*/
private function _get_collection()
{
if ( ! $this->valid_database) return [];
$query = $this->db->select('hummingbird_id, slug, title, alternate_title, show_type, age_rating, episode_count, episode_length, cover_image, notes, media.type as media')
->from('anime_set a')
->join('media', 'media.id=a.media_id', 'inner')
->order_by('media')
->order_by('title')
->get();
return $query->fetchAll(\PDO::FETCH_ASSOC);
}
/**
* Import anime into collection from a json file
*
* @return void
*/
private function json_import()
{
if ( ! file_exists('import.json')) return;
if ( ! $this->valid_database) return;
$anime = json_decode(file_get_contents("import.json"));
foreach($anime as $item)
{
$this->db->set([
'hummingbird_id' => $item->id,
'slug' => $item->slug,
'title' => $item->title,
'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'),
'episode_count' => $item->episode_count,
'episode_length' => $item->episode_length
])->insert('anime_set');
}
// Delete the import file
unlink('import.json');
// Update genre info
$this->update_genres();
}
/**
* Update genre information
*
* @return void
*/
private function update_genres()
{
$genres = [];
$flipped_genres = [];
$links = [];
// Get existing genres
$query = $this->db->select('id, genre')
->from('genres')
->get();
foreach($query->fetchAll(PDO::FETCH_ASSOC) as $genre)
{
$genres[$genre['id']] = $genre['genre'];
}
// Get existing link table entries
$query = $this->db->select('hummingbird_id, genre_id')
->from('genre_anime_set_link')
->get();
foreach($query->fetchAll(PDO::FETCH_ASSOC) as $link)
{
if (array_key_exists($link['hummingbird_id'], $links))
{
$links[$link['hummingbird_id']][] = $link['genre_id'];
}
else
{
$links[$link['hummingbird_id']] = [$link['genre_id']];
}
}
// 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');
}
}
}
}
}
// End of AnimeCollectionModel.php

View File

@ -1,245 +0,0 @@
<?php
/**
* Anime API Model
*/
namespace AnimeClient;
/**
* Model for handling requests dealing with the anime list
*/
class AnimeModel extends BaseApiModel {
/**
* The base url for api requests
* @var string $base_url
*/
protected $base_url = "https://hummingbird.me/api/v1/";
/**
* Constructor
*/
public function __construct()
{
parent::__construct();
}
/**
* Update the selected anime
*
* @param array $data
* @return array
*/
public function update($data)
{
$data['auth_token'] = $_SESSION['hummingbird_anime_token'];
$result = $this->client->post("libraries/{$data['id']}", [
'body' => $data
]);
return $result->json();
}
/**
* Get the full set of anime lists
*
* @return array
*/
public function get_all_lists()
{
$output = [
'Watching' => [],
'Plan to Watch' => [],
'On Hold' => [],
'Dropped' => [],
'Completed' => [],
];
$data = $this->_get_list();
foreach($data as $datum)
{
switch($datum['status'])
{
case "completed":
$output['Completed'][] = $datum;
break;
case "plan-to-watch":
$output['Plan to Watch'][] = $datum;
break;
case "dropped":
$output['Dropped'][] = $datum;
break;
case "on-hold":
$output['On Hold'][] = $datum;
break;
case "currently-watching":
$output['Watching'][] = $datum;
break;
}
}
// Sort anime by name
foreach($output as &$status_list)
{
$this->sort_by_name($status_list);
}
return $output;
}
/**
* Get a category out of the full list
*
* @param string $status
* @return array
*/
public function get_list($status)
{
$map = [
'currently-watching' => 'Watching',
'plan-to-watch' => 'Plan to Watch',
'on-hold' => 'On Hold',
'dropped' => 'Dropped',
'completed' => 'Completed',
];
$data = $this->_get_list($status);
$this->sort_by_name($data);
$output = [];
$output[$map[$status]] = $data;
return $output;
}
/**
* Get information about an anime from its id
*
* @param string $anime_id
* @return array
*/
public function get_anime($anime_id)
{
$config = [
'query' => [
'id' => $anime_id
]
];
$response = $this->client->get("anime/{$anime_id}", $config);
return $response->json();
}
/**
* Search for anime by name
*
* @param string $name
* @return array
*/
public function search($name)
{
global $defaultHandler;
$config = [
'query' => [
'query' => $name
]
];
$response = $this->client->get('search/anime', $config);
$defaultHandler->addDataTable('anime_search_response', (array)$response);
if ($response->getStatusCode() != 200)
{
throw new Exception($response->getEffectiveUrl());
}
return $response->json();
}
/**
* Actually retreive the data from the api
*
* @param string $status - Status to filter by
* @return array
*/
private function _get_list($status="all")
{
global $defaultHandler;
$cache_file = "{$this->config->data_cache_path}/anime-{$status}.json";
$config = [
'allow_redirects' => FALSE
];
if ($status != "all")
{
$config['query']['status'] = $status;
}
$response = $this->client->get("users/{$this->config->hummingbird_username}/library", $config);
$defaultHandler->addDataTable('anime_list_response', (array)$response);
if ($response->getStatusCode() != 200)
{
if ( ! file_exists($cache_file))
{
throw new Exception($response->getEffectiveUrl());
}
else
{
$output = json_decode(file_get_contents($cache_file), TRUE);
}
}
else
{
$output = $response->json();
$output_json = json_encode($output);
if (( ! file_exists($cache_file)) || file_get_contents($cache_file) !== $output_json)
{
// Attempt to create the cache folder if it doesn't exist
if ( ! is_dir($this->config->data_cache_path))
{
mkdir($this->config->data_cache_path);
}
// Cache the call in case of downtime
file_put_contents($cache_file, json_encode($output));
}
}
foreach($output as &$row)
{
$row['anime']['cover_image'] = $this->get_cached_image($row['anime']['cover_image'], $row['anime']['slug'], 'anime');
}
return $output;
}
/**
* Sort the list by title
*
* @param array $array
* @return void
*/
private function sort_by_name(&$array)
{
$sort = array();
foreach($array as $key => $item)
{
$sort[$key] = $item['anime']['title'];
}
array_multisort($sort, SORT_ASC, $array);
}
}
// End of AnimeModel.php

View File

@ -1,193 +0,0 @@
<?php
/**
* Manga API Model
*/
namespace AnimeClient;
/**
* Model for handling requests dealing with the manga list
*/
class MangaModel extends BaseApiModel {
/**
* The base url for api requests
* @var string
*/
protected $base_url = "https://hummingbird.me/";
/**
* Update the selected manga
*
* @param array $data
* @return array
*/
public function update($data)
{
$id = $data['id'];
unset($data['id']);
$result = $this->client->put("manga_library_entries/{$id}", [
'cookies' => ['token' => $_SESSION['hummingbird_anime_token']],
'json' => ['manga_library_entry' => $data]
]);
return $result->json();
}
/**
* Get the full set of anime lists
*
* @return array
*/
public function get_all_lists()
{
$data = $this->_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('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();
// Attempt to create the cache dir if it doesn't exist
if ( ! is_dir($this->config->data_cache_path))
{
mkdir($this->config->data_cache_path);
}
// Cache data in case of downtime
file_put_contents($cache_file, json_encode($raw_data));
}
// Bail out early if there isn't any manga data
if (empty($raw_data)) return [];
$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;
}
}
//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;
}
/**
* 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

View File

@ -1,7 +1,4 @@
<body>
<main>
<h1>404</h1>
<h2>Page Not Found</h2>
</main>
</body>
</html>
<main>
<h1>404</h1>
<h2>Page Not Found</h2>
</main>

40
app/views/anime/add.php Normal file
View File

@ -0,0 +1,40 @@
<?php if ($auth->is_authenticated()): ?>
<main>
<h2>Add Anime to your List</h2>
<form action="<?= $action_url ?>" method="post">
<section>
<label for="search">Search for anime by name:&nbsp;&nbsp;&nbsp;&nbsp;<input type="search" id="search" /></label>
<section id="series_list" class="media-wrap">
</section>
</section>
<br />
<table class="form">
<tbody>
<tr>
<td><label for="status">Watching Status</label></td>
<td>
<select name="status" id="status">
<?php foreach($status_list as $status_key => $status_title): ?>
<option value="<?= $status_key ?>"><?= $status_title ?></option>
<?php endforeach ?>
</select>
</td>
</tr>
<tr>
<td>&nbsp;</td>
<td>
<button type="submit">Save</button>
</td>
</tr>
</tbody>
</table>
</form>
</main>
<template id="show_list">
<article class="media">
<div class="name"><label><input type="radio" name="id" value="{{:slug}}" />&nbsp;<span>{{:title}}<br />{{:alternate_title}}</span></label></div>
<img src="{{:cover_image}}" alt="{{:title}}" />
</article>
</template>
<script src="<?= $urlGenerator->asset_url('js.php?g=anime_collection') ?>"></script>
<?php endif ?>

View File

@ -1,28 +0,0 @@
<main>
<?php foreach ($sections as $name => $items): ?>
<section class="status">
<h2><?= $name ?></h2>
<section class="media-wrap">
<?php foreach($items as $item): ?>
<a href="https://hummingbird.me/anime/<?= $item['slug'] ?>">
<article class="media" id="a-<?= $item['hummingbird_id'] ?>">
<img src="<?= $item['cover_image'] ?>" />
<div class="name">
<?= $item['title'] ?>
<?= ($item['alternate_title'] != "") ? "<br />({$item['alternate_title']})" : ""; ?>
</div>
<div class="table">
<div class="row">
<div class="completion">Episodes: <?= $item['episode_count'] ?></div>
<div class="media_type"><?= $item['show_type'] ?></div>
<div class="age_rating"><?= $item['age_rating'] ?></div>
</div>
</div>
</article>
</a>
<?php endforeach ?>
</section>
</section>
<?php endforeach ?>
</main>

View File

@ -1,43 +0,0 @@
<main>
<?php foreach ($sections as $name => $items): ?>
<h2><?= $name ?></h2>
<table>
<thead>
<tr>
<th>Title</th>
<th>Alternate Title</th>
<th>Episode Count</th>
<th>Episode Length</th>
<th>Show Type</th>
<th>Age Rating</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
<?php foreach($items as $item): ?>
<tr>
<td class="align_left">
<a href="https://hummingbird.me/anime/<?= $item['slug'] ?>">
<?= $item['title'] ?>
</a>
</td>
<td class="align_left"><?= $item['alternate_title'] ?></td>
<td><?= $item['episode_count'] ?></td>
<td><?= $item['episode_length'] ?></td>
<td><?= $item['show_type'] ?></td>
<td><?= $item['age_rating'] ?></td>
<td class="align_left"><?= $item['notes'] ?></td>
</tr>
<?php endforeach ?>
</tbody>
</table>
<br />
<?php endforeach ?>
</main>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
<script src="/public/js/table_sorter/jquery.tablesorter.min.js"></script>
<script>
$(function() {
$('table').tablesorter();
});
</script>

View File

@ -1,32 +1,60 @@
<main>
<?php if ($auth->is_authenticated()): ?>
<a class="bracketed" href="<?= $urlGenerator->url('anime/add', 'anime') ?>">Add Item</a>
<?php endif ?>
<?php if (empty($sections)): ?>
<h3>There's nothing here!</h3>
<?php else: ?>
<?php foreach ($sections as $name => $items): ?>
<section class="status">
<h2><?= $name ?></h2>
<h2><?= $escape->html($name) ?></h2>
<section class="media-wrap">
<?php foreach($items as $item): ?>
<article class="media" id="a-<?= $item['anime']['id'] ?>">
<?php if (is_logged_in()): ?>
<button class="plus_one" hidden>+1 Episode</button>
<?php if ($item['private'] && ! $auth->is_authenticated()) continue; ?>
<article class="media" id="<?= $item['anime']['slug'] ?>">
<?php if ($auth->is_authenticated()): ?>
<button title="Increment episode count" class="plus_one" hidden>+1 Episode</button>
<?php endif ?>
<img src="<?= $item['anime']['cover_image'] ?>" />
<?= $helper->img($item['anime']['image']); ?>
<div class="name">
<a href="<?= $item['anime']['url'] ?>">
<?= $item['anime']['title'] ?>
<a href="<?= $escape->attr($item['anime']['url']) ?>" target="_blank">
<?= $escape->html($item['anime']['title']) ?>
<?= ($item['anime']['alternate_title'] != "") ? "<br />({$item['anime']['alternate_title']})" : ""; ?>
</a>
</div>
<div class="table">
<?php if ($auth->is_authenticated()): ?>
<div class="row">
<div class="user_rating">Rating: <?= ($item['rating']['value'] > 0) ? (int)($item['rating']['value'] * 2) : " - " ?> / 10</div>
<span class="edit">
<a class="bracketed" title="Edit information about this anime" href="<?= $urlGenerator->url("anime/edit/{$item['id']}/{$item['watching_status']}") ?>">Edit</a>
</span>
</div>
<?php endif ?>
<?php if ($item['private'] || $item['rewatching']): ?>
<div class="row">
<?php foreach(['private', 'rewatching'] as $attr): ?>
<?php if($item[$attr]): ?>
<span class="item-<?= $attr ?>"><?= ucfirst($attr) ?></span>
<?php endif ?>
<?php endforeach ?>
</div>
<?php endif ?>
<?php if ($item['rewatched'] > 0): ?>
<div class="row">
<div>Rewatched <?= $item['rewatched'] ?> time(s)</div>
</div>
<?php endif ?>
<div class="row">
<div class="user_rating">Rating: <?= $item['user_rating'] ?> / 10</div>
<div class="completion">Episodes:
<span class="completed_number"><?= $item['episodes_watched'] ?></span> /
<span class="total_number"><?= ($item['anime']['episode_count'] != 0) ? $item['anime']['episode_count'] : "-" ?></span>
<span class="completed_number"><?= $item['episodes']['watched'] ?></span> /
<span class="total_number"><?= $item['episodes']['total'] ?></span>
</div>
</div>
<div class="row">
<div class="media_type"><?= $item['anime']['show_type'] ?></div>
<div class="airing_status"><?= $item['anime']['status'] ?></div>
<div class="age_rating"><?= $item['anime']['age_rating'] ?></div>
<div class="media_type"><?= $escape->html($item['anime']['type']) ?></div>
<div class="airing_status"><?= $escape->html($item['airing']['status']) ?></div>
<div class="age_rating"><?= $escape->html($item['anime']['age_rating']) ?></div>
</div>
</div>
</article>
@ -34,7 +62,8 @@
</section>
</section>
<?php endforeach ?>
</main>
<?php if (is_logged_in()): ?>
<script src="<?= asset_url('js.php?g=edit') ?>"></script>
<?php endif ?>
</main>
<?php if ($auth->is_authenticated()): ?>
<script src="<?= $urlGenerator->asset_url('js.php?g=edit') ?>"></script>
<?php endif ?>

View File

@ -1,8 +1,89 @@
<body>
<?php include 'nav.php' ?>
<?php if ($auth->is_authenticated()): ?>
<main>
<h2>Edit Anime List Item</h2>
<form action="<?= $action ?>" method="post">
<table class="form">
<thead>
<tr>
<th>
<h3><?= $escape->html($item['anime']['title']) ?></h3>
<?php if($item['anime']['alternate_title'] != ""): ?>
<h4><?= $escape->html($item['anime']['alternate_title']) ?></h4>
<?php endif ?>
</th>
<th>
<article class="media">
<?= $helper->img($item['anime']['image']); ?>
</article>
</th>
</tr>
</thead>
<tbody>
<tr>
<td><label for="private">Is Private?</label></td>
<td>
<input type="checkbox" name="private" id="private"
<?php if($item['private']): ?>checked="checked"<?php endif ?>
/>
</td>
</tr>
<tr>
<td><label for="watching_status">Watching Status</label></td>
<td>
<select name="watching_status" id="watching_status">
<?php foreach($statuses as $status_key => $status_title): ?>
<option <?php if($item['watching_status'] === $status_key): ?>selected="selected"<?php endif ?>
value="<?= $status_key ?>"><?= $status_title ?></option>
<?php endforeach ?>
</select>
</td>
</tr>
<tr>
<td><label for="series_rating">Rating</label></td>
<td>
<input type="number" min="0" max="10" maxlength="2" name="user_rating" id="series_rating" value="<?= $item['user_rating'] ?>" id="series_rating" size="2" /> / 10
</td>
</tr>
<tr>
<td><label for="episodes_watched">Episodes Watched</label></td>
<td>
<input type="number" min="0" size="4" maxlength="4" value="<?= $item['episodes']['watched'] ?>" name="episodes_watched" id="episodes_watched" />
<?php if($item['episodes']['total'] > 0): ?>
/ <?= $item['episodes']['total'] ?>
<?php endif ?>
</td>
</tr>
<tr>
<td><label for="rewatching_flag">Rewatching?</label></td>
<td>
<input type="checkbox" name="rewatching" id="rewatching_flag"
<?php if($item['rewatching'] === TRUE): ?>checked="checked"<?php endif ?>
/>
</td>
</tr>
<tr>
<td><label for="rewatched">Rewatch Count</label></td>
<td>
<input type="number" min="0" id="rewatched" name="rewatched" value="<?= $item['rewatched'] ?>" />
</td>
</tr>
<tr>
<td><label for="notes">Notes</label></td>
<td>
<textarea name="notes" id="notes"><?= $escape->html($item['notes']) ?></textarea>
</td>
</tr>
<tr>
<td>&nbsp;</td>
<td>
<input type="hidden" value="<?= $item['anime']['slug'] ?>" name="id" />
<input type="hidden" value="true" name="edit" />
<button type="submit">Submit</button>
</td>
</tr>
</tbody>
</table>
</form>
</main>
</body>
</html>
<script src="<?= $urlGenerator->asset_url('js.php?g=edit') ?>"></script>
<?php endif ?>

View File

@ -1,36 +1,77 @@
<main>
<?php if ($auth->is_authenticated()): ?>
<a class="bracketed" href="<?= $urlGenerator->url('anime/add', 'anime') ?>">Add Item</a>
<?php endif ?>
<?php if (empty($sections)): ?>
<h3>There's nothing here!</h3>
<?php else: ?>
<?php foreach ($sections as $name => $items): ?>
<h2><?= $name ?></h2>
<table>
<thead>
<tr>
<?php if($auth->is_authenticated()): ?>
<th>&nbsp;</th>
<?php endif ?>
<th>Title</th>
<th>Alternate Title</th>
<th>Airing Status</th>
<th>Score</th>
<th>Type</th>
<th>Progress</th>
<th>Rated</th>
<th>Attributes</th>
<th>Notes</th>
<th>Genres</th>
</tr>
</thead>
<tbody>
<?php foreach($items as $item): ?>
<tr id="a-<?= $item['anime']['id'] ?>">
<?php if ($item['private'] && ! $auth->is_authenticated()) continue; ?>
<tr id="a-<?= $item['id'] ?>">
<?php if ($auth->is_authenticated()): ?>
<td>
<a class="bracketed" href="<?= $urlGenerator->url("/anime/edit/{$item['id']}/{$item['watching_status']}") ?>">Edit</a>
</td>
<?php endif ?>
<td class="align_left">
<a href="<?= $item['anime']['url'] ?>">
<a href="<?= $item['anime']['url'] ?>" target="_blank">
<?= $item['anime']['title'] ?>
</a>
<?= ( ! empty($item['anime']['alternate_title'])) ? " <br /> " . $item['anime']['alternate_title'] : "" ?>
</td>
<td class="align_left"><?= $item['airing']['status'] ?></td>
<td><?= $item['user_rating'] ?> / 10 </td>
<td><?= $item['anime']['type'] ?></td>
<td id="<?= $item['anime']['slug'] ?>">
Episodes: <br />
<span class="completed_number"><?= $item['episodes']['watched'] ?></span>&nbsp;/&nbsp;<span class="total_number"><?= $item['episodes']['total'] ?></span>
</td>
<td class="align_left"><?= $item['anime']['alternate_title'] ?></td>
<td class="align_left"><?= $item['anime']['status'] ?></td>
<td><?= (int)($item['rating']['value'] * 2) ?> / 10 </td>
<td><?= $item['anime']['show_type'] ?></td>
<td>Episodes: <?= $item['episodes_watched'] ?> / <?= $item['anime']['episode_count'] ?></td>
<td><?= $item['anime']['age_rating'] ?></td>
<td>
<?php if ($item['rewatched'] > 0): ?>
Rewatched <?= $item['rewatched'] ?> time(s)<br />
<?php endif ?>
<?php $attr_list = []; ?>
<?php foreach(['private','rewatching'] as $attr): ?>
<?php if($item[$attr]): ?>
<?php $attr_list[] = ucfirst($attr); ?>
<?php endif ?>
<?php endforeach ?>
<?= implode(', ', $attr_list); ?>
</td>
<td>
<p><?= $escape->html($item['notes']) ?></p>
</td>
<td class="align_left">
<?php sort($item['anime']['genres']) ?>
<?= join(', ', $item['anime']['genres']) ?>
</td>
</tr>
<?php endforeach ?>
</tbody>
</table>
<?php endforeach ?>
<?php endif ?>
</main>
<script src="<?= asset_url('js.php?g=table') ?>"></script>
<?php $group = ($auth->is_authenticated()) ? 'table_edit' : 'table' ?>
<script src="<?= $urlGenerator->asset_url("js.php?g={$group}") ?>"></script>

View File

@ -0,0 +1,44 @@
<?php if ($auth->is_authenticated()): ?>
<main>
<h2>Add Anime to your Collection</h2>
<form action="<?= $action_url ?>" method="post">
<section>
<label for="search">Search for anime by name:&nbsp;&nbsp;&nbsp;&nbsp;<input type="search" id="search" name="search" /></label>
<section id="series_list" class="media-wrap">
</section>
</section>
<br />
<table class="form">
<tbody>
<tr>
<td><label for="media_id">Media</label></td>
<td>
<select name="media_id" id="media_id">
<?php foreach($media_items as $id => $name): ?>
<option value="<?= $id ?>"><?= $name ?></option>
<?php endforeach ?>
</select>
</td>
</tr>
<tr>
<td><label for="notes">Notes</label></td>
<td><textarea id="notes" name="notes"></textarea></td>
</tr>
<tr>
<td>&nbsp;</td>
<td>
<button type="submit">Save</button>
</td>
</tr>
</tbody>
</table>
</form>
</main>
<template id="show_list">
<article class="media">
<div class="name"><label><input type="radio" name="id" value="{{:id}}" />&nbsp;<span>{{:title}}<br />{{:alternate_title}}</span></label></div>
<img src="{{:cover_image}}" alt="{{:title}}" />
</article>
</template>
<script src="<?= $urlGenerator->asset_url('js.php?g=anime_collection') ?>"></script>
<?php endif ?>

View File

@ -0,0 +1,40 @@
<main>
<?php if ($auth->is_authenticated()): ?>
<a class="bracketed" href="<?= $urlGenerator->url('collection/add', 'anime') ?>">Add Item</a>
<?php endif ?>
<?php if (empty($sections)): ?>
<h3>There's nothing here!</h3>
<?php else: ?>
<?php foreach ($sections as $name => $items): ?>
<section class="status">
<h2><?= $name ?></h2>
<section class="media-wrap">
<?php foreach($items as $item): ?>
<article class="media" id="a-<?= $item['hummingbird_id'] ?>">
<img src="<?= $urlGenerator->asset_url('images', 'anime', basename($item['cover_image'])) ?>" />
<div class="name">
<a href="https://hummingbird.me/anime/<?= $item['slug'] ?>">
<?= $item['title'] ?>
<?= ($item['alternate_title'] != "") ? "<br />({$item['alternate_title']})" : ""; ?>
</a>
</div>
<div class="table">
<?php if ($auth->is_authenticated()): ?>
<div class="row">
<span class="edit"><a class="bracketed" href="<?= $urlGenerator->url("collection/edit/{$item['hummingbird_id']}") ?>">Edit</a></span>
<?php /*<span class="delete"><a class="bracketed" href="<?= $urlGenerator->url("collection/delete/{$item['hummingbird_id']}") ?>">Delete</a></span> */ ?>
</div>
<?php endif ?>
<div class="row">
<div class="completion">Episodes: <?= $item['episode_count'] ?></div>
<div class="media_type"><?= $item['show_type'] ?></div>
<div class="age_rating"><?= $item['age_rating'] ?></div>
</div>
</div>
</article>
<?php endforeach ?>
</section>
</section>
<?php endforeach ?>
<?php endif ?>
</main>

View File

@ -0,0 +1,56 @@
<?php if ($auth->is_authenticated()): ?>
<main>
<h2>Edit Anime Collection Item</h2>
<form action="<?= $action_url ?>" method="post">
<table class="form">
<thead>
<tr>
<th>
<h3><?= $escape->html($item['title']) ?></h3>
<?php if($item['alternate_title'] != ""): ?>
<h4><?= $escape->html($item['alternate_title']) ?></h4>
<?php endif ?>
</th>
<th>
<article class="media">
<?= $helper->img($item['cover_image']); ?>
</article>
</th>
</tr>
</thead>
<tbody>
<tr>
<td><label for="media_id">Media</label></td>
<td>
<select name="media_id" id="media_id">
<?php foreach($media_items as $id => $name): ?>
<option <?= $item['media_id'] == $id ? 'selected="selected"' : '' ?> value="<?= $id ?>"><?= $name ?></option>
<?php endforeach ?>
</select>
</td>
</tr>
<tr>
<td><label for="notes">Notes</label></td>
<td><textarea id="notes" name="notes"><?= $escape->html($item['notes']) ?></textarea></td>
</tr>
<tr>
<td>&nbsp;</td>
<td>
<?php if($action === 'Edit'): ?>
<input type="hidden" name="hummingbird_id" value="<?= $item['hummingbird_id'] ?>" />
<?php endif ?>
<button type="submit">Save</button>
</td>
</tr>
</tbody>
</table>
</form>
</main>
<template id="show_list">
<article class="media">
<div class="name"><label><input type="radio" name="id" value="{{:id}}" />&nbsp;<span>{{:title}}<br />{{:alternate_title}}</span></label></div>
<img src="{{:cover_image}}" alt="{{:title}}" />
</article>
</template>
<script src="<?= $urlGenerator->asset_url('js.php?g=anime_collection') ?>"></script>
<?php endif ?>

View File

@ -0,0 +1,52 @@
<main>
<?php if ($auth->is_authenticated()): ?>
<a class="bracketed" href="<?= $urlGenerator->full_url('collection/add', 'anime') ?>">Add Item</a>
<?php endif ?>
<?php if (empty($sections)): ?>
<h3>There's nothing here!</h3>
<?php else: ?>
<?php foreach ($sections as $name => $items): ?>
<h2><?= $name ?></h2>
<table>
<thead>
<tr>
<?php if($auth->is_authenticated()): ?>
<th>Actions</th>
<?php endif ?>
<th>Title</th>
<th>Episode Count</th>
<th>Episode Length</th>
<th>Show Type</th>
<th>Age Rating</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
<?php foreach($items as $item): ?>
<tr>
<?php if($auth->is_authenticated()): ?>
<td>
<a class="bracketed" href="<?= $urlGenerator->full_url("collection/edit/{$item['hummingbird_id']}") ?>">Edit</a>
<?php /*<a class="bracketed" href="<?= $urlGenerator->full_url("collection/delete/{$item['hummingbird_id']}") ?>">Delete</a>*/ ?>
</td>
<?php endif ?>
<td class="align_left">
<a href="https://hummingbird.me/anime/<?= $item['slug'] ?>">
<?= $item['title'] ?>
</a>
<?= ( ! empty($item['alternate_title'])) ? " &middot; " . $item['alternate_title'] : "" ?>
</td>
<td><?= $item['episode_count'] ?></td>
<td><?= $item['episode_length'] ?></td>
<td><?= $item['show_type'] ?></td>
<td><?= $item['age_rating'] ?></td>
<td class="align_left"><?= $item['notes'] ?></td>
</tr>
<?php endforeach ?>
</tbody>
</table>
<br />
<?php endforeach ?>
<?php endif ?>
</main>
<script src="<?= $urlGenerator->asset_url('js.php?g=table') ?>"></script>

5
app/views/error.php Normal file
View File

@ -0,0 +1,5 @@
<main>
<h1><?= $title ?></h1>
<h2><?= $message ?></h2>
<div><?= $long_message ?></div>
</main>

View File

@ -1,35 +1,58 @@
<?php namespace Aviat\AnimeClient ?>
<!DOCTYPE html>
<html lang="en">
<head>
<title><?= $title ?></title>
<meta charset="utf-8" />
<link rel="stylesheet" href="<?= asset_url('css.php?g=base') ?>" />
<link rel="stylesheet" href="<?= $urlGenerator->asset_url('css.php?g=base') ?>" />
<script>
var BASE_URL = "<?= base_url($url_type) ?>";
var BASE_URL = "<?= $urlGenerator->base_url($url_type) ?>";
var CONTROLLER = "<?= $url_type ?>";
</script>
</head>
<body class="<?= $url_type ?> list">
<h1 class="flex flex-align-end flex-wrap">
<span class="flex-no-wrap grow-1"><?= WHOSE ?> <?= ucfirst($url_type) ?> <?= (strpos($route_path, 'collection') !== FALSE) ? 'Collection' : 'List' ?> [<a href="<?= full_url("", $other_type) ?>"><?= ucfirst($other_type) ?> List</a>]</span>
<span class="flex-no-wrap small-font">
<?php if (is_logged_in()): ?>
[<a href="<?= full_url("/logout", $url_type) ?>">Logout</a>]
<?php else: ?>
[<a href="<?= full_url("/login", $url_type) ?>"><?= WHOSE ?> Login</a>]
<body class="<?= $escape->attr($url_type) ?> list">
<header>
<h1 class="flex flex-align-end flex-wrap">
<span class="flex-no-wrap grow-1">
<?php if(strpos($route_path, 'collection') === FALSE): ?>
<a href="<?= $escape->attr($urlGenerator->default_url($url_type)) ?>">
<?= $config->get('whose_list') ?>'s <?= ucfirst($url_type) ?> List
</a>
<?php if($config->get("show_{$url_type}_collection")): ?>
[<a href="<?= $urlGenerator->url('collection/view') ?>"><?= ucfirst($url_type) ?> Collection</a>]
<?php endif ?>
[<a href="<?= $urlGenerator->default_url($other_type) ?>"><?= ucfirst($other_type) ?> List</a>]
<?php else: ?>
<a href="<?= $urlGenerator->url('collection/view') ?>">
<?= $config->get('whose_list') ?>'s <?= ucfirst($url_type) ?> Collection
</a>
[<a href="<?= $urlGenerator->default_url('anime') ?>">Anime List</a>]
[<a href="<?= $urlGenerator->default_url('manga') ?>">Manga List</a>]
<?php endif ?>
</span>
<span class="flex-no-wrap small-font">
<?php if ($auth->is_authenticated()): ?>
<a class="bracketed" href="<?= $urlGenerator->url("/{$url_type}/logout", $url_type) ?>">Logout</a>
<?php else: ?>
[<a href="<?= $urlGenerator->url("/{$url_type}/login", $url_type) ?>"><?= $config->get('whose_list') ?>'s Login</a>]
<?php endif ?>
</span>
</h1>
<nav>
<?php if ($container->get('anime-client')->is_view_page()): ?>
<?= $helper->menu($menu_name) ?>
<br />
<ul>
<li class="<?= AnimeClient::is_not_selected('list', $urlGenerator->last_segment()) ?>"><a href="<?= $urlGenerator->url($route_path) ?>">Cover View</a></li>
<li class="<?= AnimeClient::is_selected('list', $urlGenerator->last_segment()) ?>"><a href="<?= $urlGenerator->url("{$route_path}/list") ?>">List View</a></li>
</ul>
<?php endif ?>
</span>
</h1>
<nav>
<ul>
<?php foreach($nav_routes as $title => $nav_path): ?>
<li class="<?= is_selected($nav_path, $route_path) ?>"><a href="<?= full_url($nav_path, $url_type) ?>"><?= $title ?></a></li>
<?php endforeach ?>
</ul>
<br />
<ul>
<li class="<?= is_not_selected('list', last_segment()) ?>"><a href="<?= full_url($route_path, $url_type) ?>">Cover View</a></li>
<li class="<?= is_selected('list', last_segment()) ?>"><a href="<?= full_url("{$route_path}/list", $url_type) ?>">List View</a></li>
</ul>
</nav>
<br />
</nav>
</header>
<?php if(isset($message) && is_array($message)): ?>
<div class="message <?= $escape->attr($message['message_type']) ?>">
<span class="icon"></span>
<?= $escape->html($message['message']) ?>
<span class="close" onclick="this.parentElement.style.display='none'">x</span>
</div>
<?php endif ?>

View File

@ -1,17 +1,16 @@
<main>
<h2><?= $config->get('whose_list'); ?>'s Login</h2>
<?= $message ?>
<aside>
<form method="post" action="<?= full_url('/login', $url_type) ?>">
<dl>
<dt><label for="username">Username: </label></dt>
<dd><input type="text" id="username" name="username" required="required" /></dd>
<dt><label for="password">Password: </label></dt>
<dd><input type="password" id="password" name="password" required="required" /></dd>
<dt>&nbsp;</dt>
<dd><input type="submit" value="Login" /></dd>
</dl>
</form>
</aside>
<form method="post" action="<?= $urlGenerator->full_url($urlGenerator->path(), $url_type) ?>">
<table class="form invisible">
<tr>
<td><label for="password">Password: </label></td>
<td><input type="password" id="password" name="password" required="required" /></td>
</tr>
<tr>
<td>&nbsp;</td>
<td><button type="submit">Login</button></td>
</tr>
</table>
</form>
</main>

0
app/views/manga/add.php Normal file
View File

View File

@ -1,49 +1,57 @@
<main>
<?php if (empty($sections)): ?>
<h3>There's nothing here!</h3>
<?php else: ?>
<?php foreach ($sections as $name => $items): ?>
<section class="status">
<h2><?= $name ?></h2>
<h2><?= $escape->html($name) ?></h2>
<section class="media-wrap">
<?php foreach($items as $item): ?>
<article class="media" id="manga-<?= $item['id'] ?>">
<?php if (is_logged_in()): ?>
<?php if ($auth->is_authenticated()): ?>
<div class="edit_buttons" hidden>
<button class="plus_one_chapter">+1 Chapter</button>
<button class="plus_one_volume">+1 Volume</button>
</div>
<?php endif ?>
<img src="<?= $item['manga']['poster_image'] ?>" />
<img src="<?= $escape->attr($item['manga']['image']) ?>" />
<div class="name">
<a href="https://hummingbird.me/manga/<?= $item['manga_id'] ?>">
<?= $item['manga']['romaji_title'] ?>
<?= (isset($item['manga']['english_title'])) ? "<br />({$item['manga']['english_title']})" : ""; ?>
<a href="<?= $item['manga']['url'] ?>">
<?= $escape->html($item['manga']['title']) ?>
<?= (isset($item['manga']['alternate_title'])) ? "<br />({$item['manga']['alternate_title']})" : ""; ?>
</a>
</div>
<div class="table">
<?php if ($auth->is_authenticated()): ?>
<div class="row">
<div class="user_rating">Rating: <?= ($item['rating'] > 0) ? (int)($item['rating'] * 2) : '-' ?> / 10</div>
<span class="edit">
<a class="bracketed" title="Edit information about this manga" href="<?= $urlGenerator->url("manga/edit/{$item['id']}/{$name}") ?>">Edit</a>
</span>
</div>
<?php endif ?>
<div class="row">
<div class="user_rating">Rating: <?= $item['user_rating'] ?> / 10</div>
</div>
<div class="row">
<div class="chapter_completion">
Chapters: <span class="chapters_read"><?= $item['chapters_read'] ?></span> /
<span class="chapter_count"><?= ($item['manga']['chapter_count'] > 0) ? $item['manga']['chapter_count'] : "-" ?></span>
Chapters: <span class="chapters_read"><?= $item['chapters']['read'] ?></span> /
<span class="chapter_count"><?= $item['chapters']['total'] ?></span>
</div>
</div>
<div class="row">
<div class="volume_completion">
Volumes: <span class="volumes_read"><?= $item['volumes_read'] ?></span> /
<span class="volume_count"><?= ($item['manga']['volume_count'] > 0) ? $item['manga']['volume_count'] : "-" ?></span>
Volumes: <span class="volumes_read"><?= $item['volumes']['read'] ?></span> /
<span class="volume_count"><?= $item['volumes']['total'] ?></span>
</div>
</div>
</div>
<?php /*<div class="medium_metadata">
<div class="media_type"><?= $item['manga']['manga_type'] ?></div>
</div> */ ?>
</article>
<?php endforeach ?>
</section>
</section>
<?php endforeach ?>
<?php endif ?>
</main>
<?php if (is_logged_in()): ?>
<script src="<?= asset_url('js.php?g=edit') ?>"></script>
<?php endif ?>
<?php if ($auth->is_authenticated()): ?>
<script src="<?= $urlGenerator->asset_url('js.php?g=edit') ?>"></script>
<?php endif ?>

89
app/views/manga/edit.php Normal file
View File

@ -0,0 +1,89 @@
<?php if ($auth->is_authenticated()): ?>
<main>
<h1>
Edit <?= $item['manga']['title'] ?>
<?= ($item['manga']['alternate_title'] != "") ? "({$item['manga']['alternate_title']})" : ""; ?>
</h1>
<form action="<?= $action ?>" method="post">
<table class="form">
<thead>
<tr>
<th>
<h3><?= $escape->html($item['manga']['title']) ?></h3>
<?php if($item['manga']['alternate_title'] != ""): ?>
<h4><?= $escape->html($item['manga']['alternate_title']) ?></h4>
<?php endif ?>
</th>
<th>
<article class="media">
<?= $helper->img($item['manga']['image']); ?>
</article>
</th>
</tr>
</thead>
<tbody>
<tr>
<td><label for="status">Reading Status</label></td>
<td>
<select name="status" id="status">
<?php foreach($status_list as $status): ?>
<option <?php if($item['reading_status'] === $status): ?>selected="selected"<?php endif ?>
value="<?= $status ?>"><?= $status ?></option>
<?php endforeach ?>
</select>
</td>
</tr>
<tr>
<td><label for="series_rating">Rating</label></td>
<td>
<input type="number" min="0" max="10" maxlength="2" name="new_rating" value="<?= $item['user_rating'] ?>" id="series_rating" size="2" /> / 10
</td>
</tr>
<tr>
<td><label for="chapters_read">Chapters Read</label></td>
<td>
<input type="number" min="0" name="chapters_read" id="chapters_read" value="<?= $item['chapters']['read'] ?>" /> / <?= $item['chapters']['total'] ?>
</td>
</tr>
<tr>
<td><label for="volumes_read">Volumes Read</label></td>
<td>
<input type="number" min="0" name="volumes_read" id="volumes_read" value="<?= $item['volumes']['read'] ?>" /> / <?= $item['volumes']['total'] ?>
</td>
</tr>
<tr>
<td><label for="rereading_flag">Rereading?</label></td>
<td>
<input type="checkbox" name="reareading" id="rereading_flag"
<?php if($item['rereading'] === TRUE): ?>checked="checked"<?php endif ?>
/>
</td>
</tr>
<tr>
<td><label for="reread_count">Reread Count</label></td>
<td>
<input type="number" min="0" id="reread_count" name="reread_count" value="<?= $item['reread'] ?>" />
</td>
</tr>
<tr>
<td><label for="notes">Notes</label></td>
<td>
<textarea name="notes" id="notes"><?= $escape->html($item['notes']) ?></textarea>
</td>
</tr>
<tr>
<td>&nbsp;</td>
<td>
<input type="hidden" value="<?= $item['id'] ?>" name="id" />
<input type="hidden" value="<?= $item['manga']['slug'] ?>" name="manga_id" />
<input type="hidden" value="<?= $item['user_rating'] ?>" name="old_rating" />
<input type="hidden" value="true" name="edit" />
<button type="submit">Submit</button>
</td>
</tr>
</tbody>
</table>
</form>
</main>
<script src="<?= $urlGenerator->asset_url('js.php?g=edit') ?>"></script>
<?php endif ?>

View File

@ -1,9 +1,15 @@
<main>
<?php if (empty($sections)): ?>
<h3>There's nothing here!</h3>
<?php else: ?>
<?php foreach ($sections as $name => $items): ?>
<h2><?= $name ?></h2>
<table>
<thead>
<tr>
<?php if ($auth->is_authenticated()): ?>
<th>&nbsp;</th>
<?php endif ?>
<th>Title</th>
<th>Rating</th>
<th>Chapters</th>
@ -13,21 +19,27 @@
</thead>
<tbody>
<?php foreach($items as $item): ?>
<tr id="manga-<?= $item['manga']['id'] ?>">
<td class="align_left">
<a href="https://hummingbird.me/manga/<?= $item['manga']['id'] ?>">
<?= $item['manga']['romaji_title'] ?>
</a>
<?= (array_key_exists('english_title', $item['manga'])) ? " &middot; " . $item['manga']['english_title'] : "" ?>
<tr id="manga-<?= $item['id'] ?>">
<?php if($auth->is_authenticated()): ?>
<td>
<a class="bracketed" href="<?= $urlGenerator->url("manga/edit/{$item['id']}/{$name}") ?>">Edit</a>
</td>
<td><?= ($item['rating'] > 0) ? (int)($item['rating'] * 2) : '-' ?> / 10</td>
<td><?= $item['chapters_read'] ?> / <?= ($item['manga']['chapter_count'] > 0) ? $item['manga']['chapter_count'] : "-" ?></td>
<td><?= $item['volumes_read'] ?> / <?= ($item['manga']['volume_count'] > 0) ? $item['manga']['volume_count'] : "-" ?></td>
<td><?= $item['manga']['manga_type'] ?></td>
<?php endif ?>
<td class="align_left">
<a href="<?= $item['manga']['url'] ?>">
<?= $item['manga']['title'] ?>
</a>
<?= ( ! is_null($item['manga']['alternate_title'])) ? " &middot; " . $item['manga']['alternate_title'] : "" ?>
</td>
<td><?= $item['user_rating'] ?> / 10</td>
<td><?= $item['chapters']['read'] ?> / <?= $item['chapters']['total'] ?></td>
<td><?= $item['volumes']['read'] ?> / <?= $item['volumes']['total'] ?></td>
<td><?= $item['manga']['type'] ?></td>
</tr>
<?php endforeach ?>
</tbody>
</table>
<?php endforeach ?>
<?php endif ?>
</main>
<script src="<?= asset_url('js.php?g=table') ?>"></script>
<script src="<?= $urlGenerator->asset_url('js.php?g=table') ?>"></script>

View File

@ -1,5 +1,5 @@
<div class="message <?= $stat_class ?>">
<div class="message <?= $escape->attr($message_type) ?>">
<span class="icon"></span>
<?= $message ?>
<?= $escape->html($message) ?>
<span class="close" onclick="this.parentElement.style.display='none'">x</span>
</div>

158
build.xml Normal file
View File

@ -0,0 +1,158 @@
<?xml version="1.0" encoding="UTF-8"?>
<project default="full-build" name="animeclient" basedir=".">
<!-- By default, we assume all tools to be on the $PATH -->
<property name="pdepend" value="pdepend" />
<property name="phpcpd" value="phpcpd" />
<property name="phpdox" value="phpdox" />
<property name="phploc" value="phploc" />
<property name="phpmd" value="phpmd" />
<property name="phpunit" value="phpunit" />
<property name="sonar" value="sonar-runner" />
<target name="full-build"
depends="prepare,static-analysis,phpunit,phpdox,sonar"
description="Performs static analysis, runs the tests, and generates project documentation"
/>
<target name="full-build-parallel"
depends="prepare,static-analysis-parallel,phpunit,phpdox"
description="Performs static analysis (executing the tools in parallel), runs the tests, and generates project documentation"
/>
<target name="quick-build"
depends="prepare,lint,phpunit-no-coverage"
description="Performs a lint check and runs the tests (without generating code coverage reports)"
/>
<target name="static-analysis"
depends="lint,phploc-ci,pdepend,phpcpd-ci"
description="Performs static analysis"
/>
<!-- Adjust the threadCount attribute's value to the number of CPUs -->
<target name="static-analysis-parallel" description="Performs static analysis (executing the tools in parallel)">
<parallel threadCount="6">
<sequential>
<antcall target="pdepend" />
</sequential>
<antcall target="lint" />
<antcall target="phpcpd-ci" />
<antcall target="phploc-ci" />
</parallel>
</target>
<target name="clean" unless="clean.done" description="Cleanup build artifacts">
<delete dir="build/api" />
<delete dir="build/coverage" />
<delete dir="build/logs" />
<delete dir="build/pdepend" />
<delete dir="build/phpdox" />
<property name="clean.done" value="true" />
</target>
<target name="prepare" depends="clean" unless="prepare.done" description="Prepare for build">
<mkdir dir="build/api" />
<mkdir dir="build/coverage" />
<mkdir dir="build/logs" />
<mkdir dir="build/pdepend" />
<mkdir dir="build/phpdox" />
<property name="prepare.done" value="true" />
</target>
<target name="lint" unless="lint.done" description="Perform syntax check of sourcecode files">
<apply executable="php" taskname="lint">
<arg value="-l" />
<fileset dir="src">
<include name="**/*.php" />
</fileset>
<fileset dir="tests">
<include name="**/*.php" />
</fileset>
</apply>
<property name="lint.done" value="true" />
</target>
<target name="phploc" unless="phploc.done" description="Measure project size using PHPLOC and print human readable output. Intended for usage on the command line.">
<exec executable="${phploc}" taskname="phploc">
<arg value="--count-tests" />
<arg path="src" />
<arg path="tests" />
</exec>
<property name="phploc.done" value="true" />
</target>
<target name="phploc-ci" depends="prepare" unless="phploc.done" description="Measure project size using PHPLOC and log result in CSV and XML format. Intended for usage within a continuous integration environment.">
<exec executable="${phploc}" taskname="phploc">
<arg value="--count-tests" />
<arg value="--log-csv" />
<arg path="build/logs/phploc.csv" />
<arg value="--log-xml" />
<arg path="build/logs/phploc.xml" />
<arg path="src" />
<arg path="tests" />
</exec>
<property name="phploc.done" value="true" />
</target>
<target name="pdepend" depends="prepare" unless="pdepend.done" description="Calculate software metrics using PHP_Depend and log result in XML format. Intended for usage within a continuous integration environment.">
<exec executable="${pdepend}" taskname="pdepend">
<arg value="--jdepend-xml=build/logs/jdepend.xml" />
<arg value="--jdepend-chart=build/pdepend/dependencies.svg" />
<arg value="--overview-pyramid=build/pdepend/overview-pyramid.svg" />
<arg path="src" />
</exec>
<property name="pdepend.done" value="true" />
</target>
<target name="phpcpd" unless="phpcpd.done" description="Find duplicate code using PHPCPD and print human readable output. Intended for usage on the command line before committing.">
<exec executable="${phpcpd}" taskname="phpcpd">
<arg path="src" />
</exec>
<property name="phpcpd.done" value="true" />
</target>
<target name="phpcpd-ci" depends="prepare" unless="phpcpd.done" description="Find duplicate code using PHPCPD and log result in XML format. Intended for usage within a continuous integration environment.">
<exec executable="${phpcpd}" taskname="phpcpd">
<arg value="--log-pmd" />
<arg path="build/logs/pmd-cpd.xml" />
<arg path="src" />
</exec>
<property name="phpcpd.done" value="true" />
</target>
<target name="phpunit" unless="phpunit.done" depends="prepare" description="Run unit tests with PHPUnit">
<exec executable="${phpunit}" taskname="phpunit">
<arg value="--configuration" />
<arg path="build/phpunit.xml" />
</exec>
<property name="phpunit.done" value="true" />
</target>
<target name="phpunit-no-coverage" depends="prepare" unless="phpunit.done" description="Run unit tests with PHPUnit (without generating code coverage reports)">
<exec executable="${phpunit}" failonerror="true" taskname="phpunit">
<arg value="--configuration" />
<arg path="build/phpunit.xml" />
<arg value="--no-coverage" />
</exec>
<property name="phpunit.done" value="true" />
</target>
<target name="phpdox" depends="phploc-ci,phpunit" unless="phpdox.done" description="Generate project documentation using phpDox">
<exec dir="build" executable="${phpdox}" taskname="phpdox" />
<property name="phpdox.done" value="true" />
</target>
<target name="sonar" depends="phpunit" unless="sonar.done" description="Generate code analysis with sonarqube">
<exec executable="${sonar}" taskname="sonar" />
<property name="sonar.done" value="true" />
</target>
</project>

View File

@ -0,0 +1,11 @@
/**
* Hummingbird Anime Client
*
* An API client for Hummingbird to manage anime and manga watch lists
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren
* @copyright Copyright (c) 2015 - 2016
* @link https://github.com/timw4mail/HummingBirdAnimeClient
* @license MIT
*/

View File

@ -0,0 +1,10 @@
/**
* Ion
*
* Building blocks for web development
*
* @package Ion
* @author Timothy J. Warren
* @copyright Copyright (c) 2015 - 2016
* @license MIT
*/

131
build/phpdox.xml Normal file
View File

@ -0,0 +1,131 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- This is a skeleton phpDox config file - Check http://phpDox.de for latest version and more info -->
<phpdox xmlns="http://xml.phpdox.net/config" silent="false">
<!-- @silent: true | false to enable or disable visual output of progress -->
<!-- Additional bootstrap files to load for additional parsers, enrichers and/or engines -->
<!-- Place as many require nodes as you feel like in this container -->
<!-- syntax: <require file="/path/to/file.php" /> -->
<bootstrap />
<!-- A phpDox project to process, you can have multiple projects in one config file -->
<project name="Hummingbird Anime Client" source="../src" workdir="phpdox/xml">
<!-- @name - The name of the project -->
<!-- @source - The source directory of the application to process -->
<!-- @workdir - The directory to store the xml data files in -->
<!-- A phpDox config file can define additional variables (properties) per project -->
<!-- <property name="some.name" value="the.value" /> -->
<!-- Values can make use of previously defined properties -->
<!-- The following are defined by default:
${basedir} Directory the loaded config file is in
${phpDox.home} Directory of the phpDox installation
${phpDox.file} The current config file
${phpDox.version} phpDox' version number
${phpDox.project.name} The value of project/@name if set, otherwise 'unnamed'
${phpDox.project.source} The value of project/@source if set, otherwise '${basedir}/src'
${phpDox.project.workdir} The value of project/@workdir if set, otherwise '${basedir}/build/phpdox/xml'
${phpDox.php.version} The PHP Version of the interpreter in use
-->
<!-- Additional configuration for the collecting process (parsing of php code, generation of xml data) -->
<collector publiconly="false" backend="parser" encoding="auto">
<!-- @publiconly - Flag to disable/enable processing of non public methods and members -->
<!-- @backend - The collector backend to use, currently only shipping with 'parser' -->
<!-- @encoding - Charset encoding of source files (overwrite default 'auto' if detection fails) -->
<!-- <include / exclude filter for filelist generator, mask must follow fnmatch() requirements -->
<include mask="*.php" />
<exclude mask="" />
<!-- How to handle inheritance -->
<inheritance resolve="true">
<!-- @resolve - Flag to enable/disable resolving of inheritance -->
<!-- You can define multiple (external) dependencies to be included -->
<!-- <dependency path="" -->
<!-- @path - path to a directory containing an index.xml for a dependency project -->
</inheritance>
</collector>
<!-- Configuration of generation process -->
<generator output="../docs">
<!-- @output - (Base-)Directory to store output data in -->
<!-- A generation process consists of one or more build tasks and of (optional) enrich sources -->
<enrich base="logs">
<!-- @base - (Base-)Directory of datafiles used for enrich process -->
<!--<source type="...">-->
<!-- @type - the handler for the enrichment -->
<!-- known types by default are: build, checkstyle, git, phpcs, phploc, phpunit, pmd -->
<!-- every enrichment source can have additional configuration nodes, most probably need a logfile -->
<!-- <file name="path/to/log.xml" /> -->
<!--</source> -->
<!-- add phploc output -->
<source type="phploc">
<file name="phploc.xml" />
</source>
<!-- git vcs information -->
<source type="git">
<git binary="/usr/bin/git" />
<history enabled="true" limit="15" cache="${phpDox.project.workdir}/gitlog.xml" />
</source>
<!-- PHP Code Sniffer findings -->
<!--
<source type="phpcs">
<file name="logs/phpcs.xml" />
</source>
-->
<!-- PHPMessDetector -->
<!--
<source type="pmd">
<file name="pmd.xml" />
</source>
-->
<!-- PHPUnit Coverage XML -->
<source type="phpunit">
<!-- <coverage path="clover.xml" />-->
<!-- @path - the directory where the xml code coverage report can be found -->
<!--<filter directory="${phpDox.project.source}" />-->
<!-- @directory - path of the phpunit config whitelist filter directory -->
</source>
<!--
<source type="phpunit">
<filter directory="${phpDox.project.source}" />
</source>
-->
</enrich>
<!-- <build engine="..." enabled="true" output="..." /> -->
<!-- @engine - The name of the engine this build task uses, use ./phpDox - -engines to get a list of available engines -->
<!-- @enabled - Flag to enable/disable this engine, default: enabled=true -->
<!-- @output - (optional) Output directory; if relative (no / as first char) it is interpreted as relative to generator/@output -->
<!-- An engine and thus build node can have additional configuration child nodes, please check the documentation for the engine to find out more -->
<!-- default engine "html" -->
<build engine="html" enabled="true">
<template dir="${phpDox.home}/templates/html" />
<file extension="html" />
</build>
</generator>
</project>
</phpdox>

37
build/phpunit.xml Normal file
View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit
colors="true"
stopOnFailure="false"
bootstrap="../tests/bootstrap.php"
beStrictAboutTestsThatDoNotTestAnything="true"
checkForUnintentionallyCoveredCode="true"
>
<filter>
<whitelist>
<directory suffix=".php">../src/Aviat/Ion</directory>
<directory suffix=".php">../src/Aviat/AnimeClient</directory>
</whitelist>
</filter>
<testsuites>
<testsuite name="Ion">
<directory>../tests/Ion</directory>
</testsuite>
<testsuite name="AnimeClient">
<directory>../tests/AnimeClient</directory>
</testsuite>
</testsuites>
<logging>
<log type="coverage-html" target="coverage"/>
<log type="coverage-clover" target="logs/clover.xml"/>
<log type="coverage-crap4j" target="logs/crap4j.xml"/>
<log type="coverage-xml" target="logs/coverage" />
<log type="junit" target="logs/junit.xml" logIncompleteSkipped="false"/>
</logging>
<php>
<server name="HTTP_USER_AGENT" value="Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:38.0) Gecko/20100101 Firefox/38.0" />
<server name="HTTP_HOST" value="localhost" />
<server name="SERVER_NAME" value="localhost" />
<server name="REQUEST_URI" value="/" />
<server name="REQUEST_METHOD" value="GET" />
</php>
</phpunit>

View File

@ -0,0 +1,89 @@
<?php
$animeclient_file_patterns = [
'app/config/*.php',
'app/booststrap.php',
'src/functions.php',
'src/Aviat/AnimeClient/*.php'
];
$ion_file_patterns = [
'src/Aviat/Ion/*.php'
];
if ( ! function_exists('glob_recursive'))
{
// Does not support flag GLOB_BRACE
function glob_recursive($pattern, $flags = 0)
{
$files = glob($pattern, $flags);
foreach (glob(dirname($pattern) . '/*', GLOB_ONLYDIR | GLOB_NOSORT) as $dir)
{
$files = array_merge($files, glob_recursive($dir . '/' . basename($pattern), $flags));
}
return $files;
}
}
function get_text_to_replace($tokens)
{
if ($tokens[0][0] !== T_OPEN_TAG)
{
return NULL;
}
// If there is already a docblock, as the second token after the
// open tag, get the contents of that token to replace
if ($tokens[1][0] === T_DOC_COMMENT)
{
return "<?php\n" . $tokens[1][1];
}
else if ($tokens[1][0] !== T_DOC_COMMENT)
{
return "<?php";
}
}
function get_tokens($source)
{
return token_get_all($source);
}
function replace_files(array $files, $template)
{
foreach ($files as $file)
{
$source = file_get_contents($file);
$tokens = get_tokens($source);
$text_to_replace = get_text_to_replace($tokens);
$header = file_get_contents(__DIR__ . $template);
$new_text = "<?php\n{$header}";
$new_source = str_replace($text_to_replace, $new_text, $source);
file_put_contents($file, $new_source);
}
}
foreach ($animeclient_file_patterns as $glob)
{
$files = glob_recursive($glob);
replace_files($files, '/animeclient_header_comment.txt');
}
$loose_files = [
__DIR__ . '/../index.php',
__DIR__ . '/../public/css.php',
__DIR__ . '/../public/js.php'
];
replace_files($loose_files, '/animeclient_header_comment.txt');
foreach ($ion_file_patterns as $glob)
{
$files = glob_recursive($glob);
replace_files($files, '/ion_header_comment.txt');
}
echo "Successfully updated headers \n";

View File

@ -1,11 +1,21 @@
{
"name": "timw4mail/hummingbird-anime-client",
"description": "A self-hosted anime/manga client for hummingbird.",
"license":"MIT",
"require": {
"guzzlehttp/guzzle": "5.3.*",
"filp/whoops": "1.1.*",
"abeautifulsite/simpleimage": "2.5.*",
"aura/html": "2.*",
"aura/router": "2.2.*",
"aura/web": "2.0.*",
"aviat4ion/query": "2.0.*",
"robmorgan/phinx": "*",
"abeautifulsite/simpleimage": "*"
"aura/session": "2.*",
"aura/web": "2.*",
"aviat4ion/query": "2.5.*",
"container-interop/container-interop": "1.*",
"danielstjules/stringy": "~2.1",
"filp/whoops": "2.0.*",
"guzzlehttp/guzzle": "6.*",
"monolog/monolog": "1.*",
"psr/log": "~1.0",
"robmorgan/phinx": "0.4.*",
"yosymfony/toml": "0.3.*"
}
}

105
index.php
View File

@ -1,24 +1,17 @@
<?php
/**
* Here begins everything!
* Hummingbird Anime Client
*
* An API client for Hummingbird to manage anime and manga watch lists
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren
* @copyright Copyright (c) 2015 - 2016
* @link https://github.com/timw4mail/HummingBirdAnimeClient
* @license MIT
*/
namespace AnimeClient;
// -----------------------------------------------------------------------------
// ! Start config
// -----------------------------------------------------------------------------
/**
* Well, whose list is it?
*/
define('WHOSE', "Tim's");
// -----------------------------------------------------------------------------
// ! End config
// -----------------------------------------------------------------------------
\session_start();
use Whoops\Handler\PrettyPageHandler;
use Whoops\Handler\JsonResponseHandler;
// Work around the silly timezone error
$timezone = ini_get('date.timezone');
@ -27,17 +20,75 @@ if ($timezone === '' || $timezone === FALSE)
ini_set('date.timezone', 'GMT');
}
/**
* Joins paths together. Variadic to take an
* arbitrary number of arguments
*
* @return string
*/
function _dir()
{
return implode(DIRECTORY_SEPARATOR, func_get_args());
}
// Define base directories
define('ROOT_DIR', __DIR__);
define('APP_DIR', ROOT_DIR . DIRECTORY_SEPARATOR . 'app');
define('CONF_DIR', APP_DIR . DIRECTORY_SEPARATOR . 'config');
define('BASE_DIR', APP_DIR . DIRECTORY_SEPARATOR . 'base');
require BASE_DIR . DIRECTORY_SEPARATOR . 'pre_conf_functions.php';
$APP_DIR = _dir(__DIR__, 'app');
$SRC_DIR = _dir(__DIR__, 'src');
$CONF_DIR = _dir($APP_DIR, 'config');
// Setup autoloaders
_setup_autoloaders();
/**
* Set up autoloaders
*
* @codeCoverageIgnore
* @return void
*/
spl_autoload_register(function($class) use ($SRC_DIR) {
$class_parts = explode('\\', $class);
$ns_path = $SRC_DIR . '/' . implode('/', $class_parts) . ".php";
// Do dependency injection, and go!
require _dir(APP_DIR, 'bootstrap.php');
if (file_exists($ns_path))
{
require_once($ns_path);
return;
}
});
require _dir(__DIR__, '/vendor/autoload.php');
// -------------------------------------------------------------------------
// Setup error handling
// -------------------------------------------------------------------------
$whoops = new \Whoops\Run();
// Set up default handler for general errors
$defaultHandler = new PrettyPageHandler();
$whoops->pushHandler($defaultHandler);
// Set up json handler for ajax errors
$jsonHandler = new JsonResponseHandler();
$whoops->pushHandler($jsonHandler);
// Register as the error handler
$whoops->register();
// -----------------------------------------------------------------------------
// Dependency Injection setup
// -----------------------------------------------------------------------------
require _dir($CONF_DIR, 'base_config.php'); // $base_config
require _dir($CONF_DIR, 'config.php'); // $config
$config_array = array_merge($base_config, $config);
$di = require _dir($APP_DIR, 'bootstrap.php');
// Unset 'constants'
unset($APP_DIR);
unset($SRC_DIR);
unset($CONF_DIR);
$container = $di($config_array);
// -----------------------------------------------------------------------------
// Dispatch to the current route
// -----------------------------------------------------------------------------
$container->get('dispatcher')->__invoke();
// End of index.php

View File

@ -1,23 +1,16 @@
<?xml version="1.0" encoding="UTF-8" ?>
<phpdoc>
<title>Hummingbird Anime Client</title>
<parser>
<target>docs</target>
</parser>
<transformer>
<target>docs</target>
</transformer>
<transformations>
<template name="clean" />
</transformations>
<files>
<directory>.</directory>
<directory>app</directory>
<ignore>public/*</ignore>
<ignore>app/views/*</ignore>
<ignore>app/config/*</ignore>
<ignore>migrations/*</ignore>
<ignore>tests/*</ignore>
<ignore>vendor/*</ignore>
</files>
<?xml version="1.0" encoding="UTF-8" ?>
<phpdoc>
<title>Hummingbird Anime Client</title>
<parser>
<target>docs</target>
</parser>
<transformer>
<target>docs</target>
</transformer>
<transformations>
<template name="clean" />
</transformations>
<files>
<directory>src/Aviat</directory>
</files>
</phpdoc>

View File

@ -1,22 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit
colors="true"
stopOnFailure="false"
bootstrap="tests/bootstrap.php">
<filter>
<whitelist>
<directory suffix=".php">app/base</directory>
<directory suffix=".php">app/controllers</directory>
<directory suffix=".php">app/models</directory>
</whitelist>
</filter>
<testsuites>
<testsuite name="BaseTests">
<directory>tests/base</directory>
</testsuite>
</testsuites>
<php>
<server name="HTTP_USER_AGENT" value="Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:38.0) Gecko/20100101 Firefox/38.0" />
<server name="HTTP_HOST" value="localhost" />
</php>
</phpunit>

View File

@ -1,13 +1,14 @@
<?php
/**
* Easy Min
* Hummingbird Anime Client
*
* Simple minification for better website performance
* An API client for Hummingbird to manage anime and manga watch lists
*
* @author Timothy J. Warren
* @copyright Copyright (c) 2012
* @link https://github.com/aviat4ion/Easy-Min
* @license http://philsturgeon.co.uk/code/dbad-license
* @package HummingbirdAnimeClient
* @author Timothy J. Warren
* @copyright Copyright (c) 2015 - 2016
* @link https://github.com/timw4mail/HummingBirdAnimeClient
* @license MIT
*/
// --------------------------------------------------------------------------

View File

@ -1,7 +1,16 @@
template {
display: none;
}
body {
margin: 0.5em;
}
a:hover,
a:active {
color: #7d12db;
}
table {
width: 85%;
margin: 0 auto;
@ -11,7 +20,65 @@ tbody > tr:nth-child(odd) {
background: #ddd;
}
input[type=number] {
width: 4em;
}
.form {
width: 100%;
}
.form tr > td:nth-child(odd) {
text-align: right;
min-width: 25px;
max-width: 30%;
}
.form tr > td:nth-child(even) {
text-align: left;
min-width: 70%;
}
.form thead th,
.form thead tr {
background: inherit;
border: 0;
}
.form.invisible tr:nth-child(odd) {
background: inherit;
}
.form.invisible tr,
.form.invisible td,
.form.invisible th {
border: 0;
}
.bracketed,
h1 a {
text-shadow: 1px 1px 1px #000;
}
.bracketed:before {
content: '[\00a0';
}
.bracketed:after {
content: '\00a0]';
}
.bracketed {
color: #12db18;
}
.bracketed:hover,
.bracketed:active {
color: #db7d12;
}
.grow-1 {
-webkit-box-flex: 1;
-webkit-flex-grow: 1;
-ms-flex-positive: 1;
flex-grow: 1;
@ -30,16 +97,32 @@ tbody > tr:nth-child(odd) {
}
.flex-align-end {
-webkit-box-align: end;
-webkit-align-items: flex-end;
-ms-flex-align: end;
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 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
@ -49,6 +132,10 @@ tbody > tr:nth-child(odd) {
font-size: 1.6rem;
}
.align_center {
text-align: center;
}
.align_left {
text-align: left;
}
@ -57,22 +144,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;
@ -134,11 +205,15 @@ button {
.media:hover > .media_metadata > div,
.media:hover > .medium_metadata > div,
.media:hover > .table .row {
-webkit-transition: .25s ease;
transition: .25s ease;
background: rgba(0,0,0,0.75);
}
.media:hover > button[hidden],
.media:hover > .edit_buttons[hidden] {
-webkit-transition: .25s ease;
transition: .25s ease;
display: block;
}
@ -217,7 +292,9 @@ button {
.anime .airing_status,
.anime .user_rating,
.anime .completion,
.anime .age_rating {
.anime .age_rating,
.anime .edit,
.anime .delete {
background: none;
text-align: center;
}
@ -234,6 +311,7 @@ button {
.manga .row {
width: 100%;
background: rgba(0, 0, 0, 0.45);
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
@ -247,6 +325,11 @@ button {
padding: 0 inherit;
}
.anime .row > span,
.manga .row > span {
text-align: left;
}
.anime .row > div,
.manga .row > div {
font-size: 0.8em;
@ -260,7 +343,9 @@ button {
.anime .media > button.plus_one {
position: absolute;
top: 138px;
top: calc(50% - 21.5px);
left: 44px;
left: calc(50% - 66.5px);
}
@ -268,6 +353,10 @@ button {
Manga-list-specific styles
------------------------------------------------------------------------------*/
.manga .row {
padding: 1px;
}
.manga .media {
border: 1px solid #ddd;
width: 200px;
@ -277,6 +366,8 @@ button {
.manga .media > .edit_buttons {
position: absolute;
top: 86px;
top: calc(50% - 58.5px);
left: 5px;
left: calc(50% - 95px);
}

View File

@ -1,13 +1,20 @@
:root {
--link-shadow: 1px 1px 1px #000;
--shadow: 1px 2px 1px rgba(0, 0, 0, 0.85);
--title-overlay: rgba(0, 0, 0, 0.45);
--text-color: #ffffff;
--normal-padding: 0.25em;
--radius: 0.5em;
--link-hover-color: #7d12db;
--edit-link-hover-color: #db7d12;
--edit-link-color: #12db18;
}
body {
margin: 0.5em;
template {display:none}
body {margin: 0.5em;}
a:hover, a:active {
color: var(--link-hover-color)
}
table {
@ -19,40 +26,61 @@ tbody > tr:nth-child(odd) {
background: #ddd;
}
input[type=number] {
width: 4em;
}
.form { width:100%; }
.form tr > td:nth-child(odd) {
text-align:right;
min-width:25px;
max-width:30%;
}
.form tr > td:nth-child(even) {
text-align:left;
min-width:70%;
}
.form thead th, .form thead tr {
background: inherit;
border:0;
}
.form.invisible tr:nth-child(odd) {
background: inherit;
}
.form.invisible tr, .form.invisible td, .form.invisible th {
border:0;
}
.bracketed, h1 a {
text-shadow: var(--link-shadow);
}
.bracketed:before {content: '[\00a0'}
.bracketed:after {content: '\00a0]'}
.bracketed {
color: var(--edit-link-color);
}
.bracketed:hover, .bracketed:active {
color: var(--edit-link-hover-color)
}
.grow-1 {flex-grow: 1}
.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;
@ -115,12 +143,14 @@ button {
.media:hover > .medium_metadata > div,
.media:hover > .table .row
{
transition: .25s ease;
background:rgba(0,0,0,0.75);
}
.media:hover > button[hidden],
.media:hover > .edit_buttons[hidden]
{
transition: .25s ease;
display:block;
}
@ -197,7 +227,9 @@ button {
.anime .airing_status,
.anime .user_rating,
.anime .completion,
.anime .age_rating {
.anime .age_rating,
.anime .edit,
.anime .delete {
background: none;
text-align:center;
}
@ -220,6 +252,10 @@ button {
padding:0 inherit;
}
.anime .row > span, .manga .row > span {
text-align:left;
}
.anime .row > div, .manga .row > div {
font-size:0.8em;
display:flex-item;
@ -230,13 +266,19 @@ button {
.anime .media > button.plus_one {
position:absolute;
top: calc(50% - (43px / 2));
left: calc(50% - (97px / 2 + 18));
top: 138px;
top: calc(50% - 21.5px);
left: 44px;
left: calc(50% - 66.5px);
}
/* -----------------------------------------------------------------------------
Manga-list-specific styles
------------------------------------------------------------------------------*/
.manga .row {
padding:1px;
}
.manga .media {
border:1px solid #ddd;
width:200px;
@ -246,6 +288,8 @@ button {
.manga .media > .edit_buttons {
position:absolute;
top: calc(50% - (117px / 2));
left: calc(50% - (190px / 2));
top: 86px;
top: calc(50% - 58.5px);
left: 5px;
left: calc(50% - 95px);
}

View File

@ -1,167 +1,229 @@
:root {
box-sizing: border-box;
cursor: default;
font-family: 'Open Sans', 'Helvetica Neue', Helvetica, 'Lucida Grande', sans-serif;
font-family: 'Open Sans', 'Nimbus Sans L', 'Helvetica Neue', Helvetica, 'Lucida Grande', sans-serif;
line-height: 1.4;
overflow-y: scroll;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
text-size-adjust: 100%; }
-ms-text-size-adjust: 100%;
text-size-adjust: 100%;
}
audio:not([controls]) {
display: none; }
display: none;
}
details {
display: block; }
display: block;
}
/*input[type="number"] {
width: auto; }*/
input[type="number"] {
width: auto; }
input[type="search"] {
-webkit-appearance: textfield; }
input[type="search"]::-webkit-search-cancel-button, input[type="search"]::-webkit-search-decoration {
-webkit-appearance: none; }
-webkit-appearance: textfield;
}
input[type="search"]::-webkit-search-cancel-button,
input[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
main {
display: block; }
display: block;
}
summary {
display: block; }
display: block;
}
pre {
overflow: auto; }
overflow: auto;
}
progress {
display: inline-block; }
display: inline-block;
}
small {
font-size: 75%; }
font-size: 75%;
}
big {
font-size: 125%; }
font-size: 125%;
}
template {
display: none; }
display: none;
}
textarea {
overflow: auto;
resize: vertical; }
resize: vertical;
}
[hidden] {
display: none; }
display: none;
}
[unselectable] {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none; }
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
*, ::before, ::after {
*,
::before,
::after {
border-style: solid;
border-width: 0;
box-sizing: inherit; }
box-sizing: inherit;
}
* {
font-size: inherit;
line-height: inherit;
margin: 0;
padding: 0; }
padding: 0;
}
::before, ::after {
::before,
::after {
text-decoration: inherit;
vertical-align: inherit; }
vertical-align: inherit;
}
a {
text-decoration: none; }
text-decoration: none;
}
audio, canvas, iframe, img, svg, video {
vertical-align: middle; }
audio,
canvas,
iframe,
img,
svg,
video {
vertical-align: middle;
}
button, input, select, textarea {
background-color: transparent;
button,
input,
select,
textarea {
/*background-color: transparent;*/
border: .1rem solid #ccc;
color: inherit;
font-family: inherit;
font-style: inherit;
font-weight: inherit;
min-height: 1.4em; }
min-height: 1.4em;
}
code, kbd, pre, samp {
font-family: Menlo, Monaco, Consolas, 'Courier New', monospace, monospace; }
code,
kbd,
pre,
samp {
font-family: Menlo, Monaco, Consolas, 'Courier New', monospace, monospace;
}
table {
border-collapse: collapse;
border-spacing: 0; }
border-spacing: 0;
}
::-moz-selection {
background-color: #b3d4fc;
text-shadow: none; }
text-shadow: none;
}
::selection {
background-color: #b3d4fc;
text-shadow: none; }
text-shadow: none;
}
button::-moz-focus-inner {
border: 0; }
border: 0;
}
@media screen {
[hidden~="screen"] {
display: inherit; }
display: inherit;
}
[hidden~="screen"]:not(:active):not(:focus):not(:target) {
clip: rect(0 0 0 0) !important;
position: absolute !important; } }
position: absolute !important;
}
}
body {
color: #444;
font-family: 'Open Sans', 'Helvetica Neue', Helvetica, 'Lucida Grande', sans-serif;
font-family: 'Open Sans', 'Nimbus Sans L', 'Helvetica Neue', Helvetica, 'Lucida Grande', sans-serif;
font-size: 1.6rem;
font-style: normal;
font-weight: 400; }
font-weight: 400;
}
p {
margin: 0 0 1.6rem; }
margin: 0 0 1.6rem;
}
h1, h2, h3, h4, h5, h6 {
font-family: 'Lato', 'Open Sans', 'Helvetica Neue', Helvetica, 'Lucida Grande', sans-serif;
margin: 2rem 0 1.6rem; }
h1,
h2,
h3,
h4,
h5,
h6 {
font-family: 'Lato', 'Open Sans', 'Nimbus Sans L', 'Helvetica Neue', Helvetica, 'Lucida Grande', sans-serif;
margin: 2rem 0 1.6rem;
}
h1 {
border-bottom: .1rem solid rgba(0, 0, 0, 0.2);
font-size: 3.6rem;
font-style: normal;
font-weight: 500; }
font-weight: 500;
}
h2 {
font-size: 3rem;
font-style: normal;
font-weight: 500; }
font-weight: 500;
}
h3 {
font-size: 2.4rem;
font-style: normal;
font-weight: 500;
margin: 1.6rem 0 0.4rem; }
margin: 1.6rem 0 0.4rem;
}
h4 {
font-size: 1.8rem;
font-style: normal;
font-weight: 600;
margin: 1.6rem 0 0.4rem; }
margin: 1.6rem 0 0.4rem;
}
h5 {
font-size: 1.6rem;
font-style: normal;
font-weight: 600;
margin: 1.6rem 0 0.4rem; }
margin: 1.6rem 0 0.4rem;
}
h6 {
color: #777;
font-size: 1.4rem;
font-style: normal;
font-weight: 600;
margin: 1.6rem 0 0.4rem; }
margin: 1.6rem 0 0.4rem;
}
small {
color: #777; }
color: #777;
}
pre {
background: #efefef;
@ -172,7 +234,8 @@ pre {
margin: 1.6rem 0;
padding: 1.6rem;
word-break: break-all;
word-wrap: break-word; }
word-wrap: break-word;
}
code {
background: #efefef;
@ -180,51 +243,73 @@ code {
font-family: Menlo, Monaco, Consolas, 'Courier New', monospace;
font-size: 1.4rem;
word-break: break-all;
word-wrap: break-word; }
word-wrap: break-word;
}
a {
color: #1271db;
-webkit-transition: .25s ease;
transition: .25s ease; }
a:hover, a:focus {
text-decoration: none; }
transition: .25s ease;
}
a:hover,
a:focus {
text-decoration: none;
}
dl {
margin-bottom: 1.6rem; }
margin-bottom: 1.6rem;
}
dd {
margin-left: 4rem; }
margin-left: 4rem;
}
ul, ol {
ul,
ol {
margin-bottom: 0.8rem;
padding-left: 2rem; }
padding-left: 2rem;
}
blockquote {
border-left: .2rem solid #1271db;
font-family: Georgia, Times, 'Times New Roman', serif;
font-style: italic;
margin: 1.6rem 0;
padding-left: 1.6rem; }
padding-left: 1.6rem;
}
figcaption {
font-family: Georgia, Times, 'Times New Roman', serif; }
font-family: Georgia, Times, 'Times New Roman', serif;
}
html {
font-size: 62.5%; }
font-size: 62.5%;
}
body {
padding: 0; }
padding: 0;
}
main, header, footer, article, section, aside, details, summary {
main,
header,
footer,
article,
section,
aside,
details,
summary {
display: block;
height: auto;
margin: 0 auto;
width: 100%; }
width: 100%;
}
main {
display: block;
margin: 0 auto;
padding: 0 1.6rem 1.6rem; }
padding: 0 1.6rem 1.6rem;
}
footer {
border-top: .1rem solid rgba(0, 0, 0, 0.2);
@ -233,33 +318,57 @@ footer {
float: left;
max-width: 100%;
padding: 1rem 0;
text-align: center; }
text-align: center;
}
hr {
border-top: .1rem solid rgba(0, 0, 0, 0.2);
display: block;
margin-bottom: 1.6rem;
width: 100%; }
width: 100%;
}
img {
height: auto;
max-width: 100%;
vertical-align: baseline; }
vertical-align: baseline;
}
@media screen and (max-width: 40rem) {
article, section, aside {
article,
section,
aside {
clear: both;
display: block;
max-width: 100%; }
img {
margin-right: 1.6rem; } }
max-width: 100%;
}
input[type="text"], input[type="password"], input[type="email"], input[type="url"], input[type="date"], input[type="month"], input[type="time"], input[type="datetime"], input[type="datetime-local"], input[type="week"], input[type="number"], input[type="search"], input[type="tel"], input[type="color"], select {
img {
margin-right: 1.6rem;
}
}
input[type="text"],
input[type="password"],
input[type="email"],
input[type="url"],
input[type="date"],
input[type="month"],
input[type="time"],
input[type="datetime"],
input[type="datetime-local"],
input[type="week"],
input[type="number"],
input[type="search"],
input[type="tel"],
input[type="color"],
select {
border: .1rem solid #ccc;
border-radius: 0;
display: inline-block;
padding: 0.8rem;
vertical-align: middle; }
vertical-align: middle;
}
input:not([type]) {
-webkit-appearance: none;
@ -270,59 +379,113 @@ input:not([type]) {
color: #444;
display: inline-block;
padding: 0.8rem;
text-align: left; }
text-align: left;
}
input[type="color"] {
padding: 0.8rem 1.6rem; }
padding: 0.8rem 1.6rem;
}
input[type="text"]:focus, input[type="password"]:focus, input[type="email"]:focus, input[type="url"]:focus, input[type="date"]:focus, input[type="month"]:focus, input[type="time"]:focus, input[type="datetime"]:focus, input[type="datetime-local"]:focus, input[type="week"]:focus, input[type="number"]:focus, input[type="search"]:focus, input[type="tel"]:focus, input[type="color"]:focus, select:focus, textarea:focus {
border-color: #b3d4fc; }
input[type="text"]:focus,
input[type="password"]:focus,
input[type="email"]:focus,
input[type="url"]:focus,
input[type="date"]:focus,
input[type="month"]:focus,
input[type="time"]:focus,
input[type="datetime"]:focus,
input[type="datetime-local"]:focus,
input[type="week"]:focus,
input[type="number"]:focus,
input[type="search"]:focus,
input[type="tel"]:focus,
input[type="color"]:focus,
select:focus,
textarea:focus {
border-color: #b3d4fc;
}
input:not([type]):focus {
border-color: #b3d4fc; }
border-color: #b3d4fc;
}
input[type="radio"], input[type="checkbox"] {
vertical-align: middle; }
input[type="radio"],
input[type="checkbox"] {
vertical-align: middle;
}
input[type="file"]:focus, input[type="radio"]:focus, input[type="checkbox"]:focus {
outline: .1rem solid thin #444; }
input[type="file"]:focus,
input[type="radio"]:focus,
input[type="checkbox"]:focus {
outline: .1rem solid thin #444;
}
input[type="text"][disabled], input[type="password"][disabled], input[type="email"][disabled], input[type="url"][disabled], input[type="date"][disabled], input[type="month"][disabled], input[type="time"][disabled], input[type="datetime"][disabled], input[type="datetime-local"][disabled], input[type="week"][disabled], input[type="number"][disabled], input[type="search"][disabled], input[type="tel"][disabled], input[type="color"][disabled], select[disabled], textarea[disabled] {
input[type="text"][disabled],
input[type="password"][disabled],
input[type="email"][disabled],
input[type="url"][disabled],
input[type="date"][disabled],
input[type="month"][disabled],
input[type="time"][disabled],
input[type="datetime"][disabled],
input[type="datetime-local"][disabled],
input[type="week"][disabled],
input[type="number"][disabled],
input[type="search"][disabled],
input[type="tel"][disabled],
input[type="color"][disabled],
select[disabled],
textarea[disabled] {
background-color: #efefef;
color: #777;
cursor: not-allowed; }
cursor: not-allowed;
}
input:not([type])[disabled] {
background-color: #efefef;
color: #777;
cursor: not-allowed; }
cursor: not-allowed;
}
input[readonly], select[readonly], textarea[readonly] {
input[readonly],
select[readonly],
textarea[readonly] {
background-color: #efefef;
border-color: #ccc;
color: #777; }
color: #777;
}
input:focus:invalid, textarea:focus:invalid, select:focus:invalid {
input:focus:invalid,
textarea:focus:invalid,
select:focus:invalid {
border-color: #e9322d;
color: #b94a48; }
color: #b94a48;
}
input[type="file"]:focus:invalid:focus, input[type="radio"]:focus:invalid:focus, input[type="checkbox"]:focus:invalid:focus {
outline-color: #ff4136; }
input[type="file"]:focus:invalid:focus,
input[type="radio"]:focus:invalid:focus,
input[type="checkbox"]:focus:invalid:focus {
outline-color: #ff4136;
}
select {
background-color: #fff;
border: .1rem solid #ccc; }
border: .1rem solid #ccc;
}
select[multiple] {
height: auto; }
height: auto;
}
label {
line-height: 2; }
line-height: 2;
}
fieldset {
border: 0;
margin: 0;
padding: 0.8rem 0; }
padding: 0.8rem 0;
}
legend {
border-bottom: .1rem solid #ccc;
@ -330,7 +493,8 @@ legend {
display: block;
margin-bottom: 0.8rem;
padding: 0.8rem 0;
width: 100%; }
width: 100%;
}
textarea {
border: .1rem solid #ccc;
@ -338,9 +502,11 @@ textarea {
display: block;
margin-bottom: 0.8rem;
padding: 0.8rem;
vertical-align: middle; }
vertical-align: middle;
}
input[type=submit], button {
input[type=submit],
button {
background-color: transparent;
border: .2rem solid #444;
border-radius: 0;
@ -354,83 +520,119 @@ input[type=submit], button {
text-decoration: none;
text-transform: uppercase;
-webkit-transition: .25s ease;
transition: .25s ease;
transition: .25s ease;
-webkit-user-drag: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
vertical-align: baseline; }
input[type=submit] a, button a {
color: #444; }
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
vertical-align: baseline;
}
input[type=submit]::-moz-focus-inner, button::-moz-focus-inner {
padding: 0; }
input[type=submit] a,
button a {
color: #444;
}
input[type=submit]:hover, button:hover {
input[type=submit]::-moz-focus-inner,
button::-moz-focus-inner {
padding: 0;
}
input[type=submit]:hover,
button:hover {
background: #444;
border-color: #444;
color: #fff; }
input[type=submit]:hover a, button:hover a {
color: #fff; }
color: #fff;
}
input[type=submit]:active, button:active {
input[type=submit]:hover a,
button:hover a {
color: #fff;
}
input[type=submit]:active,
button:active {
background: #6a6a6a;
border-color: #6a6a6a;
color: #fff; }
input[type=submit]:active a, button:active a {
color: #fff; }
color: #fff;
}
input[type=submit]:disabled, button:disabled {
input[type=submit]:active a,
button:active a {
color: #fff;
}
input[type=submit]:disabled,
button:disabled {
box-shadow: none;
cursor: not-allowed;
opacity: .40; }
opacity: .40;
}
nav ul {
list-style: none;
margin: 0;
padding: 0;
padding-top: 1.6rem;
text-align: center; }
nav ul li {
display: inline; }
text-align: center;
}
nav ul li {
display: inline;
}
nav a {
border-bottom: .2rem solid transparent;
color: #444;
padding: 0.8rem 1.6rem;
text-decoration: none;
-webkit-transition: .25s ease;
transition: .25s ease; }
nav a:hover, nav li.selected a {
border-color: rgba(0, 0, 0, 0.2); }
nav a:active {
border-color: rgba(0, 0, 0, 0.56); }
transition: .25s ease;
}
nav a:hover,
nav li.selected a {
border-color: rgba(0, 0, 0, 0.2);
}
nav a:active {
border-color: rgba(0, 0, 0, 0.56);
}
table {
margin-bottom: 1.6rem; }
margin-bottom: 1.6rem;
}
caption {
padding: 0.8rem 0; }
padding: 0.8rem 0;
}
thead th {
background: #efefef;
color: #444; }
color: #444;
}
tr {
background: #fff;
margin-bottom: 0.8rem; }
margin-bottom: 0.8rem;
}
th, td {
th,
td {
border: .1rem solid #ccc;
padding: 0.8rem 1.6rem;
text-align: center;
vertical-align: inherit; }
vertical-align: inherit;
}
tfoot tr {
background: none; }
background: none;
}
tfoot td {
color: #efefef;
font-size: 0.8rem;
font-style: italic;
padding: 1.6rem 0.4rem; }
padding: 1.6rem 0.4rem;
}

437
public/css/marx.myth.css Normal file
View File

@ -0,0 +1,437 @@
:root {
--default-font-list: 'Open Sans', 'Nimbus Sans L', 'Helvetica Neue', Helvetica, 'Lucida Grande', sans-serif;
box-sizing: border-box;
cursor: default;
font-family: var(--default-font-list);
line-height: 1.4;
overflow-y: scroll;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
text-size-adjust: 100%; }
audio:not([controls]) {
display: none; }
details {
display: block; }
/*input[type="number"] {
width: auto; }*/
input[type="search"] {
-webkit-appearance: textfield; }
input[type="search"]::-webkit-search-cancel-button, input[type="search"]::-webkit-search-decoration {
-webkit-appearance: none; }
main {
display: block; }
summary {
display: block; }
pre {
overflow: auto; }
progress {
display: inline-block; }
small {
font-size: 75%; }
big {
font-size: 125%; }
template {
display: none; }
textarea {
overflow: auto;
resize: vertical; }
[hidden] {
display: none; }
[unselectable] {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none; }
*, ::before, ::after {
border-style: solid;
border-width: 0;
box-sizing: inherit; }
* {
font-size: inherit;
line-height: inherit;
margin: 0;
padding: 0; }
::before, ::after {
text-decoration: inherit;
vertical-align: inherit; }
a {
text-decoration: none; }
audio, canvas, iframe, img, svg, video {
vertical-align: middle; }
button, input, select, textarea {
/*background-color: transparent;*/
border: .1rem solid #ccc;
color: inherit;
font-family: inherit;
font-style: inherit;
font-weight: inherit;
min-height: 1.4em; }
code, kbd, pre, samp {
font-family: Menlo, Monaco, Consolas, 'Courier New', monospace, monospace; }
table {
border-collapse: collapse;
border-spacing: 0; }
::-moz-selection {
background-color: #b3d4fc;
text-shadow: none; }
::selection {
background-color: #b3d4fc;
text-shadow: none; }
button::-moz-focus-inner {
border: 0; }
@media screen {
[hidden~="screen"] {
display: inherit; }
[hidden~="screen"]:not(:active):not(:focus):not(:target) {
clip: rect(0 0 0 0) !important;
position: absolute !important; } }
body {
color: #444;
font-family: var(--default-font-list);
font-size: 1.6rem;
font-style: normal;
font-weight: 400; }
p {
margin: 0 0 1.6rem; }
h1, h2, h3, h4, h5, h6 {
font-family: 'Lato', var(--default-font-list);
margin: 2rem 0 1.6rem; }
h1 {
border-bottom: .1rem solid rgba(0, 0, 0, 0.2);
font-size: 3.6rem;
font-style: normal;
font-weight: 500; }
h2 {
font-size: 3rem;
font-style: normal;
font-weight: 500; }
h3 {
font-size: 2.4rem;
font-style: normal;
font-weight: 500;
margin: 1.6rem 0 0.4rem; }
h4 {
font-size: 1.8rem;
font-style: normal;
font-weight: 600;
margin: 1.6rem 0 0.4rem; }
h5 {
font-size: 1.6rem;
font-style: normal;
font-weight: 600;
margin: 1.6rem 0 0.4rem; }
h6 {
color: #777;
font-size: 1.4rem;
font-style: normal;
font-weight: 600;
margin: 1.6rem 0 0.4rem; }
small {
color: #777; }
pre {
background: #efefef;
color: #444;
display: block;
font-family: Menlo, Monaco, Consolas, 'Courier New', monospace;
font-size: 1.4rem;
margin: 1.6rem 0;
padding: 1.6rem;
word-break: break-all;
word-wrap: break-word; }
code {
background: #efefef;
color: #444;
font-family: Menlo, Monaco, Consolas, 'Courier New', monospace;
font-size: 1.4rem;
word-break: break-all;
word-wrap: break-word; }
a {
color: #1271db;
-webkit-transition: .25s ease;
transition: .25s ease; }
a:hover, a:focus {
text-decoration: none; }
dl {
margin-bottom: 1.6rem; }
dd {
margin-left: 4rem; }
ul, ol {
margin-bottom: 0.8rem;
padding-left: 2rem; }
blockquote {
border-left: .2rem solid #1271db;
font-family: Georgia, Times, 'Times New Roman', serif;
font-style: italic;
margin: 1.6rem 0;
padding-left: 1.6rem; }
figcaption {
font-family: Georgia, Times, 'Times New Roman', serif; }
html {
font-size: 62.5%; }
body {
padding: 0; }
main, header, footer, article, section, aside, details, summary {
display: block;
height: auto;
margin: 0 auto;
width: 100%; }
main {
display: block;
margin: 0 auto;
padding: 0 1.6rem 1.6rem; }
footer {
border-top: .1rem solid rgba(0, 0, 0, 0.2);
clear: both;
display: inline-block;
float: left;
max-width: 100%;
padding: 1rem 0;
text-align: center; }
hr {
border-top: .1rem solid rgba(0, 0, 0, 0.2);
display: block;
margin-bottom: 1.6rem;
width: 100%; }
img {
height: auto;
max-width: 100%;
vertical-align: baseline; }
@media screen and (max-width: 40rem) {
article, section, aside {
clear: both;
display: block;
max-width: 100%; }
img {
margin-right: 1.6rem; } }
input[type="text"], input[type="password"], input[type="email"], input[type="url"], input[type="date"], input[type="month"], input[type="time"], input[type="datetime"], input[type="datetime-local"], input[type="week"], input[type="number"], input[type="search"], input[type="tel"], input[type="color"], select {
border: .1rem solid #ccc;
border-radius: 0;
display: inline-block;
padding: 0.8rem;
vertical-align: middle; }
input:not([type]) {
-webkit-appearance: none;
background-clip: padding-box;
background-color: #fff;
border: .1rem solid #ccc;
border-radius: 0;
color: #444;
display: inline-block;
padding: 0.8rem;
text-align: left; }
input[type="color"] {
padding: 0.8rem 1.6rem; }
input[type="text"]:focus, input[type="password"]:focus, input[type="email"]:focus, input[type="url"]:focus, input[type="date"]:focus, input[type="month"]:focus, input[type="time"]:focus, input[type="datetime"]:focus, input[type="datetime-local"]:focus, input[type="week"]:focus, input[type="number"]:focus, input[type="search"]:focus, input[type="tel"]:focus, input[type="color"]:focus, select:focus, textarea:focus {
border-color: #b3d4fc; }
input:not([type]):focus {
border-color: #b3d4fc; }
input[type="radio"], input[type="checkbox"] {
vertical-align: middle; }
input[type="file"]:focus, input[type="radio"]:focus, input[type="checkbox"]:focus {
outline: .1rem solid thin #444; }
input[type="text"][disabled], input[type="password"][disabled], input[type="email"][disabled], input[type="url"][disabled], input[type="date"][disabled], input[type="month"][disabled], input[type="time"][disabled], input[type="datetime"][disabled], input[type="datetime-local"][disabled], input[type="week"][disabled], input[type="number"][disabled], input[type="search"][disabled], input[type="tel"][disabled], input[type="color"][disabled], select[disabled], textarea[disabled] {
background-color: #efefef;
color: #777;
cursor: not-allowed; }
input:not([type])[disabled] {
background-color: #efefef;
color: #777;
cursor: not-allowed; }
input[readonly], select[readonly], textarea[readonly] {
background-color: #efefef;
border-color: #ccc;
color: #777; }
input:focus:invalid, textarea:focus:invalid, select:focus:invalid {
border-color: #e9322d;
color: #b94a48; }
input[type="file"]:focus:invalid:focus, input[type="radio"]:focus:invalid:focus, input[type="checkbox"]:focus:invalid:focus {
outline-color: #ff4136; }
select {
background-color: #fff;
border: .1rem solid #ccc; }
select[multiple] {
height: auto; }
label {
line-height: 2; }
fieldset {
border: 0;
margin: 0;
padding: 0.8rem 0; }
legend {
border-bottom: .1rem solid #ccc;
color: #444;
display: block;
margin-bottom: 0.8rem;
padding: 0.8rem 0;
width: 100%; }
textarea {
border: .1rem solid #ccc;
border-radius: 0;
display: block;
margin-bottom: 0.8rem;
padding: 0.8rem;
vertical-align: middle; }
input[type=submit], button {
background-color: transparent;
border: .2rem solid #444;
border-radius: 0;
color: #444;
cursor: pointer;
display: inline-block;
margin-bottom: 0.8rem;
margin-right: 0.4rem;
padding: 0.8rem 1.6rem;
text-align: center;
text-decoration: none;
text-transform: uppercase;
-webkit-transition: .25s ease;
transition: .25s ease;
-webkit-user-drag: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
vertical-align: baseline; }
input[type=submit] a, button a {
color: #444; }
input[type=submit]::-moz-focus-inner, button::-moz-focus-inner {
padding: 0; }
input[type=submit]:hover, button:hover {
background: #444;
border-color: #444;
color: #fff; }
input[type=submit]:hover a, button:hover a {
color: #fff; }
input[type=submit]:active, button:active {
background: #6a6a6a;
border-color: #6a6a6a;
color: #fff; }
input[type=submit]:active a, button:active a {
color: #fff; }
input[type=submit]:disabled, button:disabled {
box-shadow: none;
cursor: not-allowed;
opacity: .40; }
nav ul {
list-style: none;
margin: 0;
padding: 0;
padding-top: 1.6rem;
text-align: center; }
nav ul li {
display: inline; }
nav a {
border-bottom: .2rem solid transparent;
color: #444;
padding: 0.8rem 1.6rem;
text-decoration: none;
-webkit-transition: .25s ease;
transition: .25s ease; }
nav a:hover, nav li.selected a {
border-color: rgba(0, 0, 0, 0.2); }
nav a:active {
border-color: rgba(0, 0, 0, 0.56); }
table {
margin-bottom: 1.6rem; }
caption {
padding: 0.8rem 0; }
thead th {
background: #efefef;
color: #444; }
tr {
background: #fff;
margin-bottom: 0.8rem; }
th, td {
border: .1rem solid #ccc;
padding: 0.8rem 1.6rem;
text-align: center;
vertical-align: inherit; }
tfoot tr {
background: none; }
tfoot td {
color: #efefef;
font-size: 0.8rem;
font-style: italic;
padding: 1.6rem 0.4rem; }

View File

@ -1,23 +1,29 @@
<?php
/**
* Easy Min
* Hummingbird Anime Client
*
* Simple minification for better website performance
* An API client for Hummingbird to manage anime and manga watch lists
*
* @author Timothy J. Warren
* @copyright Copyright (c) 2012
* @link https://github.com/aviat4ion/Easy-Min
* @license http://philsturgeon.co.uk/code/dbad-license
* @package HummingbirdAnimeClient
* @author Timothy J. Warren
* @copyright Copyright (c) 2015 - 2016
* @link https://github.com/timw4mail/HummingBirdAnimeClient
* @license MIT
*/
// --------------------------------------------------------------------------
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\Request;
//Get config files
require('../app/config/minify_config.php');
require_once('../app/config/minify_config.php');
//Include the js groups
$groups_file = "../app/config/minify_js_groups.php";
$groups = require($groups_file);
$groups = require_once($groups_file);
// Include guzzle
require_once('../vendor/autoload.php');
//The name of this file
$this_file = __FILE__;
@ -40,7 +46,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 +63,49 @@ function get_files()
*/
function google_min($new_file)
{
//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_close($ch);
return $output;
$options = [
'output_info' => 'errors',
'output_format' => 'json',
'compilation_level' => 'SIMPLE_OPTIMIZATIONS',
'js_code' => $new_file,
'language' => 'ECMASCRIPT5',
'language_out' => 'ECMASCRIPT5_STRICT'
];
// First check for errors
$error_client = new Client();
$error_res = $error_client->post('http://closure-compiler.appspot.com/compile', [
'headers' => [
'Accept-Encoding' => 'gzip',
"Content-type" => "application/x-www-form-urlencoded"
],
'form_params' => $options
]);
$error_json = $error_res->getBody();
$error_obj = json_decode($error_json);
if ( ! empty($error_obj->errors))
{
?><pre><?= json_encode($err_obj, JSON_PRETTY_PRINT) ?></pre><?php
die();
}
// Now actually retrieve the compiled code
$options['output_info'] = 'compiled_code';
$client = new Client();
$res = $client->post('http://closure-compiler.appspot.com/compile', [
'headers' => [
'Accept-Encoding' => 'gzip',
"Content-type" => "application/x-www-form-urlencoded"
],
'form_params' => $options
]);
$json = $res->getBody();
$obj = json_decode($json);
return $obj->compiledCode;
}
// --------------------------------------------------------------------------
@ -146,7 +187,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 +202,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
{

17
public/js/anime_collection.js Executable file
View File

@ -0,0 +1,17 @@
(function($, undefined) {
function search(query, callback)
{
$.get(BASE_URL + 'collection/search', {'query':query}, callback);
}
$("#search").on('keypress', $.throttle(750, 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));

24
public/js/anime_edit.js Normal file → Executable file
View File

@ -1,17 +1,19 @@
/**
* Javascript for editing anime, if logged in
*/
(function($, undefined){
(function($){
"use strict";
if (CONTROLLER !== "anime") return;
// Action to increment episode count
$(".media button.plus_one").on("click", function(e) {
$(".plus_one").on("click", function(e) {
e.stopPropagation();
var this_sel = $(this);
var parent_sel = $(this).closest("article");
var self = this;
var this_sel = $(this);
var parent_sel = $(this).closest("article, td");
var watched_count = parseInt(parent_sel.find('.completed_number').text(), 10);
var total_count = parseInt(parent_sel.find('.total_number').text(), 10);
@ -19,7 +21,7 @@
// Setup the update data
var data = {
id: this_sel.parent('article').attr('id').replace('a-', ''),
id: this_sel.parent('article, td').attr('id'),
increment_episodes: true
};
@ -38,14 +40,22 @@
}
// okay, lets actually make some changes!
$.post(BASE_URL + 'update', data, function(res) {
$.ajax({
data: data,
dataType: 'json',
method: 'POST',
mimeType: 'application/json',
url: BASE_URL + CONTROLLER + '/update'
}).done(function(res) {
if (res.status === 'completed')
{
this_sel.parent('article').hide();
$(self).closest('article, tr').hide();
}
add_message('success', "Sucessfully updated " + title);
parent_sel.find('.completed_number').text(++watched_count);
}).fail(function() {
add_message('error', "Failed to updated " + title);
});
});

View File

@ -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 <jQuery.throttle> 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
// <jQuery.debounce> 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 <jQuery.throttle>
// 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);

1958
public/js/lib/jsrender.js Normal file

File diff suppressed because it is too large Load Diff

14
public/js/manga_edit.js Normal file → Executable file
View File

@ -1,7 +1,9 @@
/**
* Javascript for editing manga, if logged in
*/
(function ($, undefined) {
(function ($) {
"use strict";
if (CONTROLLER !== "manga") return;
@ -28,10 +30,18 @@
// Update the total count
data[type + "s_read"] = ++completed;
$.post(BASE_URL + 'update', data, function(res) {
$.ajax({
data: data,
dataType: 'json',
method: 'POST',
mimeType: 'application/json',
url: BASE_URL + CONTROLLER + '/update'
}).done(function(res) {
console.table(res);
parent_sel.find("."+type+"s_read").text(completed);
add_message('success', "Sucessfully updated " + res.manga[0].romaji_title);
}).fail(function() {
add_message('error', "Failed to updated " + res.manga[0].romaji_title);
});
});

6
sonar-project.properties Normal file
View File

@ -0,0 +1,6 @@
sonar.projectKey=animeclient
sonar.projectName=Anime Client
sonar.projectVersion=2.1.0
sonar.sources=src
sonar.php.coverage.reportPath=build/logs/clover.xml
sonar.php.tests.reportPath=build/logs/junit.xml

View File

@ -0,0 +1,94 @@
<?php
/**
* Hummingbird Anime Client
*
* An API client for Hummingbird to manage anime and manga watch lists
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren
* @copyright Copyright (c) 2015 - 2016
* @link https://github.com/timw4mail/HummingBirdAnimeClient
* @license MIT
*/
namespace Aviat\AnimeClient;
define('SRC_DIR', realpath(__DIR__ . '/../../'));
/**
* Odds and Ends class
*/
class AnimeClient {
use \Aviat\Ion\Di\ContainerAware;
const SESSION_SEGMENT = 'Aviat\AnimeClient\Auth';
const DEFAULT_CONTROLLER_NAMESPACE = 'Aviat\AnimeClient\Controller';
const DEFAULT_CONTROLLER = 'Aviat\AnimeClient\Controller\Anime';
const DEFAULT_CONTROLLER_METHOD = 'index';
const NOT_FOUND_METHOD = 'not_found';
const ERROR_MESSAGE_METHOD = 'error_page';
const SRC_DIR = SRC_DIR;
private static $form_pages = [
'edit',
'add',
'update',
'update_form',
'login',
'logout'
];
/**
* HTML selection helper function
*
* @param string $a - First item to compare
* @param string $b - Second item to compare
* @return string
*/
public static function is_selected($a, $b)
{
return ($a === $b) ? 'selected' : '';
}
/**
* Inverse of selected helper function
*
* @param string $a - First item to compare
* @param string $b - Second item to compare
* @return string
*/
public static function is_not_selected($a, $b)
{
return ($a !== $b) ? 'selected' : '';
}
/**
* Determine whether to show the sub-menu
*
* @return bool
*/
public function is_view_page()
{
$url = $this->container->get('request')
->url->get();
$page_segments = explode("/", $url);
$intersect = array_intersect($page_segments, self::$form_pages);
return empty($intersect);
}
/**
* Determine whether the page is a page with a form, and
* not suitable for redirection
*
* @return boolean
*/
public function is_form_page()
{
return ! $this->is_view_page();
}
}
// End of anime_client.php

View File

@ -0,0 +1,107 @@
<?php
/**
* Hummingbird Anime Client
*
* An API client for Hummingbird to manage anime and manga watch lists
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren
* @copyright Copyright (c) 2015 - 2016
* @link https://github.com/timw4mail/HummingBirdAnimeClient
* @license MIT
*/
namespace Aviat\AnimeClient\Auth;
use Aviat\Ion\Di\ContainerInterface;
use Aviat\AnimeClient\AnimeClient;
use Aviat\AnimeClient\Model\API;
/**
* Hummingbird API Authentication
*/
class HummingbirdAuth {
use \Aviat\Ion\Di\ContainerAware;
/**
* Anime API Model
*
* @var \Aviat\AnimeClient\Model\API
*/
protected $model;
/**
* Session object
*
* @var Aura\Session\Segment
*/
protected $segment;
/**
* Constructor
*
* @param ContainerInterface $container
*/
public function __construct(ContainerInterface $container)
{
$this->setContainer($container);
$this->segment = $container->get('session')
->getSegment(AnimeClient::SESSION_SEGMENT);
$this->model = $container->get('api-model');
}
/**
* Make the appropriate authentication call,
* and save the resulting auth token if successful
*
* @param string $password
* @return boolean
*/
public function authenticate($password)
{
$username = $this->container->get('config')
->get('hummingbird_username');
$auth_token = $this->model->authenticate($username, $password);
if (FALSE !== $auth_token)
{
$this->segment->set('auth_token', $auth_token);
return TRUE;
}
return FALSE;
}
/**
* Check whether the current user is authenticated
*
* @return boolean
*/
public function is_authenticated()
{
return ($this->get_auth_token() !== FALSE);
}
/**
* Clear authentication values
*
* @return void
*/
public function logout()
{
$this->segment->clear();
}
/**
* Retrieve the authentication token from the session
*
* @return string|false
*/
public function get_auth_token()
{
return $this->segment->get('auth_token', FALSE);
}
}
// End of HummingbirdAuth.php

View File

@ -0,0 +1,103 @@
<?php
/**
* Hummingbird Anime Client
*
* An API client for Hummingbird to manage anime and manga watch lists
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren
* @copyright Copyright (c) 2015 - 2016
* @link https://github.com/timw4mail/HummingBirdAnimeClient
* @license MIT
*/
namespace Aviat\AnimeClient;
use InvalidArgumentException;
/**
* Wrapper for configuration values
*/
class Config {
use \Aviat\Ion\ArrayWrapper;
/**
* Config object
*
* @var \Aviat\Ion\Type\ArrayType
*/
protected $map = [];
/**
* Constructor
*
* @param array $config_array
*/
public function __construct(array $config_array = [])
{
$this->map = $this->arr($config_array);
}
/**
* Get a config value
*
* @param array|string $key
* @return mixed
*/
public function get($key)
{
if (is_array($key))
{
return $this->map->get_deep_key($key);
}
return $this->map->get($key);
}
/**
* Remove a config value
*
* @param string|array $key
* @return void
*/
public function delete($key)
{
if (is_array($key))
{
$this->map->set_deep_key($key, NULL);
}
else
{
$pos =& $this->map->get($key);
$pos = NULL;
}
}
/**
* Set a config value
*
* @param integer|string|array $key
* @param mixed $value
* @throws InvalidArgumentException
* @return Config
*/
public function set($key, $value)
{
if (is_array($key))
{
$this->map->set_deep_key($key, $value);
}
else if (is_scalar($key) && ! empty($key))
{
$this->map->set($key, $value);
}
else
{
throw new InvalidArgumentException("Key must be integer, string, or array, and cannot be empty");
}
return $this;
}
}
// End of config.php

View File

@ -0,0 +1,406 @@
<?php
/**
* Hummingbird Anime Client
*
* An API client for Hummingbird to manage anime and manga watch lists
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren
* @copyright Copyright (c) 2015 - 2016
* @link https://github.com/timw4mail/HummingBirdAnimeClient
* @license MIT
*/
namespace Aviat\AnimeClient;
use Aviat\Ion\Di\ContainerInterface;
use Aviat\Ion\View\HttpView;
use Aviat\Ion\View\HtmlView;
use Aviat\Ion\View\JsonView;
use Aviat\AnimeClient\AnimeClient;
/**
* Controller base, defines output methods
*
* @property Response object $response
* @property Config object $config
*/
class Controller {
use \Aviat\Ion\Di\ContainerAware;
/**
* The global configuration object
* @var object $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 generatation class
* @var UrlGenerator
*/
protected $urlGenerator;
/**
* Session segment
* @var [type]
*/
protected $session;
/**
* Common data to be sent to views
* @var array
*/
protected $base_data = [
'url_type' => 'anime',
'other_type' => 'manga',
'menu_name' => ''
];
/**
* Constructor
*
* @param ContainerInterface $container
*/
public function __construct(ContainerInterface $container)
{
$this->setContainer($container);
$urlGenerator = $container->get('url-generator');
$this->config = $container->get('config');
$this->request = $container->get('request');
$this->response = $container->get('response');
$this->base_data['urlGenerator'] = $urlGenerator;
$this->base_data['auth'] = $container->get('auth');
$this->base_data['config'] = $this->config;
$this->urlGenerator = $urlGenerator;
$session = $container->get('session');
$this->session = $session->getSegment(AnimeClient::SESSION_SEGMENT);
// Set a 'previous' flash value for better redirects
$this->session->setFlash('previous', $this->request->server->get('HTTP_REFERER'));
// Set a message box if available
$this->base_data['message'] = $this->session->getFlash('message');
}
/**
* Redirect to the default controller/url from an empty path
*/
public function redirect_to_default()
{
$default_type = $this->config->get(['routes', 'route_config', 'default_list']);
$this->redirect($this->urlGenerator->default_url($default_type), 303);
}
/**
* Redirect to the previous page
*
* @return void
*/
public function redirect_to_previous()
{
$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 set_session_redirect($url = NULL)
{
$anime_client = $this->container->get('anime-client');
$double_form_page = $this->request->server->get('HTTP_REFERER') == $this->request->url->get();
// 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 ($double_form_page)
{
return;
}
if (is_null($url))
{
$url = ($anime_client->is_view_page())
? $this->request->url->get()
: $this->request->server->get('HTTP_REFERER');
}
$this->session->set('redirect_url', $url);
}
/**
* Redirect to the url previously set in the session
*
* @return void
*/
public function session_redirect()
{
$target = $this->session->get('redirect_url');
if (empty($target))
{
$this->not_found();
}
else
{
$this->redirect($target, 303);
$this->session->set('redirect_url', NULL);
}
}
/**
* Get a class member
*
* @param string $key
* @return object
*/
public function __get($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
* @return string
*/
protected function load_partial($view, $template, array $data = [])
{
$router = $this->container->get('dispatcher');
if (isset($this->base_data))
{
$data = array_merge($this->base_data, $data);
}
$route = $router->get_route();
$data['route_path'] = ($route) ? $router->get_route()->path : "";
$template_path = _dir($this->config->get('view_path'), "{$template}.php");
if ( ! is_file($template_path))
{
throw new \InvalidArgumentException("Invalid template : {$template}");
}
return $view->render_template($template_path, (array)$data);
}
/**
* Render a template with header and footer
*
* @param HtmlView $view
* @param string $template
* @param array $data
* @return void
*/
protected function render_full_page($view, $template, array $data)
{
$view->appendOutput($this->load_partial($view, 'header', $data));
if (array_key_exists('message', $data) && is_array($data['message']))
{
$view->appendOutput($this->load_partial($view, 'message', $data['message']));
}
$view->appendOutput($this->load_partial($view, $template, $data));
$view->appendOutput($this->load_partial($view, 'footer', $data));
}
/**
* Show the login form
*
* @codeCoverageIgnore
* @param string $status
* @return void
*/
public function login($status = "")
{
$message = "";
$view = new HtmlView($this->container);
if ($status != "")
{
$message = $this->show_message($view, 'error', $status);
}
// Set the redirect url
$this->set_session_redirect();
$this->outputHTML('login', [
'title' => 'Api login',
'message' => $message
], $view);
}
/**
* Attempt login authentication
*
* @return void
*/
public function login_action()
{
$auth = $this->container->get('auth');
if ($auth->authenticate($this->request->post->get('password')))
{
return $this->session_redirect();
}
$this->login("Invalid username or password.");
}
/**
* Deauthorize the current user
*
* @return void
*/
public function logout()
{
$auth = $this->container->get('auth');
$auth->logout();
$this->redirect_to_default();
}
/**
* 404 action
*
* @return void
*/
public function not_found()
{
$this->outputHTML('404', [
'title' => 'Sorry, page not found'
], NULL, 404);
}
/**
* Display a generic error page
*
* @param int $http_code
* @param string $title
* @param string $message
* @param string $long_message
* @return void
*/
public function error_page($http_code, $title, $message, $long_message = "")
{
$this->outputHTML('error', [
'title' => $title,
'message' => $message,
'long_message' => $long_message
], NULL, $http_code);
}
/**
* Set a session flash variable to display a message on
* next page load
*
* @param string $message
* @param string $type
* @return void
*/
public function set_flash_message($message, $type = "info")
{
$this->session->setFlash('message', [
'message_type' => $type,
'message' => $message
]);
}
/**
* Add a message box to the page
*
* @codeCoverageIgnore
* @param HtmlView $view
* @param string $type
* @param string $message
* @return string
*/
protected function show_message($view, $type, $message)
{
return $this->load_partial($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->render_full_page($view, $template, $data);
}
/**
* Output a JSON Response
*
* @param mixed $data
* @return void
*/
protected function outputJSON($data = [])
{
$view = new JsonView($this->container);
$view->setOutput($data);
}
/**
* 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);
}
}
// End of BaseController.php

View File

@ -0,0 +1,259 @@
<?php
/**
* Hummingbird Anime Client
*
* An API client for Hummingbird to manage anime and manga watch lists
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren
* @copyright Copyright (c) 2015 - 2016
* @link https://github.com/timw4mail/HummingBirdAnimeClient
* @license MIT
*/
namespace Aviat\AnimeClient\Controller;
use Aviat\Ion\Di\ContainerInterface;
use Aviat\AnimeClient\Controller as BaseController;
use Aviat\AnimeClient\Hummingbird\Enum\AnimeWatchingStatus;
use Aviat\AnimeClient\Model\Anime as AnimeModel;
use Aviat\AnimeClient\Hummingbird\Transformer\AnimeListTransformer;
/**
* Controller for Anime-related pages
*/
class Anime extends BaseController {
use \Aviat\Ion\StringWrapper;
/**
* The anime list model
* @var object $model
*/
protected $model;
/**
* Data to ve sent to all routes in this controller
* @var array $base_data
*/
protected $base_data;
/**
* Constructor
*
* @param ContainerInterface $container
*/
public function __construct(ContainerInterface $container)
{
parent::__construct($container);
$this->model = $container->get('anime-model');
$this->base_data = array_merge($this->base_data, [
'menu_name' => 'anime_list',
'url_type' => 'anime',
'other_type' => 'manga',
'config' => $this->config,
]);
}
/**
* Show a portion, or all of the anime list
*
* @param string $type - The section of the list
* @param string $view - List or cover view
* @return void
*/
public function index($type = "watching", $view = '')
{
$type_title_map = [
'all' => 'All',
'watching' => 'Currently Watching',
'plan_to_watch' => 'Plan to Watch',
'on_hold' => 'On Hold',
'dropped' => 'Dropped',
'completed' => 'Completed'
];
$model_map = [
'watching' => AnimeWatchingStatus::WATCHING,
'plan_to_watch' => AnimeWatchingStatus::PLAN_TO_WATCH,
'on_hold' => AnimeWatchingStatus::ON_HOLD,
'all' => 'all',
'dropped' => AnimeWatchingStatus::DROPPED,
'completed' => AnimeWatchingStatus::COMPLETED
];
if (array_key_exists($type, $type_title_map))
{
$title = $this->config->get('whose_list') .
"'s Anime List &middot; {$type_title_map[$type]}";
}
else
{
$title = '';
}
$view_map = [
'' => 'cover',
'list' => 'list'
];
$data = ($type != 'all')
? $this->model->get_list($model_map[$type])
: $this->model->get_all_lists();
$this->outputHTML('anime/' . $view_map[$view], [
'title' => $title,
'sections' => $data
]);
}
/**
* Form to add an anime
*
* @return void
*/
public function add_form()
{
$raw_status_list = AnimeWatchingStatus::getConstList();
$statuses = [];
foreach ($raw_status_list as $status_item)
{
$statuses[$status_item] = (string)$this->string($status_item)
->underscored()
->humanize()
->titleize();
}
$this->set_session_redirect();
$this->outputHTML('anime/add', [
'title' => $this->config->get('whose_list') .
"'s Anime List &middot; Add",
'action_url' => $this->urlGenerator->url('anime/add'),
'status_list' => $statuses
]);
}
/**
* Add an anime to the list
*
* @return void
*/
public function add()
{
$data = $this->request->post->get();
if ( ! array_key_exists('id', $data))
{
$this->redirect("anime/add", 303);
}
$result = $this->model->update($data);
if ($result['statusCode'] == 201)
{
$this->set_flash_message('Added new anime to list', 'success');
}
else
{
$this->set_flash_message('Failed to add new anime to list', 'error');
}
$this->session_redirect();
}
/**
* Form to edit details about a series
*
* @param int $id
* @param string $status
* @return void
*/
public function edit($id, $status = "all")
{
$item = $this->model->get_library_item($id, $status);
$raw_status_list = AnimeWatchingStatus::getConstList();
$statuses = [];
foreach ($raw_status_list as $status_item)
{
$statuses[$status_item] = (string)$this->string($status_item)
->underscored()
->humanize()
->titleize();
}
$this->set_session_redirect($this->request->server->get('HTTP_REFERRER'));
$this->outputHTML('anime/edit', [
'title' => $this->config->get('whose_list') .
"'s Anime List &middot; Edit",
'item' => $item,
'statuses' => $statuses,
'action' => $this->container->get('url-generator')
->url('/anime/update_form'),
]);
}
/**
* Search for anime
*
* @return void
*/
public function search()
{
$query = $this->request->query->get('query');
$this->outputJSON($this->model->search($query));
}
/**
* Update an anime item via a form submission
*
* @return void
*/
public function form_update()
{
$post_data = $this->request->post->get();
// Do some minor data manipulation for
// large form-based updates
$transformer = new AnimeListTransformer();
$post_data = $transformer->untransform($post_data);
$full_result = $this->model->update($post_data);
$result = $full_result['body'];
if (array_key_exists('anime', $result))
{
$title = ( ! empty($result['anime']['alternate_title']))
? "{$result['anime']['title']} ({$result['anime']['alternate_title']})"
: "{$result['anime']['title']}";
$this->set_flash_message("Successfully updated {$title}.", 'success');
}
else
{
$this->set_flash_message('Failed to update anime.', 'error');
}
$this->session_redirect();
}
/**
* Update an anime item
*
* @return boolean|null
*/
public function update()
{
$this->outputJSON(
$this->model->update(
$this->request->post->get()
)
);
}
}
// End of AnimeController.php

View File

@ -0,0 +1,186 @@
<?php
/**
* Hummingbird Anime Client
*
* An API client for Hummingbird to manage anime and manga watch lists
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren
* @copyright Copyright (c) 2015 - 2016
* @link https://github.com/timw4mail/HummingBirdAnimeClient
* @license MIT
*/
namespace Aviat\AnimeClient\Controller;
use Aviat\Ion\Di\ContainerInterface;
use Aviat\AnimeClient\Controller as BaseController;
use Aviat\AnimeClient\Config;
use Aviat\AnimeClient\UrlGenerator;
use Aviat\AnimeClient\Model\Anime as AnimeModel;
use Aviat\AnimeClient\Model\AnimeCollection as AnimeCollectionModel;
/**
* Controller for Anime collection pages
*/
class Collection extends BaseController {
/**
* The anime collection model
* @var AnimeCollectionModel $anime_collection_model
*/
private $anime_collection_model;
/**
* The anime API model
* @var AnimeModel $anime_model
*/
private $anime_model;
/**
* Data to ve sent to all routes in this controller
* @var array $base_data
*/
protected $base_data;
/**
* Url Generator class
* @var UrlGenerator
*/
protected $urlGenerator;
/**
* Constructor
*
* @param ContainerInterface $container
*/
public function __construct(ContainerInterface $container)
{
parent::__construct($container);
$this->urlGenerator = $container->get('url-generator');
$this->anime_model = $container->get('anime-model');
$this->anime_collection_model = $container->get('anime-collection-model');
$this->base_data = array_merge($this->base_data, [
'menu_name' => 'collection',
'url_type' => 'anime',
'other_type' => 'manga',
'config' => $this->config,
]);
}
/**
* Search for anime
*
* @return void
*/
public function search()
{
$query = $this->request->query->get('query');
$this->outputJSON($this->anime_model->search($query));
}
/**
* Show the anime collection page
*
* @param string $view
* @return void
*/
public function index($view)
{
$view_map = [
'' => 'cover',
'list' => 'list'
];
$data = $this->anime_collection_model->get_collection();
$this->outputHTML('collection/' . $view_map[$view], [
'title' => $this->config->get('whose_list') . "'s Anime Collection",
'sections' => $data,
'genres' => $this->anime_collection_model->get_genre_list()
]);
}
/**
* Show the anime collection add/edit form
*
* @param integer|null $id
* @return void
*/
public function form($id = NULL)
{
$this->set_session_redirect();
$action = (is_null($id)) ? "Add" : "Edit";
$this->outputHTML('collection/' . strtolower($action), [
'action' => $action,
'action_url' => $this->urlGenerator->full_url('collection/' . strtolower($action)),
'title' => $this->config->get('whose_list') . " Anime Collection &middot; {$action}",
'media_items' => $this->anime_collection_model->get_media_type_list(),
'item' => ($action === "Edit") ? $this->anime_collection_model->get($id) : []
]);
}
/**
* Update a collection item
*
* @return void
*/
public function edit()
{
$data = $this->request->post->get();
if (array_key_exists('hummingbird_id', $data))
{
$this->anime_collection_model->update($data);
$this->set_flash_message('Successfully updated collection item.', 'success');
}
else
{
$this->set_flash_message('Failed to update collection item', 'error');
}
$this->session_redirect();
}
/**
* Add a collection item
*
* @return void
*/
public function add()
{
$data = $this->request->post->get();
if (array_key_exists('id', $data))
{
$this->anime_collection_model->add($data);
$this->set_flash_message('Successfully added collection item', 'success');
}
else
{
$this->set_flash_message('Failed to add collection item.', 'error');
}
$this->session_redirect();
}
/**
* Remove a collection item
*
* @return void
*/
public function delete()
{
$data = $this->request->post->get();
if ( ! array_key_exists('id', $data))
{
$this->redirect("collection/view", 303);
}
$this->anime_collection_model->delete($data);
$this->redirect("collection/view", 303);
}
}
// End of CollectionController.php

View File

@ -0,0 +1,160 @@
<?php
/**
* Hummingbird Anime Client
*
* An API client for Hummingbird to manage anime and manga watch lists
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren
* @copyright Copyright (c) 2015 - 2016
* @link https://github.com/timw4mail/HummingBirdAnimeClient
* @license MIT
*/
namespace Aviat\AnimeClient\Controller;
use Aviat\Ion\Di\ContainerInterface;
use Aviat\AnimeClient\Controller;
use Aviat\AnimeClient\Config;
use Aviat\AnimeClient\Model\Manga as MangaModel;
use Aviat\AnimeClient\Hummingbird\Enum\MangaReadingStatus;
use Aviat\AnimeClient\Hummingbird\Transformer\MangaListTransformer;
/**
* Controller for manga list
*/
class Manga extends Controller {
use \Aviat\Ion\StringWrapper;
/**
* The manga model
* @var object $model
*/
protected $model;
/**
* Data to ve sent to all routes in this controller
* @var array $base_data
*/
protected $base_data;
/**
* Constructor
*
* @param ContainerInterface $container
*/
public function __construct(ContainerInterface $container)
{
parent::__construct($container);
$this->model = $container->get('manga-model');
$this->base_data = array_merge($this->base_data, [
'menu_name' => 'manga_list',
'config' => $this->config,
'url_type' => 'manga',
'other_type' => 'anime'
]);
}
/**
* Get a section of the manga list
*
* @param string $status
* @param string $view
* @return void
*/
public function index($status = "all", $view = "")
{
$map = [
'all' => 'All',
'plan_to_read' => MangaModel::PLAN_TO_READ,
'reading' => MangaModel::READING,
'completed' => MangaModel::COMPLETED,
'dropped' => MangaModel::DROPPED,
'on_hold' => MangaModel::ON_HOLD
];
$title = $this->config->get('whose_list') . "'s Manga List &middot; {$map[$status]}";
$view_map = [
'' => 'cover',
'list' => 'list'
];
$data = ($status !== 'all')
? [$map[$status] => $this->model->get_list($map[$status])]
: $this->model->get_all_lists();
$this->outputHTML('manga/' . $view_map[$view], [
'title' => $title,
'sections' => $data,
]);
}
/**
* Show the manga edit form
*
* @param string $id
* @param string $status
* @return void
*/
public function edit($id, $status = "All")
{
$this->set_session_redirect();
$item = $this->model->get_library_item($id, $status);
$title = $this->config->get('whose_list') . "'s Manga List &middot; Edit";
$this->outputHTML('manga/edit', [
'title' => $title,
'status_list' => MangaReadingStatus::getConstList(),
'item' => $item,
'action' => $this->container->get('url-generator')
->url('/manga/update_form'),
]);
}
/**
* Update an anime item via a form submission
*
* @return void
*/
public function form_update()
{
$post_data = $this->request->post->get();
// Do some minor data manipulation for
// large form-based updates
$transformer = new MangaListTransformer();
$post_data = $transformer->untransform($post_data);
$full_result = $this->model->update($post_data);
$result = $full_result['body'];
if (array_key_exists('manga', $result))
{
$m =& $result['manga'][0];
$title = ( ! empty($m['english_title']))
? "{$m['romaji_title']} ({$m['english_title']})"
: "{$m['romaji_title']}";
$this->set_flash_message("Successfully updated {$title}.", 'success');
}
else
{
$this->set_flash_message('Failed to update anime.', 'error');
}
$this->session_redirect();
}
/**
* Update an anime item
*
* @return boolean|null
*/
public function update()
{
$this->outputJSON($this->model->update($this->request->post->get()));
}
}
// End of MangaController.php

View File

@ -0,0 +1,348 @@
<?php
/**
* Hummingbird Anime Client
*
* An API client for Hummingbird to manage anime and manga watch lists
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren
* @copyright Copyright (c) 2015 - 2016
* @link https://github.com/timw4mail/HummingBirdAnimeClient
* @license MIT
*/
namespace Aviat\AnimeClient;
use Aura\Web\Request;
use Aura\Web\Response;
use Aviat\Ion\Di\ContainerInterface;
use Aviat\AnimeClient\AnimeClient;
/**
* Basic routing/ dispatch
*/
class Dispatcher extends RoutingBase {
/**
* The route-matching object
* @var object $router
*/
protected $router;
/**
* Class wrapper for input superglobals
* @var object
*/
protected $request;
/**
* Routes added to router
* @var array $output_routes
*/
protected $output_routes;
/**
* Constructor
*
* @param ContainerInterface $container
*/
public function __construct(ContainerInterface $container)
{
parent::__construct($container);
$this->router = $container->get('aura-router');
$this->request = $container->get('request');
$this->output_routes = $this->_setup_routes();
}
/**
* Get the current route object, if one matches
*
* @return object
*/
public function get_route()
{
$logger = $this->container->getLogger('default');
$raw_route = $this->request->url->get(PHP_URL_PATH);
$route_path = "/" . trim($raw_route, '/');
$logger->debug('Dispatcher - Routing data from get_route method');
$logger->debug(print_r([
'route_path' => $route_path
], TRUE));
return $this->router->match($route_path, $_SERVER);
}
/**
* Get list of routes applied
*
* @return array
*/
public function get_output_routes()
{
return $this->output_routes;
}
/**
* Handle the current route
*
* @codeCoverageIgnore
* @param object|null $route
* @return void
*/
public function __invoke($route = NULL)
{
$logger = $this->container->getLogger('default');
if (is_null($route))
{
$route = $this->get_route();
$logger->debug('Dispatcher - Route invoke arguments');
$logger->debug(print_r($route, TRUE));
}
if($route)
{
$parsed = $this->process_route($route);
$controller_name = $parsed['controller_name'];
$action_method = $parsed['action_method'];
$params = $parsed['params'];
}
else
{
// If not route was matched, return an appropriate http
// error message
$error_route = $this->get_error_params();
$controller_name = AnimeClient::DEFAULT_CONTROLLER;
$action_method = $error_route['action_method'];
$params = $error_route['params'];
}
// Actually instantiate the controller
$this->call($controller_name, $action_method, $params);
}
/**
* Parse out the arguments for the appropriate controller for
* the current route
*
* @param \Aura\Router\Route $route
* @return array
*/
protected function process_route($route)
{
if (array_key_exists('controller', $route->params))
{
$controller_name = $route->params['controller'];
}
else
{
throw new \LogicException("Missing controller");
}
// Get the full namespace for a controller if a short name is given
if (strpos($controller_name, '\\') === FALSE)
{
$map = $this->get_controller_list();
$controller_name = $map[$controller_name];
}
$action_method = (array_key_exists('action', $route->params))
? $route->params['action']
: AnimeClient::NOT_FOUND_METHOD;
$params = (array_key_exists('params', $route->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];
}
}
}
return [
'controller_name' => $controller_name,
'action_method' => $action_method,
'params' => $params
];
}
/**
* Get the type of route, to select the current controller
*
* @return string
*/
public function get_controller()
{
$route_type = $this->__get('default_list');
$request_uri = $this->request->url->get(PHP_URL_PATH);
$path = trim($request_uri, '/');
$segments = explode('/', $path);
$controller = reset($segments);
if (empty($controller))
{
$controller = $route_type;
}
return $controller;
}
/**
* Get the list of controllers in the default namespace
*
* @return array
*/
public function get_controller_list()
{
$default_namespace = AnimeClient::DEFAULT_CONTROLLER_NAMESPACE;
$path = str_replace('\\', '/', $default_namespace);
$path = trim($path, '/');
$actual_path = realpath(\_dir(AnimeClient::SRC_DIR, $path));
$class_files = glob("{$actual_path}/*.php");
$controllers = [];
foreach ($class_files as $file)
{
$raw_class_name = basename(str_replace(".php", "", $file));
$path = strtolower(basename($raw_class_name));
$class_name = trim($default_namespace . '\\' . $raw_class_name, '\\');
$controllers[$path] = $class_name;
}
return $controllers;
}
/**
* Create the controller object and call the appropriate
* method
*
* @param string $controller_name - The full namespace of the controller class
* @param string $method
* @param array $params
* @return void
*/
protected function call($controller_name, $method, array $params)
{
$logger = $this->container->getLogger('default');
$controller = new $controller_name($this->container);
// Run the appropriate controller method
$logger->debug('Dispatcher - controller arguments');
$logger->debug(print_r($params, TRUE));
call_user_func_array([$controller, $method], $params);
}
/**
* Get the appropriate params for the error page
* pased on the failed route
*
* @return array|false
*/
protected function get_error_params()
{
$logger = $this->container->getLogger('default');
$failure = $this->router->getFailedRoute();
$logger->info('Dispatcher - failed route');
$logger->info(print_r($failure, TRUE));
$action_method = AnimeClient::ERROR_MESSAGE_METHOD;
$params = [];
if ($failure->failedMethod())
{
$params = [
'http_code' => 405,
'title' => '405 Method Not Allowed',
'message' => 'Invalid HTTP Verb'
];
}
else if($failure->failedAccept())
{
$params = [
'http_code' => 406,
'title' => '406 Not Acceptable',
'message' => 'Unacceptable content type'
];
}
else
{
// Fall back to a 404 message
$action_method = AnimeClient::NOT_FOUND_METHOD;
}
return [
'params' => $params,
'action_method' => $action_method
];
}
/**
* Select controller based on the current url, and apply its relevent routes
*
* @return array
*/
protected function _setup_routes()
{
$route_type = $this->get_controller();
// Add routes
$routes = [];
foreach ($this->routes as $name => &$route)
{
$path = $route['path'];
unset($route['path']);
$controller_map = $this->get_controller_list();
$controller_class = (array_key_exists($route_type, $controller_map))
? $controller_map[$route_type]
: AnimeClient::DEFAULT_CONTROLLER;
if (array_key_exists($route_type, $controller_map))
{
$controller_class = $controller_map[$route_type];
}
// Prepend the controller to the route parameters
$route['controller'] = $controller_class;
// Select the appropriate router method based on the http verb
$add = (array_key_exists('verb', $route))
? "add" . ucfirst(strtolower($route['verb']))
: "addGet";
// Add the route to the router object
if ( ! array_key_exists('tokens', $route))
{
$routes[] = $this->router->$add($name, $path)->addValues($route);
}
else
{
$tokens = $route['tokens'];
unset($route['tokens']);
$routes[] = $this->router->$add($name, $path)
->addValues($route)
->addTokens($tokens);
}
}
return $routes;
}
}
// End of Dispatcher.php

View File

@ -0,0 +1,38 @@
<?php
/**
* Hummingbird Anime Client
*
* An API client for Hummingbird to manage anime and manga watch lists
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren
* @copyright Copyright (c) 2015 - 2016
* @link https://github.com/timw4mail/HummingBirdAnimeClient
* @license MIT
*/
namespace Aviat\AnimeClient\Helper;
use Aviat\AnimeClient\MenuGenerator;
/**
* MenuGenerator helper wrapper
*/
class Menu {
use \Aviat\Ion\Di\ContainerAware;
/**
* Create the html for the selected menu
*
* @param string $menu_name
* @return string
*/
public function __invoke($menu_name)
{
$generator = new MenuGenerator($this->container);
return $generator->generate($menu_name);
}
}
// End of Menu.php

View File

@ -0,0 +1,26 @@
<?php
/**
* Hummingbird Anime Client
*
* An API client for Hummingbird to manage anime and manga watch lists
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren
* @copyright Copyright (c) 2015 - 2016
* @link https://github.com/timw4mail/HummingBirdAnimeClient
* @license MIT
*/
namespace Aviat\AnimeClient\Hummingbird\Enum;
use Aviat\Ion\Enum as BaseEnum;
/**
* Status of when anime is being/was/will be aired
*/
class AnimeAiringStatus extends BaseEnum {
const NOT_YET_AIRED = 'Not Yet Aired';
const AIRING = 'Currently Airing';
const FINISHED_AIRING = 'Finished Airing';
}
// End of AnimeAiringStatus.php

View File

@ -0,0 +1,29 @@
<?php
/**
* Hummingbird Anime Client
*
* An API client for Hummingbird to manage anime and manga watch lists
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren
* @copyright Copyright (c) 2015 - 2016
* @link https://github.com/timw4mail/HummingBirdAnimeClient
* @license MIT
*/
namespace Aviat\AnimeClient\Hummingbird\Enum;
use Aviat\Ion\Enum as BaseEnum;
/**
* Type of Anime
*/
class AnimeShowType extends BaseEnum {
const TV = 'TV';
const MOVIE = 'Movie';
const OVA = 'OVA';
const ONA = 'ONA';
const SPECIAL = 'Special';
const MUSIC = 'Music';
}
// End of AnimeShowType.php

View File

@ -0,0 +1,28 @@
<?php
/**
* Hummingbird Anime Client
*
* An API client for Hummingbird to manage anime and manga watch lists
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren
* @copyright Copyright (c) 2015 - 2016
* @link https://github.com/timw4mail/HummingBirdAnimeClient
* @license MIT
*/
namespace Aviat\AnimeClient\Hummingbird\Enum;
use Aviat\Ion\Enum as BaseEnum;
/**
* Possible values for watching status for the current anime
*/
class AnimeWatchingStatus extends BaseEnum {
const WATCHING = 'currently-watching';
const PLAN_TO_WATCH = 'plan-to-watch';
const COMPLETED = 'completed';
const ON_HOLD = 'on-hold';
const DROPPED = 'dropped';
}
// End of AnimeWatchingStatus.php

View File

@ -0,0 +1,28 @@
<?php
/**
* Hummingbird Anime Client
*
* An API client for Hummingbird to manage anime and manga watch lists
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren
* @copyright Copyright (c) 2015 - 2016
* @link https://github.com/timw4mail/HummingBirdAnimeClient
* @license MIT
*/
namespace Aviat\AnimeClient\Hummingbird\Enum;
use Aviat\Ion\Enum as BaseEnum;
/**
* Possible values for current reading status of manga
*/
class MangaReadingStatus extends BaseEnum {
const READING = 'Currently Reading';
const PLAN_TO_READ = 'Plan to Read';
const DROPPED = 'Dropped';
const ON_HOLD = 'On Hold';
const COMPLETED = 'Completed';
}
// End of MangaReadingStatus.php

View File

@ -0,0 +1,144 @@
<?php
/**
* Hummingbird Anime Client
*
* An API client for Hummingbird to manage anime and manga watch lists
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren
* @copyright Copyright (c) 2015 - 2016
* @link https://github.com/timw4mail/HummingBirdAnimeClient
* @license MIT
*/
namespace Aviat\AnimeClient\Hummingbird\Transformer;
use Aviat\Ion\Transformer\AbstractTransformer;
/**
* Transformer for anime list
*/
class AnimeListTransformer extends AbstractTransformer {
/**
* Convert raw api response to a more
* logical and workable structure
*
* @param array $item API library item
* @return array
*/
public function transform($item)
{
$anime =& $item['anime'];
$genres = $this->linearize_genres($item['anime']['genres']);
$rating = NULL;
if ($item['rating']['type'] === 'advanced')
{
$rating = (is_numeric($item['rating']['value']))
? intval(2 * $item['rating']['value'])
: '-';
}
$total_episodes = (is_numeric($anime['episode_count']))
? $anime['episode_count']
: '-';
$alternate_title = NULL;
if (array_key_exists('alternate_title', $anime))
{
// If the alternate title is very similar, or
// a subset of the main title, don't list the
// alternate title
$not_subset = stripos($anime['title'], $anime['alternate_title']) === FALSE;
$diff = levenshtein($anime['title'], $anime['alternate_title']);
if ($not_subset && $diff >= 5)
{
$alternate_title = $anime['alternate_title'];
}
}
return [
'id' => $item['id'],
'episodes' => [
'watched' => $item['episodes_watched'],
'total' => $total_episodes,
'length' => $anime['episode_length'],
],
'airing' => [
'status' => $anime['status'],
'started' => $anime['started_airing'],
'ended' => $anime['finished_airing']
],
'anime' => [
'age_rating' => $anime['age_rating'],
'title' => $anime['title'],
'alternate_title' => $alternate_title,
'slug' => $anime['slug'],
'url' => $anime['url'],
'type' => $anime['show_type'],
'image' => $anime['cover_image'],
'genres' => $genres,
],
'watching_status' => $item['status'],
'notes' => $item['notes'],
'rewatching' => (bool) $item['rewatching'],
'rewatched' => $item['rewatched_times'],
'user_rating' => $rating,
'private' => (bool) $item['private'],
];
}
/**
* Convert transformed data to
* api response format
*
* @param array $item Transformed library item
* @return array API library item
*/
public function untransform($item)
{
// Messy mapping of boolean values to their API string equivalents
$privacy = 'public';
if (array_key_exists('private', $item) && $item['private'])
{
$privacy = 'private';
}
$rewatching = 'false';
if (array_key_exists('rewatching', $item) && $item['rewatching'])
{
$rewatching = 'true';
}
return [
'id' => $item['id'],
'status' => $item['watching_status'],
'sane_rating_update' => $item['user_rating'] / 2,
'rewatching' => $rewatching,
'rewatched_times' => $item['rewatched'],
'notes' => $item['notes'],
'episodes_watched' => $item['episodes_watched'],
'privacy' => $privacy
];
}
/**
* Simplify structure of genre list
*
* @param array $raw_genres
* @return array
*/
protected function linearize_genres(array $raw_genres)
{
$genres = [];
foreach ($raw_genres as $genre)
{
$genres[] = $genre['name'];
}
return $genres;
}
}
// End of AnimeListTransformer.php

View File

@ -0,0 +1,118 @@
<?php
/**
* Hummingbird Anime Client
*
* An API client for Hummingbird to manage anime and manga watch lists
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren
* @copyright Copyright (c) 2015 - 2016
* @link https://github.com/timw4mail/HummingBirdAnimeClient
* @license MIT
*/
namespace Aviat\AnimeClient\Hummingbird\Transformer;
use Aviat\Ion\Transformer\AbstractTransformer;
/**
* Data transformation class for zippered Hummingbird manga
*/
class MangaListTransformer extends AbstractTransformer {
use \Aviat\Ion\StringWrapper;
/**
* Remap zipped anime data to a more logical form
*
* @param array $item manga entry item
* @return array
*/
public function transform($item)
{
$manga =& $item['manga'];
$rating = (is_numeric($item['rating']))
? intval(2 * $item['rating'])
: '-';
$total_chapters = ($manga['chapter_count'] > 0)
? $manga['chapter_count']
: '-';
$total_volumes = ($manga['volume_count'] > 0)
? $manga['volume_count']
: '-';
$map = [
'id' => $item['id'],
'chapters' => [
'read' => $item['chapters_read'],
'total' => $total_chapters
],
'volumes' => [
'read' => $item['volumes_read'],
'total' => $total_volumes
],
'manga' => [
'title' => $manga['romaji_title'],
'alternate_title' => NULL,
'slug' => $manga['id'],
'url' => 'https://hummingbird.me/manga/' . $manga['id'],
'type' => $manga['manga_type'],
'image' => $manga['poster_image_thumb'],
'genres' => $manga['genres'],
],
'reading_status' => $item['status'],
'notes' => $item['notes'],
'rereading' => (bool)$item['rereading'],
'reread' => $item['reread_count'],
'user_rating' => $rating,
];
if (array_key_exists('english_title', $manga))
{
$diff = levenshtein($manga['romaji_title'], $manga['english_title']);
// If the titles are REALLY similar, don't bother showing both
if ($diff >= 5)
{
$map['manga']['alternate_title'] = $manga['english_title'];
}
}
return $map;
}
/**
* Untransform data to update the api
*
* @param array $item
* @return array
*/
public function untransform($item)
{
$rereading = (array_key_exists('rereading', $item)) && (bool)$item['rereading'];
$map = [
'id' => $item['id'],
'manga_id' => $item['manga_id'],
'status' => $item['status'],
'chapters_read' => (int)$item['chapters_read'],
'volumes_read' => (int)$item['volumes_read'],
'rereading' => $rereading,
'reread_count' => (int)$item['reread_count'],
'notes' => $item['notes'],
];
if ($item['new_rating'] !== $item['old_rating'])
{
$map['rating'] = ($item['new_rating'] > 0)
? $item['new_rating'] / 2
: $item['old_rating'] / 2;
}
return $map;
}
}
// End of MangaListTransformer.php

View File

@ -0,0 +1,89 @@
<?php
/**
* Hummingbird Anime Client
*
* An API client for Hummingbird to manage anime and manga watch lists
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren
* @copyright Copyright (c) 2015 - 2016
* @link https://github.com/timw4mail/HummingBirdAnimeClient
* @license MIT
*/
namespace Aviat\AnimeClient\Hummingbird\Transformer;
/**
* Merges the two separate manga lists together
*/
class MangaListsZipper {
/**
* List of manga information
*
* @var array
*/
protected $manga_series_list = [];
/**
* List of manga tracking information
*
* @var array
*/
protected $manga_tracking_list = [];
/**
* Create the transformer
*
* @param array $merge_lists The raw manga data
*/
public function __construct(array $merge_lists)
{
$this->manga_series_list = $merge_lists['manga'];
$this->manga_tracking_list = $merge_lists['manga_library_entries'];
}
/**
* Do the transformation, and return the output
*
* @return array
*/
public function transform()
{
$this->index_manga_entries();
$output = [];
foreach ($this->manga_tracking_list as &$entry)
{
$id = $entry['manga_id'];
$entry['manga'] = $this->manga_series_list[$id];
unset($entry['manga_id']);
$output[] = $entry;
}
return $output;
}
/**
* Index manga series by the id
*
* @return void
*/
protected function index_manga_entries()
{
$orig_list = $this->manga_series_list;
$indexed_list = [];
foreach ($orig_list as $manga)
{
$id = $manga['id'];
$indexed_list[$id] = $manga;
}
$this->manga_series_list = $indexed_list;
}
}
// End of ManagListsZipper.php

View File

@ -0,0 +1,112 @@
<?php
/**
* Hummingbird Anime Client
*
* An API client for Hummingbird to manage anime and manga watch lists
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren
* @copyright Copyright (c) 2015 - 2016
* @link https://github.com/timw4mail/HummingBirdAnimeClient
* @license MIT
*/
namespace Aviat\AnimeClient;
use Aviat\Ion\Di\ContainerInterface;
/**
* Helper object to manage menu creation and selection
*/
class MenuGenerator extends UrlGenerator {
use \Aviat\Ion\StringWrapper;
use \Aviat\Ion\ArrayWrapper;
/**
* Html generation helper
*
* @var Aura\Html\HelperLocator
*/
protected $helper;
/**
* Request object
*
* @var Aura\Web\Request
*/
protected $request;
/**
* Create menu generator
*
* @param ContainerInterface $container
*/
public function __construct(ContainerInterface $container)
{
parent::__construct($container);
$this->helper = $container->get('html-helper');
$this->request = $container->get('request');
}
/**
* Generate the full menu structure from the config files
*
* @param array $menus
* @return array
*/
protected function parse_config(array $menus)
{
$parsed = [];
foreach ($menus as $name => $menu)
{
$parsed[$name] = [];
foreach ($menu['items'] as $path_name => $partial_path)
{
$title = (string)$this->string($path_name)->humanize()->titleize();
$parsed[$name][$title] = (string)$this->string($menu['route_prefix'])->append($partial_path);
}
}
return $parsed;
}
/**
* Generate the html structure of the menu selected
*
* @param string $menu
* @return string
*/
public function generate($menu)
{
$menus = $this->config->get('menus');
$parsed_config = $this->parse_config($menus);
// Bail out early on invalid menu
if ( ! $this->arr($parsed_config)->has_key($menu))
{
return '';
}
$menu_config = $parsed_config[$menu];
foreach ($menu_config as $title => $path)
{
$has = $this->string($path)->contains($this->path());
$selected = ($has && strlen($path) >= strlen($this->path()));
$link = $this->helper->a($this->url($path), $title);
$attrs = ($selected)
? ['class' => 'selected']
: [];
$this->helper->ul()->rawItem($link, $attrs);
}
// Create the menu html
return $this->helper->ul();
}
}
// End of MenuGenerator.php

View File

@ -1,29 +1,48 @@
<?php
/**
* Base for base models
* Hummingbird Anime Client
*
* An API client for Hummingbird to manage anime and manga watch lists
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren
* @copyright Copyright (c) 2015 - 2016
* @link https://github.com/timw4mail/HummingBirdAnimeClient
* @license MIT
*/
namespace AnimeClient;
namespace Aviat\AnimeClient;
use abeautifulsite\SimpleImage;
use Aviat\Ion\Di\ContainerInterface;
/**
* Common base for all Models
*/
class BaseModel {
class Model {
use \Aviat\Ion\StringWrapper;
/**
* The global configuration object
* @var object $config
* @var Config
*/
protected $config;
/**
* Constructor
* The container object
* @var ContainerInterface
*/
public function __construct()
protected $container;
/**
* Constructor
*
* @param ContainerInterface $container
*/
public function __construct(ContainerInterface $container)
{
global $config;
$this->config = $config;
$this->container = $container;
$this->config = $container->get('config');
}
/**
@ -36,7 +55,7 @@ class BaseModel {
* @param string $type - Anime or Manga, controls cache path
* @return string - the frontend path for the cached image
*/
public function get_cached_image($api_path, $series_slug, $type="anime")
public function get_cached_image($api_path, $series_slug, $type = "anime")
{
$api_path = str_replace("jjpg", "jpg", $api_path);
$path_parts = explode('?', basename($api_path));
@ -45,22 +64,25 @@ class BaseModel {
$ext = end($ext_parts);
// Workaround for some broken extensions
if ($ext == "jjpg") $ext = "jpg";
if ($ext == "jjpg")
{
$ext = "jpg";
}
// Failsafe for weird urls
if (strlen($ext) > 3) return $api_path;
if (strlen($ext) > 3)
{
return $api_path;
}
$img_cache_path = $this->config->get('img_cache_path');
$cached_image = "{$series_slug}.{$ext}";
$cached_path = "{$this->config->img_cache_path}/{$type}/{$cached_image}";
$cached_path = "{$img_cache_path}/{$type}/{$cached_image}";
// Cache the file if it doesn't already exist
if ( ! file_exists($cached_path))
{
if (ini_get('allow_url_fopen'))
{
copy($api_path, $cached_path);
}
elseif (function_exists('curl_init'))
if (function_exists('curl_init'))
{
$ch = curl_init($api_path);
$fp = fopen($cached_path, 'wb');
@ -70,11 +92,15 @@ class BaseModel {
]);
curl_exec($ch);
curl_close($ch);
fclose($ch);
fclose($fp);
}
else if (ini_get('allow_url_fopen'))
{
copy($api_path, $cached_path);
}
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
@ -100,7 +126,7 @@ class BaseModel {
private function _resize($path, $width, $height)
{
$img = new SimpleImage($path);
$img->resize($width,$height)->save();
$img->resize($width, $height)->save();
}
}
// End of BaseModel.php

View File

@ -0,0 +1,182 @@
<?php
/**
* Hummingbird Anime Client
*
* An API client for Hummingbird to manage anime and manga watch lists
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren
* @copyright Copyright (c) 2015 - 2016
* @link https://github.com/timw4mail/HummingBirdAnimeClient
* @license MIT
*/
namespace Aviat\AnimeClient\Model;
use GuzzleHttp\Client;
use GuzzleHttp\Cookie\CookieJar;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\ResponseInterface;
use GuzzleHttp\Exception\ClientException;
use Aviat\Ion\Di\ContainerInterface;
use Aviat\AnimeClient\Model as BaseModel;
/**
* Base model for api interaction
*
* @method ResponseInterface get(string $uri, array $options);
* @method ResponseInterface delete(string $uri, array $options);
* @method ResponseInterface head(string $uri, array $options);
* @method ResponseInterface options(string $uri, array $options);
* @method ResponseInterface patch(string $uri, array $options);
* @method ResponseInterface post(string $uri, array $options);
* @method ResponseInterface put(string $uri, array $options);
*/
class API extends BaseModel {
/**
* Base url for making api requests
* @var string
*/
protected $base_url = '';
/**
* The Guzzle http client object
* @var object
*/
protected $client;
/**
* Cookie jar object for api requests
* @var object
*/
protected $cookieJar;
/**
* Constructor
*
* @param ContainerInterface $container
*/
public function __construct(ContainerInterface $container)
{
parent::__construct($container);
$this->init();
}
/**
* Set up the class properties
*
* @return void
*/
protected function init()
{
$this->cookieJar = new CookieJar();
$this->client = new Client([
'base_uri' => $this->base_url,
'cookies' => TRUE,
'http_errors' => FALSE,
'defaults' => [
'cookies' => $this->cookieJar,
'headers' => [
'User-Agent' => "Tim's Anime Client/2.0",
'Accept-Encoding' => 'application/json'
],
'timeout' => 25,
'connect_timeout' => 25
]
]);
}
/**
* Magic methods to call guzzle api client
*
* @param string $method
* @param array $args
* @return ResponseInterface|null
*/
public function __call($method, $args)
{
$valid_methods = [
'get',
'delete',
'head',
'options',
'patch',
'post',
'put'
];
if ( ! in_array($method, $valid_methods))
{
return NULL;
}
array_unshift($args, strtoupper($method));
return call_user_func_array([$this->client, 'request'], $args);
}
/**
* Get the data for the specified library entry
*
* @param string $id
* @param string $status
* @return array
*/
public function get_library_item($id, $status)
{
$data = $this->_get_list_from_api($status);
$index_array = array_column($data, 'id');
$key = array_search($id, $index_array);
return $key !== FALSE
? $data[$key]
: [];
}
/**
* Sort the manga entries by their title
*
* @codeCoverageIgnore
* @param array $array
* @param string $sort_key
* @return void
*/
protected function sort_by_name(&$array, $sort_key)
{
$sort = array();
foreach ($array as $key => $item)
{
$sort[$key] = $item[$sort_key]['title'];
}
array_multisort($sort, SORT_ASC, $array);
}
/**
* Attempt login via the api
*
* @codeCoverageIgnore
* @param string $username
* @param string $password
* @return string|false
*/
public function authenticate($username, $password)
{
$response = $this->post('https://hummingbird.me/api/v1/users/authenticate', [
'form_params' => [
'username' => $username,
'password' => $password
]
]);
if ($response->getStatusCode() === 201)
{
return json_decode($response->getBody(), TRUE);
}
return FALSE;
}
}
// End of BaseApiModel.php

View File

@ -0,0 +1,241 @@
<?php
/**
* Hummingbird Anime Client
*
* An API client for Hummingbird to manage anime and manga watch lists
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren
* @copyright Copyright (c) 2015 - 2016
* @link https://github.com/timw4mail/HummingBirdAnimeClient
* @license MIT
*/
namespace Aviat\AnimeClient\Model;
use Aviat\Ion\Json;
use Aviat\AnimeClient\Hummingbird\Enum\AnimeWatchingStatus;
use Aviat\AnimeClient\Hummingbird\Transformer\AnimeListTransformer;
/**
* Model for handling requests dealing with the anime list
*/
class Anime extends API {
// Display constants
const WATCHING = 'Watching';
const PLAN_TO_WATCH = 'Plan to Watch';
const DROPPED = 'Dropped';
const ON_HOLD = 'On Hold';
const COMPLETED = 'Completed';
/**
* The base url for api requests
* @var string $base_url
*/
protected $base_url = "https://hummingbird.me/api/v1/";
/**
* Map of API status constants to display constants
* @var array
*/
protected $const_map = [
AnimeWatchingStatus::WATCHING => self::WATCHING,
AnimeWatchingStatus::PLAN_TO_WATCH => self::PLAN_TO_WATCH,
AnimeWatchingStatus::ON_HOLD => self::ON_HOLD,
AnimeWatchingStatus::DROPPED => self::DROPPED,
AnimeWatchingStatus::COMPLETED => self::COMPLETED,
];
/**
* Update the selected anime
*
* @param array $data
* @return array|false
*/
public function update($data)
{
$auth = $this->container->get('auth');
if ( ! $auth->is_authenticated() || ! array_key_exists('id', $data))
{
return FALSE;
}
$id = $data['id'];
$data['auth_token'] = $auth->get_auth_token();
$response = $this->client->post("libraries/{$id}", [
'form_params' => $data
]);
return [
'statusCode' => $response->getStatusCode(),
'body' => Json::decode($response->getBody(), TRUE)
];
}
/**
* Get the full set of anime lists
*
* @return array
*/
public function get_all_lists()
{
$output = [
self::WATCHING => [],
self::PLAN_TO_WATCH => [],
self::ON_HOLD => [],
self::DROPPED => [],
self::COMPLETED => [],
];
$data = $this->_get_list_from_api();
foreach ($data as $datum)
{
$output[$this->const_map[$datum['watching_status']]][] = $datum;
}
// Sort anime by name
foreach ($output as &$status_list)
{
$this->sort_by_name($status_list, 'anime');
}
return $output;
}
/**
* Get a category out of the full list
*
* @param string $status
* @return array
*/
public function get_list($status)
{
$data = $this->_get_list_from_api($status);
$this->sort_by_name($data, 'anime');
$output = [];
$output[$this->const_map[$status]] = $data;
return $output;
}
/**
* Get information about an anime from its id
*
* @param string $anime_id
* @return array
*/
public function get_anime($anime_id)
{
$config = [
'query' => [
'id' => $anime_id
]
];
$response = $this->client->get("anime/{$anime_id}", $config);
return Json::decode($response->getBody(), TRUE);
}
/**
* Search for anime by name
*
* @param string $name
* @return array
*/
public function search($name)
{
$logger = $this->container->getLogger('default');
$config = [
'query' => [
'query' => $name
]
];
$response = $this->get('search/anime', $config);
if ($response->getStatusCode() != 200)
{
$logger->warning("Non 200 response for search api call");
$logger->warning($response->getBody());
throw new RuntimeException($response->getEffectiveUrl());
}
return Json::decode($response->getBody(), TRUE);
}
/**
* Retrieve data from the api
*
* @codeCoverageIgnore
* @param string $status
* @return array
*/
protected function _get_list_from_api($status = "all")
{
$config = [
'allow_redirects' => FALSE
];
if ($status != "all")
{
$config['query']['status'] = $status;
}
$username = $this->config->get('hummingbird_username');
$auth = $this->container->get('auth');
if ($auth->is_authenticated())
{
$config['query']['auth_token'] = $auth->get_auth_token();
}
$response = $this->get("users/{$username}/library", $config);
$output = $this->_check_cache($status, $response);
foreach ($output as &$row)
{
$row['anime']['image'] = $this->get_cached_image($row['anime']['image'], $row['anime']['slug'], 'anime');
}
return $output;
}
/**
* Handle caching of transformed api data
*
* @codeCoverageIgnore
* @param string $status
* @param \GuzzleHttp\Message\Response
* @return array
*/
protected function _check_cache($status, $response)
{
$cache_file = _dir($this->config->get('data_cache_path'), "anime-{$status}.json");
$transformed_cache_file = _dir($this->config->get('data_cache_path'), "anime-{$status}-transformed.json");
$cached = (file_exists($cache_file))
? Json::decodeFile($cache_file)
: [];
$api_data = Json::decode($response->getBody(), TRUE);
if ($api_data === $cached && file_exists($transformed_cache_file))
{
return Json::decodeFile($transformed_cache_file);
}
else
{
Json::encodeFile($cache_file, $api_data);
$transformer = new AnimeListTransformer();
$transformed = $transformer->transform_collection($api_data);
Json::encodeFile($transformed_cache_file, $transformed);
return $transformed;
}
}
}
// End of AnimeModel.php

View File

@ -0,0 +1,441 @@
<?php
/**
* Hummingbird Anime Client
*
* An API client for Hummingbird to manage anime and manga watch lists
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren
* @copyright Copyright (c) 2015 - 2016
* @link https://github.com/timw4mail/HummingBirdAnimeClient
* @license MIT
*/
namespace Aviat\AnimeClient\Model;
use Aviat\Ion\Json;
use Aviat\Ion\Di\ContainerInterface;
use Aviat\AnimeClient\AnimeClient;
use Aviat\AnimeClient\Model\Anime as AnimeModel;
/**
* Model for getting anime collection data
*/
class AnimeCollection extends DB {
/**
* Anime API Model
* @var object $anime_model
*/
private $anime_model;
/**
* Whether the database is valid for querying
* @var bool
*/
private $valid_database = FALSE;
/**
* Constructor
*
* @param ContainerInterface $container
*/
public function __construct(ContainerInterface $container)
{
parent::__construct($container);
try
{
$this->db = \Query($this->db_config['collection']);
}
catch (\PDOException $e)
{
$this->valid_database = FALSE;
return FALSE;
}
$this->anime_model = $container->get('anime-model');
// Is database valid? If not, set a flag so the
// app can be run without a valid database
$db_file_name = $this->db_config['collection']['file'];
if ($db_file_name !== ':memory:')
{
if (file_exists($db_file_name))
{
$db_file = file_get_contents($db_file_name);
$this->valid_database = (strpos($db_file, 'SQLite format 3') === 0);
}
else
{
$this->valid_database = FALSE;
}
}
else
{
$this->valid_database = TRUE;
}
// Do an import if an import file exists
$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
*
* @return array
*/
public function get_collection()
{
$raw_collection = $this->_get_collection();
$collection = [];
foreach ($raw_collection as $row)
{
if (array_key_exists($row['media'], $collection))
{
$collection[$row['media']][] = $row;
}
else
{
$collection[$row['media']] = [$row];
}
}
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
*
* @return array
*/
private function _get_collection()
{
if ( ! $this->valid_database)
{
return [];
}
$query = $this->db->select('hummingbird_id, slug, title, alternate_title, show_type,
age_rating, episode_count, episode_length, cover_image, notes, media.type as media')
->from('anime_set a')
->join('media', 'media.id=a.media_id', 'inner')
->order_by('media')
->order_by('title')
->get();
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');
}
/**
* Remove a colleciton item
* @param array $data
* @return void
*/
public function delete($data)
{
// If there's no id to update, don't delete
if ( ! array_key_exists('hummingbird_id', $data))
{
return;
}
$this->db->where('hummingbird_id', $data['hummingbird_id'])
->delete('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
*
* @return void
*/
private function json_import()
{
if ( ! file_exists('import.json') || ! $this->valid_database)
{
return;
}
$anime = Json::decodeFile("import.json");
foreach ($anime as $item)
{
$this->db->set([
'hummingbird_id' => $item->id,
'slug' => $item->slug,
'title' => $item->title,
'alternate_title' => $item->alternate_title,
'show_type' => $item->show_type,
'age_rating' => $item->age_rating,
'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');
}
// Delete the import file
unlink('import.json');
// Update genre info
$this->update_genres();
}
/**
* Update genre information for selected anime
*
* @param int $anime_id The current anime
* @return void
*/
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 = [];
$links = [];
// Get existing genres
$query = $this->db->select('id, genre')
->from('genres')
->get();
foreach ($query->fetchAll(\PDO::FETCH_ASSOC) as $genre)
{
$genres[$genre['id']] = $genre['genre'];
}
// Get existing link table entries
$query = $this->db->select('hummingbird_id, genre_id')
->from('genre_anime_set_link')
->get();
foreach ($query->fetchAll(\PDO::FETCH_ASSOC) as $link)
{
if (array_key_exists($link['hummingbird_id'], $links))
{
$links[$link['hummingbird_id']][] = $link['genre_id'];
}
else
{
$links[$link['hummingbird_id']] = [$link['genre_id']];
}
}
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
$this->update_genre($anime['hummingbird_id']);
}
}
}
// End of AnimeCollectionModel.php

View File

@ -0,0 +1,45 @@
<?php
/**
* Hummingbird Anime Client
*
* An API client for Hummingbird to manage anime and manga watch lists
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren
* @copyright Copyright (c) 2015 - 2016
* @link https://github.com/timw4mail/HummingBirdAnimeClient
* @license MIT
*/
namespace Aviat\AnimeClient\Model;
use Aviat\Ion\Di\ContainerInterface;
use Aviat\AnimeClient\Model as BaseModel;
/**
* Base model for database interaction
*/
class DB extends BaseModel {
/**
* The query builder object
* @var object $db
*/
protected $db;
/**
* The database connection information array
* @var array $db_config
*/
protected $db_config;
/**
* Constructor
*
* @param ContainerInterface $container
*/
public function __construct(ContainerInterface $container)
{
parent::__construct($container);
$this->db_config = (array)$this->config->get('database');
}
}
// End of BaseDBModel.php

View File

@ -0,0 +1,224 @@
<?php
/**
* Hummingbird Anime Client
*
* An API client for Hummingbird to manage anime and manga watch lists
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren
* @copyright Copyright (c) 2015 - 2016
* @link https://github.com/timw4mail/HummingBirdAnimeClient
* @license MIT
*/
namespace Aviat\AnimeClient\Model;
use GuzzleHttp\Cookie\Cookiejar;
use GuzzleHttp\Cookie\SetCookie;
use Aviat\Ion\Json;
use Aviat\AnimeClient\Model\API;
use Aviat\AnimeClient\Hummingbird\Transformer;
use Aviat\AnimeClient\Hummingbird\Enum\MangaReadingStatus;
/**
* Model for handling requests dealing with the manga list
*/
class Manga extends API {
const READING = 'Reading';
const PLAN_TO_READ = 'Plan to Read';
const DROPPED = 'Dropped';
const ON_HOLD = 'On Hold';
const COMPLETED = 'Completed';
/**
* Map API constants to display constants
* @var array
*/
protected $const_map = [
MangaReadingStatus::READING => self::READING,
MangaReadingStatus::PLAN_TO_READ => self::PLAN_TO_READ,
MangaReadingStatus::ON_HOLD => self::ON_HOLD,
MangaReadingStatus::DROPPED => self::DROPPED,
MangaReadingStatus::COMPLETED => self::COMPLETED
];
/**
* The base url for api requests
* @var string
*/
protected $base_url = "https://hummingbird.me/";
/**
* Update the selected manga
*
* @param array $data
* @return array
*/
public function update($data)
{
$id = $data['id'];
$token = $this->container->get('auth')
->get_auth_token();
// Set the token cookie, with the authentication token
// from the auth class.
$cookieJar = $this->cookieJar;
$cookie_data = new SetCookie([
'Name' => 'token',
'Value' => $token,
'Domain' => 'hummingbird.me'
]);
$cookieJar->setCookie($cookie_data);
$result = $this->put("manga_library_entries/{$id}", [
'cookies' => $cookieJar,
'json' => ['manga_library_entry' => $data]
]);
return [
'statusCode' => $result->getStatusCode(),
'body' => Json::decode($result->getBody(), TRUE)
];
}
/**
* Get the full set of anime lists
*
* @return array
*/
public function get_all_lists()
{
$data = $this->_get_list_from_api();
foreach ($data as &$val)
{
$this->sort_by_name($val, 'manga');
}
return $data;
}
/**
* Get a category out of the full list
*
* @param string $status
* @return array
*/
public function get_list($status)
{
$data = $this->_get_list_from_api($status);
$this->sort_by_name($data, 'manga');
return $data;
}
/**
* Retrieve the list from the hummingbird api
*
* @param string $status
* @return array
*/
protected function _get_list_from_api($status = "All")
{
$config = [
'query' => [
'user_id' => $this->config->get('hummingbird_username')
],
'allow_redirects' => FALSE
];
$response = $this->get('manga_library_entries', $config);
$data = $this->_check_cache($response);
$output = $this->map_by_status($data);
return (array_key_exists($status, $output))
? $output[$status]
: $output;
}
/**
* Check the status of the cache and return the appropriate response
*
* @param \GuzzleHttp\Message\Response $response
* @codeCoverageIgnore
* @return array
*/
private function _check_cache($response)
{
// Bail out early if there isn't any manga data
$api_data = Json::decode($response->getBody(), TRUE);
if ( ! array_key_exists('manga', $api_data))
{
return [];
}
$cache_file = _dir($this->config->get('data_cache_path'), 'manga.json');
$transformed_cache_file = _dir(
$this->config->get('data_cache_path'),
'manga-transformed.json'
);
$cached_data = file_exists($cache_file)
? Json::decodeFile($cache_file)
: [];
if ($cached_data === $api_data && file_exists($transformed_cache_file))
{
return Json::decodeFile($transformed_cache_file);
}
else
{
Json::encodeFile($cache_file, $api_data);
$zippered_data = $this->zipper_lists($api_data);
$transformer = new Transformer\MangaListTransformer();
$transformed_data = $transformer->transform_collection($zippered_data);
Json::encodeFile($transformed_cache_file, $transformed_data);
return $transformed_data;
}
}
/**
* Map transformed anime data to be organized by reading status
*
* @param array $data
* @return array
*/
private function map_by_status($data)
{
$output = [
self::READING => [],
self::PLAN_TO_READ => [],
self::ON_HOLD => [],
self::DROPPED => [],
self::COMPLETED => [],
];
foreach ($data as &$entry)
{
$entry['manga']['image'] = $this->get_cached_image(
$entry['manga']['image'],
$entry['manga']['slug'],
'manga'
);
$key = $this->const_map[$entry['reading_status']];
$output[$key][] = $entry;
}
return $output;
}
/**
* Combine the two manga lists into one
* @param array $raw_data
* @return array
*/
private function zipper_lists($raw_data)
{
return (new Transformer\MangaListsZipper($raw_data))->transform();
}
}
// End of MangaModel.php

View File

@ -0,0 +1,122 @@
<?php
/**
* Hummingbird Anime Client
*
* An API client for Hummingbird to manage anime and manga watch lists
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren
* @copyright Copyright (c) 2015 - 2016
* @link https://github.com/timw4mail/HummingBirdAnimeClient
* @license MIT
*/
namespace Aviat\AnimeClient;
use Aviat\Ion\Di\ContainerInterface;
/**
* Base for routing/url classes
*/
class RoutingBase {
use \Aviat\Ion\StringWrapper;
/**
* Injection Container
* @var ContainerInterface $container
*/
protected $container;
/**
* Config Object
* @var Config
*/
protected $config;
/**
* Routing array
* @var array
*/
protected $routes;
/**
* Constructor
*
* @param ContainerInterface $container
*/
public function __construct(ContainerInterface $container)
{
$this->container = $container;
$this->config = $container->get('config');
$this->base_routes = $this->config->get('routes');
$this->routes = $this->base_routes['routes'];
}
/**
* Retreive the appropriate value for the routing key
*
* @param string $key
* @return mixed
*/
public function __get($key)
{
$routing_config = $this->base_routes['route_config'];
if (array_key_exists($key, $routing_config))
{
return $routing_config[$key];
}
}
/**
* Get the current url path
*
* @return string
*/
public function path()
{
$request = $this->container->get('request');
$path = $request->url->get(PHP_URL_PATH);
$cleaned_path = $this->string($path)
->trim()
->trimRight('/')
->ensureLeft('/');
return (string)$cleaned_path;
}
/**
* Get the url segments
*
* @return array
*/
public function segments()
{
$path = $this->path();
return explode('/', $path);
}
/**
* Get a segment of the current url
*
* @param int $num
* @return string|null
*/
public function get_segment($num)
{
$segments = $this->segments();
return (array_key_exists($num, $segments)) ? $segments[$num] : NULL;
}
/**
* Retrieve the last url segment
*
* @return string
*/
public function last_segment()
{
$segments = $this->segments();
return end($segments);
}
}
// End of RoutingBase.php

View File

@ -0,0 +1,148 @@
<?php
/**
* Hummingbird Anime Client
*
* An API client for Hummingbird to manage anime and manga watch lists
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren
* @copyright Copyright (c) 2015 - 2016
* @link https://github.com/timw4mail/HummingBirdAnimeClient
* @license MIT
*/
namespace Aviat\AnimeClient;
use Aviat\Ion\Di\ContainerInterface;
/**
* UrlGenerator class.
*/
class UrlGenerator extends RoutingBase {
/**
* The current HTTP host
*/
protected $host;
/**
* Constructor
*
* @param ContainerInterface $container
*/
public function __construct(ContainerInterface $container)
{
parent::__construct($container);
$this->host = $container->get('request')->server->get('HTTP_HOST');
}
/**
* Get the base url for css/js/images
*
* @return string
*/
public function asset_url(/*...*/)
{
$args = func_get_args();
$base_url = rtrim($this->url(""), '/');
$base_url = "{$base_url}" . $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
*/
public function base_url($type = "anime")
{
$config_path = trim($this->__get("{$type}_path"), "/");
$path = ($config_path !== '') ? $config_path : "";
return implode("/", ['/', $this->host, $path]);
}
/**
* Generate a proper url from the path
*
* @param string $path
* @return string
*/
public function url($path)
{
$path = trim($path, '/');
$path = preg_replace('`{/.*?}`i', '', $path);
// Remove any optional parameters from the route
// and replace them with existing route parameters, if they exist
$path_segments = explode('/', $path);
$segment_count = count($path_segments);
$segments = $this->segments();
for ($i = 0; $i < $segment_count; $i++)
{
if ( ! array_key_exists($i + 1, $segments))
{
$segments[$i + 1] = "";
}
$path_segments[$i] = preg_replace('`{.*?}`i', $segments[$i + 1], $path_segments[$i]);
}
$path = implode('/', $path_segments);
return "//{$this->host}/{$path}";
}
/**
* Full default path for the list pages
*
* @param string $type
* @return string
*/
public function default_url($type)
{
$type = trim($type);
$default_path = $this->__get("default_{$type}_list_path");
if ( ! is_null($default_path))
{
return $this->url("{$type}/{$default_path}");
}
throw new \InvalidArgumentException("Invalid default type: '{$type}'");
}
/**
* 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
*/
public function full_url($path = "", $type = "anime")
{
$config_default_route = $this->__get("default_{$type}_path");
// Remove beginning/trailing slashes
$path = trim($path, '/');
// Set the default view
if ($path === '')
{
$path .= trim($config_default_route, '/');
if ($this->__get('default_to_list_view'))
{
$path .= '/list';
}
}
return $this->url($path);
}
}
// End of UrlGenerator.php

View File

@ -0,0 +1,34 @@
<?php
/**
* Ion
*
* Building blocks for web development
*
* @package Ion
* @author Timothy J. Warren
* @copyright Copyright (c) 2015 - 2016
* @license MIT
*/
namespace Aviat\Ion;
use Aviat\Ion\Type\ArrayType;
/**
* Wrapper to shortcut creating ArrayType objects
*/
trait ArrayWrapper {
/**
* Convenience method for wrapping an array
* with the array type class
*
* @param array $arr
* @return ArrayType
*/
public function arr(array $arr)
{
return new ArrayType($arr);
}
}
// End of ArrayWrapper.php

View File

@ -0,0 +1,134 @@
<?php
/**
* Ion
*
* Building blocks for web development
*
* @package Ion
* @author Timothy J. Warren
* @copyright Copyright (c) 2015 - 2016
* @license MIT
*/
namespace Aviat\Ion\Di;
use Psr\Log\LoggerInterface;
/**
* Dependency container
*/
class Container implements ContainerInterface {
/**
* Array with class instances
*
* @var array
*/
protected $container = [];
/**
* Map of logger instances
*
* @var array
*/
protected $loggers = [];
/**
* Constructor
*
* @param array $values (optional)
*/
public function __construct(array $values = [])
{
$this->container = $values;
$this->loggers = [];
}
/**
* Finds an entry of the container by its identifier and returns it.
*
* @param string $id Identifier of the entry to look for.
*
* @throws NotFoundException No entry was found for this identifier.
* @throws ContainerException Error while retrieving the entry.
*
* @return mixed Entry.
*/
public function get($id)
{
if ( ! is_string($id))
{
throw new Exception\ContainerException("Id must be a string");
}
if ($this->has($id))
{
return $this->container[$id];
}
throw new Exception\NotFoundException("Item '{$id}' does not exist in container.");
}
/**
* Add a value to the container
*
* @param string $id
* @param mixed $value
* @return ContainerInterface
*/
public function set($id, $value)
{
$this->container[$id] = $value;
return $this;
}
/**
* Returns true if the container can return an entry for the given identifier.
* Returns false otherwise.
*
* @param string $id Identifier of the entry to look for.
*
* @return boolean
*/
public function has($id)
{
return array_key_exists($id, $this->container);
}
/**
* Determine whether a logger channel is registered
* @param string $key The logger channel
* @return boolean
*/
public function hasLogger($key = 'default')
{
return array_key_exists($key, $this->loggers);
}
/**
* Add a logger to the Container
*
* @param LoggerInterface $logger
* @param string $key The logger 'channel'
* @return ContainerInterface
*/
public function setLogger(LoggerInterface $logger, $key = 'default')
{
$this->loggers[$key] = $logger;
return $this;
}
/**
* Retrieve a logger for the selected channel
*
* @param string $key The logger to retreive
* @return LoggerInterface|null
*/
public function getLogger($key = 'default')
{
return ($this->hasLogger($key))
? $this->loggers[$key]
: NULL;
}
}
// End of Container.php

View File

@ -0,0 +1,49 @@
<?php
/**
* Ion
*
* Building blocks for web development
*
* @package Ion
* @author Timothy J. Warren
* @copyright Copyright (c) 2015 - 2016
* @license MIT
*/
namespace Aviat\Ion\Di;
/**
* Trait implementation of ContainerAwareInterface
*/
trait ContainerAware {
/**
* Di Container
*
* @var ContainerInterface
*/
protected $container;
/**
* Set the container for the current object
*
* @param ContainerInterface $container
* @return $this
*/
public function setContainer(ContainerInterface $container)
{
$this->container = $container;
return $this;
}
/**
* Get the container object
*
* @return ContainerInterface
*/
public function getContainer()
{
return $this->container;
}
}
// End of ContainerAware.php

View File

@ -0,0 +1,36 @@
<?php
/**
* Ion
*
* Building blocks for web development
*
* @package Ion
* @author Timothy J. Warren
* @copyright Copyright (c) 2015 - 2016
* @license MIT
*/
namespace Aviat\Ion\Di;
/**
* Interface for a class that is aware of the Di Container
*/
interface ContainerAwareInterface {
/**
* Set the container for the current object
*
* @param ContainerInterface $container
* @return void
*/
public function setContainer(ContainerInterface $container);
/**
* Get the container object
*
* @return ContainerInterface
*/
public function getContainer();
}
// End of ContainerAwareInterface.php

View File

@ -0,0 +1,47 @@
<?php
/**
* Ion
*
* Building blocks for web development
*
* @package Ion
* @author Timothy J. Warren
* @copyright Copyright (c) 2015 - 2016
* @license MIT
*/
namespace Aviat\Ion\Di;
use Psr\Log\LoggerInterface;
/**
* Interface for the Dependency Injection Container
*/
interface ContainerInterface extends \Interop\Container\ContainerInterface {
/**
* Add a value to the container
*
* @param string $key
* @param mixed $value
* @return ContainerInterface
*/
public function set($key, $value);
/**
* Add a logger to the Container
*
* @param LoggerInterface $logger
* @param string $key The logger 'channel'
* @return Container
*/
public function setLogger(LoggerInterface $logger, $key = 'default');
/**
* Retrieve a logger for the selected channel
*
* @param string $key The logger to retreive
* @return LoggerInterface|null
*/
public function getLogger($key = 'default');
}

View File

@ -0,0 +1,21 @@
<?php
/**
* Ion
*
* Building blocks for web development
*
* @package Ion
* @author Timothy J. Warren
* @copyright Copyright (c) 2015 - 2016
* @license MIT
*/
namespace Aviat\Ion\Di\Exception;
/**
* Generic exception for Di Container
*/
class ContainerException extends \Exception implements \Interop\Container\Exception\ContainerException {
}
// End of ContainerException.php

View File

@ -0,0 +1,22 @@
<?php
/**
* Ion
*
* Building blocks for web development
*
* @package Ion
* @author Timothy J. Warren
* @copyright Copyright (c) 2015 - 2016
* @license MIT
*/
namespace Aviat\Ion\Di\Exception;
/**
* Exception for Di Container when trying to access a
* key that doesn't exist in the container
*/
class NotFoundException extends ContainerException implements \Interop\Container\Exception\NotFoundException {
}
// End of NotFoundException.php

49
src/Aviat/Ion/Enum.php Normal file
View File

@ -0,0 +1,49 @@
<?php
/**
* Ion
*
* Building blocks for web development
*
* @package Ion
* @author Timothy J. Warren
* @copyright Copyright (c) 2015 - 2016
* @license MIT
*/
namespace Aviat\Ion;
use ReflectionClass;
/**
* Class emulating an enumeration type
*
* @method bool isValid(mixed $key)
* @method array getConstList()
*/
abstract class Enum {
use StaticInstance;
/**
* Return the list of constant values for the Enum
*
* @return array
*/
protected function getConstList()
{
$reflect = new ReflectionClass($this);
return $reflect->getConstants();
}
/**
* Verify that a constant value is valid
* @param mixed $key
* @return boolean
*/
protected function isValid($key)
{
$values = array_values($this->getConstList());
return in_array($key, $values);
}
}
// End of Enum.php

Some files were not shown because too many files have changed in this diff Show More