First commit

This commit is contained in:
Timothy Warren 2015-05-22 12:36:26 -04:00
commit 77d2c29e38
15 changed files with 1039 additions and 0 deletions

View File

@ -0,0 +1,77 @@
<?php
class BaseController {
public function __construct()
{
}
public function __destruct()
{
}
public function login()
{
}
public function logout()
{
}
public function search()
{
}
/**
* Output a template to HTML, using the provided data
*
* @param string $template
* @param array/object $data
* @return void
*/
public function outputHTML($template, $data=[])
{
global $router;
$route = $router->get_route();
$data['route_path'] = ($route) ? $router->get_route()->path : "";
$path = realpath(__DIR__ . "/../views/{$template}.php");
if ( ! is_file($path))
{
throw new Exception("Invalid template : {$path}");
}
ob_start();
extract($data);
include $path;
$buffer = ob_get_contents();
ob_end_clean();
header("Content-type: text/html;charset=utf-8");
echo $buffer;
die();
}
/**
* Output json with the proper content type
*
* @param mixed data
* @return void
*/
public function outputJSON($data)
{
if ( ! is_string($data))
{
$data = json_encode($data);
}
header("Content-type: application/json");
echo $data;
}
}

39
app/base/BaseModel.php Normal file
View File

@ -0,0 +1,39 @@
<?php
use \GuzzleHttp\Client;
use \GuzzleHttp\Cookie\CookieJar;
class BaseModel {
protected $client;
protected $cookieJar;
public function __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
]
]);
}
/**
* Get the full url path, since the base_url in Guzzle doesn't work correctly
*
* @param string $path
* @return string
*/
protected function _url($path)
{
return "{$this->base_url}{$path}";
}
}
// End of BaseModel.php

88
app/base/Router.php Normal file
View File

@ -0,0 +1,88 @@
<?php
use Aura\Router\RouterFactory;
class Router {
public function __construct()
{
$router_factory = new RouterFactory();
$router = $router_factory->newInstance();
$this->router = $router_factory->newInstance();
$this->_setup_routes();
}
/**
* Get the current route object, if one matches
*
* @return object
*/
public function get_route()
{
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$route = $this->router->match($path, $_SERVER);
return $route;
}
/**
* Handle the current route
*
* @param [object] $route
* @return void
*/
public function dispatch($route = NULL)
{
if (is_null($route))
{
$route = $this->get_route();
}
if ( ! $route)
{
$controller_name = 'BaseController';
$action_method = 'outputHTML';
$params = [
'template' => '404',
'data' => [
'title' => 'Page Not Found'
]
];
}
else
{
$controller_name = $route->params['controller'];
$action_method = $route->params['action'];
$params = (isset($route->params['params'])) ? $route->params['params'] : [];
}
$controller = new $controller_name();
// Run the appropriate controller method
call_user_func_array([$controller, $action_method], $params);
}
private function _setup_routes()
{
$host = $_SERVER['HTTP_HOST'];
$controller_class = ($host == "anime.timshomepage.net") ? "AnimeController" : "MangaController";
$routes = require __DIR__ . '/../config/routes.php';
// Add the default route
$this->router->add('home', '/')->addValues([
'controller' => $controller_class,
'action' => 'index'
]);
// Add routes by the configuration file
foreach($routes as $name => $route)
{
$path = $route['path'];
unset($route['path']);
$this->router->add($name, $path)->addValues($route);
}
}
}
// End of Router.php

17
app/base/autoloader.php Normal file
View File

@ -0,0 +1,17 @@
<?php
function anime_autoloader($class) {
$dirs = ["base", "controllers", "models"];
foreach($dirs as $dir)
{
$file = realpath(__DIR__ . "/../{$dir}/{$class}.php");
if (file_exists($file))
{
require_once $file;
return;
}
}
}
spl_autoload_register('anime_autoloader');

42
app/config/routes.php Normal file
View File

