Merge branch 'develop' of timw4mail/HummingBirdAnimeClient into master

This commit is contained in:
Timothy Warren 2018-11-05 13:15:58 -05:00 committed by GitLab
commit 99c94963e6
260 changed files with 9952 additions and 5471 deletions

120
.gitignore vendored
View File

@ -1,3 +1,118 @@
# Created by https://www.gitignore.io/api/macos,jetbrains+all
### JetBrains+all ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### JetBrains+all Patch ###
# Ignores the whole .idea folder and all .iml files
# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360
.idea/
# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023
*.iml
modules.xml
.idea/misc.xml
*.ipr
### macOS ###
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
# End of https://www.gitignore.io/api/macos,jetbrains+all
.codelite
.phing_targets
.sonar/
@ -23,10 +138,11 @@ build/**
app/config/*.toml
!app/config/*.toml.example
phinx.yml
.idea/
Caddyfile
build/humbuglog.txt
public/images/anime/**
public/images/avatars/**
public/images/manga/**
public/images/characters/**
public/images/characters/**
public/images/people/**
public/mal_mappings.json

View File

@ -1,21 +0,0 @@
test:7.1:
stage: test
before_script:
- sh build/docker_install.sh > /dev/null
- apk add --no-cache php7-phpdbg
- curl -sS https://getcomposer.org/installer | php
- php composer.phar install --ignore-platform-reqs
image: php:7.1-alpine
script:
- phpdbg -qrr -- ./vendor/bin/phpunit --coverage-text --colors=never
test:7.2:
stage: test
before_script:
- sh build/docker_install.sh > /dev/null
- apk add --no-cache php7-phpdbg
- curl -sS https://getcomposer.org/installer | php
- php composer.phar install --ignore-platform-reqs
image: php:7.2-alpine
script:
- phpdbg -qrr -- ./vendor/bin/phpunit --coverage-text --colors=never

View File

@ -1,11 +1,20 @@
# Changelog
## Version 4.1
* Removed MAL integration, added Anilist Integration
* Now uses WebP cache images when the browser supports it
* Replaces JS minifier with pre-minified scripts (Removes the need for one caching folder, too)
* Updated console command to sync Kitsu and Anilist data (Kitsu can sync MAL, and MAL's API broke, so MAL sync was removed)
* Added page to update settings without having to edit config files
* Defaulted to secure (HTTPS) urls
* Updated Character pages to show voice actors
* Added People pages, showing which works they contributed to, and in what role
## Version 4
* Updated to use Kitsu API after discontinuation of Hummingbird
* Added streaming links to list entries from the Kitsu API
* Added simple integration with MyAnimeList, so an update can cross-post to both Kitsu and MyAnimeList (anime and manga)
* Added console command to sync Kitsu and MyAnimeList data
* Added character pages
## Version 3

37
Jenkinsfile vendored Normal file
View File

@ -0,0 +1,37 @@
pipeline {
agent none
stages {
stage('PHP 7.1') {
agent {
docker {
image 'php:7.1-alpine'
args '-u root --privileged'
}
}
steps {
sh 'chmod +x ./build/docker_install.sh'
sh 'sh build/docker_install.sh'
sh 'apk add --no-cache php7-phpdbg'
sh 'curl -sS https://getcomposer.org/installer | php'
sh 'php composer.phar install --ignore-platform-reqs'
sh 'phpdbg -qrr -- ./vendor/bin/phpunit --coverage-text --colors=never'
}
}
stage('PHP 7.2') {
agent {
docker {
image 'php:7.2-alpine'
args '-u root --privileged'
}
}
steps {
sh 'chmod +x ./build/docker_install.sh'
sh 'sh build/docker_install.sh'
sh 'apk add --no-cache php7-phpdbg'
sh 'curl -sS https://getcomposer.org/installer | php'
sh 'php composer.phar install --ignore-platform-reqs'
sh 'phpdbg -qrr -- ./vendor/bin/phpunit --coverage-text --colors=never'
}
}
}
}

View File

@ -3,7 +3,7 @@
Update your anime/manga list on Kitsu.io and MyAnimeList.net
[![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)
[![Build Status](https://jenkins.timshomepage.net/buildStatus/icon?job=aviat/HummingBirdAnimeClient/develop)](https://jenkins.timshomepage.net/job/aviat/HummingBirdAnimeClient/develop)
[[Hosted Example](https://list.timshomepage.net)]
@ -33,22 +33,25 @@ Update your anime/manga list on Kitsu.io and MyAnimeList.net
* PHP 7.1+
* PDO SQLite or PDO PostgreSQL (For collection tab)
* GD
* GD extension for caching images
### Highly Recommended
* Redis or Memcached for caching
### Installation
1. Install via git, then install dependencies via composer: `composer install`
2. Duplicate `app/config/*.toml.example` files as `app/config/*.toml`
2. Duplicate `app/config/config.toml.example` file as `app/config/config.toml`
3. Configure settings in `app/config/config.toml` to your liking
4. Create the following directories if they don't exist, and make sure they are world writable
* app/config
* app/logs
* public/js/cache
* public/images/avatars
* public/images/anime
* public/images/characters
* public/images/manga
5. Make sure the `console` script is executable
6. Additional settings are on the settings page once you log in.
### Server Setup

View File

@ -28,6 +28,24 @@ $tomlConfig = loadToml(__DIR__);
return array_merge($tomlConfig, [
'asset_dir' => "{$ROOT_DIR}/public",
'base_config_dir' => __DIR__,
'config_dir' => "{$APP_DIR}/config",
// No config defaults
'kitsu_username' => 'timw4mail',
'whose_list' => 'Someone',
'cache' => [
'connection' => [],
'driver' => 'null',
],
'secure_urls' => TRUE,
// Routing defaults
'asset_path' => '/public',
'default_list' => 'anime', //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', // cover_view|list_view
// Template file path
'view_path' => "{$APP_DIR}/views",

View File

@ -1,69 +0,0 @@
<?php declare(strict_types=1);
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu and MyAnimeList to manage anime and manga watch lists
*
* PHP version 7
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2017 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.0
* @link https://github.com/timw4mail/HummingBirdAnimeClient
*/
// --------------------------------------------------------------------------
return [
/*
|--------------------------------------------------------------------------
| JS Folder
|--------------------------------------------------------------------------
|
| The folder where javascript files exist, in relation to the document root
|
*/
'js_root' => 'js/',
/*
|--------------------------------------------------------------------------
| JS Groups
|--------------------------------------------------------------------------
|
| Config array for javascript files to concatenate and minify
|
*/
'groups' => [
'base' => [
'base/classList.js',
'base/AnimeClient.js',
],
'event' => [
'base/events.js',
],
'table' => [
'base/sort_tables.js',
],
'table_edit' => [
'base/sort_tables.js',
'anime_edit.js',
'manga_edit.js',
],
'edit' => [
'anime_edit.js',
'manga_edit.js',
],
'anime_collection' => [
'anime_search_results.js',
'anime_collection.js',
],
'manga_collection' => [
'manga_search_results.js',
'manga_collection.js',
],
]
];
// End of minify_config.php

View File

@ -1,19 +0,0 @@
################################################################################
# Route config
#
# Default views and paths
################################################################################
# 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"

View File

