Merge branch 'develop' of timw4mail/HummingBirdAnimeClient into master
All checks were successful
timw4mail/HummingBirdAnimeClient/master This commit looks good
118
.gitignore
vendored
@ -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/people/**
|
||||
public/mal_mappings.json
|
@ -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
|
11
CHANGELOG.md
@ -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
@ -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'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
11
README.md
@ -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
|
||||
|
||||
|
@ -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",
|
||||
|
@ -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
|
@ -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"
|
@ -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;
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -2,7 +2,6 @@
|
||||
# Database Configuration #
|
||||
################################################################################
|
||||
|
||||
[collection]
|
||||
type = "sqlite"
|
||||
host = ""
|
||||
user = ""
|
||||
|
@ -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"
|
@ -9,7 +9,7 @@
|
||||
<div class="cssload-inner cssload-three"></div>
|
||||
</div>
|
||||
<label for="search">Search for anime by name: <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 ?>
|
91
app/views/anime/cover-item.php
Normal 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>
|
@ -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 ?>
|
@ -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>
|
||||
@ -26,7 +28,8 @@
|
||||
<?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>
|
||||
<td><abbr title="<?= $show_data['age_rating_guide'] ?>"><?= $show_data['age_rating'] ?></abbr>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endif ?>
|
||||
<tr>
|
||||
@ -36,21 +39,21 @@
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div>
|
||||
<h2><a rel="external" href="<?= $show_data['url'] ?>"><?= $show_data['title'] ?></a></h2>
|
||||
</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">
|
||||
<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>
|
||||
@ -58,14 +61,27 @@
|
||||
<tbody>
|
||||
<?php foreach ($show_data['streaming_links'] as $link): ?>
|
||||
<tr>
|
||||
<td class="align_left">
|
||||
<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" />
|
||||
<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",
|
||||
]); ?>
|
||||
<?= $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" />
|
||||
<?= $helper->picture("images/{$link['meta']['image']}", 'svg', [
|
||||
'class' => 'streaming-logo',
|
||||
'width' => 50,
|
||||
'height' => 50,
|
||||
'alt' => "{$link['meta']['name']} logo",
|
||||
]); ?>
|
||||
<?= $link['meta']['name'] ?>
|
||||
<?php endif ?>
|
||||
</td>
|
||||
@ -77,32 +93,83 @@
|
||||
</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>
|
||||
<?php endif ?>
|
||||
<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 ?>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<?php if (count($characters) > 0): ?>
|
||||
<hr />
|
||||
<section>
|
||||
<h2>Characters</h2>
|
||||
<section class="align_center media-wrap">
|
||||
<?php foreach($characters as $id => $char): ?>
|
||||
|
||||
<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="character">
|
||||
<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->img($urlGenerator->assetUrl("images/characters/{$id}.jpg"), [
|
||||
'width' => '225'
|
||||
]) ?>
|
||||
<?= $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>
|
@ -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> </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 />
|
||||
<form class="js-delete" action="<?= $url->generate('anime.delete') ?>" method="post">
|
||||
<fieldset>
|
||||
<legend>Danger Zone</legend>
|
||||
<form class="js-delete" action="<?= $url->generate('anime.delete') ?>" method="post">
|
||||
<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>
|
||||
</form>
|
||||
</main>
|
||||
<script defer="defer" src="<?= $urlGenerator->assetUrl('js.php/g/edit') ?>"></script>
|
||||
<?php endif ?>
|
@ -14,7 +14,7 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<?php if($auth->isAuthenticated()): ?>
|
||||
<td class="no_border"> </td>
|
||||
<td class="no-border"> </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>
|
@ -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>
|
221
app/views/character/details.php
Normal 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>
|
@ -9,7 +9,7 @@
|
||||
<div class="cssload-inner cssload-three"></div>
|
||||
</div>
|
||||
<label for="search">Search for <?= $collection_type ?> by name: <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 ?>
|
@ -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>
|
@ -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'; ?>
|
||||
|
@ -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 ?>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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';
|
||||
|
@ -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"> </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"> </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>
|
||||
<?php else: ?>
|
||||
<span class="flex-no-wrap small-font">
|
||||
[<?= $helper->a($url->generate('login'), "{$whose} Login") ?>]
|
||||
</span>
|
||||
<?php endif ?>
|
||||
</div>
|
||||
<nav>
|
||||
<?php if ($container->get('util')->isViewPage()): ?>
|
||||
<?php if ($container->get('util')->isViewPage() && ($hasAnime || $hasManga)): ?>
|
||||
<?= $helper->menu($menu_name) ?>
|
||||
<br />
|
||||
<ul>
|
||||
|
@ -9,7 +9,7 @@
|
||||
<div class="cssload-inner cssload-three"></div>
|
||||
</div>
|
||||
<label for="search">Search for manga by name: <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 ?>
|
@ -18,12 +18,12 @@
|
||||
<?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']) ?>
|
||||
@ -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 ?>
|
@ -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): ?>
|
||||
<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="character">
|
||||
<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->img($urlGenerator->assetUrl('images/characters', "{$id}.jpg"), [
|
||||
'width' => '225'
|
||||
]) ?>
|
||||
<?= $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>
|
@ -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>
|
||||
|
67
app/views/person/character-mapping.php
Normal 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>
|
67
app/views/person/details.php
Normal 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>
|
24
app/views/settings/_form.php
Normal 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 ?>
|
66
app/views/settings/settings.php
Normal 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
@ -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 ?>
|
@ -1,27 +1,31 @@
|
||||
<?php use Aviat\AnimeClient\API\Kitsu; ?>
|
||||
<main class="user-page details">
|
||||
<section class="flex flex-no-wrap">
|
||||
<div>
|
||||
<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);
|
||||
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">
|
||||
<aside class="info">
|
||||
<center>
|
||||
<?php
|
||||
$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 endif ?>
|
||||
<?php $i++; ?>
|
||||
<?php endif ?>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
</article>
|
||||
</section>
|
||||
</main>
|
@ -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
|
||||
|
@ -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
|
||||
*/
|
||||
|
||||
|
@ -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",
|
||||
|
6
console
@ -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)
|
||||
|
25
index.php
@ -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
@ -0,0 +1,4 @@
|
||||
@import "./marx.css";
|
||||
@import "./general.css";
|
||||
@import "./components.css";
|
||||
@import "./responsive.css";
|
2
public/css/app.min.css
vendored
264
public/css/components.css
Normal 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;
|
||||
}
|
||||
|
@ -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 {
|
||||
.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
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
CSS Tabs
|
||||
-----------------------------------------------------------------------------*/
|
||||
.tabs {
|
||||
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: 20px 30px 30px;
|
||||
background: #fff;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tabs .content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (min-width: 600px) {
|
||||
.tabs label {
|
||||
.settings.form .content article {
|
||||
margin: 1em;
|
||||
display: inline-block;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.tabs .content {
|
||||
order: 99;
|
||||
}
|
||||
/* ----------------------------------------------------------------------------
|
||||
iFrame container
|
||||
-----------------------------------------------------------------------------*/
|
||||
|
||||
.responsive-iframe {
|
||||
margin-top: 1em;
|
||||
overflow: hidden;
|
||||
padding-bottom: 56.25%;
|
||||
position: relative;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.responsive-iframe iframe {
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
}
|
133
public/css/responsive.css
Normal 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;
|
||||
}
|
||||
}
|
0
public/js/cache/.gitkeep → public/images/people/.gitkeep
Executable file → Normal file
BIN
public/images/placeholder.png
Normal file
After Width: | Height: | Size: 807 B |
BIN
public/images/placeholder.webp
Normal file
After Width: | Height: | Size: 1.3 KiB |
@ -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 |
@ -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 |
Before Width: | Height: | Size: 8.0 KiB After Width: | Height: | Size: 1.1 KiB |
@ -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 |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 7.8 KiB |
@ -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 |
@ -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 |
@ -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 |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 1.3 KiB |
371
public/js.php
@ -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
|
@ -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);
|
@ -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);
|
@ -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('');
|
||||
}
|
@ -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);
|
@ -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);
|
@ -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);
|
@ -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);
|
@ -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
@ -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
|
1
public/js/scripts-authed.min.js.map
Normal file
11
public/js/scripts.min.js
vendored
Normal 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
|
1
public/js/scripts.min.js.map
Normal file
91
public/js/src/anime.js
Normal 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();
|
||||
}
|
||||
});
|
||||
});
|
337
public/js/src/base/AnimeClient.js
Normal 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;
|
38
public/js/src/base/events.js
Normal 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',
|
||||
});
|
||||
});
|
@ -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();
|
4
public/js/src/index-authed.js
Normal file
@ -0,0 +1,4 @@
|
||||
import './index.js';
|
||||
|
||||
import './anime.js';
|
||||
import './manga.js';
|
10
public/js/src/index.js
Normal 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
@ -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();
|
||||
}
|
||||
});
|
||||
});
|
89
public/js/src/template-helpers.js
Normal 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
@ -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
|
1
public/js/tables.min.js.map
Normal 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;"}
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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
@ -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,
|
||||
}];
|
||||
|
@ -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);
|
1480
public/yarn.lock
@ -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
|
||||
*
|
||||
|
@ -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,
|
||||
];
|
||||
}
|
||||
}
|
@ -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,7 +26,7 @@ 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
|
||||
@ -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',
|
||||
];
|
||||
}
|
@ -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()));
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
mutation (
|
||||
$id: Int,
|
||||
$status: MediaListStatus,
|
||||
) {
|
||||
SaveMediaListEntry (
|
||||
mediaId: $id,
|
||||
status: $status
|
||||
) {
|
||||
mediaId
|
||||
status
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
mutation (
|
||||
$id: Int
|
||||
) {
|
||||
DeleteMediaListEntry (
|
||||
id: $id
|
||||
) {
|
||||
deleted
|
||||
}
|
||||
}
|