@ -0,0 +1,42 @@
<?php
return [
'anime_all' => [
'path' => '/all',
'controller' => 'AnimeController',
'action' => 'all',
'params' => []
],
'anime_plan_to_watch' => [
'path' => '/plan_to_watch',
'controller' => 'AnimeController',
'action' => 'anime_list',
'params' => [
'type' => 'plan-to-watch'
]
],
'anime_on_hold' => [
'path' => '/on_hold',
'controller' => 'AnimeController',
'action' => 'anime_list',
'params' => [
'type' => 'on-hold'
]
],
'anime_dropped' => [
'path' => '/dropped',
'controller' => 'AnimeController',
'action' => 'anime_list',
'params' => [
'type' => 'dropped'
]
],
'anime_completed' => [
'path' => '/completed',
'controller' => 'AnimeController',
'action' => 'anime_list',
'params' => [
'type' => 'completed'
]
]
];

View File

@ -0,0 +1,41 @@
<?php
class AnimeController extends BaseController {
private $model;
public function __construct()
{
parent::__construct();
$this->model = new AnimeModel();
}
public function __destruct()
{
parent::__destruct();
}
public function index()
{
$this->anime_list('currently-watching');
}
public function all()
{
$data = $this->model->get_all_lists();
$this->outputHTML('anime_list', [
'title' => "Tim's Anime List &middot; All",
'sections' => $data
]);
}
public function anime_list($type, $title="Tim's Anime List")
{
$data = $this->model->get_list($type);
$this->outputHTML('anime_list', [
'title' => $title,
'sections' => $data
]);
}
}
// End of AnimeController.php

134
app/models/AnimeModel.php Normal file
View File