@ -15,6 +15,9 @@
*/
use const Aviat\AnimeClient\{
ALPHA_SLUG_PATTERN,
NUM_PATTERN,
SLUG_PATTERN,
DEFAULT_CONTROLLER_METHOD,
DEFAULT_CONTROLLER
};
@ -24,14 +27,13 @@ use const Aviat\AnimeClient\{
//
// Maps paths to controllers and methods
// -------------------------------------------------------------------------
return [
$routes = [
// ---------------------------------------------------------------------
// Anime List Routes
// ---------------------------------------------------------------------
'anime.add.get' => [
'path' => '/anime/add',
'action' => 'addForm',
'verb' => 'get',
],
'anime.add.post' => [
'path' => '/anime/add',
@ -42,7 +44,7 @@ return [
'path' => '/anime/details/{id}',
'action' => 'details',
'tokens' => [
'id' => '[a-z0-9\-]+',
'id' => SLUG_PATTERN,
],
],
'anime.delete' => [
@ -60,7 +62,6 @@ return [
'manga.add.get' => [
'path' => '/manga/add',
'action' => 'addForm',
'verb' => 'get',
],
'manga.add.post' => [
'path' => '/manga/add',
@ -76,7 +77,7 @@ return [
'path' => '/manga/details/{id}',
'action' => 'details',
'tokens' => [
'id' => '[a-z0-9\-]+',
'id' => SLUG_PATTERN,
],
],
// ---------------------------------------------------------------------
@ -89,13 +90,12 @@ return [
'anime.collection.add.get' => [
'path' => '/anime-collection/add',
'action' => 'form',
'params' => [],
],
'anime.collection.edit.get' => [
'path' => '/anime-collection/edit/{id}',
'action' => 'form',
'tokens' => [
'id' => '[0-9]+',
'id' => NUM_PATTERN,
],
],
'anime.collection.add.post' => [
@ -110,10 +110,8 @@ return [
],
'anime.collection.view' => [
'path' => '/anime-collection/view{/view}',
'action' => 'index',
'params' => [],
'tokens' => [
'view' => '[a-z_]+',
'view' => ALPHA_SLUG_PATTERN,
],
],
'anime.collection.delete' => [
@ -131,13 +129,12 @@ return [
'manga.collection.add.get' => [
'path' => '/manga-collection/add',
'action' => 'form',
'params' => [],
],
'manga.collection.edit.get' => [
'path' => '/manga-collection/edit/{id}',
'action' => 'form',
'tokens' => [
'id' => '[0-9]+',
'id' => NUM_PATTERN,
],
],
'manga.collection.add.post' => [
@ -152,10 +149,8 @@ return [
],
'manga.collection.view' => [
'path' => '/manga-collection/view{/view}',
'action' => 'index',
'params' => [],
'tokens' => [
'view' => '[a-z_]+',
'view' => ALPHA_SLUG_PATTERN,
],
],
'manga.collection.delete' => [
@ -168,17 +163,27 @@ return [
// ---------------------------------------------------------------------
'character' => [
'path' => '/character/{slug}',
'action' => 'index',
'params' => [],
'tokens' => [
'slug' => '[a-z0-9\-]+'
'slug' => SLUG_PATTERN
]
],
'user_info' => [
'person' => [
'path' => '/people/{id}',
'tokens' => [
'id' => SLUG_PATTERN
]
],
'default_user_info' => [
'path' => '/me',
'action' => 'me',
'controller' => 'me',
'verb' => 'get',
'controller' => 'user',
],
'user_info' => [
'path' => '/user/{username}',
'controller' => 'user',
'tokens' => [
'username' => '.*?'
]
],
// ---------------------------------------------------------------------
// Default / Shared routes
@ -186,52 +191,61 @@ return [
'anilist-redirect' => [
'path' => '/anilist-redirect',
'action' => 'anilistRedirect',
'controller' => DEFAULT_CONTROLLER,
'controller' => 'settings',
],
'anilist-oauth' => [
'anilist-callback' => [
'path' => '/anilist-oauth',
'action' => 'anilistCallback',
'controller' => DEFAULT_CONTROLLER,
'controller' => 'settings',
],
'image_proxy' => [
'path' => '/public/images/{type}/{file}',
'action' => 'images',
'controller' => DEFAULT_CONTROLLER,
'verb' => 'get',
'action' => 'cache',
'controller' => 'images',
'tokens' => [
'type' => '[a-z0-9\-]+',
'file' => '[a-z0-9\-]+\.[a-z]{3}'
'type' => SLUG_PATTERN,
'file' => '[a-z0-9\-]+\.[a-z]{3,4}'
]
],
'cache_purge' => [
'path' => '/cache_purge',
'action' => 'clearCache',
'controller' => DEFAULT_CONTROLLER,
'verb' => 'get',
],
'settings' => [
'path' => '/settings',
],
'settings-post' => [
'path' => '/settings/update',
'action' => 'update',
'verb' => 'post',
],
'login' => [
'path' => '/login',
'action' => 'login',
'controller' => DEFAULT_CONTROLLER,
'verb' => 'get',
],
'login.post' => [
'path' => '/login',
'action' => 'loginAction',
'controller' => DEFAULT_CONTROLLER,
'verb' => 'post',
],
'logout' => [
'path' => '/logout',
'action' => 'logout',
'controller' => DEFAULT_CONTROLLER,
],
'increment' => [
'path' => '/{controller}/increment',
'action' => 'increment',
'verb' => 'post',
'tokens' => [
'controller' => ALPHA_SLUG_PATTERN,
],
],
'update' => [
'path' => '/{controller}/update',
'action' => 'update',
'verb' => 'post',
'tokens' => [
'controller' => '[a-z_]+',
'controller' => ALPHA_SLUG_PATTERN,
],
],
'update.post' => [
@ -239,28 +253,46 @@ return [
'action' => 'formUpdate',
'verb' => 'post',
'tokens' => [
'controller' => '[a-z_]+',
'controller' => ALPHA_SLUG_PATTERN,
],
],
'edit' => [
'path' => '/{controller}/edit/{id}/{status}',
'action' => 'edit',
'tokens' => [
'id' => '[0-9a-z_]+',
'id' => SLUG_PATTERN,
'status' => '([a-zA-Z\-_]|%20)+',
],
],
'list' => [
'path' => '/{controller}/{type}{/view}',
'action' => DEFAULT_CONTROLLER_METHOD,
'tokens' => [
'type' => '[a-z_]+',
'view' => '[a-z_]+',
'type' => ALPHA_SLUG_PATTERN,
'view' => ALPHA_SLUG_PATTERN,
],
],
'index_redirect' => [
'path' => '/',
'controller' => DEFAULT_CONTROLLER,
'action' => 'redirectToDefaultRoute',
],
];
];
$defaultMap = [
'action' => DEFAULT_CONTROLLER_METHOD,
'controller' => DEFAULT_CONTROLLER,
'params' => [],
'verb' => 'get',
];
foreach ($routes as &$route)
{
foreach($defaultMap as $key => $val)
{
if ( ! array_key_exists($key, $route))
{
$route[$key] = $val;
}
}
}
return $routes;

View File

@ -2,15 +2,15 @@
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu and MyAnimeList to manage anime and manga watch lists
* An API client for Kitsu to manage anime and manga watch lists
*
* PHP version 7
* PHP version 7.1
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2018 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.0
* @version 4.1
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
@ -22,9 +22,7 @@ use Aura\Session\SessionFactory;
use Aviat\AnimeClient\API\{
Anilist,
Kitsu,
MAL,
Kitsu\KitsuRequestBuilder,
MAL\MALRequestBuilder
Kitsu\KitsuRequestBuilder
};
use Aviat\AnimeClient\Model;
use Aviat\Banker\Pool;
@ -37,7 +35,7 @@ use Zend\Diactoros\{Response, ServerRequestFactory};
// -----------------------------------------------------------------------------
// Setup DI container
// -----------------------------------------------------------------------------
return function (array $configArray = []) {
return function ($configArray = []) {
$container = new Container();
// -------------------------------------------------------------------------
@ -50,12 +48,9 @@ return function (array $configArray = []) {
$anilistRequestLogger->pushHandler(new RotatingFileHandler(__DIR__ . '/logs/anilist_request.log', Logger::NOTICE));
$kitsuRequestLogger = new Logger('kitsu-request');
$kitsuRequestLogger->pushHandler(new RotatingFileHandler(__DIR__ . '/logs/kitsu_request.log', Logger::NOTICE));
$malRequestLogger = new Logger('mal-request');
$malRequestLogger->pushHandler(new RotatingFileHandler(__DIR__ . '/logs/mal_request.log', Logger::NOTICE));
$container->setLogger($appLogger);
$container->setLogger($anilistRequestLogger, 'anilist-request');
$container->setLogger($kitsuRequestLogger, 'kitsu-request');
$container->setLogger($malRequestLogger, 'mal-request');
// -------------------------------------------------------------------------
// Injected Objects
@ -86,6 +81,16 @@ return function (array $configArray = []) {
$menuHelper->setContainer($container);
return $menuHelper;
});
$htmlHelper->set('field', function() use ($container) {
$formHelper = new Helper\Form();
$formHelper->setContainer($container);
return $formHelper;
});
$htmlHelper->set('picture', function() use ($container) {
$pictureHelper = new Helper\Picture();
$pictureHelper->setContainer($container);
return $pictureHelper;
});
return $htmlHelper;
});
@ -131,17 +136,18 @@ return function (array $configArray = []) {
$model->setCache($cache);
return $model;
});
$container->set('mal-model', function($container) {
$requestBuilder = new MALRequestBuilder();
$requestBuilder->setLogger($container->getLogger('mal-request'));
$container->set('anilist-model', function($container) {
$requestBuilder = new Anilist\AnilistRequestBuilder();
$requestBuilder->setLogger($container->getLogger('anilist-request'));
$listItem = new MAL\ListItem();
$listItem = new Anilist\ListItem();
$listItem->setContainer($container);
$listItem->setRequestBuilder($requestBuilder);
$model = new MAL\Model($listItem);
$model = new Anilist\Model($listItem);
$model->setContainer($container);
$model->setRequestBuilder($requestBuilder);
return $model;
});
@ -160,6 +166,11 @@ return function (array $configArray = []) {
$container->set('manga-collection-model', function($container) {
return new Model\MangaCollection($container);
});
$container->set('settings-model', function($container) {
$model = new Model\Settings($container->get('config'));
$model->setContainer($container);
return $model;
});
// Miscellaneous Classes
$container->set('auth', function($container) {

View File

@ -2,7 +2,7 @@
# Cache Setup #
################################################################################
# See https://git.timshomepage.net/timw4mail/banker for more information
# See https://git.timshomepage.net/aviat/banker for more information
# Available drivers are apcu, memcache, memcached, redis or null
# Null cache driver means no caching

View File

@ -3,13 +3,34 @@
################################################################################
# Username for anime and manga lists
kitsu_username = "timw4mail"
kitsu_username = "johnsmith"
# Whose list is it?
whose_list = "Tim"
whose_list = "Someone"
# do you wish to show the anime collection?
show_anime_collection = true
# path to public directory on the server
asset_dir = "/../../public"
# do you wish to show the manga collection?
show_manga_collection = false
################################################################################
# Default views and paths
################################################################################
# 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
################################################################################
# Not on Settings Page
#
# These settings are not available to change on the settings page
################################################################################
# Use HTTPs for URLs
# It is not recommended to change this setting
secure_urls = true

View File

@ -2,7 +2,6 @@
# Database Configuration #
################################################################################
[collection]
type = "sqlite"
host = ""
user = ""

View File

@ -1,19 +0,0 @@
################################################################################
# Route config
#
# Default views and paths
################################################################################
# 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"

View File

@ -9,7 +9,7 @@
<div class="cssload-inner cssload-three"></div>
</div>
<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 id="series-list" class="media-wrap">
</section>
</section>
<br />
@ -36,5 +36,4 @@
</table>
</form>
</main>
<script defer="defer" src="<?= $urlGenerator->assetUrl('js.php/g/anime_collection') ?>"></script>
<?php endif ?>

View File

@ -0,0 +1,91 @@
<article
class="media"
data-kitsu-id="<?= $item['id'] ?>"
data-mal-id="<?= $item['mal_id'] ?>"
>
<?php if ($auth->isAuthenticated()): ?>
<button title="Increment episode count" class="plus-one" hidden>+1 Episode</button>
<?php endif ?>
<?= $helper->picture("images/anime/{$item['anime']['id']}.webp") ?>
<div class="name">
<a href="<?= $url->generate('anime.details', ['id' => $item['anime']['slug']]); ?>">
<span class="canonical"><?= $item['anime']['title'] ?></span>
<?php foreach ($item['anime']['titles'] as $title): ?>
<br/>
<small><?= $title ?></small>
<?php endforeach ?>
</a>
</div>
<div class="table">
<?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 ?>
<?php if (count($item['anime']['streaming_links']) > 0): ?>
<div class="row">
<?php foreach ($item['anime']['streaming_links'] as $link): ?>
<div class="cover-streaming-link">
<?php if ($link['meta']['link']): ?>
<a href="<?= $link['link'] ?>"
title="Stream '<?= $item['anime']['title'] ?>' on <?= $link['meta']['name'] ?>">
<?= $helper->picture("images/{$link['meta']['image']}", 'svg', [
'class' => 'streaming-logo',
'width' => 20,
'height' => 20,
'alt' => "{$link['meta']['name']} logo",
]); ?>
</a>
<?php else: ?>
<?= $helper->picture("images/{$link['meta']['image']}", 'svg', [
'class' => 'streaming-logo',
'width' => 20,
'height' => 20,
'alt' => "{$link['meta']['name']} logo",
]); ?>
<?php endif ?>
</div>
<?php endforeach ?>
</div>
<?php endif ?>
<?php if ($auth->isAuthenticated()): ?>
<div class="row">
<span class="edit">
<a class="bracketed" title="Edit information about this anime" href="<?=
$url->generate('edit', [
'controller' => 'anime',
'id' => $item['id'],
'status' => $item['watching_status']
]);
?>">Edit</a>
</span>
</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['episodes']['total'] ?></span>
</div>
</div>
<div class="row">
<div class="media_type"><?= $escape->html($item['anime']['show_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>

View File

@ -17,80 +17,7 @@
<section class="media-wrap">
<?php foreach($items as $item): ?>
<?php if ($item['private'] && ! $auth->isAuthenticated()) continue; ?>
<article class="media" data-kitsu-id="<?= $item['id'] ?>" data-mal-id="<?= $item['mal_id'] ?>">
<?php if ($auth->isAuthenticated()): ?>
<button title="Increment episode count" class="plus_one" hidden>+1 Episode</button>
<?php endif ?>
<img src="<?= $urlGenerator->assetUrl("images/anime/{$item['anime']['id']}.jpg") ?>" alt="" />
<div class="name">
<a href="<?= $url->generate('anime.details', ['id' => $item['anime']['slug']]); ?>">
<span class="canonical"><?= $item['anime']['title'] ?></span>
<?php foreach ($item['anime']['titles'] as $title): ?>
<br /><small><?= $title ?></small>
<?php endforeach ?>
</a>
</div>
<div class="table">
<?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 ?>
<?php if (count($item['anime']['streaming_links']) > 0): ?>
<div class="row">
<?php foreach($item['anime']['streaming_links'] as $link): ?>
<div class="cover_streaming_link">
<?php if($link['meta']['link']): ?>
<a href="<?= $link['link']?>" title="Stream '<?= $item['anime']['title'] ?>' on <?= $link['meta']['name'] ?>">
<img class="streaming-logo" width="20" height="20" src="<?= $urlGenerator->assetUrl('images', $link['meta']['image']) ?>" alt="<?= $link['meta']['name'] ?> logo" />
</a>
<?php else: ?>
<img class="streaming-logo" width="20" height="20" src="<?= $urlGenerator->assetUrl('images', $link['meta']['image']) ?>" alt="<?= $link['meta']['name'] ?> logo" />
<?php endif ?>
</div>
<?php endforeach ?>
</div>
<?php endif ?>
<?php if ($auth->isAuthenticated()): ?>
<div class="row">
<span class="edit">
<a class="bracketed" title="Edit information about this anime" href="<?=
$url->generate('edit', [
'controller' => 'anime',
'id' => $item['id'],
'status' => $item['watching_status']
]);
?>">Edit</a>
</span>
</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['episodes']['total'] ?></span>
</div>
</div>
<div class="row">
<div class="media_type"><?= $escape->html($item['anime']['show_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>
<?php include __DIR__ . '/cover-item.php' ?>
<?php endforeach ?>
</section>
</section>
@ -98,6 +25,3 @@
<?php endforeach ?>
<?php endif ?>
</main>
<?php if ($auth->isAuthenticated()): ?>
<script defer="defer" src="<?= $urlGenerator->assetUrl('js.php/g/edit') ?>"></script>
<?php endif ?>

View File

@ -1,12 +1,14 @@
<?php use function Aviat\AnimeClient\getLocalImg; ?>
<main class="details fixed">
<section class="flex flex-no-wrap">
<div>
<img class="cover" width="402" height="284" src="<?= $urlGenerator->assetUrl("images/anime/{$show_data['id']}.jpg") ?>" alt="" />
<section class="flex">
<aside class="info">
<?= $helper->picture("images/anime/{$show_data['id']}-original.webp") ?>
<br />
<br />
<table class="media_details">
<table class="media-details">
<tr>
<td class="align_right">Airing Status</td>
<td class="align-right">Airing Status</td>
<td><?= $show_data['status'] ?></td>
</tr>
<tr>
@ -18,16 +20,17 @@
<td><?= $show_data['episode_count'] ?? '-' ?></td>
</tr>
<?php if ( ! empty($show_data['episode_length'])): ?>
<tr>
<td>Episode Length</td>
<td><?= $show_data['episode_length'] ?> minutes</td>
</tr>
<tr>
<td>Episode Length</td>
<td><?= $show_data['episode_length'] ?> minutes</td>
</tr>
<?php endif ?>
<?php if ( ! empty($show_data['age_rating'])): ?>
<tr>
<td>Age Rating</td>
<td><abbr title="<?= $show_data['age_rating_guide'] ?>"><?= $show_data['age_rating'] ?></abbr></td>
</tr>
<tr>
<td>Age Rating</td>
<td><abbr title="<?= $show_data['age_rating_guide'] ?>"><?= $show_data['age_rating'] ?></abbr>
</td>
</tr>
<?php endif ?>
<tr>
<td>Genres</td>
@ -36,73 +39,137 @@
</td>
</tr>
</table>
</div>
<div>
<h2><a rel="external" href="<?= $show_data['url'] ?>"><?= $show_data['title'] ?></a></h2>
<?php foreach ($show_data['titles'] as $title): ?>
<h3><?= $title ?></h3>
<?php endforeach ?>
</aside>
<article class="text">
<h2 class="toph"><a rel="external" href="<?= $show_data['url'] ?>"><?= $show_data['title'] ?></a></h2>
<?php foreach ($show_data['titles'] as $title): ?>
<h3><?= $title ?></h3>
<?php endforeach ?>
<br />
<p><?= nl2br($show_data['synopsis']) ?></p>
<p class="description"><?= nl2br($show_data['synopsis']) ?></p>
<?php if (count($show_data['streaming_links']) > 0): ?>
<hr />
<h4>Streaming on:</h4>
<table class="full_width invisible">
<thead>
<hr />
<h4>Streaming on:</h4>
<table class="full-width invisible streaming-links">
<thead>
<tr>
<th class="align_left">Service</th>
<th class="align-left">Service</th>
<th>Subtitles</th>
<th>Dubs</th>
</tr>
</thead>
<tbody>
<?php foreach($show_data['streaming_links'] as $link): ?>
<tr>
<td class="align_left">
<?php if ($link['meta']['link'] !== FALSE): ?>
<a href="<?= $link['link'] ?>" title="Stream '<?= $show_data['title'] ?>' on <?= $link['meta']['name'] ?>">
<img class="streaming-logo" width="50" height="50" src="<?= $urlGenerator->assetUrl('images', $link['meta']['image']) ?>" alt="<?= $link['meta']['name'] ?> logo" />
&nbsp;&nbsp;<?= $link['meta']['name'] ?>
</a>
<?php else: ?>
<img class="streaming-logo" width="50" height="50" src="<?= $urlGenerator->assetUrl('images', $link['meta']['image']) ?>" alt="<?= $link['meta']['name'] ?> logo" />
&nbsp;&nbsp;<?= $link['meta']['name'] ?>
<?php endif ?>
</td>
<td><?= implode(', ', $link['subs']) ?></td>
<td><?= implode(', ', $link['dubs']) ?></td>
</tr>
<?php endforeach ?>
</tbody>
</table>
</thead>
<tbody>
<?php foreach ($show_data['streaming_links'] as $link): ?>
<tr>
<td class="align-left">
<?php if ($link['meta']['link'] !== FALSE): ?>
<a
href="<?= $link['link'] ?>"
title="Stream '<?= $show_data['title'] ?>' on <?= $link['meta']['name'] ?>"
>
<?= $helper->picture("images/{$link['meta']['image']}", 'svg', [
'class' => 'streaming-logo',
'width' => 50,
'height' => 50,
'alt' => "{$link['meta']['name']} logo",
]); ?>
&nbsp;&nbsp;<?= $link['meta']['name'] ?>
</a>
<?php else: ?>
<?= $helper->picture("images/{$link['meta']['image']}", 'svg', [
'class' => 'streaming-logo',
'width' => 50,
'height' => 50,
'alt' => "{$link['meta']['name']} logo",
]); ?>
&nbsp;&nbsp;<?= $link['meta']['name'] ?>
<?php endif ?>
</td>
<td><?= implode(', ', $link['subs']) ?></td>
<td><?= implode(', ', $link['dubs']) ?></td>
</tr>
<?php endforeach ?>
</tbody>
</table>
<?php endif ?>
<?php if ( ! empty($show_data['trailer_id'])): ?>
<hr />
<div class="responsive-iframe">
<h4>Trailer</h4>
<iframe width="560" height="315" src="https://www.youtube.com/embed/<?= $show_data['trailer_id'] ?>" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen></iframe>
<iframe
width="560"
height="315"
src="https://www.youtube.com/embed/<?= $show_data['trailer_id'] ?>"
frameborder="0"
allow="autoplay; encrypted-media"
allowfullscreen
></iframe>
</div>
<?php endif ?>
</div>
</article>
</section>
<?php if (count($characters) > 0): ?>
<hr />
<h2>Characters</h2>
<section class="align_center media-wrap">
<?php foreach($characters as $id => $char): ?>
<?php if ( ! empty($char['image']['original'])): ?>
<article class="character">
<?php $link = $url->generate('character', ['slug' => $char['slug']]) ?>
<div class="name">
<?= $helper->a($link, $char['name']); ?>
</div>
<a href="<?= $link ?>">
<?= $helper->img($urlGenerator->assetUrl("images/characters/{$id}.jpg"), [
'width' => '225'
]) ?>
</a>
</article>
<?php endif ?>
<?php endforeach ?>
<section>
<h2>Characters</h2>
<div class="tabs">
<?php $i = 0 ?>
<?php foreach ($characters as $role => $list): ?>
<input
type="radio" name="character-types"
id="character-types-<?= $i ?>" <?= ($i === 0) ? 'checked' : '' ?> />
<label for="character-types-<?= $i ?>"><?= ucfirst($role) ?></label>
<section class="content media-wrap flex flex-wrap flex-justify-start">
<?php foreach ($list as $id => $char): ?>
<?php if ( ! empty($char['image']['original'])): ?>
<article class="<?= $role === 'supporting' ? 'small-' : '' ?>character">
<?php $link = $url->generate('character', ['slug' => $char['slug']]) ?>
<div class="name">
<?= $helper->a($link, $char['name']); ?>
</div>
<a href="<?= $link ?>">
<?= $helper->picture("images/characters/{$id}.webp") ?>
</a>
</article>
<?php endif ?>
<?php endforeach ?>
</section>
<?php $i++; ?>
<?php endforeach ?>
</div>
</section>
<?php endif ?>
<?php if (count($staff) > 0): ?>
<?php //dump($staff); ?>
<section>
<h2>Staff</h2>
<div class="vertical-tabs">
<?php $i = 0; ?>
<?php foreach ($staff as $role => $people): ?>
<div class="tab">
<input type="radio" name="staff-roles" id="staff-role<?= $i ?>" <?= $i === 0 ? 'checked' : '' ?> />
<label for="staff-role<?= $i ?>"><?= $role ?></label>
<section class='content media-wrap flex flex-wrap flex-justify-start'>
<?php foreach ($people as $pid => $person): ?>
<article class='character small-person'>
<?php $link = $url->generate('person', ['id' => $person['id']]) ?>
<div class="name">
<a href="<?= $link ?>">
<?= $person['name'] ?>
</a>
</div>
<a href="<?= $link ?>">
<?= $helper->picture(getLocalImg($person['image']['original'] ?? NULL)) ?>
</a>
</article>
<?php endforeach ?>
</section>
</div>
<?php $i++; ?>
<?php endforeach ?>
</div>
</section>
<?php endif ?>
</main>

View File

@ -17,7 +17,7 @@
<tr>
<td rowspan="9">
<article class="media">
<?= $helper->img($urlGenerator->assetUrl('images/anime', "{$item['anime']['id']}.jpg")) ?>
<?= $helper->picture("images/anime/{$item['anime']['id']}.webp") ?>
</article>
</td>
</tr>
@ -79,7 +79,9 @@
<td>&nbsp;</td>
<td>
<input type="hidden" value="<?= $item['id'] ?>" name="id" />
<input type="hidden" value="<?= $item['mal_id'] ?>" name="mal_id" />
<?php if ( ! empty($item['mal_id'])): ?>
<input type="hidden" value="<?= $item['mal_id'] ?? '' ?>" name="mal_id" />
<?php endif ?>
<input type="hidden" value="true" name="edit" />
<button type="submit">Submit</button>
</td>
@ -87,11 +89,9 @@
</tbody>
</table>
</form>
<br />
<br />
<fieldset>
<legend>Danger Zone</legend>
<form class="js-delete" action="<?= $url->generate('anime.delete') ?>" method="post">
<form class="js-delete" action="<?= $url->generate('anime.delete') ?>" method="post">
<fieldset>
<legend>Danger Zone</legend>
<table class="form invisible">
<tbody>
<tr>
@ -100,14 +100,15 @@
</td>
<td>
<input type="hidden" value="<?= $item['id'] ?>" name="id" />
<input type="hidden" value="<?= $item['mal_id'] ?>" name="mal_id" />
<?php if (!empty($item['mal_id'])): ?>
<input type="hidden" value="<?= $item['mal_id'] ?? '' ?>" name="mal_id" />
<?php endif ?>
<button type="submit" class="danger">Delete Entry</button>
</td>
</tr>
</tbody>
</table>
</form>
</fieldset>
</fieldset>
</form>
</main>
<script defer="defer" src="<?= $urlGenerator->assetUrl('js.php/g/edit') ?>"></script>
<?php endif ?>

View File

@ -14,7 +14,7 @@
<thead>
<tr>
<?php if($auth->isAuthenticated()): ?>
<td class="no_border">&nbsp;</td>
<td class="no-border">&nbsp;</td>
<?php endif ?>
<th>Title</th>
<th>Airing Status</th>
@ -72,17 +72,27 @@
<?php foreach($item['anime']['streaming_links'] as $link): ?>
<?php if ($link['meta']['link'] !== FALSE): ?>
<a href="<?= $link['link'] ?>" title="Stream '<?= $item['anime']['title'] ?>' on <?= $link['meta']['name'] ?>">
<img class="streaming-logo" width="50" height="50" src="<?= $urlGenerator->assetUrl('images', $link['meta']['image']) ?>" alt="<?= $link['meta']['name'] ?> logo" />
<?= $helper->picture("images/{$link['meta']['image']}", 'svg', [
'class' => 'streaming-logo',
'width' => 50,
'height' => 50,
'alt' => "{$link['meta']['name']} logo",
]); ?>
</a>
<?php else: ?>
<img class="streaming-logo" width="50" height="50" src="<?= $urlGenerator->assetUrl('images', $link['meta']['image']) ?>" alt="<?= $link['meta']['name'] ?> logo" />
<?= $helper->picture("images/{$link['meta']['image']}", 'svg', [
'class' => 'streaming-logo',
'width' => 50,
'height' => 50,
'alt' => "{$link['meta']['name']} logo",
]); ?>
<?php endif ?>
<?php endforeach ?>
</td>
<td>
<p><?= $escape->html($item['notes']) ?></p>
</td>
<td class="align_left">
<td class="align-left">
<?php sort($item['anime']->genres) ?>
<?= implode(', ', $item['anime']->genres) ?>
</td>
@ -94,5 +104,4 @@
<?php endforeach ?>
<?php endif ?>
</main>
<?php $group = ($auth->isAuthenticated()) ? 'table_edit' : 'table' ?>
<script defer="defer" src="<?= $urlGenerator->assetUrl("js.php/g/{$group}") ?>"></script>
<script defer="defer" src="<?= $urlGenerator->assetUrl('js/tables.min.js') ?>"></script>

View File

@ -1,128 +0,0 @@
<?php use Aviat\AnimeClient\API\Kitsu; ?>
<main class="details fixed">
<section class="flex flex-no-wrap">
<div>
<img class="cover" width="284" src="<?= $urlGenerator->assetUrl("images/characters/{$data[0]['id']}.jpg") ?>" alt="" />
</div>
<div>
<h2><?= $data[0]['attributes']['name'] ?></h2>
<p class="description"><?= $data[0]['attributes']['description'] ?></p>
</div>
</section>
<?php if (array_key_exists('anime', $data['included']) || array_key_exists('manga', $data['included'])): ?>
<h3>Media</h3>
<section class="flex flex-no-wrap">
<?php if (array_key_exists('anime', $data['included'])): ?>
<div>
<h4>Anime</h4>
<section class="align_left media-wrap">
<?php foreach($data['included']['anime'] as $id => $anime): ?>
<article class="media">
<?php
$link = $url->generate('anime.details', ['id' => $anime['attributes']['slug']]);
$titles = Kitsu::filterTitles($anime['attributes']);
?>
<a href="<?= $link ?>">
<img src="<?= $urlGenerator->assetUrl("images/anime/{$id}.jpg") ?>" width="220" alt="" />
</a>
<div class="name">
<a href="<?= $link ?>">
<?= array_shift($titles) ?>
<?php foreach ($titles as $title): ?>
<br /><small><?= $title ?></small>
<?php endforeach ?>
</a>
</div>
</article>
<?php endforeach ?>
</section>
</div>
<?php endif ?>
</section>
<section class="flex flex-no-wrap">
<?php if (array_key_exists('manga', $data['included'])): ?>
<div>
<h4>Manga</h4>
<section class="align_left media-wrap">
<?php foreach($data['included']['manga'] as $id => $manga): ?>
<article class="media">
<?php
$link = $url->generate('manga.details', ['id' => $manga['attributes']['slug']]);
$titles = Kitsu::filterTitles($manga['attributes']);
?>
<a href="<?= $link ?>">
<img src="<?= $urlGenerator->assetUrl("images/manga/{$id}.jpg") ?>" width="220" alt="" />
</a>
<div class="name">
<a href="<?= $link ?>">
<?= array_shift($titles) ?>
<?php foreach ($titles as $title): ?>
<br /><small><?= $title ?></small>
<?php endforeach ?>
</a>
</div>
</article>
<?php endforeach ?>
</section>
</div>
<?php endif ?>
</section>
<?php endif ?>
<section>
<?php if ($castCount > 0): ?>
<h3>Castings</h3>
<?php foreach($castings as $role => $entries): ?>
<h4><?= $role ?></h4>
<?php foreach($entries as $language => $casting): ?>
<h5><?= $language ?></h5>
<table class="min-table">
<tr>
<th>Cast Member</th>
<th>Series</th>
</tr>
<?php foreach($casting as $c):?>
<tr>
<td style="width:229px">
<article class="character">
<img src="<?= $c['person']['image'] ?>" alt="" />
<div class="name">
<?= $c['person']['name'] ?>
</div>
</article>
</td>
<td>
<section class="align_left media-wrap">
<?php foreach($c['series'] as $series): ?>
<article class="media">
<?php
$link = $url->generate('anime.details', ['id' => $series['attributes']['slug']]);
$titles = Kitsu::filterTitles($series['attributes']);
?>
<a href="<?= $link ?>">
<img src="<?= $series['attributes']['posterImage']['small'] ?>" width="220" alt="" />
</a>
<div class="name">
<a href="<?= $link ?>">
<?= array_shift($titles) ?>
<?php foreach ($titles as $title): ?>
<br /><small><?= $title ?></small>
<?php endforeach ?>
</a>
</div>
</article>
<?php endforeach ?>
</section>
</td>
</tr>
<?php endforeach; ?>
</table>
<?php endforeach ?>
<?php endforeach ?>
<?php endif ?>
</section>
</main>

View File

@ -0,0 +1,221 @@
<?php
use function Aviat\AnimeClient\getLocalImg;
use Aviat\AnimeClient\API\Kitsu;
?>
<main class="details fixed">
<section class="flex flex-no-wrap">
<div>
<?= $helper->picture("images/characters/{$data[0]['id']}-original.webp") ?>
<?php if ( ! empty($data[0]['attributes']['otherNames'])): ?>
<h3>Nicknames / Other names</h3>
<?php foreach ($data[0]['attributes']['otherNames'] as $name): ?>
<h4><?= $name ?></h4>
<?php endforeach ?>
<?php endif ?>
</div>
<div>
<h2 class="toph"><?= $data['name'] ?></h2>
<?php foreach ($data['names'] as $name): ?>
<h3><?= $name ?></h3>
<?php endforeach ?>
<hr />
<p class="description"><?= $data[0]['attributes']['description'] ?></p>
</div>
</section>
<?php if (array_key_exists('anime', $data['included']) || array_key_exists('manga', $data['included'])): ?>
<h3>Media</h3>
<div class="tabs">
<?php if (array_key_exists('anime', $data['included'])): ?>
<input checked="checked" type="radio" id="media-anime" name="media-tabs" />
<label for="media-anime">Anime</label>
<section class="media-wrap content">
<?php foreach ($data['included']['anime'] as $id => $anime): ?>
<article class="media">
<?php
$link = $url->generate('anime.details', ['id' => $anime['attributes']['slug']]);
$titles = Kitsu::filterTitles($anime['attributes']);
?>
<a href="<?= $link ?>">
<?= $helper->picture("images/anime/{$id}.webp") ?>
</a>
<div class="name">
<a href="<?= $link ?>">
<?= array_shift($titles) ?>
<?php foreach ($titles as $title): ?>
<br />
<small><?= $title ?></small>
<?php endforeach ?>
</a>
</div>
</article>
<?php endforeach ?>
</section>
<?php endif ?>
<?php if (array_key_exists('manga', $data['included'])): ?>
<input type="radio" id="media-manga" name="media-tabs" />
<label for="media-manga">Manga</label>
<section class="media-wrap content">
<?php foreach ($data['included']['manga'] as $id => $manga): ?>
<article class="media">
<?php
$link = $url->generate('manga.details', ['id' => $manga['attributes']['slug']]);
$titles = Kitsu::filterTitles($manga['attributes']);
?>
<a href="<?= $link ?>">
<?= $helper->picture("images/manga/{$id}.webp") ?>
</a>
<div class="name">
<a href="<?= $link ?>">
<?= array_shift($titles) ?>
<?php foreach ($titles as $title): ?>
<br />
<small><?= $title ?></small>
<?php endforeach ?>
</a>
</div>
</article>
<?php endforeach ?>
</section>
<?php endif ?>
</div>
<?php endif ?>
<section>
<?php if ($castCount > 0): ?>
<h3>Castings</h3>
<?php
$vas = $castings['Voice Actor'];
unset($castings['Voice Actor']);
ksort($vas)
?>
<?php if ( ! empty($vas)): ?>
<h4>Voice Actors</h4>
<div class="tabs">
<?php $i = 0; ?>
<?php foreach ($vas as $language => $casting): ?>
<input <?= $i === 0 ? 'checked="checked"' : '' ?> type="radio" id="character-va<?= $i ?>"
name="character-vas"
/>
<label for="character-va<?= $i ?>"><?= $language ?></label>
<section class="content">
<table class="borderless max-table">
<tr>
<th>Cast Member</th>
<th>Series</th>
</tr>
<?php foreach ($casting as $cid => $c): ?>
<tr>
<td>
<article class="character">
<?php
$link = $url->generate('person', ['id' => $c['person']['id']]);
?>
<a href="<?= $link ?>">
<?= $helper->picture(getLocalImg($c['person']['image'])) ?>
<div class="name">
<?= $c['person']['name'] ?>
</div>
</a>
</article>
</td>
<td width="75%">
<section class="align-left media-wrap-flex">
<?php foreach ($c['series'] as $series): ?>
<article class="media">
<?php
$link = $url->generate('anime.details', ['id' => $series['attributes']['slug']]);
$titles = Kitsu::filterTitles($series['attributes']);
?>
<a href="<?= $link ?>">
<?= $helper->picture(getLocalImg($series['attributes']['posterImage']['small'], TRUE)) ?>
</a>
<div class="name">
<a href="<?= $link ?>">
<?= array_shift($titles) ?>
<?php foreach ($titles as $title): ?>
<br />
<small><?= $title ?></small>
<?php endforeach ?>
</a>
</div>
</article>
<?php endforeach ?>
</section>
</td>
</tr>
<?php endforeach ?>
</table>
</section>
<?php $i++ ?>
<?php endforeach ?>
</div>
<?php endif ?>
<?php foreach ($castings as $role => $entries): ?>
<h4><?= $role ?></h4>
<?php foreach ($entries as $language => $casting): ?>
<h5><?= $language ?></h5>
<table class="min-table">
<tr>
<th>Cast Member</th>
<th>Series</th>
</tr>
<?php foreach ($casting as $cid => $c): ?>
<tr>
<td style="width:229px">
<article class="character">
<?php
$link = $url->generate('person', ['id' => $c['person']['id']]);
?>
<a href="<?= $link ?>">
<?= $helper->picture(getLocalImg($c['person']['image'], TRUE)) ?>
<div class="name">
<?= $c['person']['name'] ?>
</div>
</a>
</article>
</td>
<td>
<section class="align-left media-wrap">
<?php foreach ($c['series'] as $series): ?>
<article class="media">
<?php
$link = $url->generate('anime.details', ['id' => $series['attributes']['slug']]);
$titles = Kitsu::filterTitles($series['attributes']);
?>
<a href="<?= $link ?>">
<?= $helper->picture(getLocalImg($series['attributes']['posterImage']['small'], TRUE)) ?>
</a>
<div class="name">
<a href="<?= $link ?>">
<?= array_shift($titles) ?>
<?php foreach ($titles as $title): ?>
<br />
<small><?= $title ?></small>
<?php endforeach ?>
</a>
</div>
</article>
<?php endforeach ?>
</section>
</td>
</tr>
<?php endforeach; ?>
</table>
<?php endforeach ?>
<?php endforeach ?>
<?php endif ?>
</section>
</main>

View File

@ -9,7 +9,7 @@
<div class="cssload-inner cssload-three"></div>
</div>
<label for="search">Search for <?= $collection_type ?> by name:&nbsp;&nbsp;&nbsp;&nbsp;<input type="search" id="search" name="search" /></label>
<section id="series_list" class="media-wrap">
<section id="series-list" class="media-wrap">
</section>
</section>
<br />
@ -39,5 +39,4 @@
</table>
</form>
</main>
<script defer="defer" src="<?= $urlGenerator->assetUrl("js.php/g/{$collection_type}_collection") ?>"></script>
<?php endif ?>

View File

@ -1,6 +1,5 @@
<article class="media" id="a-<?= $item['hummingbird_id'] ?>">
<img src="<?= $urlGenerator->assetUrl("images/anime/{$item['hummingbird_id']}.jpg") ?>"
alt="<?= $item['title'] ?> cover image"/>
<?= $helper->picture("images/anime/{$item['hummingbird_id']}.webp") ?>
<div class="name">
<a href="<?= $url->generate('anime.details', ['id' => $item['slug']]) ?>">
<?= $item['title'] ?>
@ -21,7 +20,7 @@
<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 class="age-rating"><?= $item['age_rating'] ?></div>
</div>
</div>
</article>

View File

@ -9,9 +9,8 @@
<?php $i = 0; ?>
<?php foreach ($sections as $name => $items): ?>
<input <?= $i === 0 ? 'checked="checked"' : '' ?> type="radio" id="collection-tab-<?= $i ?>" name="collection-tabs" />
<label for="collection-tab-<?= $i ?>"><?= $name ?></label>
<div class="content">
<h2><?= $name ?></h2>
<label for="collection-tab-<?= $i ?>"><h2><?= $name ?></h2></label>
<div class="content full-height">
<section class="media-wrap">
<?php foreach ($items as $item): ?>
<?php include __DIR__ . '/cover-item.php'; ?>

View File

@ -3,27 +3,29 @@
<h2>Edit Anime Collection Item</h2>
<form action="<?= $action_url ?>" method="post">
<table class="invisible form" style="border:0">
<thead>
<tr>
<th>
<h3><?= $escape->html($item['title']) ?></h3>
<?php if($item['alternate_title'] != ""): ?>
<h4><?= $item['alternate_title'] ?></h4>
<?php endif ?>
</th>
</tr>
</thead>
<tbody>
<tr>
<td rowspan="4" class="align_center">
<td rowspan="6" class="align-center">
<article class="media">
<?= $helper->img($urlGenerator->assetUrl("images/anime/{$item['hummingbird_id']}.jpg")); ?>
</article>
</td>
</tr>
<tr>
<td class="align_right"><label for="media_id">Media</label></td>
<td class="align_left">
<td class="align-right"><label for="title">Title</label></td>
<td class="align-left">
<input type="text" name="title" value="<?= $item['title'] ?>" />
</td>
</tr>
<tr>
<td class="align-right"><label for="title">Alternate Title</label></td>
<td class="align-left">
<input type="text" name="alternate_title" value="<?= $item['alternate_title'] ?>"/>
</td>
</tr>
<tr>
<td class="align-right"><label for="media_id">Media</label></td>
<td class="align-left">
<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>
@ -64,5 +66,4 @@
</form>
</fieldset>
</main>
<script defer="defer" src="<?= $urlGenerator->assetUrl('js.php/g/anime_collection') ?>"></script>
<?php endif ?>

View File

@ -5,7 +5,7 @@
href="<?= $url->generate($collection_type . '.collection.edit.get', ['id' => $item['hummingbird_id']]) ?>">Edit</a>
</td>
<?php endif ?>
<td class="align_left">
<td class="align-left">
<a href="<?= $url->generate('anime.details', ['id' => $item['slug']]) ?>">
<?= $item['title'] ?>
</a>
@ -15,5 +15,6 @@
<td><?= $item['episode_length'] ?></td>
<td><?= $item['show_type'] ?></td>
<td><?= $item['age_rating'] ?></td>
<td class="align_left"><?= $item['notes'] ?></td>
<td class="align-left"><?= implode(', ', $item['genres']) ?></td>
<td class="align-left"><?= $item['notes'] ?></td>
</tr>

View File

@ -10,10 +10,9 @@
<?php foreach ($sections as $name => $items): ?>
<input <?= $i === 0 ? 'checked="checked"' : '' ?> type="radio" id="collection-tab-<?= $i ?>"
name="collection-tabs"/>
<label for="collection-tab-<?= $i ?>"><?= $name ?></label>
<div class="content">
<h2><?= $name ?></h2>
<table>
<label for="collection-tab-<?= $i ?>"><h2><?= $name ?></h2></label>
<div class="content full-height">
<table class="full-width">
<thead>
<tr>
<?php if ($auth->isAuthenticated()): ?>
@ -24,6 +23,7 @@
<th>Episode Length</th>
<th>Show Type</th>
<th>Age Rating</th>
<th>Genres</th>
<th>Notes</th>
</tr>
</thead>
@ -39,4 +39,4 @@
</div>
<?php endif ?>
</main>
<script defer="defer" src="<?= $urlGenerator->assetUrl('js.php/g/table') ?>"></script>
<script defer="defer" src="<?= $urlGenerator->assetUrl('js/tables.min.js') ?>"></script>

View File

@ -10,6 +10,12 @@
</div>
</div>
</section>
<script defer="defer" src="<?= $urlGenerator->assetUrl('js.php/g/event') ?>"></script>
<?php if ($auth->isAuthenticated()): ?>
<script nomodule async="async" defer="defer" src="<?= $urlGenerator->assetUrl('js/scripts-authed.min.js') ?>"></script>
<script type="module" src="<?= $urlGenerator->assetUrl('js/src/index-authed.js') ?>"></script>
<?php else: ?>
<script nomodule async="async" defer="defer" src="<?= $urlGenerator->assetUrl('js/scripts.min.js') ?>"></script>
<script type="module" src="<?= $urlGenerator->assetUrl('js/src/index.js') ?>"></script>
<?php endif ?>
</body>
</html>

View File

@ -21,9 +21,10 @@
<link rel="icon" type="image/png" sizes="32x32" href="<?= $urlGenerator->assetUrl('images/icons/favicon-32x32.png') ?>">
<link rel="icon" type="image/png" sizes="96x96" href="<?= $urlGenerator->assetUrl('images/icons/favicon-96x96.png') ?>">
<link rel="icon" type="image/png" sizes="16x16" href="<?= $urlGenerator->assetUrl('images/icons/favicon-16x16.png') ?>">
<script defer="defer" src="<?= $urlGenerator->assetUrl('js.php/g/base') ?>"></script>
</head>
<body class="<?= $escape->attr($url_type) ?> list">
<?php include 'setup-check.php' ?>
<header>
<?php
include 'main-menu.php';

View File

@ -5,6 +5,8 @@ namespace Aviat\AnimeClient;
$whose = $config->get('whose_list') . "'s ";
$lastSegment = $urlGenerator->lastSegment();
$extraSegment = $lastSegment === 'list' ? '/list' : '';
$hasAnime = stripos($_SERVER['REQUEST_URI'], 'anime') !== FALSE;
$hasManga = stripos($_SERVER['REQUEST_URI'], 'manga') !== FALSE;
?>
<div id="main-nav" class="flex flex-align-end flex-wrap">
@ -41,35 +43,41 @@ $extraSegment = $lastSegment === 'list' ? '/list' : '';
[<?= $helper->a($urlGenerator->defaultUrl('anime') . $extraSegment, 'Anime List') ?>]
[<?= $helper->a($urlGenerator->defaultUrl('manga') . $extraSegment, 'Manga List') ?>]
<?php endif ?>
<?php if ($auth->isAuthenticated() && $config->get(['cache', 'driver']) !== 'null'): ?>
<span class="flex-no-wrap small-font">
<button type="button" class="js-clear-cache user-btn">Clear API Cache</button>
</span>
<?php endif ?>
</span>
<span class="flex-no-wrap small-font">[<?= $helper->a(
$url->generate('user_info'),
$url->generate('default_user_info'),
'About '. $config->get('whose_list')
) ?>]</span>
<?php if ($auth->isAuthenticated()): ?>
<span class="flex-no-wrap">&nbsp;</span>
<span class="flex-no-wrap small-font">
<button type="button" class="js-clear-cache user-btn">Clear API Cache</button>
<?= $helper->a(
$url->generate('settings'),
'Settings',
['class' => 'bracketed']
) ?>
</span>
<span class="flex-no-wrap small-font">
<?= $helper->a(
$url->generate('logout'),
'Logout',
['class' => 'bracketed']
) ?>
</span>
<?php else: ?>
<span class="flex-no-wrap small-font">
[<?= $helper->a($url->generate('login'), "{$whose} Login") ?>]
</span>
<span class="flex-no-wrap">&nbsp;</span>
<?php endif ?>
<span class="flex-no-wrap small-font">
<?php if ($auth->isAuthenticated()): ?>
<?= $helper->a(
$url->generate('logout'),
'Logout',
['class' => 'bracketed']
) ?>
<?php else: ?>
[<?= $helper->a($url->generate('login'), "{$whose} Login") ?>]
<?php endif ?>
</span>
</div>
<nav>
<?php if ($container->get('util')->isViewPage()): ?>
<?php if ($container->get('util')->isViewPage() && ($hasAnime || $hasManga)): ?>
<?= $helper->menu($menu_name) ?>
<br />
<ul>

View File

@ -9,7 +9,7 @@
<div class="cssload-inner cssload-three"></div>
</div>
<label for="search">Search for manga by name:&nbsp;&nbsp;&nbsp;&nbsp;<input type="search" id="search" /></label>
<section id="series_list" class="media-wrap">
<section id="series-list" class="media-wrap">
</section>
</section>
<br />
@ -36,5 +36,4 @@
</table>
</form>
</main>
<script defer="defer" src="<?= $urlGenerator->assetUrl('js.php/g/manga_collection') ?>"></script>
<?php endif ?>

View File

@ -18,18 +18,18 @@
<?php foreach($items as $item): ?>
<article class="media" data-kitsu-id="<?= $item['id'] ?>" data-mal-id="<?= $item['mal_id'] ?>">
<?php if ($auth->isAuthenticated()): ?>
<div class="edit_buttons" hidden>
<button class="plus_one_chapter">+1 Chapter</button>
<?php /* <button class="plus_one_volume">+1 Volume</button> */ ?>
<div class="edit-buttons" hidden>
<button class="plus-one-chapter">+1 Chapter</button>
<?php /* <button class="plus-one-volume">+1 Volume</button> */ ?>
</div>
<?php endif ?>
<img src="<?= $urlGenerator->assetUrl('images/manga', "{$item['manga']['id']}.jpg") ?>" />
<?= $helper->picture("images/manga/{$item['manga']['id']}.webp") ?>
<div class="name">
<a href="<?= $url->generate('manga.details', ['id' => $item['manga']['slug']]) ?>">
<?= $escape->html($item['manga']['title']) ?>
<?php foreach($item['manga']['titles'] as $title): ?>
<br /><small><?= $title ?></small>
<?php endforeach ?>
<?php foreach($item['manga']['titles'] as $title): ?>
<br /><small><?= $title ?></small>
<?php endforeach ?>
</a>
</div>
<div class="table">
@ -49,7 +49,8 @@
</div>
<?php endif ?>
<div class="row">
<div class="user_rating">Rating: <?= $item['user_rating'] ?> / 10</div>
<div><?= $item['manga']['type'] ?></div>
<div class="user-rating">Rating: <?= $item['user_rating'] ?> / 10</div>
</div>
<?php if ($item['rereading']): ?>
@ -88,6 +89,3 @@
<?php endforeach ?>
<?php endif ?>
</main>
<?php if ($auth->isAuthenticated()): ?>
<script defer="defer" src="<?= $urlGenerator->assetUrl('js.php/g/edit') ?>"></script>
<?php endif ?>

View File

@ -1,13 +1,14 @@
<main class="details fixed">
<section class="flex flex-no-wrap">
<div>
<img class="cover" src="<?= $urlGenerator->assetUrl('images/manga', "{$data['id']}.jpg") ?>" alt="<?= $data['title'] ?> cover image" />
<aside class="info">
<?= $helper->picture("images/manga/{$data['id']}-original.webp", 'jpg', ['class' => 'cover']) ?>
<br />
<br />
<table>
<table class="media-details">
<tr>
<td>Manga Type</td>
<td><?= $data['manga_type'] ?></td>
<td><?= ucfirst($data['manga_type']) ?></td>
</tr>
<tr>
<td>Volume Count</td>
@ -24,36 +25,75 @@
</td>
</tr>
</table>
</div>
<div>
<h2><a rel="external" href="<?= $data['url'] ?>"><?= $data['title'] ?></a></h2>
<?php if( ! empty($data['en_title'])): ?>
<h3><?= $data['en_title'] ?></h3>
<?php endif ?>
</aside>
<article class="text">
<h2 class="toph"><a rel="external" href="<?= $data['url'] ?>"><?= $data['title'] ?></a></h2>
<?php foreach ($data['titles'] as $title): ?>
<h3><?= $title ?></h3>
<?php endforeach ?>
<br />
<p><?= nl2br($data['synopsis']) ?></p>
</div>
</article>
</section>
<?php if (count($characters) > 0): ?>
<h2>Characters</h2>
<section class="media-wrap">
<?php foreach($characters as $id => $char): ?>
<?php if ( ! empty($char['image']['original'])): ?>
<article class="character">
<?php $link = $url->generate('character', ['slug' => $char['slug']]) ?>
<div class="name">
<?= $helper->a($link, $char['name']); ?>
</div>
<a href="<?= $link ?>">
<?= $helper->img($urlGenerator->assetUrl('images/characters', "{$id}.jpg"), [
'width' => '225'
]) ?>
</a>
</article>
<?php endif ?>
<?php endforeach ?>
</section>
<h2>Characters</h2>
<div class="tabs">
<?php $i = 0 ?>
<?php foreach ($characters as $role => $list): ?>
<input
type="radio" name="character-role-tabs"
id="character-tabs<?= $i ?>" <?= $i === 0 ? 'checked' : '' ?> />
<label for="character-tabs<?= $i ?>"><?= ucfirst($role) ?></label>
<section class="content media-wrap flex flex-wrap flex-justify-start">
<?php foreach ($list as $id => $char): ?>
<?php if ( ! empty($char['image']['original'])): ?>
<article class="<?= $role === 'supporting' ? 'small-' : '' ?>character">
<?php $link = $url->generate('character', ['slug' => $char['slug']]) ?>
<div class="name">
<?= $helper->a($link, $char['name']); ?>
</div>
<a href="<?= $link ?>">
<?= $helper->picture("images/characters/{$id}.webp") ?>
</a>
</article>
<?php endif ?>
<?php endforeach ?>
</section>
<?php $i++ ?>
<?php endforeach ?>
</div>
<?php endif ?>
<?php if (count($staff) > 0): ?>
<h2>Staff</h2>
<div class="vertical-tabs">
<?php $i = 0 ?>
<?php foreach ($staff as $role => $people): ?>
<div class="tab">
<input
type="radio" name="staff-roles" id="staff-role<?= $i ?>" <?= $i === 0 ? 'checked' : '' ?> />
<label for="staff-role<?= $i ?>"><?= $role ?></label>
<section class='content media-wrap flex flex-wrap flex-justify-start'>
<?php foreach ($people as $pid => $person): ?>
<article class='character person'>
<?php $link = $url->generate('person', ['id' => $pid]) ?>
<div class="name">
<a href="<?= $link ?>">
<?= $person['name'] ?>
</a>
</div>
<a href="<?= $link ?>">
<?= $helper->picture("images/people/{$pid}.webp") ?>
</a>
</article>
<?php endforeach ?>
</section>
</div>
<?php $i++ ?>
<?php endforeach ?>
</div>
<?php endif ?>
</main>

View File

@ -37,7 +37,7 @@
]) ?>">Edit</a>
</td>
<?php endif ?>
<td class="align_left">
<td class="align-left">
<a href="<?= $url->generate('manga.details', ['id' => $item['manga']['slug']]) ?>">
<?= $item['manga']['title'] ?>
</a>
@ -61,7 +61,7 @@
</ul>
</td>
<td><?= $item['manga']['type'] ?></td>
<td class="align_left">
<td class="align-left">
<?= implode(', ', $item['manga']['genres']) ?>
</td>
</tr>
@ -72,4 +72,4 @@
<?php endforeach ?>
<?php endif ?>
</main>
<script defer="defer" src="<?= $urlGenerator->assetUrl('js.php/g/table') ?>"></script>
<script defer="defer" src="<?= $urlGenerator->assetUrl('js/tables.min.js') ?>"></script>

View File

@ -0,0 +1,67 @@
<?php
use function Aviat\AnimeClient\getLocalImg;
use Aviat\AnimeClient\API\Kitsu;
?>
<h3>Voice Acting Roles</h3>
<div class="tabs">
<?php $i = 0; ?>
<?php foreach($characters as $role => $characterList): ?>
<input <?= $i === 0 ? 'checked="checked"' : '' ?> type="radio" name="character-type-tabs" id="character-type-<?= $i ?>" />
<label for="character-type-<?= $i ?>"><h5><?= ucfirst($role) ?></h5></label>
<section class="content">
<table class="borderless max-table">
<tr>
<th>Character</th>
<th>Series</th>
</tr>
<?php foreach ($characterList as $cid => $character): ?>
<tr>
<td style="width:229px">
<article class="character">
<?php
$link = $url->generate('character', ['slug' => $character['character']['slug']]);
?>
<a href="<?= $link ?>">
<?php $imgPath = ($character['character']['image'] === NULL)
? 'images/characters/empty.png'
: getLocalImg($character['character']['image']['original']);
echo $helper->picture($imgPath);
?>
<div class="name">
<?= $character['character']['canonicalName'] ?>
</div>
</a>
</article>
</td>
<td>
<section class="align-left media-wrap">
<?php foreach ($character['media'] as $sid => $series): ?>
<article class="media">
<?php
$link = $url->generate('anime.details', ['id' => $series['slug']]);
$titles = Kitsu::filterTitles($series);
?>
<a href="<?= $link ?>">
<?= $helper->picture("images/anime/{$sid}.webp") ?>
</a>
<div class="name">
<a href="<?= $link ?>">
<?= array_shift($titles) ?>
<?php foreach ($titles as $title): ?>
<br />
<small><?= $title ?></small>
<?php endforeach ?>
</a>
</div>
</article>
<?php endforeach ?>
</section>
</td>
</tr>
<?php endforeach; ?>
</table>
</section>
<?php $i++ ?>
<?php endforeach ?>
</div>

View File

@ -0,0 +1,67 @@
<?php
use Aviat\AnimeClient\API\Kitsu;
?>
<main class="details fixed">
<section class="flex flex-no-wrap">
<div>
<?= $helper->picture("images/people/{$data['id']}-original.webp", 'jpg', ['class' => 'cover' ]) ?>
</div>
<div>
<h2 class="toph"><?= $data['attributes']['name'] ?></h2>
</div>
</section>
<?php if ( ! empty($staff)): ?>
<section>
<h3>Castings</h3>
<div class="vertical-tabs">
<?php $i = 0 ?>
<?php foreach ($staff as $role => $entries): ?>
<div class="tab">
<input
type="radio" name="staff-roles" id="staff-role<?= $i ?>" <?= $i === 0 ? 'checked' : '' ?> />
<label for="staff-role<?= $i ?>"><?= $role ?></label>
<?php foreach ($entries as $type => $casting): ?>
<?php if ($type === 'characters') continue; ?>
<?php if ( ! (empty($entries['manga']) || empty($entries['anime']))): ?>
<h4><?= ucfirst($type) ?></h4>
<?php endif ?>
<section class="content">
<?php foreach ($casting as $sid => $series): ?>
<article class="media">
<?php
$mediaType = (in_array($type, ['anime', 'manga'])) ? $type : 'anime';
$link = $url->generate("{$mediaType}.details", ['id' => $series['slug']]);
$titles = Kitsu::filterTitles($series);
?>
<a href="<?= $link ?>">
<?= $helper->picture("images/{$type}/{$sid}.webp") ?>
</a>
<div class="name">
<a href="<?= $link ?>">
<?= array_shift($titles) ?>
<?php foreach ($titles as $title): ?>
<br />
<small><?= $title ?></small>
<?php endforeach ?>
</a>
</div>
</article>
<?php endforeach; ?>
</section>
<?php endforeach ?>
</div>
<?php $i++ ?>
<?php endforeach ?>
</div>
</section>
<?php endif ?>
<?php if ( ! (empty($characters['main']) || empty($characters['supporting']))): ?>
<section>
<?php include 'character-mapping.php' ?>
</section>
<?php endif ?>
</main>

View File

@ -0,0 +1,24 @@
<?php
// Higher scoped variables:
// $fields
// $hiddenFields
// $nestedPrefix
?>
<?php foreach ($fields as $name => $field): ?>
<?php $fieldname = ($section === 'config' || $nestedPrefix !== 'config') ? "{$nestedPrefix}[{$name}]" : "{$nestedPrefix}[{$section}][{$name}]"; ?>
<?php if ($field['type'] === 'subfield'): ?>
<section>
<h4><?= $field['title'] ?></h4>
<?php include_once '_form.php'; ?>
</section>
<?php elseif ( ! empty($field['display'])): ?>
<article>
<label for="<?= $fieldname ?>"><?= $field['title'] ?></label><br />
<small><?= $field['description'] ?></small><br />
<?= $helper->field($fieldname, $field); ?>
</article>
<?php else: ?>
<?php $hiddenFields[] = $helper->field($fieldname, $field); ?>
<?php endif ?>
<?php endforeach ?>

View File

@ -0,0 +1,66 @@
<?php
if ( ! $auth->isAuthenticated())
{
echo '<h1>Not Authorized</h1>';
return;
}
$sectionMapping = [
'anilist' => 'Anilist API Integration',
'config' => 'General Settings',
'cache' => 'Caching',
'database' => 'Collection Database Settings',
];
$hiddenFields = [];
$nestedPrefix = 'config';
?>
<form action="<?= $url->generate('settings-post') ?>" method="POST">
<main class='settings form'>
<button type="submit">Save Changes</button>
<div class="tabs">
<?php $i = 0; ?>
<?php foreach ($form as $section => $fields): ?>
<input <?= $i === 0 ? 'checked="checked"' : '' ?> type="radio" id="settings-tab<?= $i ?>"
name="settings-tabs"
/>
<label for="settings-tab<?= $i ?>"><h3><?= $sectionMapping[$section] ?></h3></label>
<section class="content">
<?php require __DIR__ . '/_form.php' ?>
<?php if ($section === 'anilist'): ?>
<hr />
<?php $auth = $anilistModel->checkAuth(); ?>
<?php if (array_key_exists('errors', $auth)): ?>
<p class="static-message error">Not Authorized.</p>
<?= $helper->a(
$url->generate('anilist-redirect'),
'Link Anilist Account'
) ?>
<?php else: ?>
<?php $expires = $config->get(['anilist', 'access_token_expires']); ?>
<p class="static-message info">
Linked to Anilist. Your access token will expire around <?= date('F j, Y, g:i a T', $expires) ?>
</p>
<?= $helper->a(
$url->generate('anilist-redirect'),
'Update Access Token'
) ?>
<?php endif ?>
<?php endif ?>
</section>
<?php $i++; ?>
<?php endforeach ?>
</div>
<br />
<?php foreach ($hiddenFields as $field): ?>
<?= $field->__toString() ?>
<?php endforeach ?>
<button type="submit">Save Changes</button>
</main>
</form>

27
app/views/setup-check.php Normal file
View File

@ -0,0 +1,27 @@
<?php
$setupErrors = \Aviat\AnimeClient\checkFolderPermissions($container->get('config'));
?>
<?php if ( ! empty($setupErrors)): ?>
<aside class="message error">
<h1>Issues with server setup:</h1>
<?php if (array_key_exists('missing', $setupErrors)): ?>
<h3>The following folders need to be created, and writable.</h3>
<ul>
<?php foreach ($setupErrors['missing'] as $error): ?>
<li><?= $error ?></li>
<?php endforeach ?>
</ul>
<?php endif ?>
<?php if (array_key_exists('writable', $setupErrors)): ?>
<h3>The following folders are not writable by the server.</h3>
<ul>
<?php foreach($setupErrors['writable'] as $error): ?>
<li><?= $error ?></li>
<?php endforeach ?>
</ul>
<?php endif ?>
</aside>
<?php endif ?>

View File

@ -1,27 +1,31 @@
<?php use Aviat\AnimeClient\API\Kitsu; ?>
<?php
use function Aviat\AnimeClient\getLocalImg;
use Aviat\AnimeClient\API\Kitsu;
?>
<main class="user-page details">
<h2 class="toph">
<?= $helper->a(
"https://kitsu.io/users/{$attributes['slug']}",
$attributes['name'], [
'title' => 'View profile on Kitsu'
])
?>
</h2>
<p><?= $escape->html($attributes['about']) ?></p>
<section class="flex flex-no-wrap">
<div>
<aside class="info">
<center>
<h2>
<a title='View profile on Kisu'
href="https://kitsu.io/users/<?= $attributes['name'] ?>">
<?= $attributes['name'] ?>
</a>
</h2>
<?php
$file = basename(parse_url($attributes['avatar']['original'], \PHP_URL_PATH));
$parts = explode('.', $file);
$ext = end($parts);
$avatar = $urlGenerator->assetUrl(
getLocalImg($attributes['avatar']['original'], FALSE)
);
echo $helper->img($avatar, ['alt' => '']);
?>
<img src="<?= $urlGenerator->assetUrl('images/avatars', "{$data['id']}.{$ext}") ?>" alt="" />
</center>
<br />
<br />
<table class="media_details">
<tr>
<th colspan="2">General</th>
</tr>
<table class="media-details">
<tr>
<td>Location</td>
<td><?= $attributes['location'] ?></td>
@ -38,54 +42,69 @@
$character = $relationships['waifu']['attributes'];
echo $helper->a(
$url->generate('character', ['slug' => $character['slug']]),
$character['name']
$character['canonicalName']
);
?>
</td>
</tr>
<?php endif ?>
</table>
<h3>User Stats</h3><br />
<table class="media-details">
<tr>
<th colspan="2">User Stats</th>
<td>Time spent watching anime:</td>
<td><?= $timeOnAnime ?></td>
</tr>
<tr>
<td># of Anime episodes watched</td>
<td><?= number_format($stats['anime-amount-consumed']['units']) ?></td>
</tr>
<tr>
<td># of Manga chapters read</td>
<td><?= number_format($stats['manga-amount-consumed']['units']) ?></td>
</tr>
<tr>
<td># of Posts</td>
<td><?= $attributes['postsCount'] ?></td>
<td><?= number_format($attributes['postsCount']) ?></td>
</tr>
<tr>
<td># of Comments</td>
<td><?= $attributes['commentsCount'] ?></td>
<td><?= number_format($attributes['commentsCount']) ?></td>
</tr>
<tr>
<td># of Media Rated</td>
<td><?= $attributes['ratingsCount'] ?></td>
<td><?= number_format($attributes['ratingsCount']) ?></td>
</tr>
</table>
</div>
<div>
<dl>
<dt>About:</dt>
<dd><?= $escape->html($attributes['about']) ?></dd>
</dl>
</aside>
<article>
<?php if ( ! empty($favorites)): ?>
<h3>Favorites</h3>
<div class="tabs">
<?php $i = 0 ?>
<?php if ( ! empty($favorites['characters'])): ?>
<h4>Favorite Characters</h4>
<section class="media-wrap">
<input type="radio" name="user-favorites" id="user-fav-chars" <?= $i === 0 ? 'checked' : '' ?> />
<label for="user-fav-chars">Characters</label>
<section class="content full-width media-wrap">
<?php foreach($favorites['characters'] as $id => $char): ?>
<?php if ( ! empty($char['image']['original'])): ?>
<article class="small_character">
<article class="character">
<?php $link = $url->generate('character', ['slug' => $char['slug']]) ?>
<div class="name"><?= $helper->a($link, $char['name']); ?></div>
<div class="name"><?= $helper->a($link, $char['canonicalName']); ?></div>
<a href="<?= $link ?>">
<?= $helper->img($urlGenerator->assetUrl('images/characters', "{$char['id']}.jpg")) ?>
<?= $helper->picture("images/characters/{$char['id']}.webp") ?>
</a>
</article>
<?php endif ?>
<?php endforeach ?>
</section>
<?php $i++; ?>
<?php endif ?>
<?php if ( ! empty($favorites['anime'])): ?>
<h4>Favorite Anime</h4>
<section class="media-wrap">
<input type="radio" name="user-favorites" id="user-fav-anime" <?= $i === 0 ? 'checked' : '' ?> />
<label for="user-fav-anime">Anime</label>
<section class="content full-width media-wrap">
<?php foreach($favorites['anime'] as $anime): ?>
<article class="media">
<?php
@ -93,7 +112,7 @@
$titles = Kitsu::filterTitles($anime);
?>
<a href="<?= $link ?>">
<img src="<?= $urlGenerator->assetUrl('images/anime', "{$anime['id']}.jpg") ?>" width="220" alt="" />
<?= $helper->picture("images/anime/{$anime['id']}.webp") ?>
</a>
<div class="name">
<a href="<?= $link ?>">
@ -106,10 +125,12 @@
</article>
<?php endforeach ?>
</section>
<?php $i++; ?>
<?php endif ?>
<?php if ( ! empty($favorites['manga'])): ?>
<h4>Favorite Manga</h4>
<section class="media-wrap">
<input type="radio" name="user-favorites" id="user-fav-manga" <?= $i === 0 ? 'checked' : '' ?> />
<label for="user-fav-manga">Manga</label>
<section class="content full-width media-wrap">
<?php foreach($favorites['manga'] as $manga): ?>
<article class="media">
<?php
@ -117,7 +138,7 @@
$titles = Kitsu::filterTitles($manga);
?>
<a href="<?= $link ?>">
<img src="<?= $urlGenerator->assetUrl('images/manga', "{$manga['id']}.jpg") ?>" width="220" alt="" />
<?= $helper->picture("images/manga/{$manga['id']}.webp") ?>
</a>
<div class="name">
<a href="<?= $link ?>">
@ -130,8 +151,10 @@
</article>
<?php endforeach ?>
</section>
<?php $i++; ?>
<?php endif ?>
</div>
<?php endif ?>
</div>
</article>
</section>
</main>

View File

@ -6,7 +6,7 @@
set -xe
# Install git (the php image doesn't have it) which is required by composer
echo -e 'http://dl-cdn.alpinelinux.org/alpine/edge/main\nhttp://dl-cdn.alpinelinux.org/alpine/edge/community\nhttp://dl-cdn.alpinelinux.org/alpine/edge/testing' > /etc/apk/repositories
apk add --no-cache \
# echo -e 'http://dl-cdn.alpinelinux.org/alpine/edge/main\nhttp://dl-cdn.alpinelinux.org/alpine/edge/community\nhttp://dl-cdn.alpinelinux.org/alpine/edge/testing' > /etc/apk/repositories
apk --update add --no-cache \
curl \
git

View File

@ -1,15 +1,15 @@
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu and MyAnimeList to manage anime and manga watch lists
* An API client for Kitsu to manage anime and manga watch lists
*
* PHP version 7
* PHP version 7.1
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2018 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.0
* @version 4.1
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/

View File

@ -23,13 +23,17 @@
"aura/router": "^3.0",
"aura/session": "^2.0",
"aviat/banker": "^1.0.0",
"aviat/ion": "^2.3.0",
"aviat/ion": "^2.4.1",
"ext-iconv": "*",
"ext-json": "*",
"ext-gd":"*",
"ext-pdo": "*",
"maximebf/consolekit": "^1.0",
"monolog/monolog": "^1.0",
"psr/http-message": "~1.0",
"psr/log": "~1.0",
"yosymfony/toml": "^1.0",
"zendframework/zend-diactoros": "^1.3"
"zendframework/zend-diactoros": "^2.0.0"
},
"require-dev": {
"consolidation/robo": "~1.0",
@ -40,7 +44,7 @@
"phpstan/phpstan": "^0.9.1",
"phpunit/phpunit": "^6.0",
"roave/security-advisories": "dev-master",
"robmorgan/phinx": "^0.9.1",
"robmorgan/phinx": "^0.10.6",
"sebastian/phpcpd": "^3.0",
"spatie/phpunit-snapshot-assertions": "^1.2.0",
"squizlabs/php_codesniffer": "^3.2.2",

View File

@ -17,7 +17,13 @@ try
(new Console([
'cache:clear' => Command\CacheClear::class,
'cache:refresh' => Command\CachePrime::class,
'clear:cache' => Command\CacheClear::class,
'clear:thumbnails' => Command\ClearThumbnails::class,
'refresh:cache' => Command\CachePrime::class,
'refresh:thumbnails' => Command\UpdateThumbnails::class,
'regenerate-thumbnails' => Command\UpdateThumbnails::class,
'lists:sync' => Command\SyncLists::class,
'mal_id:check' => Command\MALIDCheck::class,
]))->run();
}
catch (\Exception $e)

View File

@ -2,22 +2,26 @@
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu and MyAnimeList to manage anime and manga watch lists
* An API client for Kitsu to manage anime and manga watch lists
*
* PHP version 7
* PHP version 7.1
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2018 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.0
* @version 4.1
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient;
use Aviat\AnimeClient\Types\Config as ConfigType;
use function Aviat\Ion\_dir;
setlocale(LC_CTYPE, 'en_US');
// Work around the silly timezone error
$timezone = ini_get('date.timezone');
if ($timezone === '' || $timezone === FALSE)
@ -43,16 +47,23 @@ $CONF_DIR = _dir($APP_DIR, 'config');
// -----------------------------------------------------------------------------
// Dependency Injection setup
// -----------------------------------------------------------------------------
$base_config = require $APPCONF_DIR . '/base_config.php';
$baseConfig = require $APPCONF_DIR . '/base_config.php';
$di = require $APP_DIR . '/bootstrap.php';
$config = loadToml($CONF_DIR);
$config_array = array_merge($base_config, $config);
$container = $di($config_array);
$overrideFile = $CONF_DIR . '/admin-override.toml';
$overrideConfig = file_exists($overrideFile)
? loadTomlFile($overrideFile)
: [];
$configArray = array_replace_recursive($baseConfig, $config, $overrideConfig);
$checkedConfig = (new ConfigType($configArray))->toArray();
$container = $di($checkedConfig);
// Unset 'constants'
unset($APP_DIR, $APPCONF_DIR);
unset($APP_DIR, $CONF_DIR, $APPCONF_DIR);
// -----------------------------------------------------------------------------
// Dispatch to the current route

4
public/css/all.css Normal file
View File

@ -0,0 +1,4 @@
@import "./marx.css";
@import "./general.css";
@import "./components.css";
@import "./responsive.css";

File diff suppressed because one or more lines are too long

264
public/css/components.css Normal file
View File

@ -0,0 +1,264 @@
/* -----------------------------------------------------------------------------
CSS loading icon
------------------------------------------------------------------------------*/
.cssload-loader {
position: relative;
left: calc(50% - 31px);
width: 62px;
height: 62px;
border-radius: 50%;
perspective: 780px;
}
.cssload-inner {
position: absolute;
width: 100%;
height: 100%;
box-sizing: border-box;
border-radius: 50%;
}
.cssload-inner.cssload-one {
left: 0%;
top: 0%;
animation: cssload-rotate-one 1.15s linear infinite;
border-bottom: 3px solid rgb(0, 0, 0);
}
.cssload-inner.cssload-two {
right: 0%;
top: 0%;
animation: cssload-rotate-two 1.15s linear infinite;
border-right: 3px solid rgb(0, 0, 0);
}
.cssload-inner.cssload-three {
right: 0%;
bottom: 0%;
animation: cssload-rotate-three 1.15s linear infinite;
border-top: 3px solid rgb(0, 0, 0);
}
@keyframes cssload-rotate-one {
0% {
transform: rotateX(35deg) rotateY(-45deg) rotateZ(0deg);
}
100% {
transform: rotateX(35deg) rotateY(-45deg) rotateZ(360deg);
}
}
@keyframes cssload-rotate-two {
0% {
transform: rotateX(50deg) rotateY(10deg) rotateZ(0deg);
}
100% {
transform: rotateX(50deg) rotateY(10deg) rotateZ(360deg);
}
}
@keyframes cssload-rotate-three {
0% {
transform: rotateX(35deg) rotateY(55deg) rotateZ(0deg);
}
100% {
transform: rotateX(35deg) rotateY(55deg) rotateZ(360deg);
}
}
/* ----------------------------------------------------------------------------
Loading overlay
-----------------------------------------------------------------------------*/
#loading-shadow {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
z-index: 500;
}
#loading-shadow .loading-wrapper {
position: fixed;
z-index: 501;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
#loading-shadow .loading-content {
position: relative;
color: #fff
}
.loading-content .cssload-inner.cssload-one,
.loading-content .cssload-inner.cssload-two,
.loading-content .cssload-inner.cssload-three {
border-color: #fff
}
/* ----------------------------------------------------------------------------
CSS Tabs
-----------------------------------------------------------------------------*/
.tabs {
display: inline-block;
display: flex;
flex-wrap: wrap;
background: #efefef;
box-shadow: 0 48px 80px -32px rgba(0, 0, 0, 0.3);
margin-top: 1.5em;
}
.tabs > label {
border: 1px solid #e5e5e5;
width: 100%;
padding: 20px 30px;
background: #e5e5e5;
cursor: pointer;
font-weight: bold;
font-size: 18px;
color: #7f7f7f;
transition: background 0.1s, color 0.1s;
/* margin-left: 4em; */
}
.tabs > label:hover {
background: #d8d8d8;
}
.tabs > label:active {
background: #ccc;
}
.tabs > [type=radio]:focus + label {
box-shadow: inset 0px 0px 0px 3px #2aa1c0;
z-index: 1;
}
.tabs > [type=radio] {
position: absolute;
opacity: 0;
}
.tabs > [type=radio]:checked + label {
border-bottom: 1px solid #fff;
background: #fff;
color: #000;
}
.tabs > [type=radio]:checked + label + .content {
border: 1px solid #e5e5e5;
border-top: 0;
display: block;
padding: 15px;
background: #fff;
width: 100%;
margin: 0 auto;
overflow: auto;
/* text-align: center; */
}
.tabs .content {
display: none;
max-height: 950px;
border: 1px solid #e5e5e5;
border-top: 0;
padding: 15px;
background: #fff;
width: 100%;
margin: 0 auto;
overflow: auto;
}
.tabs .content.full-height {
max-height: none;
}
@media (min-width: 800px) {
.tabs > label {
width: auto;
}
.tabs .content {
order: 99;
}
}
/* ---------------------------------------------------------------------------
Vertical Tabs
----------------------------------------------------------------------------*/
.vertical-tabs {
border: 1px solid #e5e5e5;
box-shadow: 0 48px 80px -32px rgba(0, 0, 0, 0.3);
margin: 0 auto;
position: relative;
width: 100%;
}
.vertical-tabs input[type="radio"] {
position: absolute;
opacity: 0;
}
.vertical-tabs .tab {
align-items: center;
display: inline-block;
display: flex;
flex-wrap: nowrap;
}
.vertical-tabs .tab label {
align-items: center;
background: #e5e5e5;
border: 1px solid #e5e5e5;
color: #7f7f7f;
cursor: pointer;
font-size: 18px;
font-weight: bold;
padding: 0 20px;
width: 28%;
}
.vertical-tabs .tab label:hover {
background: #d8d8d8;
}
.vertical-tabs .tab label:active {
background: #ccc;
}
.vertical-tabs .tab .content {
display: none;
border: 1px solid #e5e5e5;
border-left: 0;
border-right: 0;
max-height: 950px;
overflow: auto;
}
.vertical-tabs .tab .content.full-height {
max-height: none;
}
.vertical-tabs [type=radio]:checked + label {
border: 0;
background: #fff;
color: #000;
width: 38%;
}
.vertical-tabs [type=radio]:focus + label {
box-shadow: inset 0px 0px 0px 3px #2aa1c0;
z-index: 1;
}
.vertical-tabs [type=radio]:checked ~ .content {
display: block;
}

View File

@ -1,5 +1,3 @@
@import "./marx.css";
:root {
--blue-link: rgb(18, 113, 219);
--link-shadow: 1px 1px 1px #000;
@ -30,6 +28,7 @@ button {
table {
/* min-width: 85%; */
box-shadow: 0 48px 80px -32px rgba(0, 0, 0, 0.3);
margin: 0 auto;
}
@ -55,6 +54,11 @@ a:hover, a:active {
color: var(--link-hover-color)
}
iframe {
display: block;
margin: 0 auto;
}
/* -----------------------------------------------------------------------------
Utility classes
------------------------------------------------------------------------------*/
@ -91,6 +95,10 @@ a:hover, a:active {
flex-wrap: nowrap
}
.flex-align-start {
align-content: flex-start;
}
.flex-align-end {
align-items: flex-end
}
@ -99,15 +107,28 @@ a:hover, a:active {
align-content: space-around
}
.flex-justify-start {
justify-content: flex-start;
}
.flex-justify-space-around {
justify-content: space-around
}
.flex-center {
justify-content: center;
}
.flex-self-center {
align-self: center
}
.flex-space-evenly {
justify-content: space-evenly;
}
.flex {
display: inline-block;
display: flex
}
@ -119,23 +140,23 @@ a:hover, a:active {
text-align: justify
}
.align_center {
.align-center {
text-align: center !important
}
.align_left {
.align-left {
text-align: left !important
}
.align_right {
.align-right {
text-align: right !important
}
.valign_top {
.valign-top {
vertical-align: top
}
.no_border {
.no-border {
border: none
}
@ -145,6 +166,19 @@ a:hover, a:active {
position: relative;
}
.media-wrap-flex {
display: inline-block;
display: flex;
flex-wrap: wrap;
align-content: space-evenly;
justify-content: space-between;
position: relative;
}
td .media-wrap-flex {
justify-content: center;
}
.danger {
background-color: #ff4136;
border-color: #924949;
@ -170,10 +204,18 @@ a:hover, a:active {
background-color: var(--edit-link-hover-color);
}
.full_width {
.full-width {
width: 100%;
}
.full-height {
max-height: none;
}
.toph {
margin-top: 0;
}
/* -----------------------------------------------------------------------------
Main Nav
------------------------------------------------------------------------------*/
@ -188,81 +230,12 @@ a:hover, a:active {
font-weight: 500;
}
/* -----------------------------------------------------------------------------
CSS loading icon
------------------------------------------------------------------------------*/
.cssload-loader {
position: relative;
left: calc(50% - 31px);
width: 62px;
height: 62px;
border-radius: 50%;
perspective: 780px;
}
.cssload-inner {
position: absolute;
width: 100%;
height: 100%;
box-sizing: border-box;
border-radius: 50%;
}
.cssload-inner.cssload-one {
left: 0%;
top: 0%;
animation: cssload-rotate-one 1.15s linear infinite;
border-bottom: 3px solid rgb(0, 0, 0);
}
.cssload-inner.cssload-two {
right: 0%;
top: 0%;
animation: cssload-rotate-two 1.15s linear infinite;
border-right: 3px solid rgb(0, 0, 0);
}
.cssload-inner.cssload-three {
right: 0%;
bottom: 0%;
animation: cssload-rotate-three 1.15s linear infinite;
border-top: 3px solid rgb(0, 0, 0);
}
@keyframes cssload-rotate-one {
0% {
transform: rotateX(35deg) rotateY(-45deg) rotateZ(0deg);
}
100% {
transform: rotateX(35deg) rotateY(-45deg) rotateZ(360deg);
}
}
@keyframes cssload-rotate-two {
0% {
transform: rotateX(50deg) rotateY(10deg) rotateZ(0deg);
}
100% {
transform: rotateX(50deg) rotateY(10deg) rotateZ(360deg);
}
}
@keyframes cssload-rotate-three {
0% {
transform: rotateX(35deg) rotateY(55deg) rotateZ(0deg);
}
100% {
transform: rotateX(35deg) rotateY(55deg) rotateZ(360deg);
}
}
/* -----------------------------------------------------------------------------
Table sorting and form styles
------------------------------------------------------------------------------*/
.sorting,
.sorting_asc,
.sorting_desc {
.sorting-asc,
.sorting-desc {
vertical-align: text-bottom;
}
@ -270,11 +243,11 @@ a:hover, a:active {
content: " ↕\00a0";
}
.sorting_asc::before {
.sorting-asc::before {
content: " ↑\00a0";
}
.sorting_desc::before {
.sorting-desc::before {
content: " ↓\00a0";
}
@ -302,7 +275,14 @@ a:hover, a:active {
background: inherit;
}
.invisible tr, .invisible td, .invisible th {
.borderless,
.borderless tr,
.borderless td,
.borderless th,
.invisible tr,
.invisible td,
.invisible th {
box-shadow: none;
border: 0;
}
@ -310,7 +290,7 @@ a:hover, a:active {
Message boxes
------------------------------------------------------------------------------*/
.message {
.message, .static-message {
position: relative;
margin: 0.5em auto;
padding: 0.5em;
@ -342,7 +322,7 @@ a:hover, a:active {
margin-right: 1em;
}
.message.error {
.message.error, .static-message.error {
border: 1px solid #924949;
background: #f3e6e6;
}
@ -351,7 +331,7 @@ a:hover, a:active {
content: '✘';
}
.message.success {
.message.success, .static-message.success {
border: 1px solid #1f8454;
background: #70dda9;
}
@ -360,7 +340,7 @@ a:hover, a:active {
content: '✔'
}
.message.info {
.message.info, .static-message.info {
border: 1px solid #bfbe3a;
background: #FFFFCC;
}
@ -373,7 +353,7 @@ a:hover, a:active {
Base list styles
------------------------------------------------------------------------------*/
.media, .character, .small_character {
.media, .character, .small-character {
position: relative;
vertical-align: top;
display: inline-block;
@ -382,21 +362,28 @@ a:hover, a:active {
height: 311px;
margin: var(--normal-padding);
z-index: 0;
background: rgba(0, 0, 0, 0.15);
}
.details picture.cover,
picture.cover {
display: initial;
width: 100%;
}
.media > img,
.character > img,
.small_character > img {
.small-character > img {
width: 100%;
}
.media .edit_buttons > button {
.media .edit-buttons > button {
margin: 0.5em auto;
}
.name,
.media_metadata > div,
.medium_metadata > div,
.media-metadata > div,
.medium-metadata > div,
.row {
text-shadow: var(--shadow);
color: var(--text-color);
@ -405,17 +392,17 @@ a:hover, a:active {
z-index: 2;
}
.media_type, .age_rating {
.media-type, .age-rating {
text-align: left;
}
.media > .media_metadata {
.media > .media-metadata {
position: absolute;
bottom: 0;
right: 0;
}
.media > .medium_metadata {
.media > .medium-metadata {
position: absolute;
bottom: 0;
left: 0;
@ -427,6 +414,7 @@ a:hover, a:active {
}
.media > .name a {
display: inline-block;
transition: none;
}
@ -466,7 +454,7 @@ a:hover, a:active {
}
.media:hover > button[hidden],
.media:hover > .edit_buttons[hidden] {
.media:hover > .edit-buttons[hidden] {
transition: .25s ease;
display: block;
@ -476,8 +464,8 @@ a:hover, a:active {
transition: .25s ease;
}
.small_character > .name a,
.small_character > .name a small,
.small-character > .name a,
.small-character > .name a small,
.character > .name a,
.character > .name a small,
.media > .name a,
@ -498,11 +486,11 @@ a:hover, a:active {
padding: 0.5em 0.25em;
}
.anime .media_type,
.anime .airing_status,
.anime .user_rating,
.anime .media-type,
.anime .airing-status,
.anime .user-rating,
.anime .completion,
.anime .age_rating,
.anime .age-rating,
.anime .edit,
.anime .delete {
background: none;
@ -518,6 +506,7 @@ a:hover, a:active {
.anime .row, .manga .row {
width: 100%;
display: inline-block;
display: flex;
align-content: space-around;
justify-content: space-around;
@ -532,6 +521,7 @@ a:hover, a:active {
.anime .row > div, .manga .row > div {
font-size: 0.8em;
display: inline-block;
display: flex-item;
align-self: center;
text-align: center;
@ -539,7 +529,7 @@ a:hover, a:active {
z-index: 2;
}
.anime .media > button.plus_one {
.anime .media > button.plus-one {
border-color: hsla(0, 0%, 100%, .65);
position: absolute;
top: 138px;
@ -549,12 +539,12 @@ a:hover, a:active {
z-index: 50;
}
.anime .media > button.plus_one:hover {
.anime .media > button.plus-one:hover {
color: hsla(0, 0%, 100%, .65);
background: #888;
}
.anime .media > button.plus_one:active {
.anime .media > button.plus-one:active {
background: #444;
}
@ -571,7 +561,7 @@ a:hover, a:active {
margin: 0.25em;
}
.manga .media > .edit_buttons {
.manga .media > .edit-buttons {
position: absolute;
top: 86px;
/* top: calc(50% - 58.5px); */
@ -581,16 +571,16 @@ a:hover, a:active {
z-index: 40;
}
.manga .media > .edit_buttons button {
.manga .media > .edit-buttons button {
border-color: hsla(0, 0%, 100%, .65);
}
.manga .media > .edit_buttons:hover button {
.manga .media > .edit-buttons:hover button {
color: hsla(0, 0%, 100%, .65);
background: #888;
}
.manga .media > .edit_buttons button:active {
.manga .media > .edit-buttons button:active {
background: #444;
}
@ -605,7 +595,11 @@ a:hover, a:active {
background-repeat: no-repeat;
}
.big-check {
.media.search > .row {
z-index: 6;
}
.big-check, .mal-check {
display: none;
}
@ -628,11 +622,11 @@ a:hover, a:active {
z-index: 5;
}
#series_list article.media {
#series-list article.media {
position: relative;
}
#series_list .name, #series_list .name label {
#series-list .name, #series-list .name label {
position: absolute;
display: block;
top: 0;
@ -643,7 +637,7 @@ a:hover, a:active {
line-height: 1.25em;
}
#series_list .name small {
#series-list .name small {
color: #fff;
}
@ -661,28 +655,24 @@ a:hover, a:active {
}
.fixed {
max-width: 93rem;
/* max-width: 100rem; */
max-width: 80%;
margin: 0 auto;
}
.fixed .text {
max-width: 40em;
}
.details .cover {
display: block;
width: 284px;
/* height: 402px; */
}
.details h2 {
margin-top: 0;
}
.details .flex > div {
.details .flex > * {
margin: 1rem;
}
.details .media_details {
max-width: 300px;
}
.details .media_details td {
.details .media-details td {
padding: 0 1.5rem;
}
@ -690,37 +680,58 @@ a:hover, a:active {
text-align: justify;
}
.details .media_details td:nth-child(odd) {
.details .media-details td:nth-child(odd) {
width: 1%;
white-space: nowrap;
text-align: right;
}
.details .media_details td:nth-child(even) {
.details .media-details td:nth-child(even) {
text-align: left;
}
.details a h1,
.details a h2 {
margin-top: 0;
}
.character,
.small_character {
.small-character,
.person {
/* background: rgba(0,0,0,0.5); */
width: 225px;
height: 350px;
vertical-align: middle;
white-space: nowrap;
position: relative;
}
.person {
width: 225px;
height: 338px;
}
.small-person {
width: 200px;
height: 300px;
}
.character a {
height: 350px;
}
.character:hover .name,
.small_character:hover .name {
.small-character:hover .name {
background: rgba(0, 0, 0, 0.8);
}
.small_character a {
.small-character a {
display: inline-block;
width: 100%;
height: 100%;
}
.small_character .name,
.small-character .name,
.character .name {
position: absolute;
bottom: 0;
@ -728,13 +739,30 @@ a:hover, a:active {
z-index: 10;
}
.small_character img,
.character img {
position: relative;
.small-character img,
.character img,
.small-character picture,
.character picture,
.person img,
.person picture {
position: absolute;
top: 50%;
transform: translateY(-50%);
left: 50%;
transform: translate(-50%, -50%);
z-index: 5;
width: 100%;
max-height: 350px;
max-width: 225px;
}
.person img,
.person picture {
max-height: 338px;
}
.small-person img,
.small-person picture {
max-height: 300px;
max-width: 200px;
}
.min-table {
@ -742,14 +770,47 @@ a:hover, a:active {
margin-left: 0;
}
.max-table {
min-width: 100%;
margin: 0;
}
aside.info {
/* max-width: 390px; */
max-width: 33%;
}
.fixed aside.info {
max-width: 390px;
}
/* .fixed aside.info + article {
max-width: inherit;
} */
aside.info picture, aside.info img {
display: block;
margin: 0 auto;
}
aside.info + article {
max-width: 66%;
}
/* ----------------------------------------------------------------------------
User page styles
-----------------------------------------------------------------------------*/
.small_character {
.small-character {
width: 160px;
height: 250px;
}
.small-character img,
.small-character picture {
max-height: 250px;
max-width: 160px;
}
.user-page .media-wrap {
text-align: left;
}
@ -760,26 +821,6 @@ a:hover, a:active {
height: 100%;
}
/* ----------------------------------------------------------------------------
Viewport-based styles
-----------------------------------------------------------------------------*/
@media screen and (max-width: 40em) {
nav a {
line-height: 4em;
line-height: 4rem;
}
.media {
margin: 2px 0;
}
main {
padding: 0 0, 5em 0.5em;
padding: 0 0.5rem 0.5rem;
}
}
/* ----------------------------------------------------------------------------
Images / Logos
-----------------------------------------------------------------------------*/
@ -789,15 +830,15 @@ a:hover, a:active {
vertical-align: middle;
}
.cover_streaming_link {
display:none;
.cover-streaming-link {
display: none;
}
.media:hover .cover_streaming_link {
.media:hover .cover-streaming-link {
display: block;
}
.cover_streaming_link .streaming-logo {
.cover-streaming-link .streaming-logo {
width: 20px;
height: 20px;
-webkit-filter: drop-shadow(0 -1px 4px #fff);
@ -805,109 +846,30 @@ a:hover, a:active {
}
/* ----------------------------------------------------------------------------
Loading overlay
Settings Form
-----------------------------------------------------------------------------*/
#loading-shadow {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
z-index: 500;
}
#loading-shadow .loading-wrapper {
position: fixed;
z-index: 501;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
#loading-shadow .loading-content {
position: relative;
color: #fff
}
.loading-content .cssload-inner.cssload-one,
.loading-content .cssload-inner.cssload-two,
.loading-content .cssload-inner.cssload-three {
border-color: #fff
.settings.form .content article {
margin: 1em;
display: inline-block;
width: auto;
}
/* ----------------------------------------------------------------------------
CSS Tabs
iFrame container
-----------------------------------------------------------------------------*/
.tabs {
display: flex;
flex-wrap: wrap;
background: #efefef;
box-shadow: 0 48px 80px -32px rgba(0, 0, 0, 0.3);
margin-top: 1.5em;
.responsive-iframe {
margin-top: 1em;
overflow: hidden;
padding-bottom: 56.25%;
position: relative;
height: 0;
}
.tabs label {
border: 1px solid #e5e5e5;
.responsive-iframe iframe {
left: 0;
top: 0;
height: 100%;
width: 100%;
padding: 20px 30px;
background: #e5e5e5;
cursor: pointer;
font-weight: bold;
font-size: 18px;
color: #7f7f7f;
transition: background 0.1s, color 0.1s;
/* margin-left: 4em; */
}
.tabs label:hover {
background: #d8d8d8;
}
.tabs label:active {
background: #ccc;
}
.tabs [type=radio]:focus + label {
box-shadow: inset 0px 0px 0px 3px #2aa1c0;
z-index: 1;
}
.tabs [type=radio] {
position: absolute;
opacity: 0;
}
.tabs [type=radio]:checked + label {
border-bottom: 1px solid #fff;
background: #fff;
color: #000;
}
.tabs [type=radio]:checked + label + .content {
border: 1px solid #e5e5e5;
border-top: 0;
display: block;
padding: 20px 30px 30px;
background: #fff;
width: 100%;
}
.tabs .content {
display: none;
}
@media (min-width: 600px) {
.tabs label {
width: auto;
}
.tabs .content {
order: 99;
}
}
}

133
public/css/responsive.css Normal file
View File

@ -0,0 +1,133 @@
/* ----------------------------------------------------------------------------
Viewport-based styles
-----------------------------------------------------------------------------*/
@media screen and (max-width: 1100px) {
.flex {
flex-wrap: wrap;
}
aside.info,
aside.info + article,
.fixed aside.info,
.fixed aside.info + article {
max-width: none;
width: 100%;
}
/* aside.info {
order: 1;
} */
}
@media screen and (max-width: 800px) {
* {
max-width: none;
}
table {
box-shadow: none;
}
body,
.details .flex > * {
margin: 0;
}
table,
table th,
table td,
table .align-right,
table.align-center {
border: 0;
display: block;
margin: 0 auto;
text-align: left;
width: 100%;
}
table tbody {
width: 100%;
}
table td {
display: inline-block;
}
table.media-details td {
display: block;
text-align: left !important;
}
table thead {
display: none;
}
.details .media-details td:nth-child(2n+1) {
font-weight: bold;
width: 100%;
}
table.streaming-links tr td:not(:first-child) {
display:none;
}
}
@media screen and (max-width: 40em) {
nav a {
line-height: 4em;
line-height: 4rem;
}
img,
picture {
width: 100%;
}
main {
padding: 0 0, 5em 0.5em;
padding: 0 0.5rem 0.5rem;
}
.media {
margin: 2px 0;
}
.details {
padding: 0.5em;
padding: 0.5rem;
}
/* Expand tabs */
.tabs > [type="radio"]:checked + label {
background: #fff;
}
/* Expand vertical tabs */
.vertical-tabs .tab {
flex-wrap: wrap;
}
.tabs .content,
.tabs > [type="radio"]:checked + label + .content,
.vertical-tabs .tab .content {
display: block;
border: 0;
max-height: none;
}
.tabs > label,
.tabs > label:active,
.tabs > label:hover,
.tabs > [type="radio"]:checked + label,
.vertical-tabs .tab label,
.vertical-tabs .tab label:active,
.vertical-tabs .tab label:hover,
.vertical-tabs [type=radio]:focus + label,
.vertical-tabs [type=radio]:checked + label {
background: #fff;
border: 0;
width: 100%;
cursor: default;
color: #000;
}
}

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 807 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -1,3 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 13.229 13.229">
<path d="M6.615 0A6.615 6.615 0 0 0 0 6.615a6.615 6.615 0 0 0 6.615 6.614 6.615 6.615 0 0 0 6.614-6.614A6.615 6.615 0 0 0 6.615 0zM4.464 3.73l2.952 1.435 2.952 1.434-2.952 1.434-2.952 1.434V6.599z" fill="#7cac3f"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 13 13"><path fill="#7cac3f" d="M7 0a7 7 0 0 0-7 7 7 7 0 0 0 7 6 7 7 0 0 0 6-6 7 7 0 0 0-6-7zM4 4l3 1 3 2-3 1-3 1V7z"/></svg>

Before

Width:  |  Height:  |  Size: 298 B

After

Width:  |  Height:  |  Size: 177 B

View File

@ -1,12 +1 @@
<svg viewBox="0 0 50 50" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g fill="#F78B24">
<g transform="translate(0.481873, 0.171360)">
<g transform="translate(0.008636, 0.108555)">
<path d="M22.058368,48.8652576 C21.2437999,48.7878567 19.1006792,48.4091282 18.3056997,48.2020939 C11.4329681,46.4122332 5.61265555,41.6119035 2.53259335,35.1931456 C0.844060443,31.6742922 0.140352507,28.52742 0.143129097,24.5078496 C0.145875175,20.4833961 0.860425034,17.2734889 2.5216192,13.8225546 C3.7658265,11.2378565 5.19692984,9.21306983 7.22861063,7.162902 C11.0289996,3.32792889 15.8428542,0.953911552 21.2955685,0.225599773 C23.0782309,-0.0125069821 26.85181,0.0649676307 28.5167368,0.373857722 C31.9792362,1.01624832 35.0879288,2.27794927 37.8737436,4.17151736 C43.66151,8.10556494 47.4155513,14.1221132 48.3942332,21.0325352 C48.6037692,22.5120388 48.7250543,25.5909577 48.5839262,25.8481746 C48.5069546,25.9884715 48.4669433,25.8414833 48.4165375,25.2333344 C48.1666749,22.2183685 46.8880704,18.5727523 45.124,15.8454988 C39.7621503,7.55607105 29.7583608,4.03921889 20.4961649,7.18752901 C13.3471574,9.61754455 7.974181,15.7826973 6.49950655,23.2478043 C5.77033129,26.9390602 5.98914286,30.5572318 7.15756857,34.129218 C9.2181442,40.4286112 14.1498869,45.4611558 20.4042119,47.6466277 C21.8792017,48.1620406 23.7734361,48.5915232 25.022566,48.6937587 C26.5182429,48.8161727 26.1412979,48.9325989 24.2959131,48.9181996 C23.2890178,48.9103465 22.2821225,48.8865193 22.058368,48.8652576 L22.058368,48.8652576 Z" id="path4379"></path>
<path d="M27.1945511,45.8197025 C19.4634056,45.2453398 13.0577716,39.3655323 11.7209706,31.6163312 C11.4776478,30.2058059 11.430761,27.5690797 11.625936,26.271443 C12.7849538,18.565423 18.5963365,12.7191433 26.1774851,11.6324945 C27.7140785,11.4122467 30.452254,11.4894495 31.9239078,11.7945147 C33.2040277,12.0598772 34.6231093,12.5309255 35.7379051,13.0605271 L36.6024112,13.4712263 L35.7887584,13.8631587 C32.85255,15.2775131 31.0403114,18.5857811 31.4657196,21.7549161 C31.8917278,24.9285151 34.0433512,27.4194493 37.13215,28.3149154 C38.2441591,28.6372959 39.9446018,28.6372015 41.0570787,28.3147079 C42.4955049,27.8977117 43.6235836,27.2157243 44.6502405,26.1424414 C44.9968363,25.7801082 45.3027392,25.520857 45.3300271,25.5663307 C45.357315,25.6118034 45.4365342,26.1113638 45.506071,26.6764634 C45.6776297,28.070634 45.5768893,30.723134 45.3020883,32.0474383 C44.4268623,36.2651472 42.221548,39.8053525 38.8399563,42.4210947 C35.6393311,44.8968539 31.405255,46.1325358 27.1945511,45.8197139 L27.1945511,45.8197025 Z" id="path4375"></path>
</g>
</g>
</g>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50"><g fill="#F78B24" fill-rule="evenodd"><path d="M22.5 49.1A24.4 24.4 0 0 1 3 14.1a24 24 0 0 1 35.4-9.7A24.3 24.3 0 0 1 49 26.2c-.1.2-.1 0-.2-.6a22 22 0 0 0-3.3-9.4A21 21 0 0 0 21 7.5 21.4 21.4 0 0 0 25.5 49a48.7 48.7 0 0 1-3 .2z"/><path d="M27.7 46.1A17.1 17.1 0 0 1 12 26.6a17.1 17.1 0 0 1 24.1-13.3l.9.5-.8.3a7.8 7.8 0 0 0-4.3 8 7.8 7.8 0 0 0 5.6 6.5 8 8 0 0 0 4 0 7.7 7.7 0 0 0 3.5-2.2l.7-.6.2 1.2a17.3 17.3 0 0 1-6.7 15.7 17.1 17.1 0 0 1-11.6 3.4z"/></g></svg>

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 523 B

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -1,9 +1 @@
<svg viewBox="0 0 50 50" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g fill="#411299">
<g>
<path d="M24.0655296,0.0170556553 C28.6912029,-0.156193896 33.3572711,0.991023339 37.3680431,3.30341113 C40.440754,5.06373429 43.1274686,7.49102334 45.2010772,10.3608618 C47.2603232,13.2073609 48.7073609,16.4937163 49.4084381,19.9362657 C50.2271095,23.9371634 50.0493716,28.1373429 48.8895871,32.0529623 C47.9129264,35.3491921 46.2441652,38.4389587 44.021544,41.061939 C41.8895871,43.5807899 39.2603232,45.6822262 36.3177738,47.1795332 C33.061939,48.8581688 29.4263914,49.7881508 25.7657092,49.8976661 C21.6552962,50.037702 17.5098743,49.1481149 13.8330341,47.3007181 C8.01795332,44.4290844 3.42549372,39.1938959 1.3438061,33.0502693 C-0.818671454,26.7935368 -0.354578097,19.6759425 2.64631957,13.7710952 C5.48294434,8.06912029 10.5942549,3.54937163 16.6023339,1.43536804 C18.997307,0.583482944 21.5251346,0.106822262 24.0655296,0.0170556553 L24.0655296,0.0170556553 Z M15.6508079,33.3276481 C16.1149013,35.6113106 17.5897666,37.6849192 19.6418312,38.8070018 C21.8150808,40.024237 24.4057451,40.2513465 26.8429084,39.9883303 C28.8447038,39.7710952 30.8294434,38.9955117 32.2980251,37.5906643 C33.4712747,36.4398564 34.3150808,34.9425494 34.6274686,33.3240575 C33.4389587,33.2971275 32.2504488,33.3240575 31.061939,33.2935368 C30.4937163,33.3761221 29.9245961,33.2459605 29.3572711,33.3078995 C28.1247756,33.3204668 26.8922801,33.3114901 25.6597846,33.2989228 C25.005386,33.3644524 24.3509874,33.2630162 23.6965889,33.3114901 C22.5305206,33.2576302 21.3626571,33.3536804 20.1965889,33.2863555 C18.6822262,33.3653501 17.1660682,33.2513465 15.6508079,33.3276481 L15.6508079,33.3276481 Z" id="Shape"></path>
</g>
</g>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50"><path fill="#411299" fill-rule="evenodd" d="M24.07.02A24.92 24.92 0 0 1 49.4 19.94 24.95 24.95 0 0 1 25.77 49.9c-4.11.14-8.26-.75-11.94-2.6A25.04 25.04 0 0 1 1.34 33.05a25.02 25.02 0 0 1 1.3-19.28A25.05 25.05 0 0 1 24.08.02zm-8.42 33.3a8.15 8.15 0 0 0 4 5.49c2.17 1.21 4.76 1.44 7.2 1.18 2-.22 3.98-1 5.45-2.4a8.22 8.22 0 0 0 2.33-4.27c-1.2-.02-2.38 0-3.57-.03-.57.09-1.14-.04-1.7.02-1.24.01-2.47 0-3.7-.01-.65.06-1.3-.04-1.96.01-1.17-.05-2.34.04-3.5-.02-1.52.08-3.03-.04-4.55.04z"/></svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 549 B

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

View File

@ -1,5 +1 @@
<svg viewBox="0 0 34 50" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<path d="M22.2222222,13.8888889 L11.1111111,13.8888889 L11.1111111,0 L0,0 L0,25 L0,50 L11.1111111,50 L11.1111111,27.7777778 C11.1111111,26.3888889 12.2222222,25 13.8888889,25 L19.4444444,25 C20.8333333,25 22.2222222,26.1111111 22.2222222,27.7777778 L22.2222222,50 L33.3333333,50 L33.3333333,25 C33.3333333,18.8888889 28.3333333,13.8888889 22.2222222,13.8888889 L22.2222222,13.8888889 Z" id="Shape" fill="#8BC34A"></path>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 34 50"><path fill="#8BC34A" fill-rule="evenodd" d="M22.2 13.9h-11V0H0v50h11.1V27.8c0-1.4 1.1-2.8 2.8-2.8h5.5c1.4 0 2.8 1.1 2.8 2.8V50h11.1V25c0-6.1-5-11.1-11-11.1z"/></svg>

Before

Width:  |  Height:  |  Size: 634 B

After

Width:  |  Height:  |  Size: 225 B

View File

@ -1,7 +1 @@
<svg viewBox="0 0 26 50" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g fill="#E21221">
<path d="M0.0567010309,0.257731959 C2.51804124,0.25257732 4.98195876,0.262886598 7.44587629,0.25257732 C10.3041237,8.0128866 13.0670103,15.8092784 15.9020619,23.5773196 C16.4252577,25.0180412 16.9046392,26.4742268 17.492268,27.8891753 C17.5695876,18.6804124 17.5025773,9.46907216 17.5257732,0.257731959 L25.2886598,0.257731959 L25.2886598,46.6185567 C22.4768041,46.9896907 19.6520619,47.2448454 16.8324742,47.5747423 C15.628866,44.1237113 14.435567,40.6726804 13.2190722,37.2268041 C11.4226804,32.0824742 9.66237113,26.9252577 7.81701031,21.7989691 C7.94587629,30.7525773 7.83247423,39.7113402 7.87371134,48.6701031 C5.27061856,49.0592784 2.64690722,49.306701 0.0592783505,49.7886598 C0.0515463918,33.2783505 0.0592783505,16.7680412 0.0567010309,0.257731959 L0.0567010309,0.257731959 Z" id="Shape"></path>
</g>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 26 50"><path fill="#E21221" fill-rule="evenodd" d="M0 .3h7.4L16 23.6l1.6 4.3V.3h7.8v46.3l-8.5 1-3.6-10.4C11.4 32.1 9.7 27 7.8 21.8v26.9l-7.7 1V.4z"/></svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 208 B

View File

@ -1,3 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50">
<path d="M34.864 40.63a1.134 1.134 0 0 0-1.44-.48c-6.007 2.626-12.735-1.775-12.736-8.33v-7.961c0-.626.508-1.134 1.134-1.134h11.362c.627 0 1.134-.508 1.134-1.134v-6.805c0-.626-.507-1.134-1.134-1.134H21.822a1.134 1.134 0 0 1-1.134-1.134V1.134C20.688.508 20.18 0 19.554 0h-7.96v31.82c.022 13.433 14.1 22.208 26.17 16.312.57-.274.805-.96.524-1.526z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50"><path d="M35 41a1 1 0 0 0-2-1c-6 3-12-2-12-8v-8l1-1h11l1-1v-7l-1-1H22a1 1 0 0 1-1-1V1l-1-1h-8v32c0 13 14 22 26 16v-1z"/></svg>

Before

Width:  |  Height:  |  Size: 423 B

After

Width:  |  Height:  |  Size: 186 B

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -1,371 +0,0 @@
<?php declare(strict_types=1);
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu and MyAnimeList to manage anime and manga watch lists
*
* PHP version 7
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2017 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.0
* @link https://github.com/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\EasyMin;
use function Amp\Promise\wait;
use Amp\Artax\Request;
use Aviat\AnimeClient\API\HummingbirdClient;
use Aviat\Ion\{Json, JsonException};
// Include Amp and Artax
require_once '../vendor/autoload.php';
//Creative rewriting of /g/groupname to ?g=groupname
$pi = $_SERVER['PATH_INFO'];
$pia = explode('/', $pi);
$piaLen = count($pia);
$i = 1;
while($i < $piaLen)
{
$j = $i+1;
$j = (isset($pia[$j])) ? $j : $i;
$_GET[$pia[$i]] = $pia[$j];
$i = $j + 1;
};
class FileNotChangedException extends \Exception {}
/**
* Simple Javascript minfier, using google closure compiler
*/
class JSMin {
protected $jsRoot;
protected $jsGroup;
protected $configFile;
protected $cacheFile;
protected $lastModified;
protected $requestedTime;
protected $cacheModified;
public function __construct(array $config, string $configFile)
{
$group = $_GET['g'];
$groups = $config['groups'];
$this->jsRoot = $config['js_root'];
$this->jsGroup = $groups[$group];
$this->configFile = $configFile;
$this->cacheFile = "{$this->jsRoot}cache/{$group}";
$this->lastModified = $this->getLastModified();
$this->cacheModified = (is_file($this->cacheFile))
? filemtime($this->cacheFile)
: 0;
// Output some JS!
$this->send();
}
protected function send()
{
// Override caching if debug key is set
if($this->isDebugCall())
{
return $this->output($this->getFiles());
}
// If the browser's cached version is up to date,
// don't resend the file
if($this->lastModified == $this->getIfModified() && $this->isNotDebug())
{
throw new FileNotChangedException();
}
if($this->cacheModified < $this->lastModified)
{
$js = $this->minify($this->getFiles());
//Make sure cache file gets created/updated
if (file_put_contents($this->cacheFile, $js) === FALSE)
{
echo 'Cache file was not created. Make sure you have the correct folder permissions.';
return;
}
return $this->output($js);
}
else
{
return $this->output(file_get_contents($this->cacheFile));
}
}
/**
* Makes a call to google closure compiler service
*
* @param array $options - Form parameters
* @throws \TypeError
* @return object
*/
protected function closureCall(array $options)
{
$formFields = http_build_query($options);
$request = (new Request('https://closure-compiler.appspot.com/compile'))
->withMethod('POST')
->withHeaders([
'Accept' => 'application/json',
'Accept-Encoding' => 'gzip',
'Content-type' => 'application/x-www-form-urlencoded'
])
->withBody($formFields);
$response = wait((new HummingbirdClient)->request($request, [
HummingbirdClient::OP_AUTO_ENCODING => false
]));
return $response;
}
/**
* Do a call to the closure compiler to check for compilation errors
*
* @param array $options
* @return void
*/
protected function checkMinifyErrors($options)
{
try
{
$errorRes = $this->closureCall($options);
$errorJson = wait($errorRes->getBody());
$errorObj = Json::decode($errorJson) ?: (object)[];
// Show error if exists
if ( ! empty($errorObj->errors) || ! empty($errorObj->serverErrors))
{
$errorJson = Json::encode($errorObj, JSON_PRETTY_PRINT);
header('Content-type: application/javascript');
echo "console.error(${errorJson});";
die();
}
}
catch (JsonException $e)
{
print_r($e);
die();
}
}
/**
* Get Files
*
* Concatenates the javascript files for the current
* group as a string
*
* @return string
*/
protected function getFiles()
{
$js = '';
foreach($this->jsGroup as $file)
{
$newFile = realpath("{$this->jsRoot}{$file}");
$js .= file_get_contents($newFile) . "\n\n";
}
return $js;
}
/**
* Get the most recent modified date
*
* @return int
*/
protected function getLastModified()
{
$modified = [];
foreach($this->jsGroup as $file)
{
$newFile = realpath("{$this->jsRoot}{$file}");
$modified[] = filemtime($newFile);
}
//Add this page too, as well as the groups file
$modified[] = filemtime(__FILE__);
$modified[] = filemtime($this->configFile);
rsort($modified);
$lastModified = $modified[0];
return $lastModified;
}
/**
* Minifies javascript using google's closure compiler
*
* @param string $js
* @return string
*/
protected function minify($js)
{
$options = [
'output_info' => 'errors',
'output_format' => 'json',
'compilation_level' => 'SIMPLE_OPTIMIZATIONS',
//'compilation_level' => 'ADVANCED_OPTIMIZATIONS',
'js_code' => $js,
'language' => 'ECMASCRIPT6_STRICT',
'language_out' => 'ECMASCRIPT5_STRICT'
];
// Check for errors
$this->checkMinifyErrors($options);
// Now actually retrieve the compiled code
$options['output_info'] = 'compiled_code';
$res = $this->closureCall($options);
$json = wait($res->getBody());
$obj = Json::decode($json);
//return $obj;
return $obj['compiledCode'];
}
/**
* Output the minified javascript
*
* @param string $js
* @return void
*/
protected function output($js)
{
$this->sendFinalOutput($js, 'application/javascript', $this->lastModified);
}
/**
* Get value of the if-modified-since header
*
* @return int - timestamp to compare for cache control
*/
protected function getIfModified()
{
return (array_key_exists('HTTP_IF_MODIFIED_SINCE', $_SERVER))
? strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE'])
: time();
}
/**
* Get value of etag to compare to hash of output
*
* @return string - the etag to compare
*/
protected function getIfNoneMatch()
{
return (array_key_exists('HTTP_IF_NONE_MATCH', $_SERVER))
? $_SERVER['HTTP_IF_NONE_MATCH']
: '';
}
/**
* Determine whether or not to send debug version
*
* @return boolean
*/
protected function isNotDebug()
{
return ! $this->isDebugCall();
}
/**
* Determine whether or not to send debug version
*
* @return boolean
*/
protected function isDebugCall()
{
return array_key_exists('debug', $_GET);
}
/**
* Send actual output to browser
*
* @param string $content - the body of the response
* @param string $mimeType - the content type
* @param int $lastModified - the last modified date
* @return void
*/
protected function sendFinalOutput($content, $mimeType, $lastModified)
{
//This GZIPs the CSS for transmission to the user
//making file size smaller and transfer rate quicker
ob_start("ob_gzhandler");
$expires = $lastModified + 691200;
$lastModifiedDate = gmdate('D, d M Y H:i:s', $lastModified);
$expiresDate = gmdate('D, d M Y H:i:s', $expires);
header("Content-Type: {$mimeType}; charset=utf-8");
header('Cache-control: public, max-age=691200, must-revalidate');
header("Expires: {$expiresDate} GMT");
header("Last-Modified: {$lastModifiedDate} GMT");
header('X-Content-Type-Options: no-sniff');
echo $content;
ob_end_flush();
}
/**
* Send a 304 Not Modified header
*
* @return void
*/
public static function send304()
{
header('status: 304 Not Modified', true, 304);
}
}
// --------------------------------------------------------------------------
// ! Start Minifying
// --------------------------------------------------------------------------
$configFile = realpath(__DIR__ . '/../app/appConf/minify_config.php');
$config = require_once($configFile);
$groups = $config['groups'];
$cacheDir = "{$config['js_root']}cache";
if ( ! is_dir($cacheDir))
{
mkdir($cacheDir);
}
if ( ! array_key_exists($_GET['g'], $groups))
{
throw new InvalidArgumentException('You must specify a js group that exists');
}
try
{
new JSMin($config, $configFile);
}
catch (FileNotChangedException $e)
{
JSMin::send304();
}
//end of js.php

View File

@ -1,30 +0,0 @@
((_) => {
'use strict';
const search = (query) => {
// Show the loader
_.$('.cssload-loader')[0].removeAttribute('hidden');
// Do the api search
_.get(_.url('/anime-collection/search'), {query}, (searchResults, status) => {
searchResults = JSON.parse(searchResults);
// Hide the loader
_.$('.cssload-loader')[0].setAttribute('hidden', 'hidden');
// Show the results
_.$('#series_list')[0].innerHTML = render_anime_search_results(searchResults.data);
});
};
_.on('#search', 'keyup', _.throttle(250, function() {
const query = encodeURIComponent(this.value);
if (query === '') {
return;
}
search(query);
}));
})(AnimeClient);

View File

@ -1,70 +0,0 @@
/**
* Javascript for editing anime, if logged in
*/
((_) => {
'use strict';
// Action to increment episode count
_.on('body.anime.list', 'click', '.plus_one', (e) => {
let parentSel = _.closestParent(e.target, 'article');
let watchedCount = parseInt(_.$('.completed_number', parentSel)[0].textContent, 10) || 0;
let totalCount = parseInt(_.$('.total_number', parentSel)[0].textContent, 10);
let title = _.$('.name a', parentSel)[0].textContent;
// Setup the update data
let data = {
id: parentSel.dataset.kitsuId,
mal_id: parentSel.dataset.malId,
data: {
progress: watchedCount + 1
}
};
// If the episode count is 0, and incremented,
// change status to currently watching
if (isNaN(watchedCount) || watchedCount === 0) {
data.data.status = 'current';
}
// If you increment at the last episode, mark as completed
if (( ! isNaN(watchedCount)) && (watchedCount + 1) === totalCount) {
data.data.status = 'completed';
}
_.show(_.$('#loading-shadow')[0]);
// okay, lets actually make some changes!
_.ajax(_.url('/anime/update'), {
data,
dataType: 'json',
type: 'POST',
success: (res) => {
const resData = JSON.parse(res);
if (resData.errors) {
_.hide(_.$('#loading-shadow')[ 0 ]);
_.showMessage('error', `Failed to update ${title}. `);
_.scrollToTop();
return;
}
if (resData.data.attributes.status === 'completed') {
_.hide(parentSel);
}
_.hide(_.$('#loading-shadow')[0]);
_.showMessage('success', `Successfully updated ${title}`);
_.$('.completed_number', parentSel)[0].textContent = ++watchedCount;
_.scrollToTop();
},
error: (xhr, errorType, error) => {
_.hide(_.$('#loading-shadow')[0]);
_.showMessage('error', `Failed to update ${title}. `);
_.scrollToTop();
}
});
});
})(AnimeClient);

View File

@ -1,27 +0,0 @@
function render_anime_search_results (data) {
const results = [];
data.forEach(x => {
const item = x.attributes;
const titles = item.titles.reduce((prev, current) => {
return prev + `${current}<br />`;
}, []);
results.push(`
<article class="media search">
<div class="name">
<input type="radio" class="big-check" id="${item.slug}" name="id" value="${x.id}" />
<label for="${item.slug}">
<img src="/public/images/anime/${x.id}.jpg" alt="" width="220" />
<span class="name">
${item.canonicalTitle}<br />
<small>${titles}</small>
</span>
</label>
</div>
</article>
`);
});
return results.join('');
}

View File

@ -1,321 +0,0 @@
var AnimeClient = (function(w) {
'use strict';
// -------------------------------------------------------------------------
// ! Base
// -------------------------------------------------------------------------
function matches(elm, selector) {
let matches = (elm.document || elm.ownerDocument).querySelectorAll(selector),
i = matches.length;
while (--i >= 0 && matches.item(i) !== elm);
return i > -1;
}
const _ = {
/**
* Placeholder function
*/
noop: () => {},
/**
* DOM selector
*
* @param {string} selector - The dom selector string
* @param {object} context
* @return {array} - array of dom elements
*/
$(selector, context) {
if (typeof selector !== 'string') {
return selector;
}
context = (context != null && context.nodeType === 1)
? context
: document;
let elements = [];
if (selector.match(/^#([\w]+$)/)) {
elements.push(document.getElementById(selector.split('#')[1]));
} else {
elements = [].slice.apply(context.querySelectorAll(selector));
}
return elements;
},
/**
* Scroll to the top of the Page
*
* @return {void}
*/
scrollToTop() {
w.scroll(0,0);
},
/**
* Hide the selected element
*
* @param {string|Element} sel - the selector of the element to hide
* @return {void}
*/
hide(sel) {
sel.setAttribute('hidden', 'hidden');
},
/**
* UnHide the selected element
*
* @param {string|Element} sel - the selector of the element to hide
* @return {void}
*/
show(sel) {
sel.removeAttribute('hidden');
},
/**
* Display a message box
*
* @param {String} type - message type: info, error, success
* @param {String} message - the message itself
* @return {void}
*/
showMessage(type, message) {
let template =
`<div class='message ${type}'>
<span class='icon'></span>
${message}
<span class='close'></span>
</div>`;
let sel = AnimeClient.$('.message');
if (sel[0] !== undefined) {
sel[0].remove();
}
_.$('header')[0].insertAdjacentHTML('beforeend', template);
},
/**
* Finds the closest parent element matching the passed selector
*
* @param {DOMElement} current - the current DOMElement
* @param {string} parentSelector - selector for the parent element
* @return {DOMElement|null} - the parent element
*/
closestParent(current, parentSelector) {
if (Element.prototype.closest !== undefined) {
return current.closest(parentSelector);
}
while (current !== document.documentElement) {
if (matches(current, parentSelector)) {
return current;
}
current = current.parentElement;
}
return null;
},
/**
* Generate a full url from a relative path
*
* @param {String} path - url path
* @return {String} - full url
*/
url(path) {
let uri = `//${document.location.host}`;
uri += (path.charAt(0) === '/') ? path : `/${path}`;
return uri;
},
/**
* Throttle execution of a function
*
* @see https://remysharp.com/2010/07/21/throttling-function-calls
* @see https://jsfiddle.net/jonathansampson/m7G64/
* @param {Number} interval - the minimum throttle time in ms
* @param {Function} fn - the function to throttle
* @param {Object} scope - the 'this' object for the function
* @return {void}
*/
throttle(interval, fn, scope) {
var wait = false;
return function () {
var context = scope || this;
var args = arguments;
if ( ! wait) {
fn.apply(context, args);
wait = true;
setTimeout(function() {
wait = false;
}, interval);
}
};
},
};
// -------------------------------------------------------------------------
// ! Events
// -------------------------------------------------------------------------
function addEvent(sel, event, listener) {
// Recurse!
if (! event.match(/^([\w\-]+)$/)) {
event.split(' ').forEach((evt) => {
addEvent(sel, evt, listener);
});
}
sel.addEventListener(event, listener, false);
}
function delegateEvent(sel, target, event, listener) {
// Attach the listener to the parent
addEvent(sel, event, (e) => {
// Get live version of the target selector
_.$(target, sel).forEach((element) => {
if(e.target == element) {
listener.call(element, e);
e.stopPropagation();
}
});
});
}
/**
* Add an event listener
*
* @param {string|element} sel - the parent selector to bind to
* @param {string} event - event name(s) to bind
* @param {string|element} [target] - the element to directly bind the event to
* @param {function} listener - event listener callback
* @return {void}
*/
_.on = function (sel, event, target, listener) {
if (arguments.length === 3) {
listener = target;
_.$(sel).forEach((el) => {
addEvent(el, event, listener);
});
} else {
_.$(sel).forEach((el) => {
delegateEvent(el, target, event, listener);
});
}
};
// -------------------------------------------------------------------------
// ! Ajax
// -------------------------------------------------------------------------
/**
* Url encoding for non-get requests
*
* @param data
* @returns {string}
* @private
*/
function ajaxSerialize(data) {
let pairs = [];
Object.keys(data).forEach((name) => {
let value = data[name].toString();
name = encodeURIComponent(name);
value = encodeURIComponent(value);
pairs.push(`${name}=${value}`);
});
return pairs.join('&');
}
/**
* Make an ajax request
*
* Config:{
* data: // data to send with the request
* type: // http verb of the request, defaults to GET
* success: // success callback
* error: // error callback
* }
*
* @param {string} url - the url to request
* @param {Object} config - the configuration object
* @return {void}
*/
_.ajax = function(url, config) {
// Set some sane defaults
config = config || {};
config.data = config.data || {};
config.type = config.type || 'GET';
config.dataType = config.dataType || '';
config.success = config.success || _.noop;
config.mimeType = config.mimeType || 'application/x-www-form-urlencoded';
config.error = config.error || _.noop;
let request = new XMLHttpRequest();
let method = String(config.type).toUpperCase();
if (method === 'GET') {
url += (url.match(/\?/))
? ajaxSerialize(config.data)
: `?${ajaxSerialize(config.data)}`;
}
request.open(method, url);
request.onreadystatechange = () => {
if (request.readyState === 4) {
let responseText = '';
if (request.responseType === 'json') {
responseText = JSON.parse(request.responseText);
} else {
responseText = request.responseText;
}
if (request.status > 299) {
config.error.call(null, request.status, responseText, request.response);
} else {
config.success.call(null, responseText, request.status);
}
}
};
if (config.dataType === 'json') {
config.data = JSON.stringify(config.data);
config.mimeType = 'application/json';
} else {
config.data = ajaxSerialize(config.data);
}
request.setRequestHeader('Content-Type', config.mimeType);
switch (method) {
case 'GET':
request.send(null);
break;
default:
request.send(config.data);
break;
}
};
_.get = function(url, data, callback) {
if (arguments.length === 2) {
callback = data;
data = {};
}
return _.ajax(url, {
data,
success: callback
});
};
// -------------------------------------------------------------------------
// Export
// -------------------------------------------------------------------------
return _;
})(window);

View File

@ -1,30 +0,0 @@
/**
* Event handlers
*/
((ac) => {
'use strict';
// Close event for messages
ac.on('header', 'click', '.message', function () {
ac.hide(this);
});
// Confirm deleting of list or library items
ac.on('form.js-delete', 'submit', (event) => {
const proceed = confirm('Are you ABSOLUTELY SURE you want to delete this item?');
if (proceed === false) {
event.preventDefault();
event.stopPropagation();
}
});
// Clear the api cache
ac.on('.js-clear-cache', 'click', () => {
ac.get('/cache_purge', () => {
ac.showMessage('success', 'Successfully purged api cache');
});
});
})(AnimeClient);

View File

@ -1,23 +0,0 @@
((_) => {
'use strict';
const search = (query) => {
_.$('.cssload-loader')[0].removeAttribute('hidden');
_.get(_.url('/manga/search'), {query}, (searchResults, status) => {
searchResults = JSON.parse(searchResults);
_.$('.cssload-loader')[0].setAttribute('hidden', 'hidden');
_.$('#series_list')[0].innerHTML = render_manga_search_results(searchResults.data);
});
};
_.on('#search', 'keyup', _.throttle(250, function(e) {
let query = encodeURIComponent(this.value);
if (query === '') {
return;
}
search(query);
}));
})(AnimeClient);

View File

@ -1,69 +0,0 @@
/**
* Javascript for editing manga, if logged in
*/
((_) => {
'use strict';
_.on('.manga.list', 'click', '.edit_buttons button', (e) => {
let thisSel = e.target;
let parentSel = _.closestParent(e.target, 'article');
let type = thisSel.classList.contains('plus_one_chapter') ? 'chapter' : 'volume';
let completed = parseInt(_.$(`.${type}s_read`, parentSel)[0].textContent, 10) || 0;
let total = parseInt(_.$(`.${type}_count`, parentSel)[0].textContent, 10);
let mangaName = _.$('.name', parentSel)[0].textContent;
if (isNaN(completed)) {
completed = 0;
}
// Setup the update data
let data = {
id: parentSel.dataset.kitsuId,
mal_id: parentSel.dataset.malId,
data: {
progress: completed
}
};
// If the episode count is 0, and incremented,
// change status to currently reading
if (isNaN(completed) || completed === 0) {
data.data.status = 'current';
}
// If you increment at the last chapter, mark as completed
if (( ! isNaN(completed)) && (completed + 1) === total) {
data.data.status = 'completed';
}
// Update the total count
data.data.progress = ++completed;
_.show(_.$('#loading-shadow')[0]);
_.ajax(_.url('/manga/update'), {
data,
dataType: 'json',
type: 'POST',
mimeType: 'application/json',
success: () => {
if (data.data.status === 'completed') {
_.hide(parentSel);
}
_.hide(_.$('#loading-shadow')[0]);
_.$(`.${type}s_read`, parentSel)[0].textContent = completed;
_.showMessage('success', `Sucessfully updated ${mangaName}`);
_.scrollToTop();
},
error: () => {
_.hide(_.$('#loading-shadow')[0]);
_.showMessage('error', `Failed to update ${mangaName}`);
_.scrollToTop();
}
});
});
})(AnimeClient);

View File

@ -1,27 +0,0 @@
function render_manga_search_results (data) {
const results = [];
data.forEach(x => {
const item = x.attributes;
const titles = item.titles.reduce((prev, current) => {
return prev + `${current}<br />`;
}, []);
results.push(`
<article class="media search">
<div class="name">
<input type="radio" class="big-check" id="${item.slug}" name="id" value="${x.id}" />
<label for="${item.slug}">
<img src="/public/images/manga/${x.id}.jpg" alt="" width="220" />
<span class="name">
${item.canonicalTitle}<br />
<small>${titles}</small>
</span>
</label>
</div>
</article>
`);
});
return results.join('');
}

23
public/js/scripts-authed.min.js vendored Normal file
View File

@ -0,0 +1,23 @@
(function(){var matches=function(elm,selector){var matches=(elm.document||elm.ownerDocument).querySelectorAll(selector),i=matches.length;while(--i>=0&&matches.item(i)!==elm);return i>-1};var AnimeClient={noop:function(){},$:function(selector,context){context=context===undefined?null:context;if(typeof selector!=="string")return selector;context=context!==null&&context.nodeType===1?context:document;var elements=[];if(selector.match(/^#([\w]+$)/))elements.push(document.getElementById(selector.split("#")[1]));
else elements=[].slice.apply(context.querySelectorAll(selector));return elements},hasElement:function(selector){return AnimeClient.$(selector).length>0},scrollToTop:function(){window.scroll(0,0)},hide:function(sel){sel.setAttribute("hidden","hidden")},show:function(sel){sel.removeAttribute("hidden")},showMessage:function(type,message){var template="<div class='message "+type+"'>\n\t\t\t\t<span class='icon'></span>\n\t\t\t\t"+message+"\n\t\t\t\t<span class='close'></span>\n\t\t\t</div>";var sel=AnimeClient.$(".message");
if(sel[0]!==undefined)sel[0].remove();AnimeClient.$("header")[0].insertAdjacentHTML("beforeend",template)},closestParent:function(current,parentSelector){if(Element.prototype.closest!==undefined)return current.closest(parentSelector);while(current!==document.documentElement){if(matches(current,parentSelector))return current;current=current.parentElement}return null},url:function(path){var uri="//"+document.location.host;uri+=path.charAt(0)==="/"?path:"/"+path;return uri},throttle:function(interval,
fn,scope){var wait=false;return function(args){var $jscomp$restParams=[];for(var $jscomp$restIndex=0;$jscomp$restIndex<arguments.length;++$jscomp$restIndex)$jscomp$restParams[$jscomp$restIndex-0]=arguments[$jscomp$restIndex];{var args$0=$jscomp$restParams;var context=scope||this;if(!wait){fn.apply(context,args$0);wait=true;setTimeout(function(){wait=false},interval)}}}}};function addEvent(sel,event,listener){if(!event.match(/^([\w\-]+)$/))event.split(" ").forEach(function(evt){addEvent(sel,evt,listener)});
sel.addEventListener(event,listener,false)}function delegateEvent(sel,target,event,listener){addEvent(sel,event,function(e){AnimeClient.$(target,sel).forEach(function(element){if(e.target==element){listener.call(element,e);e.stopPropagation()}})})}AnimeClient.on=function(sel,event,target,listener){if(listener===undefined){listener=target;AnimeClient.$(sel).forEach(function(el){addEvent(el,event,listener)})}else AnimeClient.$(sel).forEach(function(el){delegateEvent(el,target,event,listener)})};function ajaxSerialize(data){var pairs=
[];Object.keys(data).forEach(function(name){var value=data[name].toString();name=encodeURIComponent(name);value=encodeURIComponent(value);pairs.push(name+"="+value)});return pairs.join("&")}AnimeClient.ajax=function(url,config){var defaultConfig={data:{},type:"GET",dataType:"",success:AnimeClient.noop,mimeType:"application/x-www-form-urlencoded",error:AnimeClient.noop};config=Object.assign({},defaultConfig,config);var request=new XMLHttpRequest;var method=String(config.type).toUpperCase();if(method===
"GET")url+=url.match(/\?/)?ajaxSerialize(config.data):"?"+ajaxSerialize(config.data);request.open(method,url);request.onreadystatechange=function(){if(request.readyState===4){var responseText="";if(request.responseType==="json")responseText=JSON.parse(request.responseText);else responseText=request.responseText;if(request.status>299)config.error.call(null,request.status,responseText,request.response);else config.success.call(null,responseText,request.status)}};if(config.dataType==="json"){config.data=
JSON.stringify(config.data);config.mimeType="application/json"}else config.data=ajaxSerialize(config.data);request.setRequestHeader("Content-Type",config.mimeType);switch(method){case "GET":request.send(null);break;default:request.send(config.data);break}};AnimeClient.get=function(url,data,callback){callback=callback===undefined?null:callback;if(callback===null){callback=data;data={}}return AnimeClient.ajax(url,{data:data,success:callback})};AnimeClient.on("header","click",".message",function(e){AnimeClient.hide(e.target)});
AnimeClient.on("form.js-delete","submit",function(event){var proceed=confirm("Are you ABSOLUTELY SURE you want to delete this item?");if(proceed===false){event.preventDefault();event.stopPropagation()}});AnimeClient.on(".js-clear-cache","click",function(){AnimeClient.get("/cache_purge",function(){AnimeClient.showMessage("success","Successfully purged api cache")})});AnimeClient.on(".vertical-tabs input","change",function(event){var el=event.currentTarget.parentElement;var rect=el.getBoundingClientRect();
var top=rect.top+window.pageYOffset;window.scrollTo({top:top,behavior:"smooth"})});AnimeClient.on("main","change",".big-check",function(e){var id=e.target.id;document.getElementById("mal_"+id).checked=true});function renderAnimeSearchResults(data){var results=[];data.forEach(function(x){var item=x.attributes;var titles=item.titles.reduce(function(prev,current){return prev+(current+"<br />")},[]);results.push('\n\t\t\t<article class="media search">\n\t\t\t\t<div class="name">\n\t\t\t\t\t<input type="radio" class="mal-check" id="mal_'+
item.slug+'" name="mal_id" value="'+x.mal_id+'" />\n\t\t\t\t\t<input type="radio" class="big-check" id="'+item.slug+'" name="id" value="'+x.id+'" />\n\t\t\t\t\t<label for="'+item.slug+'">\n\t\t\t\t\t\t<picture width="220">\n\t\t\t\t\t\t\t<source srcset="/public/images/anime/'+x.id+'.webp" type="image/webp" />\n\t\t\t\t\t\t\t<source srcset="/public/images/anime/'+x.id+'.jpg" type="image/jpeg" />\n\t\t\t\t\t\t\t<img src="/public/images/anime/'+x.id+'.jpg" alt="" width="220" />\n\t\t\t\t\t\t</picture>\n\t\t\t\t\t\t\n\t\t\t\t\t\t<span class="name">\n\t\t\t\t\t\t\t'+
item.canonicalTitle+"<br />\n\t\t\t\t\t\t\t<small>"+titles+'</small>\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</label>\n\t\t\t\t</div>\n\t\t\t\t<div class="table">\n\t\t\t\t\t<div class="row">\n\t\t\t\t\t\t<span class="edit">\n\t\t\t\t\t\t\t<a class="bracketed" href="/anime/details/'+item.slug+'">Info Page</a>\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</article>\n\t\t')});return results.join("")}function renderMangaSearchResults(data){var results=[];data.forEach(function(x){var item=x.attributes;
var titles=item.titles.reduce(function(prev,current){return prev+(current+"<br />")},[]);results.push('\n\t\t\t<article class="media search">\n\t\t\t\t<div class="name">\n\t\t\t\t\t<input type="radio" id="mal_'+item.slug+'" name="mal_id" value="'+x.mal_id+'" />\n\t\t\t\t\t<input type="radio" class="big-check" id="'+item.slug+'" name="id" value="'+x.id+'" />\n\t\t\t\t\t<label for="'+item.slug+'">\n\t\t\t\t\t\t<picture width="220">\n\t\t\t\t\t\t\t<source srcset="/public/images/manga/'+x.id+'.webp" type="image/webp" />\n\t\t\t\t\t\t\t<source srcset="/public/images/manga/'+
x.id+'.jpg" type="image/jpeg" />\n\t\t\t\t\t\t\t<img src="/public/images/manga/'+x.id+'.jpg" alt="" width="220" />\n\t\t\t\t\t\t</picture>\n\t\t\t\t\t\t<span class="name">\n\t\t\t\t\t\t\t'+item.canonicalTitle+"<br />\n\t\t\t\t\t\t\t<small>"+titles+'</small>\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</label>\n\t\t\t\t</div>\n\t\t\t\t<div class="table">\n\t\t\t\t\t<div class="row">\n\t\t\t\t\t\t<span class="edit">\n\t\t\t\t\t\t\t<a class="bracketed" href="/manga/details/'+item.slug+'">Info Page</a>\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</article>\n\t\t')});
return results.join("")}var search=function(query){AnimeClient.$(".cssload-loader")[0].removeAttribute("hidden");AnimeClient.get(AnimeClient.url("/anime-collection/search"),{query:query},function(searchResults,status){searchResults=JSON.parse(searchResults);AnimeClient.$(".cssload-loader")[0].setAttribute("hidden","hidden");AnimeClient.$("#series-list")[0].innerHTML=renderAnimeSearchResults(searchResults.data)})};if(AnimeClient.hasElement(".anime #search"))AnimeClient.on("#search","keyup",AnimeClient.throttle(250,
function(e){var query=encodeURIComponent(e.target.value);if(query==="")return;search(query)}));AnimeClient.on("body.anime.list","click",".plus-one",function(e){var parentSel=AnimeClient.closestParent(e.target,"article");var watchedCount=parseInt(AnimeClient.$(".completed_number",parentSel)[0].textContent,10)||0;var totalCount=parseInt(AnimeClient.$(".total_number",parentSel)[0].textContent,10);var title=AnimeClient.$(".name a",parentSel)[0].textContent;var data={id:parentSel.dataset.kitsuId,mal_id:parentSel.dataset.malId,
data:{progress:watchedCount+1}};if(isNaN(watchedCount)||watchedCount===0)data.data.status="current";if(!isNaN(watchedCount)&&watchedCount+1===totalCount)data.data.status="completed";AnimeClient.show(AnimeClient.$("#loading-shadow")[0]);AnimeClient.ajax(AnimeClient.url("/anime/increment"),{data:data,dataType:"json",type:"POST",success:function(res){var resData=JSON.parse(res);if(resData.errors){AnimeClient.hide(AnimeClient.$("#loading-shadow")[0]);AnimeClient.showMessage("error","Failed to update "+
title+". ");AnimeClient.scrollToTop();return}if(resData.data.attributes.status==="completed")AnimeClient.hide(parentSel);AnimeClient.hide(AnimeClient.$("#loading-shadow")[0]);AnimeClient.showMessage("success","Successfully updated "+title);AnimeClient.$(".completed_number",parentSel)[0].textContent=++watchedCount;AnimeClient.scrollToTop()},error:function(){AnimeClient.hide(AnimeClient.$("#loading-shadow")[0]);AnimeClient.showMessage("error","Failed to update "+title+". ");AnimeClient.scrollToTop()}})});
var search$1=function(query){AnimeClient.$(".cssload-loader")[0].removeAttribute("hidden");AnimeClient.get(AnimeClient.url("/manga/search"),{query:query},function(searchResults,status){searchResults=JSON.parse(searchResults);AnimeClient.$(".cssload-loader")[0].setAttribute("hidden","hidden");AnimeClient.$("#series-list")[0].innerHTML=renderMangaSearchResults(searchResults.data)})};if(AnimeClient.hasElement(".manga #search"))AnimeClient.on("#search","keyup",AnimeClient.throttle(250,function(e){var query=
encodeURIComponent(e.target.value);if(query==="")return;search$1(query)}));AnimeClient.on(".manga.list","click",".edit-buttons button",function(e){var thisSel=e.target;var parentSel=AnimeClient.closestParent(e.target,"article");var type=thisSel.classList.contains("plus-one-chapter")?"chapter":"volume";var completed=parseInt(AnimeClient.$("."+type+"s_read",parentSel)[0].textContent,10)||0;var total=parseInt(AnimeClient.$("."+type+"_count",parentSel)[0].textContent,10);var mangaName=AnimeClient.$(".name",
parentSel)[0].textContent;if(isNaN(completed))completed=0;var data={id:parentSel.dataset.kitsuId,mal_id:parentSel.dataset.malId,data:{progress:completed}};if(isNaN(completed)||completed===0)data.data.status="current";if(!isNaN(completed)&&completed+1===total)data.data.status="completed";data.data.progress=++completed;AnimeClient.show(AnimeClient.$("#loading-shadow")[0]);AnimeClient.ajax(AnimeClient.url("/manga/increment"),{data:data,dataType:"json",type:"POST",mimeType:"application/json",success:function(){if(data.data.status===
"completed")AnimeClient.hide(parentSel);AnimeClient.hide(AnimeClient.$("#loading-shadow")[0]);AnimeClient.$("."+type+"s_read",parentSel)[0].textContent=completed;AnimeClient.showMessage("success","Successfully updated "+mangaName);AnimeClient.scrollToTop()},error:function(){AnimeClient.hide(AnimeClient.$("#loading-shadow")[0]);AnimeClient.showMessage("error","Failed to update "+mangaName);AnimeClient.scrollToTop()}})})})();
//# sourceMappingURL=scripts-authed.min.js.map

File diff suppressed because one or more lines are too long

11
public/js/scripts.min.js vendored Normal file
View File

@ -0,0 +1,11 @@
(function(){var matches=function(elm,selector){var matches=(elm.document||elm.ownerDocument).querySelectorAll(selector),i=matches.length;while(--i>=0&&matches.item(i)!==elm);return i>-1};var AnimeClient={noop:function(){},$:function(selector,context){context=context===undefined?null:context;if(typeof selector!=="string")return selector;context=context!==null&&context.nodeType===1?context:document;var elements=[];if(selector.match(/^#([\w]+$)/))elements.push(document.getElementById(selector.split("#")[1]));
else elements=[].slice.apply(context.querySelectorAll(selector));return elements},hasElement:function(selector){return AnimeClient.$(selector).length>0},scrollToTop:function(){window.scroll(0,0)},hide:function(sel){sel.setAttribute("hidden","hidden")},show:function(sel){sel.removeAttribute("hidden")},showMessage:function(type,message){var template="<div class='message "+type+"'>\n\t\t\t\t<span class='icon'></span>\n\t\t\t\t"+message+"\n\t\t\t\t<span class='close'></span>\n\t\t\t</div>";var sel=AnimeClient.$(".message");
if(sel[0]!==undefined)sel[0].remove();AnimeClient.$("header")[0].insertAdjacentHTML("beforeend",template)},closestParent:function(current,parentSelector){if(Element.prototype.closest!==undefined)return current.closest(parentSelector);while(current!==document.documentElement){if(matches(current,parentSelector))return current;current=current.parentElement}return null},url:function(path){var uri="//"+document.location.host;uri+=path.charAt(0)==="/"?path:"/"+path;return uri},throttle:function(interval,
fn,scope){var wait=false;return function(args){var $jscomp$restParams=[];for(var $jscomp$restIndex=0;$jscomp$restIndex<arguments.length;++$jscomp$restIndex)$jscomp$restParams[$jscomp$restIndex-0]=arguments[$jscomp$restIndex];{var args$0=$jscomp$restParams;var context=scope||this;if(!wait){fn.apply(context,args$0);wait=true;setTimeout(function(){wait=false},interval)}}}}};function addEvent(sel,event,listener){if(!event.match(/^([\w\-]+)$/))event.split(" ").forEach(function(evt){addEvent(sel,evt,listener)});
sel.addEventListener(event,listener,false)}function delegateEvent(sel,target,event,listener){addEvent(sel,event,function(e){AnimeClient.$(target,sel).forEach(function(element){if(e.target==element){listener.call(element,e);e.stopPropagation()}})})}AnimeClient.on=function(sel,event,target,listener){if(listener===undefined){listener=target;AnimeClient.$(sel).forEach(function(el){addEvent(el,event,listener)})}else AnimeClient.$(sel).forEach(function(el){delegateEvent(el,target,event,listener)})};function ajaxSerialize(data){var pairs=
[];Object.keys(data).forEach(function(name){var value=data[name].toString();name=encodeURIComponent(name);value=encodeURIComponent(value);pairs.push(name+"="+value)});return pairs.join("&")}AnimeClient.ajax=function(url,config){var defaultConfig={data:{},type:"GET",dataType:"",success:AnimeClient.noop,mimeType:"application/x-www-form-urlencoded",error:AnimeClient.noop};config=Object.assign({},defaultConfig,config);var request=new XMLHttpRequest;var method=String(config.type).toUpperCase();if(method===
"GET")url+=url.match(/\?/)?ajaxSerialize(config.data):"?"+ajaxSerialize(config.data);request.open(method,url);request.onreadystatechange=function(){if(request.readyState===4){var responseText="";if(request.responseType==="json")responseText=JSON.parse(request.responseText);else responseText=request.responseText;if(request.status>299)config.error.call(null,request.status,responseText,request.response);else config.success.call(null,responseText,request.status)}};if(config.dataType==="json"){config.data=
JSON.stringify(config.data);config.mimeType="application/json"}else config.data=ajaxSerialize(config.data);request.setRequestHeader("Content-Type",config.mimeType);switch(method){case "GET":request.send(null);break;default:request.send(config.data);break}};AnimeClient.get=function(url,data,callback){callback=callback===undefined?null:callback;if(callback===null){callback=data;data={}}return AnimeClient.ajax(url,{data:data,success:callback})};AnimeClient.on("header","click",".message",function(e){AnimeClient.hide(e.target)});
AnimeClient.on("form.js-delete","submit",function(event){var proceed=confirm("Are you ABSOLUTELY SURE you want to delete this item?");if(proceed===false){event.preventDefault();event.stopPropagation()}});AnimeClient.on(".js-clear-cache","click",function(){AnimeClient.get("/cache_purge",function(){AnimeClient.showMessage("success","Successfully purged api cache")})});AnimeClient.on(".vertical-tabs input","change",function(event){var el=event.currentTarget.parentElement;var rect=el.getBoundingClientRect();
var top=rect.top+window.pageYOffset;window.scrollTo({top:top,behavior:"smooth"})})})();
//# sourceMappingURL=scripts.min.js.map

File diff suppressed because one or more lines are too long

91
public/js/src/anime.js Normal file
View File

@ -0,0 +1,91 @@
import _ from './base/AnimeClient.js'
import { renderAnimeSearchResults } from './template-helpers.js'
const search = (query) => {
// Show the loader
_.$('.cssload-loader')[ 0 ].removeAttribute('hidden');
// Do the api search
_.get(_.url('/anime-collection/search'), { query }, (searchResults, status) => {
searchResults = JSON.parse(searchResults);
// Hide the loader
_.$('.cssload-loader')[ 0 ].setAttribute('hidden', 'hidden');
// Show the results
_.$('#series-list')[ 0 ].innerHTML = renderAnimeSearchResults(searchResults.data);
});
};
if (_.hasElement('.anime #search')) {
_.on('#search', 'keyup', _.throttle(250, (e) => {
const query = encodeURIComponent(e.target.value);
if (query === '') {
return;
}
search(query);
}));
}
// Action to increment episode count
_.on('body.anime.list', 'click', '.plus-one', (e) => {
let parentSel = _.closestParent(e.target, 'article');
let watchedCount = parseInt(_.$('.completed_number', parentSel)[ 0 ].textContent, 10) || 0;
let totalCount = parseInt(_.$('.total_number', parentSel)[ 0 ].textContent, 10);
let title = _.$('.name a', parentSel)[ 0 ].textContent;
// Setup the update data
let data = {
id: parentSel.dataset.kitsuId,
mal_id: parentSel.dataset.malId,
data: {
progress: watchedCount + 1
}
};
// If the episode count is 0, and incremented,
// change status to currently watching
if (isNaN(watchedCount) || watchedCount === 0) {
data.data.status = 'current';
}
// If you increment at the last episode, mark as completed
if ((!isNaN(watchedCount)) && (watchedCount + 1) === totalCount) {
data.data.status = 'completed';
}
_.show(_.$('#loading-shadow')[ 0 ]);
// okay, lets actually make some changes!
_.ajax(_.url('/anime/increment'), {
data,
dataType: 'json',
type: 'POST',
success: (res) => {
const resData = JSON.parse(res);
if (resData.errors) {
_.hide(_.$('#loading-shadow')[ 0 ]);
_.showMessage('error', `Failed to update ${title}. `);
_.scrollToTop();
return;
}
if (resData.data.attributes.status === 'completed') {
_.hide(parentSel);
}
_.hide(_.$('#loading-shadow')[ 0 ]);
_.showMessage('success', `Successfully updated ${title}`);
_.$('.completed_number', parentSel)[ 0 ].textContent = ++watchedCount;
_.scrollToTop();
},
error: () => {
_.hide(_.$('#loading-shadow')[ 0 ]);
_.showMessage('error', `Failed to update ${title}. `);
_.scrollToTop();
}
});
});

View File

@ -0,0 +1,337 @@
// -------------------------------------------------------------------------
// ! Base
// -------------------------------------------------------------------------
const matches = (elm, selector) => {
let matches = (elm.document || elm.ownerDocument).querySelectorAll(selector),
i = matches.length;
while (--i >= 0 && matches.item(i) !== elm) {};
return i > -1;
}
export const AnimeClient = {
/**
* Placeholder function
*/
noop: () => {},
/**
* DOM selector
*
* @param {string} selector - The dom selector string
* @param {object} [context]
* @return {[HTMLElement]} - array of dom elements
*/
$(selector, context = null) {
if (typeof selector !== 'string') {
return selector;
}
context = (context !== null && context.nodeType === 1)
? context
: document;
let elements = [];
if (selector.match(/^#([\w]+$)/)) {
elements.push(document.getElementById(selector.split('#')[1]));
} else {
elements = [].slice.apply(context.querySelectorAll(selector));
}
return elements;
},
/**
* Does the selector exist on the current page?
*
* @param {string} selector
* @returns {boolean}
*/
hasElement (selector) {
return AnimeClient.$(selector).length > 0;
},
/**
* Scroll to the top of the Page
*
* @return {void}
*/
scrollToTop () {
window.scroll(0,0);
},
/**
* Hide the selected element
*
* @param {string|Element} sel - the selector of the element to hide
* @return {void}
*/
hide (sel) {
sel.setAttribute('hidden', 'hidden');
},
/**
* UnHide the selected element
*
* @param {string|Element} sel - the selector of the element to hide
* @return {void}
*/
show (sel) {
sel.removeAttribute('hidden');
},
/**
* Display a message box
*
* @param {string} type - message type: info, error, success
* @param {string} message - the message itself
* @return {void}
*/
showMessage (type, message) {
let template =
`<div class='message ${type}'>
<span class='icon'></span>
${message}
<span class='close'></span>
</div>`;
let sel = AnimeClient.$('.message');
if (sel[0] !== undefined) {
sel[0].remove();
}
AnimeClient.$('header')[0].insertAdjacentHTML('beforeend', template);
},
/**
* Finds the closest parent element matching the passed selector
*
* @param {HTMLElement} current - the current HTMLElement
* @param {string} parentSelector - selector for the parent element
* @return {HTMLElement|null} - the parent element
*/
closestParent (current, parentSelector) {
if (Element.prototype.closest !== undefined) {
return current.closest(parentSelector);
}
while (current !== document.documentElement) {
if (matches(current, parentSelector)) {
return current;
}
current = current.parentElement;
}
return null;
},
/**
* Generate a full url from a relative path
*
* @param {string} path - url path
* @return {string} - full url
*/
url (path) {
let uri = `//${document.location.host}`;
uri += (path.charAt(0) === '/') ? path : `/${path}`;
return uri;
},
/**
* Throttle execution of a function
*
* @see https://remysharp.com/2010/07/21/throttling-function-calls
* @see https://jsfiddle.net/jonathansampson/m7G64/
* @param {Number} interval - the minimum throttle time in ms
* @param {Function} fn - the function to throttle
* @param {Object} [scope] - the 'this' object for the function
* @return {Function}
*/
throttle (interval, fn, scope) {
let wait = false;
return function (...args) {
const context = scope || this;
if ( ! wait) {
fn.apply(context, args);
wait = true;
setTimeout(function() {
wait = false;
}, interval);
}
};
},
};
// -------------------------------------------------------------------------
// ! Events
// -------------------------------------------------------------------------
function addEvent(sel, event, listener) {
// Recurse!
if (! event.match(/^([\w\-]+)$/)) {
event.split(' ').forEach((evt) => {
addEvent(sel, evt, listener);
});
}
sel.addEventListener(event, listener, false);
}
function delegateEvent(sel, target, event, listener) {
// Attach the listener to the parent
addEvent(sel, event, (e) => {
// Get live version of the target selector
AnimeClient.$(target, sel).forEach((element) => {
if(e.target == element) {
listener.call(element, e);
e.stopPropagation();
}
});
});
}
/**
* Add an event listener
*
* @param {string|HTMLElement} sel - the parent selector to bind to
* @param {string} event - event name(s) to bind
* @param {string|HTMLElement|function} target - the element to directly bind the event to
* @param {function} [listener] - event listener callback
* @return {void}
*/
AnimeClient.on = (sel, event, target, listener) => {
if (listener === undefined) {
listener = target;
AnimeClient.$(sel).forEach((el) => {
addEvent(el, event, listener);
});
} else {
AnimeClient.$(sel).forEach((el) => {
delegateEvent(el, target, event, listener);
});
}
};
// -------------------------------------------------------------------------
// ! Ajax
// -------------------------------------------------------------------------
/**
* Url encoding for non-get requests
*
* @param data
* @returns {string}
* @private
*/
function ajaxSerialize(data) {
let pairs = [];
Object.keys(data).forEach((name) => {
let value = data[name].toString();
name = encodeURIComponent(name);
value = encodeURIComponent(value);
pairs.push(`${name}=${value}`);
});
return pairs.join('&');
}
/**
* Make an ajax request
*
* Config:{
* data: // data to send with the request
* type: // http verb of the request, defaults to GET
* success: // success callback
* error: // error callback
* }
*
* @param {string} url - the url to request
* @param {Object} config - the configuration object
* @return {void}
*/
AnimeClient.ajax = (url, config) => {
// Set some sane defaults
const defaultConfig = {
data: {},
type: 'GET',
dataType: '',
success: AnimeClient.noop,
mimeType: 'application/x-www-form-urlencoded',
error: AnimeClient.noop
}
config = {
...defaultConfig,
...config,
}
let request = new XMLHttpRequest();
let method = String(config.type).toUpperCase();
if (method === 'GET') {
url += (url.match(/\?/))
? ajaxSerialize(config.data)
: `?${ajaxSerialize(config.data)}`;
}
request.open(method, url);
request.onreadystatechange = () => {
if (request.readyState === 4) {
let responseText = '';
if (request.responseType === 'json') {
responseText = JSON.parse(request.responseText);
} else {
responseText = request.responseText;
}
if (request.status > 299) {
config.error.call(null, request.status, responseText, request.response);
} else {
config.success.call(null, responseText, request.status);
}
}
};
if (config.dataType === 'json') {
config.data = JSON.stringify(config.data);
config.mimeType = 'application/json';
} else {
config.data = ajaxSerialize(config.data);
}
request.setRequestHeader('Content-Type', config.mimeType);
switch (method) {
case 'GET':
request.send(null);
break;
default:
request.send(config.data);
break;
}
};
/**
* Do a get request
*
* @param {string} url
* @param {object|function} data
* @param {function} [callback]
*/
AnimeClient.get = (url, data, callback = null) => {
if (callback === null) {
callback = data;
data = {};
}
return AnimeClient.ajax(url, {
data,
success: callback
});
};
// -------------------------------------------------------------------------
// Export
// -------------------------------------------------------------------------
export default AnimeClient;

View File

@ -0,0 +1,38 @@
import _ from './AnimeClient.js';
/**
* Event handlers
*/
// Close event for messages
_.on('header', 'click', '.message', (e) => {
_.hide(e.target);
});
// Confirm deleting of list or library items
_.on('form.js-delete', 'submit', (event) => {
const proceed = confirm('Are you ABSOLUTELY SURE you want to delete this item?');
if (proceed === false) {
event.preventDefault();
event.stopPropagation();
}
});
// Clear the api cache
_.on('.js-clear-cache', 'click', () => {
_.get('/cache_purge', () => {
_.showMessage('success', 'Successfully purged api cache');
});
});
// Alleviate some page jumping
_.on('.vertical-tabs input', 'change', (event) => {
const el = event.currentTarget.parentElement;
const rect = el.getBoundingClientRect();
const top = rect.top + window.pageYOffset;
window.scrollTo({
top,
behavior: 'smooth',
});
});

View File

@ -1,11 +1,8 @@
'use strict';
const LightTableSorter = (() => {
let th = null;
let cellIndex = null;
let order = '';
const text = (row) => {
return row.cells.item(cellIndex).textContent.toLowerCase();
};
const text = (row) => row.cells.item(cellIndex).textContent.toLowerCase();
const sort = (a, b) => {
let textA = text(a);
let textB = text(b);
@ -23,12 +20,12 @@ const LightTableSorter = (() => {
return 0;
};
const toggle = () => {
const c = order !== 'sorting_asc' ? 'sorting_asc' : 'sorting_desc';
const c = order !== 'sorting-asc' ? 'sorting-asc' : 'sorting-desc';
th.className = (th.className.replace(order, '') + ' ' + c).trim();
return order = c;
};
const reset = () => {
th.classList.remove('sorting_asc', 'sorting_desc');
th.classList.remove('sorting-asc', 'sorting-desc');
th.classList.add('sorting');
return order = '';
};
@ -43,7 +40,7 @@ const LightTableSorter = (() => {
let rows = Array.from(tbody.rows);
if (rows) {
rows.sort(sort);
if (order === 'sorting_asc') {
if (order === 'sorting-asc') {
rows.reverse();
}
toggle();

View File

@ -0,0 +1,4 @@
import './index.js';
import './anime.js';
import './manga.js';

10
public/js/src/index.js Normal file
View File

@ -0,0 +1,10 @@
import './base/events.js';
/* if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js').then(reg => {
console.log('Service worker registered', reg.scope);
}).catch(error => {
console.error('Failed to register service worker', error);
});
} */

86
public/js/src/manga.js Normal file
View File

@ -0,0 +1,86 @@
import _ from './base/AnimeClient.js'
import { renderMangaSearchResults } from './template-helpers.js'
const search = (query) => {
_.$('.cssload-loader')[ 0 ].removeAttribute('hidden');
_.get(_.url('/manga/search'), { query }, (searchResults, status) => {
searchResults = JSON.parse(searchResults);
_.$('.cssload-loader')[ 0 ].setAttribute('hidden', 'hidden');
_.$('#series-list')[ 0 ].innerHTML = renderMangaSearchResults(searchResults.data);
});
};
if (_.hasElement('.manga #search')) {
_.on('#search', 'keyup', _.throttle(250, (e) => {
let query = encodeURIComponent(e.target.value);
if (query === '') {
return;
}
search(query);
}));
}
/**
* Javascript for editing manga, if logged in
*/
_.on('.manga.list', 'click', '.edit-buttons button', (e) => {
let thisSel = e.target;
let parentSel = _.closestParent(e.target, 'article');
let type = thisSel.classList.contains('plus-one-chapter') ? 'chapter' : 'volume';
let completed = parseInt(_.$(`.${type}s_read`, parentSel)[ 0 ].textContent, 10) || 0;
let total = parseInt(_.$(`.${type}_count`, parentSel)[ 0 ].textContent, 10);
let mangaName = _.$('.name', parentSel)[ 0 ].textContent;
if (isNaN(completed)) {
completed = 0;
}
// Setup the update data
let data = {
id: parentSel.dataset.kitsuId,
mal_id: parentSel.dataset.malId,
data: {
progress: completed
}
};
// If the episode count is 0, and incremented,
// change status to currently reading
if (isNaN(completed) || completed === 0) {
data.data.status = 'current';
}
// If you increment at the last chapter, mark as completed
if ((!isNaN(completed)) && (completed + 1) === total) {
data.data.status = 'completed';
}
// Update the total count
data.data.progress = ++completed;
_.show(_.$('#loading-shadow')[ 0 ]);
_.ajax(_.url('/manga/increment'), {
data,
dataType: 'json',
type: 'POST',
mimeType: 'application/json',
success: () => {
if (data.data.status === 'completed') {
_.hide(parentSel);
}
_.hide(_.$('#loading-shadow')[ 0 ]);
_.$(`.${type}s_read`, parentSel)[ 0 ].textContent = completed;
_.showMessage('success', `Successfully updated ${mangaName}`);
_.scrollToTop();
},
error: () => {
_.hide(_.$('#loading-shadow')[ 0 ]);
_.showMessage('error', `Failed to update ${mangaName}`);
_.scrollToTop();
}
});
});

View File

@ -0,0 +1,89 @@
import _ from './base/AnimeClient.js';
// Click on hidden MAL checkbox so
// that MAL id is passed
_.on('main', 'change', '.big-check', (e) => {
const id = e.target.id;
document.getElementById(`mal_${id}`).checked = true;
});
export function renderAnimeSearchResults (data) {
const results = [];
data.forEach(x => {
const item = x.attributes;
const titles = item.titles.reduce((prev, current) => {
return prev + `${current}<br />`;
}, []);
results.push(`
<article class="media search">
<div class="name">
<input type="radio" class="mal-check" id="mal_${item.slug}" name="mal_id" value="${x.mal_id}" />
<input type="radio" class="big-check" id="${item.slug}" name="id" value="${x.id}" />
<label for="${item.slug}">
<picture width="220">
<source srcset="/public/images/anime/${x.id}.webp" type="image/webp" />
<source srcset="/public/images/anime/${x.id}.jpg" type="image/jpeg" />
<img src="/public/images/anime/${x.id}.jpg" alt="" width="220" />
</picture>
<span class="name">
${item.canonicalTitle}<br />
<small>${titles}</small>
</span>
</label>
</div>
<div class="table">
<div class="row">
<span class="edit">
<a class="bracketed" href="/anime/details/${item.slug}">Info Page</a>
</span>
</div>
</div>
</article>
`);
});
return results.join('');
}
export function renderMangaSearchResults (data) {
const results = [];
data.forEach(x => {
const item = x.attributes;
const titles = item.titles.reduce((prev, current) => {
return prev + `${current}<br />`;
}, []);
results.push(`
<article class="media search">
<div class="name">
<input type="radio" id="mal_${item.slug}" name="mal_id" value="${x.mal_id}" />
<input type="radio" class="big-check" id="${item.slug}" name="id" value="${x.id}" />
<label for="${item.slug}">
<picture width="220">
<source srcset="/public/images/manga/${x.id}.webp" type="image/webp" />
<source srcset="/public/images/manga/${x.id}.jpg" type="image/jpeg" />
<img src="/public/images/manga/${x.id}.jpg" alt="" width="220" />
</picture>
<span class="name">
${item.canonicalTitle}<br />
<small>${titles}</small>
</span>
</label>
</div>
<div class="table">
<div class="row">
<span class="edit">
<a class="bracketed" href="/manga/details/${item.slug}">Info Page</a>
</span>
</div>
</div>
</article>
`);
});
return results.join('');
}

4
public/js/tables.min.js vendored Normal file
View File

@ -0,0 +1,4 @@
(function(){var LightTableSorter=function(){var th=null;var cellIndex=null;var order="";var text=function(row){return row.cells.item(cellIndex).textContent.toLowerCase()};var sort=function(a,b){var textA=text(a);var textB=text(b);var n=parseInt(textA,10);if(n){textA=n;textB=parseInt(textB,10)}if(textA>textB)return 1;if(textA<textB)return-1;return 0};var toggle=function(){var c=order!=="sorting-asc"?"sorting-asc":"sorting-desc";th.className=(th.className.replace(order,"")+" "+c).trim();return order=
c};var reset=function(){th.classList.remove("sorting-asc","sorting-desc");th.classList.add("sorting");return order=""};var onClickEvent=function(e){if(th&&cellIndex!==e.target.cellIndex)reset();th=e.target;if(th.nodeName.toLowerCase()==="th"){cellIndex=th.cellIndex;var tbody=th.offsetParent.getElementsByTagName("tbody")[0];var rows=Array.from(tbody.rows);if(rows){rows.sort(sort);if(order==="sorting-asc")rows.reverse();toggle();tbody.innerHtml="";rows.forEach(function(row){tbody.appendChild(row)})}}};
return{init:function(){var ths=document.getElementsByTagName("th");var results=[];for(var i=0,len=ths.length;i<len;i++){var th$0=ths[i];th$0.classList.add("sorting");results.push(th$0.onclick=onClickEvent)}return results}}}();LightTableSorter.init()})();
//# sourceMappingURL=tables.min.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"tables.min.js.map","sources":["src/base/sort_tables.js"],"sourcesContent":["const LightTableSorter = (() => {\n\tlet th = null;\n\tlet cellIndex = null;\n\tlet order = '';\n\tconst text = (row) => row.cells.item(cellIndex).textContent.toLowerCase();\n\tconst sort = (a, b) => {\n\t\tlet textA = text(a);\n\t\tlet textB = text(b);\n\t\tconst n = parseInt(textA, 10);\n\t\tif (n) {\n\t\t\ttextA = n;\n\t\t\ttextB = parseInt(textB, 10);\n\t\t}\n\t\tif (textA > textB) {\n\t\t\treturn 1;\n\t\t}\n\t\tif (textA < textB) {\n\t\t\treturn -1;\n\t\t}\n\t\treturn 0;\n\t};\n\tconst toggle = () => {\n\t\tconst c = order !== 'sorting-asc' ? 'sorting-asc' : 'sorting-desc';\n\t\tth.className = (th.className.replace(order, '') + ' ' + c).trim();\n\t\treturn order = c;\n\t};\n\tconst reset = () => {\n\t\tth.classList.remove('sorting-asc', 'sorting-desc');\n\t\tth.classList.add('sorting');\n\t\treturn order = '';\n\t};\n\tconst onClickEvent = (e) => {\n\t\tif (th && (cellIndex !== e.target.cellIndex)) {\n\t\t\treset();\n\t\t}\n\t\tth = e.target;\n\t\tif (th.nodeName.toLowerCase() === 'th') {\n\t\t\tcellIndex = th.cellIndex;\n\t\t\tconst tbody = th.offsetParent.getElementsByTagName('tbody')[0];\n\t\t\tlet rows = Array.from(tbody.rows);\n\t\t\tif (rows) {\n\t\t\t\trows.sort(sort);\n\t\t\t\tif (order === 'sorting-asc') {\n\t\t\t\t\trows.reverse();\n\t\t\t\t}\n\t\t\t\ttoggle();\n\t\t\t\ttbody.innerHtml = '';\n\n\t\t\t\trows.forEach(row => {\n\t\t\t\t\ttbody.appendChild(row);\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t};\n\treturn {\n\t\tinit: () => {\n\t\t\tlet ths = document.getElementsByTagName('th');\n\t\t\tlet results = [];\n\t\t\tfor (let i = 0, len = ths.length; i < len; i++) {\n\t\t\t\tlet th = ths[i];\n\t\t\t\tth.classList.add('sorting');\n\t\t\t\tresults.push(th.onclick = onClickEvent);\n\t\t\t}\n\t\t\treturn results;\n\t\t}\n\t};\n})();\n\nLightTableSorter.init();"],"names":["LightTableSorter","th","cellIndex","order","text","row","cells","item","textContent","toLowerCase","sort","a","b","textA","textB","n","parseInt","toggle","c","className","trim","replace","reset","classList","remove","add","onClickEvent","e","target","nodeName","tbody","offsetParent","getElementsByTagName","rows","Array","from","reverse","innerHtml","forEach","appendChild","init","ths","document","results","i","len","length","push","onclick"],"mappings":"YAAA,IAAMA,iBAAoB,QAAA,EAAM,CAC/B,IAAIC,GAAK,IACT,KAAIC,UAAY,IAChB,KAAIC,MAAQ,EACZ,KAAMC,KAAOA,QAAA,CAACC,GAAD,CAAS,CAAA,MAAAA,IAAAC,MAAAC,KAAA,CAAeL,SAAf,CAAAM,YAAAC,YAAA,EAAA,CACtB,KAAMC,KAAOA,QAAA,CAACC,CAAD,CAAIC,CAAJ,CAAU,CACtB,IAAIC,MAAQT,IAAA,CAAKO,CAAL,CACZ,KAAIG,MAAQV,IAAA,CAAKQ,CAAL,CACZ,KAAMG,EAAIC,QAAA,CAASH,KAAT,CAAgB,EAAhB,CACV,IAAIE,CAAJ,CAAO,CACNF,KAAA,CAAQE,CACRD,MAAA,CAAQE,QAAA,CAASF,KAAT,CAAgB,EAAhB,CAFF,CAIP,GAAID,KAAJ,CAAYC,KAAZ,CACC,MAAO,EAER,IAAID,KAAJ,CAAYC,KAAZ,CACC,MAAQ,EAET,OAAO,EAde,CAgBvB,KAAMG,OAASA,QAAA,EAAM,CACpB,IAAMC,EAAIf,KAAA,GAAU,aAAV,CAA0B,aAA1B,CAA0C,cACpDF,GAAAkB,UAAA,CAAeC,CAACnB,EAAAkB,UAAAE,QAAA,CAAqBlB,KAArB,CAA4B,EAA5B,CAADiB,CAAmC,GAAnCA,CAAyCF,CAAzCE,MAAA,EACf,OAAOjB,MAAP;AAAee,CAHK,CAKrB,KAAMI,MAAQA,QAAA,EAAM,CACnBrB,EAAAsB,UAAAC,OAAA,CAAoB,aAApB,CAAmC,cAAnC,CACAvB,GAAAsB,UAAAE,IAAA,CAAiB,SAAjB,CACA,OAAOtB,MAAP,CAAe,EAHI,CAKpB,KAAMuB,aAAeA,QAAA,CAACC,CAAD,CAAO,CAC3B,GAAI1B,EAAJ,EAAWC,SAAX,GAAyByB,CAAAC,OAAA1B,UAAzB,CACCoB,KAAA,EAEDrB,GAAA,CAAK0B,CAAAC,OACL,IAAI3B,EAAA4B,SAAApB,YAAA,EAAJ,GAAkC,IAAlC,CAAwC,CACvCP,SAAA,CAAYD,EAAAC,UACZ,KAAM4B,MAAQ7B,EAAA8B,aAAAC,qBAAA,CAAqC,OAArC,CAAA,CAA8C,CAA9C,CACd,KAAIC,KAAOC,KAAAC,KAAA,CAAWL,KAAAG,KAAX,CACX,IAAIA,IAAJ,CAAU,CACTA,IAAAvB,KAAA,CAAUA,IAAV,CACA,IAAIP,KAAJ,GAAc,aAAd,CACC8B,IAAAG,QAAA,EAEDnB,OAAA,EACAa,MAAAO,UAAA,CAAkB,EAElBJ,KAAAK,QAAA,CAAa,QAAA,CAAAjC,GAAA,CAAO,CACnByB,KAAAS,YAAA,CAAkBlC,GAAlB,CADmB,CAApB,CARS,CAJ6B,CALb,CAuB5B;MAAO,CACNmC,KAAMA,QAAA,EAAM,CACX,IAAIC,IAAMC,QAAAV,qBAAA,CAA8B,IAA9B,CACV,KAAIW,QAAU,EACd,KAAK,IAAIC,EAAI,CAAR,CAAWC,IAAMJ,GAAAK,OAAtB,CAAkCF,CAAlC,CAAsCC,GAAtC,CAA2CD,CAAA,EAA3C,CAAgD,CAC/C,IAAI3C,KAAKwC,GAAA,CAAIG,CAAJ,CACT3C,KAAAsB,UAAAE,IAAA,CAAiB,SAAjB,CACAkB,QAAAI,KAAA,CAAa9C,IAAA+C,QAAb,CAA0BtB,YAA1B,CAH+C,CAKhD,MAAOiB,QARI,CADN,CAtDwB,CAAP,EAoEzB3C,iBAAAwC,KAAA;"}

View File

@ -1,13 +1,22 @@
{
"license": "MIT",
"scripts": {
"build": "node ./css.js",
"watch": "watch 'npm run build' --filter=./cssfilter.js"
"build": "npm run build:css && npm run build:js",
"build:css": "node ./tools/css.js",
"build:js": "rollup -c ./tools/build-js.js",
"watch:css": "watch 'npm run build:css' --filter=./tools/cssfilter.js",
"watch:js": "watch 'npm run build:js' ./js/src",
"watch": "concurrently \"npm:watch:css\" \"npm:watch:js\" --kill-others"
},
"devDependencies": {
"@ampproject/rollup-plugin-closure-compiler": "^0.8.3",
"concurrently": "^4.0.1",
"cssnano": "^4.0.5",
"postcss-cachify": "^1.3.1",
"postcss-cssnext": "^3.0.0",
"postcss-import": "^12.0.0",
"rollup": "^0.66.6",
"rollup-plugin-closure-compiler-js": "^1.0.6",
"watch": "^1.0.2"
}
}

View File

@ -20,7 +20,7 @@
</ul>
<ul id="mocha-report"></ul>
</div>
<script src="../js/base/classList.js"></script>
<script src="../js/src/base/classList.js"></script>
<script src="lib/testBundle.js"></script>
<script>
@ -29,7 +29,7 @@
</script>
<!-- include source files here... -->
<script src="../js/base/AnimeClient.js"></script>
<script src="../js/src/base/AnimeClient.js"></script>
<!-- include test files here... -->
<script src="tests/AnimeClient.js"></script>

44
public/tools/build-js.js Normal file
View File

@ -0,0 +1,44 @@
import compiler from '@ampproject/rollup-plugin-closure-compiler';
const plugins = [
compiler({
assumeFunctionWrapper: true,
compilationLevel: 'WHITESPACE_ONLY', //'ADVANCED',
createSourceMap: true,
env: 'BROWSER',
languageIn: 'ECMASCRIPT_2018',
languageOut: 'ES3'
})
];
const defaultOutput = {
format: 'iife',
sourcemap: true,
}
export default [{
input: './js/src/index.js',
output: {
...defaultOutput,
file: './js/scripts.min.js',
sourcemapFile: './js/scripts.min.js.map',
},
plugins,
}, {
input: './js/src/index-authed.js',
output: {
...defaultOutput,
file: './js/scripts-authed.min.js',
sourcemapFile: './js/scripts-authed.min.js.map',
},
plugins,
}, {
input: './js/src/base/sort_tables.js',
output: {
...defaultOutput,
file: './js/tables.min.js',
sourcemapFile: './js/tables.min.js.map',
},
plugins,
}];

View File

@ -7,7 +7,7 @@ const atImport = require('postcss-import');
const cssNext = require('postcss-cssnext');
const cssNano = require('cssnano');
const css = fs.readFileSync('css/base.css', 'utf-8');
const css = fs.readFileSync('css/all.css', 'utf-8');
postcss()
.use(atImport())
@ -21,7 +21,7 @@ postcss()
}
}))
.process(css, {
from: 'css/base.css',
from: 'css/all.css',
to: 'css/app.min.css'
}).then(result => {
fs.writeFileSync('css/app.min.css', result.css);

File diff suppressed because it is too large Load Diff

View File

@ -2,15 +2,15 @@
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu and MyAnimeList to manage anime and manga watch lists
* An API client for Kitsu to manage anime and manga watch lists
*
* PHP version 7
* PHP version 7.1
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2018 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.0
* @version 4.1
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
@ -66,6 +66,18 @@ class APIRequestBuilder {
*/
protected $request;
/**
* Do a basic minimal GET request
*
* @param string $uri
* @return Request
*/
public static function simpleRequest(string $uri): Request
{
return (new Request($uri))
->withHeader('User-Agent', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:64.0) Gecko/20100101 Firefox/64.0 ');
}
/**
* Set an authorization header
*

View File

@ -2,15 +2,15 @@
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu and MyAnimeList to manage anime and manga watch lists
* An API client for Kitsu to manage anime and manga watch lists
*
* PHP version 7
* PHP version 7.1
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2018 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.0
* @version 4.1
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
@ -29,10 +29,11 @@ use Aviat\AnimeClient\API\Enum\{
* Constants and mappings for the Anilist API
*/
final class Anilist {
const AUTH_URL = 'https://anilist.co/api/v2/oauth/authorize';
const BASE_URL = 'https://graphql.anilist.co';
public const AUTH_URL = 'https://anilist.co/api/v2/oauth/authorize';
public const TOKEN_URL = 'https://anilist.co/api/v2/oauth/token';
public const BASE_URL = 'https://graphql.anilist.co';
const KITSU_ANILIST_WATCHING_STATUS_MAP = [
public const KITSU_ANILIST_WATCHING_STATUS_MAP = [
KAWS::WATCHING => AnimeWatchingStatus::WATCHING,
KAWS::COMPLETED => AnimeWatchingStatus::COMPLETED,
KAWS::ON_HOLD => AnimeWatchingStatus::ON_HOLD,
@ -40,12 +41,28 @@ final class Anilist {
KAWS::PLAN_TO_WATCH => AnimeWatchingStatus::PLAN_TO_WATCH,
];
const ANILIST_KITSU_WATCHING_STATUS_MAP = [
'CURRENT' => KAWS::WATCHING,
'COMPLETED' => KAWS::COMPLETED,
'PAUSED' => KAWS::ON_HOLD,
'DROPPED' => KAWS::DROPPED,
'PLANNING' => KAWS::PLAN_TO_WATCH,
public const ANILIST_KITSU_WATCHING_STATUS_MAP = [
AnimeWatchingStatus::WATCHING => KAWS::WATCHING,
AnimeWatchingStatus::COMPLETED => KAWS::COMPLETED,
AnimeWatchingStatus::ON_HOLD => KAWS::ON_HOLD,
AnimeWatchingStatus::DROPPED => KAWS::DROPPED,
AnimeWatchingStatus::PLAN_TO_WATCH => KAWS::PLAN_TO_WATCH,
];
public const KITSU_ANILIST_READING_STATUS_MAP = [
KMRS::READING => MangaReadingStatus::READING,
KMRS::COMPLETED => MangaReadingStatus::COMPLETED,
KMRS::ON_HOLD => MangaReadingStatus::ON_HOLD,
KMRS::DROPPED => MangaReadingStatus::DROPPED,
KMRS::PLAN_TO_READ => MangaReadingStatus::PLAN_TO_READ,
];
public const ANILIST_KITSU_READING_STATUS_MAP = [
MangaReadingStatus::READING => KMRS::READING,
MangaReadingStatus::COMPLETED => KMRS::COMPLETED,
MangaReadingStatus::ON_HOLD => KMRS::ON_HOLD,
MangaReadingStatus::DROPPED => KMRS::DROPPED,
MangaReadingStatus::PLAN_TO_READ => KMRS::PLAN_TO_READ,
];
public static function getIdToWatchingStatusMap()
@ -67,7 +84,8 @@ final class Anilist {
'COMPLETED' => MangaReadingStatus::COMPLETED,
'PAUSED' => MangaReadingStatus::ON_HOLD,
'DROPPED' => MangaReadingStatus::DROPPED,
'PLANNING' => MangaReadingStatus::PLAN_TO_READ
'PLANNING' => MangaReadingStatus::PLAN_TO_READ,
'REPEATING' => MangaReadingStatus::READING,
];
}
}

View File

@ -2,15 +2,15 @@
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu and MyAnimeList to manage anime and manga watch lists
* An API client for Kitsu to manage anime and manga watch lists
*
* PHP version 7
* PHP version 7.1
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2018 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.0
* @version 4.1
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
@ -26,8 +26,8 @@ final class AnilistRequestBuilder extends APIRequestBuilder {
* The base url for api requests
* @var string $base_url
*/
protected $baseUrl = 'https://kitsu.io/api/edge/';
protected $baseUrl = 'https://graphql.anilist.co';
/**
* Valid HTTP request methods
* @var array
@ -41,9 +41,7 @@ final class AnilistRequestBuilder extends APIRequestBuilder {
*/
protected $defaultHeaders = [
'User-Agent' => USER_AGENT,
'Accept' => 'application/vnd.api+json',
'Content-Type' => 'application/vnd.api+json',
'CLIENT_ID' => 'dd031b32d2f56c990b1425efe6c42ad847e7fe3ab46bf1299f05ecd856bdb7dd',
'CLIENT_SECRET' => '54d7307928f63414defd96399fc31ba847961ceaecef3a5fd93144e960c0e151',
'Accept' => 'application/json',
'Content-Type' => 'application/json',
];
}

View File

@ -2,31 +2,39 @@
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu and MyAnimeList to manage anime and manga watch lists
* An API client for Kitsu to manage anime and manga watch lists
*
* PHP version 7
* PHP version 7.1
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2018 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.0
* @version 4.1
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\API\MAL;
namespace Aviat\AnimeClient\API\Anilist;
use const Aviat\AnimeClient\USER_AGENT;
use function Amp\Promise\wait;
use Amp\Artax\Request;
use Amp\Artax\Response;
use Aviat\AnimeClient\API\{
Anilist,
HummingbirdClient
};
use Aviat\Ion\Json;
use Aviat\Ion\Di\ContainerAware;
trait AnilistTrait {
use ContainerAware;
/**
* The request builder for the MAL API
* The request builder for the Anilist API
* @var AnilistRequestBuilder
*/
protected $requestBuilder;
@ -46,13 +54,13 @@ trait AnilistTrait {
'Accept' => 'application/json',
'Accept-Encoding' => 'gzip',
'Content-type' => 'application/json',
'User-Agent' => "Tim's Anime Client/4.0"
'User-Agent' => USER_AGENT,
];
/**
* Set the request builder object
*
* @param MALRequestBuilder $requestBuilder
* @param AnilistRequestBuilder $requestBuilder
* @return self
*/
public function setRequestBuilder($requestBuilder): self
@ -63,19 +71,29 @@ trait AnilistTrait {
/**
* Create a request object
*
* @param string $type
* @param string $url
* @param array $options
* @return \Amp\Artax\Response
* @return Request
*/
public function setUpRequest(string $type, string $url, array $options = [])
public function setUpRequest(string $url, array $options = []): Request
{
$config = $this->container->get('config');
$config = $this->getContainer()->get('config');
$anilistConfig = $config->get('anilist');
$request = $this->requestBuilder
->newRequest($type, $url)
->setBasicAuth($config->get(['mal','username']), $config->get(['mal','password']));
$request = $this->requestBuilder->newRequest('POST', $url);
// You can only authenticate the request if you
// actually have an access_token saved
if ($config->has(['anilist', 'access_token']))
{
$request = $request->setAuth('bearer', $anilistConfig['access_token']);
}
if (array_key_exists('form_params', $options))
{
$request = $request->setFormFields($options['form_params']);
}
if (array_key_exists('query', $options))
{
@ -84,32 +102,128 @@ trait AnilistTrait {
if (array_key_exists('body', $options))
{
$request = $request->setBody($options['body']);
$request = $request->setJsonBody($options['body']);
}
if (array_key_exists('headers', $options))
{
$request = $request->setHeaders($options['headers']);
}
return $request->getFullRequest();
}
/**
* Run a GraphQL API query
*
* @param string $name
* @param array $variables
* @return array
*/
public function runQuery(string $name, array $variables = []): array
{
$file = realpath(__DIR__ . "/GraphQL/Queries/{$name}.graphql");
if ( ! file_exists($file))
{
throw new \LogicException('GraphQL query file does not exist.');
}
// $query = str_replace(["\t", "\n"], ' ', file_get_contents($file));
$query = file_get_contents($file);
$body = [
'query' => $query
];
if ( ! empty($variables))
{
$body['variables'] = [];
foreach($variables as $key => $val)
{
$body['variables'][$key] = $val;
}
}
return $this->postRequest([
'body' => $body
]);
}
public function mutateRequest (string $name, array $variables = []): Request
{
$file = realpath(__DIR__ . "/GraphQL/Mutations/{$name}.graphql");
if (!file_exists($file))
{
throw new \LogicException('GraphQL mutation file does not exist.');
}
// $query = str_replace(["\t", "\n"], ' ', file_get_contents($file));
$query = file_get_contents($file);
$body = [
'query' => $query
];
if (!empty($variables)) {
$body['variables'] = [];
foreach ($variables as $key => $val)
{
$body['variables'][$key] = $val;
}
}
return $this->setUpRequest(Anilist::BASE_URL, [
'body' => $body,
]);
}
public function mutate (string $name, array $variables = []): array
{
$request = $this->mutateRequest($name, $variables);
$response = $this->getResponseFromRequest($request);
return Json::decode(wait($response->getBody()));
}
/**
* Make a request
*
* @param string $type
* @param string $url
* @param array $options
* @return \Amp\Artax\Response
* @return Response
*/
private function getResponse(string $type, string $url, array $options = [])
private function getResponse(string $url, array $options = []): Response
{
$logger = NULL;
if ($this->getContainer())
{
$logger = $this->container->getLogger('mal-request');
$logger = $this->container->getLogger('anilist-request');
}
$request = $this->setUpRequest($type, $url, $options);
$request = $this->setUpRequest($url, $options);
$response = wait((new HummingbirdClient)->request($request));
$logger->debug('MAL api response', [
$logger->debug('Anilist response', [
'status' => $response->getStatus(),
'reason' => $response->getReason(),
'body' => $response->getBody(),
'headers' => $response->getHeaders(),
'requestHeaders' => $request->getHeaders(),
]);
return $response;
}
private function getResponseFromRequest(Request $request): Response
{
$logger = NULL;
if ($this->getContainer())
{
$logger = $this->container->getLogger('anilist-request');
}
$response = wait((new HummingbirdClient)->request($request));
$logger->debug('Anilist response', [
'status' => $response->getStatus(),
'reason' => $response->getReason(),
'body' => $response->getBody(),
@ -121,59 +235,39 @@ trait AnilistTrait {
}
/**
* Make a request
* Remove some boilerplate for post requests
*
* @param string $type
* @param string $url
* @param array $options
* @return array
*/
private function request(string $type, string $url, array $options = []): array
protected function postRequest(array $options = []): array
{
$logger = NULL;
if ($this->getContainer())
{
$logger = $this->container->getLogger('anilist-request');
}
$response = $this->getResponse($type, $url, $options);
if ((int) $response->getStatus() > 299 OR (int) $response->getStatus() < 200)
{
if ($logger)
{
$logger->warning('Non 200 response for api call', (array)$response->getBody());
}
}
return XML::toArray(wait($response->getBody()));
}
/**
* Remove some boilerplate for post requests
*
* @param mixed ...$args
* @return array
*/
protected function postRequest(...$args): array
{
$logger = NULL;
if ($this->getContainer())
{
$logger = $this->container->getLogger('anilist-request');
}
$response = $this->getResponse('POST', ...$args);
$response = $this->getResponse(Anilist::BASE_URL, $options);
$validResponseCodes = [200, 201];
if ( ! \in_array((int) $response->getStatus(), $validResponseCodes, TRUE))
$logger = NULL;
if ($this->getContainer())
{
$logger = $this->container->getLogger('anilist-request');
$logger->debug('Anilist response', [
'status' => $response->getStatus(),
'reason' => $response->getReason(),
'body' => $response->getBody(),
'headers' => $response->getHeaders(),
//'requestHeaders' => $request->getHeaders(),
]);
}
if ( ! \in_array($response->getStatus(), $validResponseCodes, TRUE))
{
if ($logger)
{
$logger->warning('Non 201 response for POST api call', (array)$response->getBody());
$logger->warning('Non 200 response for POST api call', (array)$response->getBody());
}
}
return XML::toArray($response->getBody());
// dump(wait($response->getBody()));
return Json::decode(wait($response->getBody()));
}
}

View File

@ -0,0 +1,27 @@
mutation (
$id: Int,
$notes: String,
$private: Boolean,
$progress: Int,
$repeat: Int,
$status: MediaListStatus,
$score: Int,
) {
SaveMediaListEntry (
mediaId: $id,
notes: $notes,
private: $private,
progress: $progress,
repeat: $repeat,
scoreRaw: $score,
status: $status
) {
mediaId
notes
private
progress
repeat
score(format: POINT_10)
status
}
}

View File

@ -0,0 +1,12 @@
mutation (
$id: Int,
$status: MediaListStatus,
) {
SaveMediaListEntry (
mediaId: $id,
status: $status
) {
mediaId
status
}
}

View File

@ -0,0 +1,9 @@
mutation (
$id: Int
) {
DeleteMediaListEntry (
id: $id
) {
deleted
}
}

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