@ -0,0 +1,134 @@
<?php
/**
* Model for handling requests dealing with the anime list
*/
class AnimeModel extends BaseModel {
protected $client;
protected $cookieJar;
protected $base_url = "https://hummingbird.me/api/v1";
public function __construct()
{
parent::__construct();
}
/**
* Get the full set of anime lists
*
* @return array
*/
public function get_all_lists()
{
$output = [
'Watching' => [],
'Completed' => [],
'Plan to Watch' => [],
'On Hold' => [],
'Dropped' => []
];
$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 $type
* @return array
*/
public function get_list($type)
{
$map = [
'currently-watching' => 'Watching',
'completed' => 'Completed',
'plan-to-watch' => 'Plan to Watch',
'on-hold' => 'On Hold',
'dropped' => 'Dropped'
];
$data = $this->_get_list($type);
$this->sort_by_name($data);
$output = [];
$output[$map[$type]] = $data;
return $output;
}
private function _get_list($type="all")
{
global $defaultHandler;
$config = [
'query' => [
'username' => 'timw4mail',
],
'allow_redirects' => false
];
if ($type != "all")
{
$config['query']['status'] = $type;
}
$response = $this->client->get($this->_url('/users/timw4mail/library'), $config);
$defaultHandler->addDataTable('response', (array)$response);
if ($response->getStatusCode() != 200)
{
throw new Exception($response->getEffectiveUrl());
}
return $response->json();
}
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

8
app/views/404.php Normal file
View File

@ -0,0 +1,8 @@
<?php include 'header.php'; ?>
<body>
<main>
<h1>404</h1>
<h2>Page Not Found</h2>
</main>
</body>
</html>

39
app/views/anime_list.php Normal file
View File

@ -0,0 +1,39 @@
<?php include 'header.php' ?>
<body>
<h1>Tim's Anime List</h1>
<nav>
<ul>
<li class="<?= is_selected('/all', $route_path) ?>"><a href="/all">All</a></li>
<li class="<?= is_selected('/', $route_path) ?>"><a href="/">Watching</a></li>
<li class="<?= is_selected('/plan_to_watch', $route_path) ?>"><a href="/plan_to_watch">Plan to Watch</a></li>
<li class="<?= is_selected('/on_hold', $route_path) ?>"><a href="/on_hold">On Hold</a></li>
<li class="<?= is_selected('/dropped', $route_path) ?>"><a href="/dropped">Dropped</a></li>
<li class="<?= is_selected('/completed', $route_path) ?>"><a href="/completed">Completed</a></li>
</ul>
</nav>
<main>
<?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['anime']['id'] ?>">
<img src="<?= $item['anime']['cover_image'] ?>" />
<div class="name"><a href="<?= $item['anime']['url'] ?>">
<?= $item['anime']['title'] ?>
<?= ($item['anime']['alternate_title'] != "") ? "<br />({$item['anime']['alternate_title']})" : ""; ?>
</a></div>
<div class="media_metadata">
<div class="media_type"><?= $item['anime']['show_type'] ?></div>
<div class="airing_status"><?= $item['anime']['status'] ?></div>
<div class="user_rating"><?= (int)($item['rating']['value'] * 2) ?> / 10</div>
<div class="completion"><?= $item['episodes_watched'] ?> / <?= $item['anime']['episode_count'] ?></div>
</div>
</article>
<?php endforeach ?>
</section>
</section>
<?php endforeach ?>
</main>
</body>
</html>

0
app/views/footer.php Normal file
View File

7
app/views/header.php Normal file
View File

@ -0,0 +1,7 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title><?= $title ?></title>
<link rel="stylesheet" href="/public/css/marx.css" />
<link rel="stylesheet" href="/public/css/base.css" />
</head>

7
composer.json Normal file
View File

@ -0,0 +1,7 @@
{
"require": {
"guzzlehttp/guzzle": "5.3.*",
"filp/whoops": "1.1.*",
"aura/router": "2.2.*"
}
}

38
index.php Normal file
View File

@ -0,0 +1,38 @@
<?php
require __DIR__ . '/vendor/autoload.php';
require __DIR__ . '/app/base/autoloader.php';
use \Whoops\Handler\PrettyPageHandler;
use \Whoops\Handler\JsonResponseHandler;
function is_selected($a, $b)
{
return ($a === $b) ? 'selected' : '';
}
// -----------------------------------------------------------------------------
// 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();
$jsonHandler->onlyForAjaxRequests(true);
$whoops->pushHandler($jsonHandler);
$whoops->register();
// -----------------------------------------------------------------------------
// Router
// -----------------------------------------------------------------------------
$router = new Router();
$defaultHandler->addDataTable('route', (array)$router->get_route());
$router->dispatch();
// End of index.php

66
public/css/base.css Normal file
View File

@ -0,0 +1,66 @@
.media-wrap {
text-align:center;
margin:0 auto;
}
.media {
position:relative;
vertical-align:top;
display:inline-block;
text-align:center;
width:220px;
height:319px;
margin:0.25em;
}
.media > img {
border-radius:0.25em;
}
.name, .media_type, .airing_status, .user_rating, .completion {
background: rgba(0, 0, 0, 0.45);
color: #fff;
padding:0.25em;
text-align:right;
}
.media > .media_metadata {
position:absolute;
bottom:0;
right:0;
}
.media > .media_metadata > .media_type {
border-top-left-radius: 0.25em;
border-top-right-radius: 0.25em;
}
.media > .media_metadata > .completion {
border-bottom-left-radius: 0.25em;
border-bottom-right-radius: 0.25em;
}
.media > .name {
border-radius:0.25em;
position:absolute;
top: 0;
}
.media > .name:hover {
background:rgba(0,0,0,0.9);
}
.media > .name > a {
text-align:justify;
background:none;
color:#fff;
text-shadow: 1px 2px 1px rgba(0, 0, 0, 0.85);
}
.user_rating::before {
content: "Rating: ";
}
.completion::before {
content: "Episodes: ";
}

436
public/css/marx.css Normal file
View File

@ -0,0 +1,436 @@
:root {
box-sizing: border-box;
cursor: default;
font-family: 'Open Sans', '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%; }
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: 'Open Sans', 'Helvetica Neue', Helvetica, 'Lucida Grande', sans-serif;
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', 'Open Sans', '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; }
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; }