Version 5.1 - All the GraphQL #32
20
.editorconfig
Normal file
20
.editorconfig
Normal file
@ -0,0 +1,20 @@
|
||||
# EditorConfig is awesome: http://EditorConfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
# Unix-style newlines with a newline ending every file
|
||||
[*]
|
||||
end_of_line = lf
|
||||
insert_final_newline = false
|
||||
charset = utf-8
|
||||
indent_style = tab
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.{cpp,c,h,hpp,cxx}]
|
||||
insert_final_newline = true
|
||||
|
||||
# Yaml files
|
||||
[*.{yml,yaml}]
|
||||
indent_style = space
|
||||
indent_size = 4
|
157
.gitignore
vendored
157
.gitignore
vendored
@ -1,10 +1,157 @@
|
||||
|
||||
# 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/
|
||||
*.phprj
|
||||
*.workspace
|
||||
vendor
|
||||
app/cache/*
|
||||
public/images/*
|
||||
public/js/cache/*
|
||||
**/cache/**
|
||||
**/logs/**
|
||||
**/coverage/**
|
||||
**/docs/**
|
||||
**/node_modules/**
|
||||
composer.lock
|
||||
*.sqlite
|
||||
*.db
|
||||
*.sqlite3
|
||||
docs/*
|
||||
coverage/*
|
||||
apidocs/**
|
||||
tests/test_data/sessions/*
|
||||
cache.properties
|
||||
build/**
|
||||
!build/*.txt
|
||||
!build/*.xml
|
||||
!build/*.php
|
||||
app/config/*.toml
|
||||
!app/config/*.toml.example
|
||||
phinx.yml
|
||||
Caddyfile
|
||||
build/humbuglog.txt
|
||||
public/images/anime/**
|
||||
public/images/avatars/**
|
||||
public/images/manga/**
|
||||
public/images/characters/**
|
||||
public/images/people/**
|
||||
public/mal_mappings.json
|
||||
.phpunit.result.cache
|
||||
|
||||
.is-dev
|
||||
|
||||
tmp
|
||||
tools/vendor/
|
||||
tools/phinx/vendor/
|
||||
/.php-cs-fixer.php
|
||||
/.php-cs-fixer.cache
|
6
.htaccess
Normal file
6
.htaccess
Normal file
@ -0,0 +1,6 @@
|
||||
#Rewrite index.php out of the app urls
|
||||
RewriteEngine On
|
||||
RewriteBase /
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteRule ^(.*)$ index.php/$1 [L]
|
534
.php-cs-fixer.dist.php
Normal file
534
.php-cs-fixer.dist.php
Normal file
@ -0,0 +1,534 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
use Nexus\CsConfig\Factory;
|
||||
use PhpCsFixer\{Config, Finder};
|
||||
|
||||
$finder = Finder::create()
|
||||
->in([
|
||||
__DIR__ . '/src',
|
||||
__DIR__ . '/tests',
|
||||
__DIR__ . '/tools',
|
||||
])
|
||||
->exclude([
|
||||
'vendor',
|
||||
]);
|
||||
|
||||
return (new Config())
|
||||
->setRiskyAllowed(TRUE)
|
||||
->setFinder($finder)
|
||||
->setIndent(' ')
|
||||
->setRules([
|
||||
'align_multiline_comment' => false,
|
||||
'array_indentation' => true,
|
||||
'array_push' => true,
|
||||
'array_syntax' => ['syntax' => 'short'],
|
||||
'assign_null_coalescing_to_coalesce_equal' => true,
|
||||
'backtick_to_shell_exec' => true,
|
||||
'binary_operator_spaces' => [
|
||||
'default' => 'single_space',
|
||||
'operators' => [
|
||||
'=' => NULL,
|
||||
'&' => NULL,
|
||||
]
|
||||
],
|
||||
'blank_line_after_namespace' => true,
|
||||
'blank_line_after_opening_tag' => false,
|
||||
'blank_line_before_statement' => [
|
||||
'statements' => [
|
||||
// 'case',
|
||||
'continue',
|
||||
'declare',
|
||||
'default',
|
||||
'do',
|
||||
'exit',
|
||||
'for',
|
||||
'foreach',
|
||||
'goto',
|
||||
'return',
|
||||
'switch',
|
||||
'throw',
|
||||
'try',
|
||||
'while',
|
||||
'yield',
|
||||
'yield_from',
|
||||
],
|
||||
],
|
||||
// 'braces' => [
|
||||
// 'allow_single_line_anonymous_class_with_empty_body' => true,
|
||||
// 'allow_single_line_closure' => true,
|
||||
// 'position_after_anonymous_constructs' => 'same',
|
||||
// 'position_after_control_structures' => 'next',
|
||||
// 'position_after_functions_and_oop_constructs' => 'next',
|
||||
// ],
|
||||
'cast_spaces' => ['space' => 'single'],
|
||||
'class_attributes_separation' => [
|
||||
'elements' => [
|
||||
'const' => 'none',
|
||||
'property' => 'none',
|
||||
'method' => 'one',
|
||||
'trait_import' => 'none',
|
||||
],
|
||||
],
|
||||
'class_definition' => [
|
||||
'multi_line_extends_each_single_line' => true,
|
||||
'single_item_single_line' => true,
|
||||
'single_line' => true,
|
||||
'space_before_parenthesis' => true,
|
||||
],
|
||||
'class_reference_name_casing' => true,
|
||||
'clean_namespace' => true,
|
||||
'combine_consecutive_issets' => true,
|
||||
'combine_consecutive_unsets' => true,
|
||||
'combine_nested_dirname' => true,
|
||||
'comment_to_phpdoc' => [
|
||||
'ignored_tags' => [
|
||||
'todo',
|
||||
'codeCoverageIgnore',
|
||||
'codeCoverageIgnoreStart',
|
||||
'codeCoverageIgnoreEnd',
|
||||
'phpstan-ignore-line',
|
||||
'phpstan-ignore-next-line',
|
||||
],
|
||||
],
|
||||
'compact_nullable_typehint' => true,
|
||||
'concat_space' => ['spacing' => 'one'],
|
||||
'constant_case' => ['case' => 'upper'],
|
||||
'control_structure_braces' => true,
|
||||
'control_structure_continuation_position' => ['position' => 'next_line'],
|
||||
'curly_braces_position' => [
|
||||
'allow_single_line_anonymous_functions' => true,
|
||||
'allow_single_line_empty_anonymous_classes' => true,
|
||||
'anonymous_functions_opening_brace' => 'same_line',
|
||||
'classes_opening_brace' => 'next_line_unless_newline_at_signature_end',
|
||||
'control_structures_opening_brace' => 'next_line_unless_newline_at_signature_end',
|
||||
'functions_opening_brace' => 'next_line_unless_newline_at_signature_end',
|
||||
],
|
||||
'date_time_immutable' => false,
|
||||
'declare_equal_normalize' => ['space' => 'none'],
|
||||
'declare_parentheses' => true,
|
||||
'declare_strict_types' => true,
|
||||
'dir_constant' => true,
|
||||
'doctrine_annotation_array_assignment' => false,
|
||||
'doctrine_annotation_braces' => false,
|
||||
'doctrine_annotation_indentation' => false,
|
||||
'doctrine_annotation_spaces' => false,
|
||||
'echo_tag_syntax' => [
|
||||
'format' => 'short',
|
||||
'long_function' => 'echo',
|
||||
'shorten_simple_statements_only' => false,
|
||||
],
|
||||
'elseif' => false,
|
||||
'empty_loop_body' => ['style' => 'braces'],
|
||||
'empty_loop_condition' => ['style' => 'while'],
|
||||
'encoding' => true,
|
||||
'error_suppression' => [
|
||||
'mute_deprecation_error' => true,
|
||||
'noise_remaining_usages' => false,
|
||||
'noise_remaining_usages_exclude' => [],
|
||||
],
|
||||
'escape_implicit_backslashes' => [
|
||||
'double_quoted' => false,
|
||||
'heredoc_syntax' => false,
|
||||
'single_quoted' => false,
|
||||
],
|
||||
'explicit_indirect_variable' => false,
|
||||
'explicit_string_variable' => false,
|
||||
'final_class' => false,
|
||||
'final_internal_class' => [
|
||||
'annotation_exclude' => ['@no-final'],
|
||||
'annotation_include' => ['@internal'],
|
||||
'consider_absent_docblock_as_internal_class' => false,
|
||||
],
|
||||
'final_public_method_for_abstract_class' => false,
|
||||
'fopen_flag_order' => true,
|
||||
'fopen_flags' => ['b_mode' => true],
|
||||
'full_opening_tag' => true,
|
||||
'fully_qualified_strict_types' => true,
|
||||
'function_declaration' => ['closure_function_spacing' => 'one'],
|
||||
'function_to_constant' => [
|
||||
'functions' => [
|
||||
'get_called_class',
|
||||
'get_class',
|
||||
'get_class_this',
|
||||
'php_sapi_name',
|
||||
'phpversion',
|
||||
'pi',
|
||||
],
|
||||
],
|
||||
'function_typehint_space' => true,
|
||||
'general_phpdoc_annotation_remove' => false,
|
||||
'general_phpdoc_tag_rename' => false,
|
||||
'get_class_to_class_keyword' => false,
|
||||
'global_namespace_import' => [
|
||||
'import_constants' => true,
|
||||
'import_functions' => true,
|
||||
'import_classes' => true,
|
||||
],
|
||||
'group_import' => true,
|
||||
'header_comment' => false, // false by default
|
||||
// 'heredoc_indentation' => ['indentation' => 'start_plus_one'],
|
||||
'heredoc_to_nowdoc' => true,
|
||||
'implode_call' => true,
|
||||
'include' => true,
|
||||
'increment_style' => ['style' => 'post'],
|
||||
'indentation_type' => true,
|
||||
'integer_literal_case' => true,
|
||||
'is_null' => true,
|
||||
'lambda_not_used_import' => true,
|
||||
'line_ending' => true,
|
||||
'linebreak_after_opening_tag' => false,
|
||||
'list_syntax' => ['syntax' => 'short'],
|
||||
'logical_operators' => true,
|
||||
'lowercase_cast' => true,
|
||||
'lowercase_keywords' => true,
|
||||
'lowercase_static_reference' => true,
|
||||
'magic_constant_casing' => true,
|
||||
'magic_method_casing' => true,
|
||||
'mb_str_functions' => false,
|
||||
'method_argument_space' => [
|
||||
'after_heredoc' => false,
|
||||
'keep_multiple_spaces_after_comma' => false,
|
||||
'on_multiline' => 'ensure_fully_multiline',
|
||||
],
|
||||
'method_chaining_indentation' => true,
|
||||
'modernize_strpos' => false, // requires 8.0+
|
||||
'modernize_types_casting' => true,
|
||||
'multiline_comment_opening_closing' => true,
|
||||
'multiline_whitespace_before_semicolons' => ['strategy' => 'no_multi_line'],
|
||||
'native_constant_invocation' => false,
|
||||
'native_function_casing' => true,
|
||||
'native_function_invocation' => false,
|
||||
'native_function_type_declaration_casing' => true,
|
||||
'new_with_braces' => true,
|
||||
'no_alias_functions' => ['sets' => ['@all']],
|
||||
'no_alias_language_construct_call' => true,
|
||||
'no_alternative_syntax' => ['fix_non_monolithic_code' => false],
|
||||
'no_binary_string' => true,
|
||||
'no_blank_lines_after_class_opening' => true,
|
||||
'no_blank_lines_after_phpdoc' => true,
|
||||
'no_blank_lines_before_namespace' => false, // conflicts with `single_blank_line_before_namespace`
|
||||
'no_break_comment' => ['comment_text' => 'no break'],
|
||||
'no_closing_tag' => true,
|
||||
'no_empty_comment' => true,
|
||||
'no_empty_phpdoc' => true,
|
||||
'no_empty_statement' => true,
|
||||
'no_extra_blank_lines' => ['tokens' => ['extra']],
|
||||
'no_homoglyph_names' => true,
|
||||
'no_leading_import_slash' => true,
|
||||
'no_leading_namespace_whitespace' => true,
|
||||
'no_mixed_echo_print' => ['use' => 'echo'],
|
||||
'no_multiline_whitespace_around_double_arrow' => true,
|
||||
'no_null_property_initialization' => true,
|
||||
'no_short_bool_cast' => true,
|
||||
'no_singleline_whitespace_before_semicolons' => true,
|
||||
'no_space_around_double_colon' => true,
|
||||
'no_spaces_after_function_name' => true,
|
||||
'no_spaces_around_offset' => ['positions' => ['inside', 'outside']],
|
||||
'no_spaces_inside_parenthesis' => true,
|
||||
'no_superfluous_elseif' => true,
|
||||
'no_superfluous_phpdoc_tags' => [
|
||||
'allow_mixed' => true,
|
||||
'allow_unused_params' => true,
|
||||
'remove_inheritdoc' => false,
|
||||
],
|
||||
'no_trailing_comma_in_singleline' => true,
|
||||
'no_trailing_whitespace' => true,
|
||||
'no_trailing_whitespace_in_comment' => true,
|
||||
'no_trailing_whitespace_in_string' => true,
|
||||
'no_unneeded_control_parentheses' => [
|
||||
'statements' => [
|
||||
'break',
|
||||
'clone',
|
||||
'continue',
|
||||
'echo_print',
|
||||
'return',
|
||||
'switch_case',
|
||||
'yield',
|
||||
],
|
||||
],
|
||||
'no_unneeded_curly_braces' => ['namespaces' => true],
|
||||
'no_unneeded_final_method' => ['private_methods' => true],
|
||||
'no_unneeded_import_alias' => true,
|
||||
'no_unreachable_default_argument_value' => true,
|
||||
'no_unset_cast' => true,
|
||||
'no_unset_on_property' => false,
|
||||
'no_unused_imports' => true,
|
||||
'no_useless_else' => true,
|
||||
'no_useless_return' => true,
|
||||
'no_useless_sprintf' => true,
|
||||
'no_whitespace_before_comma_in_array' => ['after_heredoc' => true],
|
||||
'no_whitespace_in_blank_line' => true,
|
||||
'non_printable_character' => ['use_escape_sequences_in_strings' => true],
|
||||
'normalize_index_brace' => true,
|
||||
'not_operator_with_space' => true,
|
||||
'not_operator_with_successor_space' => true,
|
||||
'nullable_type_declaration_for_default_null_value' => ['use_nullable_type_declaration' => true],
|
||||
'object_operator_without_whitespace' => true,
|
||||
'operator_linebreak' => ['only_booleans' => true, 'position' => 'beginning'],
|
||||
'ordered_class_elements' => [
|
||||
'order' => [
|
||||
'use_trait',
|
||||
'case',
|
||||
'constant_public',
|
||||
'constant_protected',
|
||||
'constant_private',
|
||||
'property_public',
|
||||
'property_protected',
|
||||
'property_private',
|
||||
'construct',
|
||||
'destruct',
|
||||
'magic',
|
||||
],
|
||||
'sort_algorithm' => 'none',
|
||||
],
|
||||
'ordered_imports' => [
|
||||
'sort_algorithm' => 'alpha',
|
||||
'imports_order' => ['class', 'function', 'const'],
|
||||
],
|
||||
'ordered_interfaces' => false,
|
||||
'ordered_traits' => false,
|
||||
'php_unit_construct' => [
|
||||
'assertions' => [
|
||||
'assertSame',
|
||||
'assertEquals',
|
||||
'assertNotEquals',
|
||||
'assertNotSame',
|
||||
],
|
||||
],
|
||||
'php_unit_dedicate_assert' => ['target' => 'newest'],
|
||||
'php_unit_dedicate_assert_internal_type' => ['target' => 'newest'],
|
||||
'php_unit_expectation' => ['target' => 'newest'],
|
||||
'php_unit_fqcn_annotation' => true,
|
||||
'php_unit_internal_class' => ['types' => ['final']],
|
||||
'php_unit_method_casing' => ['case' => 'camel_case'],
|
||||
'php_unit_mock' => ['target' => 'newest'],
|
||||
'php_unit_mock_short_will_return' => true,
|
||||
'php_unit_namespaced' => ['target' => 'newest'],
|
||||
'php_unit_no_expectation_annotation' => [
|
||||
'target' => 'newest',
|
||||
'use_class_const' => true,
|
||||
],
|
||||
'php_unit_set_up_tear_down_visibility' => true,
|
||||
'php_unit_size_class' => false,
|
||||
// 'php_unit_strict' => [
|
||||
// 'assertions' => [
|
||||
// 'assertAttributeEquals',
|
||||
// 'assertAttributeNotEquals',
|
||||
// 'assertEquals',
|
||||
// 'assertNotEquals',
|
||||
// ],
|
||||
// ],
|
||||
'php_unit_test_annotation' => ['style' => 'prefix'],
|
||||
'php_unit_test_case_static_method_calls' => [
|
||||
'call_type' => 'this',
|
||||
'methods' => [],
|
||||
],
|
||||
'php_unit_test_class_requires_covers' => false,
|
||||
'phpdoc_add_missing_param_annotation' => ['only_untyped' => true],
|
||||
'phpdoc_align' => [
|
||||
'align' => 'left'
|
||||
],
|
||||
'phpdoc_annotation_without_dot' => false,
|
||||
'phpdoc_indent' => true,
|
||||
'phpdoc_inline_tag_normalizer' => [
|
||||
'tags' => [
|
||||
'example',
|
||||
'id',
|
||||
'internal',
|
||||
'inheritdoc',
|
||||
'inheritdocs',
|
||||
'link',
|
||||
'source',
|
||||
'toc',
|
||||
'tutorial',
|
||||
],
|
||||
],
|
||||
'phpdoc_line_span' => [
|
||||
'const' => 'multi',
|
||||
'method' => 'multi',
|
||||
'property' => 'multi',
|
||||
],
|
||||
'phpdoc_no_access' => true,
|
||||
'phpdoc_no_empty_return' => false,
|
||||
'phpdoc_no_package' => false,
|
||||
'phpdoc_no_useless_inheritdoc' => true,
|
||||
'phpdoc_order' => true,
|
||||
'phpdoc_order_by_value' => [
|
||||
'annotations' => [
|
||||
'author',
|
||||
'covers',
|
||||
'coversNothing',
|
||||
'dataProvider',
|
||||
'depends',
|
||||
'group',
|
||||
'internal',
|
||||
'method',
|
||||
'property',
|
||||
'property-read',
|
||||
'property-write',
|
||||
'requires',
|
||||
'throws',
|
||||
'uses',
|
||||
],
|
||||
],
|
||||
'phpdoc_return_self_reference' => [
|
||||
'replacements' => [
|
||||
'this' => '$this',
|
||||
'@this' => '$this',
|
||||
'$self' => 'self',
|
||||
'@self' => 'self',
|
||||
'$static' => 'static',
|
||||
'@static' => 'static',
|
||||
],
|
||||
],
|
||||
'phpdoc_scalar' => [
|
||||
'types' => [
|
||||
'boolean',
|
||||
'callback',
|
||||
'double',
|
||||
'integer',
|
||||
'real',
|
||||
'str',
|
||||
],
|
||||
],
|
||||
'phpdoc_separation' => false,
|
||||
'phpdoc_single_line_var_spacing' => true,
|
||||
'phpdoc_summary' => false,
|
||||
'phpdoc_tag_casing' => ['tags' => ['inheritDoc']],
|
||||
'phpdoc_tag_type' => ['tags' => ['inheritDoc' => 'inline']],
|
||||
'phpdoc_to_comment' => false,
|
||||
'phpdoc_to_param_type' => false,
|
||||
'phpdoc_to_property_type' => false,
|
||||
'phpdoc_to_return_type' => false,
|
||||
'phpdoc_trim' => true,
|
||||
'phpdoc_trim_consecutive_blank_line_separation' => true,
|
||||
'phpdoc_types' => ['groups' => ['simple', 'alias', 'meta']],
|
||||
'phpdoc_types_order' => [
|
||||
'null_adjustment' => 'always_last',
|
||||
'sort_algorithm' => 'alpha',
|
||||
],
|
||||
'phpdoc_var_annotation_correct_order' => true,
|
||||
'phpdoc_var_without_name' => true,
|
||||
'pow_to_exponentiation' => true,
|
||||
'protected_to_private' => true,
|
||||
'psr_autoloading' => ['dir' => null],
|
||||
'random_api_migration' => [
|
||||
'replacements' => [
|
||||
'getrandmax' => 'mt_getrandmax',
|
||||
'rand' => 'mt_rand',
|
||||
'srand' => 'mt_srand',
|
||||
],
|
||||
],
|
||||
'regular_callable_call' => true,
|
||||
'return_assignment' => true,
|
||||
'return_type_declaration' => ['space_before' => 'none'],
|
||||
'self_accessor' => false,
|
||||
'self_static_accessor' => true,
|
||||
'semicolon_after_instruction' => false,
|
||||
'set_type_to_cast' => true,
|
||||
'short_scalar_cast' => true,
|
||||
'simple_to_complex_string_variable' => true,
|
||||
'simplified_if_return' => true,
|
||||
'simplified_null_return' => false,
|
||||
'single_blank_line_at_eof' => true,
|
||||
'single_blank_line_before_namespace' => true,
|
||||
'single_class_element_per_statement' => ['elements' => ['const', 'property']],
|
||||
'single_import_per_statement' => false,
|
||||
'single_line_after_imports' => true,
|
||||
'single_line_comment_style' => ['comment_types' => ['asterisk', 'hash']],
|
||||
'single_line_throw' => false,
|
||||
'single_quote' => ['strings_containing_single_quote_chars' => false],
|
||||
'single_space_around_construct' => [
|
||||
'constructs_followed_by_a_single_space' => [
|
||||
'abstract',
|
||||
'as',
|
||||
'attribute',
|
||||
'break',
|
||||
'case',
|
||||
'catch',
|
||||
'class',
|
||||
'clone',
|
||||
'comment',
|
||||
'const',
|
||||
'const_import',
|
||||
'continue',
|
||||
'do',
|
||||
'echo',
|
||||
'else',
|
||||
'elseif',
|
||||
'extends',
|
||||
'final',
|
||||
'finally',
|
||||
'for',
|
||||
'foreach',
|
||||
'function',
|
||||
'function_import',
|
||||
'global',
|
||||
'goto',
|
||||
'if',
|
||||
'implements',
|
||||
'include',
|
||||
'include_once',
|
||||
'instanceof',
|
||||
'insteadof',
|
||||
'interface',
|
||||
'match',
|
||||
'named_argument',
|
||||
'new',
|
||||
'open_tag_with_echo',
|
||||
'php_doc',
|
||||
'php_open',
|
||||
'print',
|
||||
'private',
|
||||
'protected',
|
||||
'public',
|
||||
'require',
|
||||
'require_once',
|
||||
'return',
|
||||
'static',
|
||||
'throw',
|
||||
'trait',
|
||||
'try',
|
||||
'use',
|
||||
'use_lambda',
|
||||
'use_trait',
|
||||
'var',
|
||||
'while',
|
||||
'yield',
|
||||
'yield_from',
|
||||
],
|
||||
],
|
||||
'single_trait_insert_per_statement' => true,
|
||||
'space_after_semicolon' => ['remove_in_empty_for_expressions' => true],
|
||||
'standardize_increment' => true,
|
||||
'standardize_not_equals' => true,
|
||||
'statement_indentation' => true,
|
||||
'static_lambda' => true,
|
||||
'strict_comparison' => true,
|
||||
'strict_param' => true,
|
||||
'string_length_to_empty' => true,
|
||||
'string_line_ending' => true,
|
||||
'switch_case_semicolon_to_colon' => true,
|
||||
'switch_case_space' => true,
|
||||
'switch_continue_to_break' => true,
|
||||
'ternary_operator_spaces' => true,
|
||||
'ternary_to_elvis_operator' => true,
|
||||
'ternary_to_null_coalescing' => true,
|
||||
'trailing_comma_in_multiline' => [
|
||||
'after_heredoc' => true,
|
||||
'elements' => ['arrays'],
|
||||
],
|
||||
'trim_array_spaces' => true,
|
||||
'types_spaces' => ['space' => 'none'],
|
||||
'unary_operator_spaces' => false,
|
||||
'use_arrow_functions' => true,
|
||||
'visibility_required' => ['elements' => ['const', 'method', 'property']],
|
||||
'void_return' => false, // changes method signature
|
||||
'whitespace_after_comma_in_array' => true,
|
||||
'yoda_style' => [
|
||||
'equal' => false,
|
||||
'identical' => null,
|
||||
'less_and_greater' => false,
|
||||
'always_move_variable' => false,
|
||||
],
|
||||
]);
|
17
.travis.yml
17
.travis.yml
@ -1,20 +1,17 @@
|
||||
language: php
|
||||
|
||||
install:
|
||||
- composer install
|
||||
- composer install --ignore-platform-reqs
|
||||
|
||||
php:
|
||||
- 5.4
|
||||
- 5.5
|
||||
- 5.6
|
||||
- 7
|
||||
- hhvm
|
||||
- 8.0
|
||||
- 8.1
|
||||
- nightly
|
||||
|
||||
script:
|
||||
- mkdir -p build/logs
|
||||
- phpunit --coverage-clover=coverage.clover
|
||||
- php vendor/bin/phpunit -c build
|
||||
|
||||
after_script:
|
||||
- wget https://scrutinizer-ci.com/ocular.phar
|
||||
- php ocular.phar code-coverage:upload --format=php-clover coverage.clover
|
||||
#matrix:
|
||||
# allow_failures:
|
||||
# - php: nightly
|
||||
|
51
CHANGELOG.md
Normal file
51
CHANGELOG.md
Normal file
@ -0,0 +1,51 @@
|
||||
# Changelog
|
||||
|
||||
## Version 5.3
|
||||
* Update PHP requirement to 8.2
|
||||
|
||||
## Version 5.2
|
||||
* Updated PHP requirement to 8.1
|
||||
* Updated to support PHP 8.2
|
||||
* Improve Anilist <-> Kitsu mappings to be more reliable
|
||||
|
||||
## Version 5.1
|
||||
* Added session check, so when coming back to a page, if the session is expired, the page will refresh.
|
||||
* Updated logging config so that much fewer, much smaller files are generated.
|
||||
* Updated Kitsu integration to use GraphQL API, reducing a lot of internal complexity.
|
||||
|
||||
## Version 5
|
||||
* Updated PHP requirement to 7.4
|
||||
* Added anime watching history view
|
||||
* Added manga reading history view
|
||||
* Updated anime collection to have more media types
|
||||
|
||||
## Version 4.2
|
||||
* Updated dependencies
|
||||
* Updated PHP requirement to 7.3
|
||||
* Added option to automatically set dark mode based on the OS setting
|
||||
|
||||
## Version 4.1
|
||||
* Added optional dark theme
|
||||
* 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
|
||||
* Converted user configuration to toml files
|
||||
* Added a caching layer for api calls, which resets upon updates from the
|
||||
app.
|
||||
* Added a bulk thumbnail generator script
|
||||
* Removed json file "cache" from the app folder
|
||||
|
52
Jenkinsfile
vendored
Normal file
52
Jenkinsfile
vendored
Normal file
@ -0,0 +1,52 @@
|
||||
pipeline {
|
||||
agent none
|
||||
stages {
|
||||
stage('setup') {
|
||||
agent any
|
||||
steps {
|
||||
sh 'curl -sS https://getcomposer.org/installer | php'
|
||||
sh 'rm -rf ./vendor'
|
||||
sh 'rm -f composer.lock'
|
||||
sh 'php composer.phar install --ignore-platform-reqs'
|
||||
}
|
||||
}
|
||||
stage('PHP 8') {
|
||||
agent {
|
||||
docker {
|
||||
image 'php:8-cli-alpine'
|
||||
args '-u root --privileged'
|
||||
}
|
||||
}
|
||||
steps {
|
||||
sh 'apk add --no-cache git icu-dev'
|
||||
sh 'docker-php-ext-configure intl && docker-php-ext-install intl'
|
||||
sh 'php ./vendor/bin/phpunit --colors=never'
|
||||
}
|
||||
}
|
||||
stage('Latest PHP') {
|
||||
agent {
|
||||
docker {
|
||||
image 'php:cli-alpine'
|
||||
args '-u root --privileged'
|
||||
}
|
||||
}
|
||||
steps {
|
||||
sh 'apk add --no-cache git icu-dev'
|
||||
sh 'docker-php-ext-configure intl && docker-php-ext-install intl'
|
||||
sh 'php ./vendor/bin/phpunit --colors=never'
|
||||
}
|
||||
}
|
||||
stage('Coverage') {
|
||||
agent any
|
||||
steps {
|
||||
sh 'php composer.phar run-script coverage'
|
||||
step([
|
||||
$class: 'CloverPublisher',
|
||||
cloverReportDir: '',
|
||||
cloverReportFileName: 'build/logs/clover.xml',
|
||||
])
|
||||
junit 'build/logs/junit.xml'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2017 Timothy J Warren
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
51
README.md
51
README.md
@ -1,10 +1,11 @@
|
||||
# Hummingbird Anime Client
|
||||
|
||||
A self-hosted client that allows custom formatting of data from the hummingbird api
|
||||
Update your anime/manga list on Kitsu.io and Anilist
|
||||
|
||||
[![Build Status](https://travis-ci.org/timw4mail/HummingBirdAnimeClient.svg)](https://travis-ci.org/timw4mail/HummingBirdAnimeClient)
|
||||
[![Build Status](https://travis-ci.com/timw4mail/HummingBirdAnimeClient.svg?branch=master)](https://travis-ci.com/github/timw4mail/HummingBirdAnimeClient)
|
||||
[![Build Status](https://jenkins.timshome.page/buildStatus/icon?job=timw4mail/HummingBirdAnimeClient/develop)](https://jenkins.timshome.page/job/timw4mail/job/HummingBirdAnimeClient/job/develop/)
|
||||
|
||||
[[Hosted Example](https://anime.timshomepage.net)]
|
||||
[[Hosted Example](https://list.timshomepage.net)]
|
||||
|
||||
## Features
|
||||
|
||||
@ -14,7 +15,7 @@ A self-hosted client that allows custom formatting of data from the hummingbird
|
||||
* On Hold
|
||||
* Dropped
|
||||
* Completed
|
||||
* All of the above
|
||||
* Combined View
|
||||
|
||||
* Manga List views (Each with list and cover views):
|
||||
* Reading
|
||||
@ -22,7 +23,7 @@ A self-hosted client that allows custom formatting of data from the hummingbird
|
||||
* On Hold
|
||||
* Dropped
|
||||
* Completed
|
||||
* All of the above
|
||||
* Combined View
|
||||
|
||||
* Anime collection view (segmented by media type):
|
||||
* Cover Images
|
||||
@ -30,26 +31,36 @@ A self-hosted client that allows custom formatting of data from the hummingbird
|
||||
|
||||
### Requirements
|
||||
|
||||
* PHP 5.4+
|
||||
* PDO SQLite (For collection tab)
|
||||
* GD
|
||||
* PHP 8.2
|
||||
* ext-dom (For editing the DOM)
|
||||
* ext-gd (For caching images)
|
||||
* ext-intl (For time localization)
|
||||
* ext-json
|
||||
* ext-mbstring
|
||||
* ext-pdo
|
||||
|
||||
### Highly Recommended
|
||||
|
||||
* Redis or Memcached for caching
|
||||
* PDO SQLite or PDO PostgreSQL (For collection tab)
|
||||
|
||||
### Installation
|
||||
|
||||
1. Install dependencies via composer: `composer install`
|
||||
2. Change the `WHOSE` constant declaration in `index.php` to your name
|
||||
3. Configure settings in `app/config/config.php` to your liking
|
||||
1. Install via git, then install dependencies via composer: `composer install`
|
||||
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/cache
|
||||
* public/images/manga
|
||||
* app/config
|
||||
* app/logs
|
||||
* public/images/avatars
|
||||
* public/images/anime
|
||||
* public/js/cache
|
||||
* 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.
|
||||
|
||||
#### Anime Collection Additional Installation
|
||||
* Run `php /vendor/bin/phinx migrate -e development` to create the database tables
|
||||
* For importing anime:
|
||||
1. Find the anime you are looking for on the hummingbird search api page: `https://hummingbird.me/api/v1/search/anime?query=`
|
||||
2. Create an `import.json` file in the root of the app, with an array of objects from the search page that you want to import
|
||||
3. Go to the anime collection tab, and the import will be run
|
||||
### Server Setup
|
||||
|
||||
See the [wiki](https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient/wiki)
|
||||
for more in-depth information
|
||||
|
60
app/appConf/base_config.php
Normal file
60
app/appConf/base_config.php
Normal file
@ -0,0 +1,60 @@
|
||||
<?php declare(strict_types=1);
|
||||
/**
|
||||
* Hummingbird Anime List Client
|
||||
*
|
||||
* An API client for Kitsu to manage anime and manga watch lists
|
||||
*
|
||||
* PHP version 8
|
||||
*
|
||||
* @package HummingbirdAnimeClient
|
||||
* @author Timothy J. Warren <tim@timshomepage.net>
|
||||
* @copyright 2015 - 2021 Timothy J. Warren
|
||||
* @license http://www.opensource.org/licenses/mit-license.html MIT License
|
||||
* @version 5.2
|
||||
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
|
||||
*/
|
||||
|
||||
use function Aviat\AnimeClient\loadConfig;
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Lower level configuration
|
||||
//
|
||||
// You shouldn't generally need to change anything below this line
|
||||
// ----------------------------------------------------------------------------
|
||||
$APP_DIR = dirname(__DIR__);
|
||||
$ROOT_DIR = dirname($APP_DIR);
|
||||
|
||||
$tomlConfig = loadConfig(__DIR__);
|
||||
|
||||
return array_merge($tomlConfig, [
|
||||
'root' => $ROOT_DIR,
|
||||
'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",
|
||||
|
||||
// Cache paths
|
||||
'data_cache_path' => "{$APP_DIR}/cache",
|
||||
'img_cache_path' => "{$ROOT_DIR}/public/images",
|
||||
|
||||
// Included config files
|
||||
'routes' => require 'routes.php',
|
||||
]);
|
21
app/appConf/menus.toml
Normal file
21
app/appConf/menus.toml
Normal file
@ -0,0 +1,21 @@
|
||||
[anime_list]
|
||||
route_prefix = ""
|
||||
[anime_list.items]
|
||||
watch_history = '/history/anime'
|
||||
watching = '/anime/watching'
|
||||
plan_to_watch = '/anime/plan_to_watch'
|
||||
on_hold = '/anime/on_hold'
|
||||
dropped = '/anime/dropped'
|
||||
completed = '/anime/completed'
|
||||
all = '/anime/all'
|
||||
|
||||
[manga_list]
|
||||
route_prefix = ""
|
||||
[manga_list.items]
|
||||
reading_history = '/history/manga'
|
||||
reading = '/manga/reading'
|
||||
plan_to_read = '/manga/plan_to_read'
|
||||
on_hold = '/manga/on_hold'
|
||||
dropped = '/manga/dropped'
|
||||
completed = '/manga/completed'
|
||||
all = '/manga/all'
|
329
app/appConf/routes.php
Normal file
329
app/appConf/routes.php
Normal file
@ -0,0 +1,329 @@
|
||||
<?php declare(strict_types=1);
|
||||
/**
|
||||
* Hummingbird Anime List Client
|
||||
*
|
||||
* An API client for Kitsu to manage anime and manga watch lists
|
||||
*
|
||||
* PHP version 8
|
||||
*
|
||||
* @package HummingbirdAnimeClient
|
||||
* @author Timothy J. Warren <tim@timshomepage.net>
|
||||
* @copyright 2015 - 2021 Timothy J. Warren
|
||||
* @license http://www.opensource.org/licenses/mit-license.html MIT License
|
||||
* @version 5.2
|
||||
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
|
||||
*/
|
||||
|
||||
use const Aviat\AnimeClient\{
|
||||
ALPHA_SLUG_PATTERN,
|
||||
DEFAULT_CONTROLLER,
|
||||
DEFAULT_CONTROLLER_METHOD,
|
||||
NUM_PATTERN,
|
||||
SLUG_PATTERN,
|
||||
SLUG_SPACE_PATTERN,
|
||||
};
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Routing Config
|
||||
//
|
||||
// Maps paths to controllers and methods
|
||||
// -------------------------------------------------------------------------
|
||||
$routes = [
|
||||
// ---------------------------------------------------------------------
|
||||
// AJAX Routes
|
||||
// ---------------------------------------------------------------------
|
||||
'cache_purge' => [
|
||||
'path' => '/cache_purge',
|
||||
'action' => 'clearCache',
|
||||
],
|
||||
'heartbeat' => [
|
||||
'path' => '/heartbeat',
|
||||
'action' => 'heartbeat',
|
||||
],
|
||||
// ---------------------------------------------------------------------
|
||||
// Anime List Routes
|
||||
// ---------------------------------------------------------------------
|
||||
'anime.add.get' => [
|
||||
'path' => '/anime/add',
|
||||
'action' => 'addForm',
|
||||
],
|
||||
'anime.add.post' => [
|
||||
'path' => '/anime/add',
|
||||
'action' => 'add',
|
||||
'verb' => 'post',
|
||||
],
|
||||
'anime.random' => [
|
||||
'path' => '/anime/details/random',
|
||||
'action' => 'random',
|
||||
],
|
||||
'anime.details' => [
|
||||
'path' => '/anime/details/{id}',
|
||||
'action' => 'details',
|
||||
'tokens' => [
|
||||
'id' => SLUG_PATTERN,
|
||||
],
|
||||
],
|
||||
'anime.delete' => [
|
||||
'path' => '/anime/delete',
|
||||
'action' => 'delete',
|
||||
'verb' => 'post',
|
||||
],
|
||||
// ---------------------------------------------------------------------
|
||||
// Manga Routes
|
||||
// ---------------------------------------------------------------------
|
||||
'manga.search' => [
|
||||
'path' => '/manga/search',
|
||||
'action' => 'search',
|
||||
],
|
||||
'manga.add.get' => [
|
||||
'path' => '/manga/add',
|
||||
'action' => 'addForm',
|
||||
],
|
||||
'manga.add.post' => [
|
||||
'path' => '/manga/add',
|
||||
'action' => 'add',
|
||||
'verb' => 'post',
|
||||
],
|
||||
'manga.delete' => [
|
||||
'path' => '/manga/delete',
|
||||
'action' => 'delete',
|
||||
'verb' => 'post',
|
||||
],
|
||||
'manga.random' => [
|
||||
'path' => '/manga/details/random',
|
||||
'action' => 'random',
|
||||
],
|
||||
'manga.details' => [
|
||||
'path' => '/manga/details/{id}',
|
||||
'action' => 'details',
|
||||
'tokens' => [
|
||||
'id' => SLUG_PATTERN,
|
||||
],
|
||||
],
|
||||
// ---------------------------------------------------------------------
|
||||
// Anime Collection Routes
|
||||
// ---------------------------------------------------------------------
|
||||
'anime.collection.search' => [
|
||||
'path' => '/anime-collection/search',
|
||||
'action' => 'search',
|
||||
],
|
||||
'anime.collection.add.get' => [
|
||||
'path' => '/anime-collection/add',
|
||||
'action' => 'form',
|
||||
],
|
||||
'anime.collection.edit.get' => [
|
||||
'path' => '/anime-collection/edit/{id}',
|
||||
'action' => 'form',
|
||||
'tokens' => [
|
||||
'id' => NUM_PATTERN,
|
||||
],
|
||||
],
|
||||
'anime.collection.add.post' => [
|
||||
'path' => '/anime-collection/add',
|
||||
'action' => 'add',
|
||||
'verb' => 'post',
|
||||
],
|
||||
'anime.collection.edit.post' => [
|
||||
'path' => '/anime-collection/edit',
|
||||
'action' => 'edit',
|
||||
'verb' => 'post',
|
||||
],
|
||||
'anime.collection.view' => [
|
||||
'path' => '/anime-collection/view{/view}',
|
||||
'action' => 'view',
|
||||
'tokens' => [
|
||||
'view' => ALPHA_SLUG_PATTERN,
|
||||
],
|
||||
],
|
||||
'anime.collection.delete' => [
|
||||
'path' => '/anime-collection/delete',
|
||||
'action' => 'delete',
|
||||
'verb' => 'post',
|
||||
],
|
||||
'anime.collection.redirect' => [
|
||||
'path' => '/anime-collection',
|
||||
],
|
||||
'anime.collection.redirect2' => [
|
||||
'path' => '/anime-collection/',
|
||||
],
|
||||
// ---------------------------------------------------------------------
|
||||
// Manga Collection Routes
|
||||
// ---------------------------------------------------------------------
|
||||
'manga.collection.search' => [
|
||||
'path' => '/manga-collection/search',
|
||||
'action' => 'search',
|
||||
],
|
||||
'manga.collection.add.get' => [
|
||||
'path' => '/manga-collection/add',
|
||||
'action' => 'form',
|
||||
],
|
||||
'manga.collection.edit.get' => [
|
||||
'path' => '/manga-collection/edit/{id}',
|
||||
'action' => 'form',
|
||||
'tokens' => [
|
||||
'id' => NUM_PATTERN,
|
||||
],
|
||||
],
|
||||
'manga.collection.add.post' => [
|
||||
'path' => '/manga-collection/add',
|
||||
'action' => 'add',
|
||||
'verb' => 'post',
|
||||
],
|
||||
'manga.collection.edit.post' => [
|
||||
'path' => '/manga-collection/edit',
|
||||
'action' => 'edit',
|
||||
'verb' => 'post',
|
||||
],
|
||||
'manga.collection.view' => [
|
||||
'path' => '/manga-collection/view{/view}',
|
||||
'tokens' => [
|
||||
'view' => ALPHA_SLUG_PATTERN,
|
||||
],
|
||||
],
|
||||
'manga.collection.delete' => [
|
||||
'path' => '/manga-collection/delete',
|
||||
'action' => 'delete',
|
||||
'verb' => 'post',
|
||||
],
|
||||
// ---------------------------------------------------------------------
|
||||
// Other Routes
|
||||
// ---------------------------------------------------------------------
|
||||
'character' => [
|
||||
'path' => '/character/{slug}',
|
||||
'tokens' => [
|
||||
'slug' => SLUG_PATTERN,
|
||||
],
|
||||
],
|
||||
'person' => [
|
||||
'path' => '/people/{slug}',
|
||||
'tokens' => [
|
||||
'slug' => SLUG_PATTERN,
|
||||
],
|
||||
],
|
||||
'default_user_info' => [
|
||||
'path' => '/me',
|
||||
'action' => 'me',
|
||||
'controller' => 'user',
|
||||
],
|
||||
'user_info' => [
|
||||
'path' => '/user/{username}',
|
||||
'controller' => 'user',
|
||||
'action' => 'about',
|
||||
'tokens' => [
|
||||
'username' => '.*?',
|
||||
],
|
||||
],
|
||||
// ---------------------------------------------------------------------
|
||||
// Default / Shared routes
|
||||
// ---------------------------------------------------------------------
|
||||
'anilist-redirect' => [
|
||||
'path' => '/anilist-redirect',
|
||||
'action' => 'anilistRedirect',
|
||||
'controller' => 'settings',
|
||||
],
|
||||
'anilist-callback' => [
|
||||
'path' => '/anilist-oauth',
|
||||
'action' => 'anilistCallback',
|
||||
'controller' => 'settings',
|
||||
],
|
||||
'image_proxy' => [
|
||||
'path' => '/public/images/{type}/{file}',
|
||||
'action' => 'cache',
|
||||
'controller' => 'images',
|
||||
'tokens' => [
|
||||
'type' => SLUG_PATTERN,
|
||||
'file' => '[a-z0-9\-]+\.[a-z]{3,4}',
|
||||
],
|
||||
],
|
||||
'settings' => [
|
||||
'path' => '/settings',
|
||||
],
|
||||
'settings-post' => [
|
||||
'path' => '/settings/update',
|
||||
'action' => 'update',
|
||||
'verb' => 'post',
|
||||
],
|
||||
'login' => [
|
||||
'path' => '/login',
|
||||
'action' => 'login',
|
||||
],
|
||||
'login.post' => [
|
||||
'path' => '/login',
|
||||
'action' => 'loginAction',
|
||||
'verb' => 'post',
|
||||
],
|
||||
'logout' => [
|
||||
'path' => '/logout',
|
||||
'action' => 'logout',
|
||||
],
|
||||
'history' => [
|
||||
'controller' => 'history',
|
||||
'path' => '/history/{type}',
|
||||
'tokens' => [
|
||||
'type' => SLUG_PATTERN,
|
||||
],
|
||||
],
|
||||
'increment' => [
|
||||
'path' => '/{controller}/increment',
|
||||
'action' => 'increment',
|
||||
'verb' => 'post',
|
||||
'tokens' => [
|
||||
'controller' => ALPHA_SLUG_PATTERN,
|
||||
],
|
||||
],
|
||||
'update' => [
|
||||
'path' => '/{controller}/update',
|
||||
'action' => 'update',
|
||||
'verb' => 'post',
|
||||
'tokens' => [
|
||||
'controller' => ALPHA_SLUG_PATTERN,
|
||||
],
|
||||
],
|
||||
'update.post' => [
|
||||
'path' => '/{controller}/update_form',
|
||||
'action' => 'formUpdate',
|
||||
'verb' => 'post',
|
||||
'tokens' => [
|
||||
'controller' => ALPHA_SLUG_PATTERN,
|
||||
],
|
||||
],
|
||||
'edit' => [
|
||||
'path' => '/{controller}/edit/{id}/{status}',
|
||||
'action' => 'edit',
|
||||
'tokens' => [
|
||||
'id' => SLUG_PATTERN,
|
||||
'status' => SLUG_SPACE_PATTERN,
|
||||
],
|
||||
],
|
||||
'list' => [
|
||||
'path' => '/{controller}/{status}{/view}',
|
||||
'tokens' => [
|
||||
'status' => ALPHA_SLUG_PATTERN,
|
||||
'view' => ALPHA_SLUG_PATTERN,
|
||||
],
|
||||
],
|
||||
'index_redirect' => [
|
||||
'path' => '/',
|
||||
'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;
|
@ -1,81 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Base API Model
|
||||
*/
|
||||
namespace AnimeClient;
|
||||
|
||||
use \GuzzleHttp\Client;
|
||||
use \GuzzleHttp\Cookie\CookieJar;
|
||||
|
||||
/**
|
||||
* Base model for api interaction
|
||||
*/
|
||||
class BaseApiModel extends BaseModel {
|
||||
|
||||
/**
|
||||
* Base url for making api requests
|
||||
* @var string
|
||||
*/
|
||||
protected $base_url = '';
|
||||
|
||||
/**
|
||||
* The Guzzle http client object
|
||||
* @var object
|
||||
*/
|
||||
protected $client;
|
||||
|
||||
/**
|
||||
* Cookie jar object for api requests
|
||||
* @var object
|
||||
*/
|
||||
protected $cookieJar;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$this->cookieJar = new CookieJar();
|
||||
$this->client = new Client([
|
||||
'base_url' => $this->base_url,
|
||||
'defaults' => [
|
||||
'cookies' => $this->cookieJar,
|
||||
'headers' => [
|
||||
'User-Agent' => $_SERVER['HTTP_USER_AGENT'],
|
||||
'Accept-Encoding' => 'application/json'
|
||||
],
|
||||
'timeout' => 5,
|
||||
'connect_timeout' => 5
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt login via the api
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
* @param string $username
|
||||
* @param string $password
|
||||
* @return bool
|
||||
*/
|
||||
public function authenticate($username, $password)
|
||||
{
|
||||
$result = $this->client->post('https://hummingbird.me/api/v1/users/authenticate', [
|
||||
'body' => [
|
||||
'username' => $username,
|
||||
'password' => $password
|
||||
]
|
||||
]);
|
||||
|
||||
if ($result->getStatusCode() === 201)
|
||||
{
|
||||
$_SESSION['hummingbird_anime_token'] = $result->json();
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
// End of BaseApiModel.php
|
@ -1,254 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Base Controller
|
||||
*/
|
||||
namespace AnimeClient;
|
||||
|
||||
use Aura\Web\WebFactory;
|
||||
|
||||
/**
|
||||
* Base class for controllers, defines output methods
|
||||
*/
|
||||
class BaseController {
|
||||
|
||||
/**
|
||||
* The global configuration object
|
||||
* @var object $config
|
||||
*/
|
||||
protected $config;
|
||||
|
||||
/**
|
||||
* Request object
|
||||
* @var object $request
|
||||
*/
|
||||
protected $request;
|
||||
|
||||
/**
|
||||
* Response object
|
||||
* @var object $response
|
||||
*/
|
||||
protected $response;
|
||||
|
||||
/**
|
||||
* The api model for the current controller
|
||||
* @var object
|
||||
*/
|
||||
protected $model;
|
||||
|
||||
/**
|
||||
* Common data to be sent to views
|
||||
* @var array
|
||||
*/
|
||||
protected $base_data = [];
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct(Config $config, Array $web)
|
||||
{
|
||||
$this->config = $config;
|
||||
|
||||
list($request, $response) = $web;
|
||||
$this->request = $request;
|
||||
$this->response = $response;
|
||||
}
|
||||
|
||||
public function __destruct()
|
||||
{
|
||||
$this->output();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the string output of a partial template
|
||||
*
|
||||
* @param string $template
|
||||
* @param array|object $data
|
||||
* @return string
|
||||
*/
|
||||
public function load_partial($template, $data=[])
|
||||
{
|
||||
if (isset($this->base_data))
|
||||
{
|
||||
$data = array_merge($this->base_data, $data);
|
||||
}
|
||||
|
||||
global $router, $defaultHandler;
|
||||
$route = $router->get_route();
|
||||
$data['route_path'] = ($route) ? $router->get_route()->path : "";
|
||||
|
||||
$defaultHandler->addDataTable('Template Data', $data);
|
||||
|
||||
$template_path = _dir(APP_DIR, 'views', "{$template}.php");
|
||||
|
||||
if ( ! is_file($template_path))
|
||||
{
|
||||
throw new Exception("Invalid template : {$path}");
|
||||
}
|
||||
|
||||
ob_start();
|
||||
extract($data);
|
||||
include _dir(APP_DIR, 'views', 'header.php');
|
||||
include $template_path;
|
||||
include _dir(APP_DIR, 'views', 'footer.php');
|
||||
$buffer = ob_get_contents();
|
||||
ob_end_clean();
|
||||
|
||||
return $buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Output a template to HTML, using the provided data
|
||||
*
|
||||
* @param string $template
|
||||
* @param array|object $data
|
||||
* @return void
|
||||
*/
|
||||
public function outputHTML($template, $data=[])
|
||||
{
|
||||
$buffer = $this->load_partial($template, $data);
|
||||
|
||||
$this->response->content->setType('text/html');
|
||||
$this->response->content->set($buffer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Output json with the proper content type
|
||||
*
|
||||
* @param mixed $data
|
||||
* @return void
|
||||
*/
|
||||
public function outputJSON($data)
|
||||
{
|
||||
if ( ! is_string($data))
|
||||
{
|
||||
$data = json_encode($data);
|
||||
}
|
||||
|
||||
$this->response->content->setType('application/json');
|
||||
$this->response->content->set($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect to the selected page
|
||||
*
|
||||
* @param string $url
|
||||
* @param int $code
|
||||
* @return void
|
||||
*/
|
||||
public function redirect($url, $code, $type="anime")
|
||||
{
|
||||
$url = full_url($url, $type);
|
||||
|
||||
$codes = [
|
||||
301 => 'Moved Permanently',
|
||||
302 => 'Found',
|
||||
303 => 'See Other'
|
||||
];
|
||||
|
||||
header("HTTP/1.1 {$code} {$codes[$code]}");
|
||||
header("Location: {$url}");
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a message box to the page
|
||||
*
|
||||
* @param string $type
|
||||
* @param string $message
|
||||
* @return string
|
||||
*/
|
||||
public function show_message($type, $message)
|
||||
{
|
||||
return $this->load_partial('message', [
|
||||
'stat_class' => $type,
|
||||
'message' => $message
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the api session
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function logout()
|
||||
{
|
||||
session_destroy();
|
||||
$this->response->redirect->seeOther(full_url(''));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the login form
|
||||
*
|
||||
* @param string $status
|
||||
* @return void
|
||||
*/
|
||||
public function login($status="")
|
||||
{
|
||||
$message = "";
|
||||
|
||||
if ($status != "")
|
||||
{
|
||||
$message = $this->show_message('error', $status);
|
||||
}
|
||||
|
||||
$this->outputHTML('login', [
|
||||
'title' => 'Api login',
|
||||
'message' => $message
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to log in with the api
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function login_action()
|
||||
{
|
||||
if (
|
||||
$this->model->authenticate(
|
||||
$this->config->hummingbird_username,
|
||||
$this->request->post->get('password')
|
||||
)
|
||||
)
|
||||
{
|
||||
$this->response->redirect->afterPost(full_url('', $this->base_data['url_type']));
|
||||
return;
|
||||
}
|
||||
|
||||
$this->login("Invalid username or password.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the appropriate response
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function output()
|
||||
{
|
||||
// send status
|
||||
@header($this->response->status->get(), true, $this->response->status->getCode());
|
||||
|
||||
// headers
|
||||
foreach($this->response->headers->get() as $label => $value)
|
||||
{
|
||||
@header("{$label}: {$value}");
|
||||
}
|
||||
|
||||
// cookies
|
||||
foreach($this->response->cookies->get() as $name => $cookie)
|
||||
{
|
||||
@setcookie(
|
||||
$name,
|
||||
$cookie['value'],
|
||||
$cookie['expire'],
|
||||
$cookie['path'],
|
||||
$cookie['domain'],
|
||||
$cookie['secure'],
|
||||
$cookie['httponly']
|
||||
);
|
||||
}
|
||||
|
||||
// send the actual response
|
||||
echo $this->response->content->get();
|
||||
}
|
||||
}
|
||||
// End of BaseController.php
|
@ -1,32 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Base DB model
|
||||
*/
|
||||
namespace AnimeClient;
|
||||
|
||||
/**
|
||||
* Base model for database interaction
|
||||
*/
|
||||
class BaseDBModel extends BaseModel {
|
||||
/**
|
||||
* The query builder object
|
||||
* @var object $db
|
||||
*/
|
||||
protected $db;
|
||||
|
||||
/**
|
||||
* The database connection information array
|
||||
* @var array $db_config
|
||||
*/
|
||||
protected $db_config;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->db_config = $this->config->database;
|
||||
}
|
||||
}
|
||||
// End of BaseDBModel.php
|
@ -1,106 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Base for base models
|
||||
*/
|
||||
namespace AnimeClient;
|
||||
|
||||
use abeautifulsite\SimpleImage;
|
||||
|
||||
/**
|
||||
* Common base for all Models
|
||||
*/
|
||||
class BaseModel {
|
||||
|
||||
/**
|
||||
* The global configuration object
|
||||
* @var object $config
|
||||
*/
|
||||
protected $config;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
global $config;
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path of the cached version of the image. Create the cached image
|
||||
* if the file does not already exist
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
* @param string $api_path - The original image url
|
||||
* @param string $series_slug - The part of the url with the series name, becomes the image name
|
||||
* @param string $type - Anime or Manga, controls cache path
|
||||
* @return string - the frontend path for the cached image
|
||||
*/
|
||||
public function get_cached_image($api_path, $series_slug, $type="anime")
|
||||
{
|
||||
$api_path = str_replace("jjpg", "jpg", $api_path);
|
||||
$path_parts = explode('?', basename($api_path));
|
||||
$path = current($path_parts);
|
||||
$ext_parts = explode('.', $path);
|
||||
$ext = end($ext_parts);
|
||||
|
||||
// Workaround for some broken extensions
|
||||
if ($ext == "jjpg") $ext = "jpg";
|
||||
|
||||
// Failsafe for weird urls
|
||||
if (strlen($ext) > 3) return $api_path;
|
||||
|
||||
$cached_image = "{$series_slug}.{$ext}";
|
||||
$cached_path = "{$this->config->img_cache_path}/{$type}/{$cached_image}";
|
||||
|
||||
// Cache the file if it doesn't already exist
|
||||
if ( ! file_exists($cached_path))
|
||||
{
|
||||
if (ini_get('allow_url_fopen'))
|
||||
{
|
||||
copy($api_path, $cached_path);
|
||||
}
|
||||
elseif (function_exists('curl_init'))
|
||||
{
|
||||
$ch = curl_init($api_path);
|
||||
$fp = fopen($cached_path, 'wb');
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_FILE => $fp,
|
||||
CURLOPT_HEADER => 0
|
||||
]);
|
||||
curl_exec($ch);
|
||||
curl_close($ch);
|
||||
fclose($ch);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception("Couldn't cache images because they couldn't be downloaded.");
|
||||
}
|
||||
|
||||
// Resize the image
|
||||
if ($type == 'anime')
|
||||
{
|
||||
$resize_width = 220;
|
||||
$resize_height = 319;
|
||||
$this->_resize($cached_path, $resize_width, $resize_height);
|
||||
}
|
||||
}
|
||||
|
||||
return "/public/images/{$type}/{$cached_image}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Resize an image
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
* @param string $path
|
||||
* @param string $width
|
||||
* @param string $height
|
||||
*/
|
||||
private function _resize($path, $width, $height)
|
||||
{
|
||||
$img = new SimpleImage($path);
|
||||
$img->resize($width,$height)->save();
|
||||
}
|
||||
}
|
||||
// End of BaseModel.php
|
@ -1,56 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace AnimeClient;
|
||||
|
||||
/**
|
||||
* Wrapper for configuration values
|
||||
*/
|
||||
class Config {
|
||||
|
||||
/**
|
||||
* Config object
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $config = [];
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param array $config_files
|
||||
*/
|
||||
public function __construct(Array $config_files=[])
|
||||
{
|
||||
// @codeCoverageIgnoreStart
|
||||
if (empty($config_files))
|
||||
{
|
||||
require_once _dir(CONF_DIR, 'config.php'); // $config
|
||||
require_once _dir(CONF_DIR, 'base_config.php'); // $base_config
|
||||
}
|
||||
else // @codeCoverageIgnoreEnd
|
||||
{
|
||||
$config = $config_files['config'];
|
||||
$base_config = $config_files['base_config'];
|
||||
}
|
||||
|
||||
$this->config = array_merge($config, $base_config);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter for config values
|
||||
*
|
||||
* @param string $key
|
||||
* @return mixed
|
||||
*/
|
||||
public function __get($key)
|
||||
{
|
||||
if (isset($this->config[$key]))
|
||||
{
|
||||
return $this->config[$key];
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
// End of config.php
|
@ -1,175 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Routing logic
|
||||
*/
|
||||
|
||||
namespace AnimeClient;
|
||||
|
||||
/**
|
||||
* Basic routing/ dispatch
|
||||
*/
|
||||
class Router {
|
||||
|
||||
/**
|
||||
* The route-matching object
|
||||
* @var object $router
|
||||
*/
|
||||
protected $router;
|
||||
|
||||
/**
|
||||
* The global configuration object
|
||||
* @var object $config
|
||||
*/
|
||||
protected $config;
|
||||
|
||||
/**
|
||||
* Array containing request and response objects
|
||||
* @var array $web
|
||||
*/
|
||||
protected $web;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param
|
||||
*/
|
||||
public function __construct(Config $config, \Aura\Router\Router $router, \Aura\Web\Request $request, \Aura\Web\Response $response)
|
||||
{
|
||||
$this->config = $config;
|
||||
$this->router = $router;
|
||||
$this->web = [$request, $response];
|
||||
|
||||
$this->_setup_routes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current route object, if one matches
|
||||
*
|
||||
* @return object
|
||||
*/
|
||||
public function get_route()
|
||||
{
|
||||
global $defaultHandler;
|
||||
|
||||
$raw_route = $_SERVER['REQUEST_URI'];
|
||||
$route_path = str_replace([$this->config->anime_path, $this->config->manga_path], '', $raw_route);
|
||||
$route_path = "/" . trim($route_path, '/');
|
||||
|
||||
$defaultHandler->addDataTable('Route Info', [
|
||||
'route_path' => $route_path
|
||||
]);
|
||||
|
||||
$route = $this->router->match($route_path, $_SERVER);
|
||||
|
||||
return $route;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the current route
|
||||
*
|
||||
* @param [object] $route
|
||||
* @return void
|
||||
*/
|
||||
public function dispatch($route = NULL)
|
||||
{
|
||||
global $defaultHandler;
|
||||
|
||||
if (is_null($route))
|
||||
{
|
||||
$route = $this->get_route();
|
||||
}
|
||||
|
||||
if ( ! $route)
|
||||
{
|
||||
$failure = $this->router->getFailedRoute();
|
||||
$defaultHandler->addDataTable('failed_route', (array)$failure);
|
||||
|
||||
$controller_name = 'BaseController';
|
||||
$action_method = 'outputHTML';
|
||||
$params = [
|
||||
'template' => '404',
|
||||
'data' => [
|
||||
'title' => 'Page Not Found'
|
||||
]
|
||||
];
|
||||
}
|
||||
else
|
||||
{
|
||||
list($controller_name, $action_method) = $route->params['action'];
|
||||
$params = (isset($route->params['params'])) ? $route->params['params'] : [];
|
||||
|
||||
if ( ! empty($route->tokens))
|
||||
{
|
||||
foreach($route->tokens as $key => $v)
|
||||
{
|
||||
if (array_key_exists($key, $route->params))
|
||||
{
|
||||
$params[$key] = $route->params[$key];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$controller = new $controller_name($this->config, $this->web);
|
||||
|
||||
// Run the appropriate controller method
|
||||
$defaultHandler->addDataTable('controller_args', $params);
|
||||
call_user_func_array([$controller, $action_method], $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Select controller based on the current url, and apply its relevent routes
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function _setup_routes()
|
||||
{
|
||||
$route_map = [
|
||||
'anime' => '\\AnimeClient\\AnimeController',
|
||||
'manga' => '\\AnimeClient\\MangaController',
|
||||
];
|
||||
$route_type = "anime";
|
||||
|
||||
if ($this->config->manga_host !== "" && strpos($_SERVER['HTTP_HOST'], $this->config->manga_host) !== FALSE)
|
||||
{
|
||||
$route_type = "manga";
|
||||
}
|
||||
else if ($this->config->manga_path !== "" && strpos($_SERVER['REQUEST_URI'], $this->config->manga_path) !== FALSE)
|
||||
{
|
||||
$route_type = "manga";
|
||||
}
|
||||
|
||||
$routes = $this->config->routes;
|
||||
|
||||
// Add routes
|
||||
foreach(['common', $route_type] as $key)
|
||||
{
|
||||
foreach($routes[$key] as $name => &$route)
|
||||
{
|
||||
$path = $route['path'];
|
||||
unset($route['path']);
|
||||
|
||||
// Prepend the controller to the route parameters
|
||||
array_unshift($route['action'], $route_map[$route_type]);
|
||||
|
||||
// Select the appropriate router method based on the http verb
|
||||
$add = (array_key_exists('verb', $route)) ? "add" . ucfirst(strtolower($route['verb'])) : "addGet";
|
||||
|
||||
if ( ! array_key_exists('tokens', $route))
|
||||
{
|
||||
$this->router->$add($name, $path)->addValues($route);
|
||||
}
|
||||
else
|
||||
{
|
||||
$tokens = $route['tokens'];
|
||||
unset($route['tokens']);
|
||||
|
||||
$this->router->$add($name, $path)
|
||||
->addValues($route)
|
||||
->addTokens($tokens);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// End of Router.php
|
@ -1,134 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Global functions
|
||||
*/
|
||||
|
||||
/**
|
||||
* Check if the user is currently logged in
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
function is_logged_in()
|
||||
{
|
||||
return array_key_exists('hummingbird_anime_token', $_SESSION);
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML selection helper function
|
||||
*
|
||||
* @param string $a - First item to compare
|
||||
* @param string $b - Second item to compare
|
||||
* @return string
|
||||
*/
|
||||
function is_selected($a, $b)
|
||||
{
|
||||
return ($a === $b) ? 'selected' : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Inverse of selected helper function
|
||||
*
|
||||
* @param string $a - First item to compare
|
||||
* @param string $b - Second item to compare
|
||||
* @return string
|
||||
*/
|
||||
function is_not_selected($a, $b)
|
||||
{
|
||||
return ($a !== $b) ? 'selected' : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the base url for css/js/images
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function asset_url(/*...*/)
|
||||
{
|
||||
global $config;
|
||||
|
||||
$args = func_get_args();
|
||||
$base_url = rtrim($config->asset_path, '/');
|
||||
|
||||
array_unshift($args, $base_url);
|
||||
|
||||
return implode("/", $args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the base url from the config
|
||||
*
|
||||
* @param string $type - (optional) The controller
|
||||
# @param object $config - (optional) Config
|
||||
* @return string
|
||||
*/
|
||||
function base_url($type="anime", $config=NULL)
|
||||
{
|
||||
if (is_null($config)) global $config;
|
||||
|
||||
|
||||
$config_path = trim($config->{"{$type}_path"}, "/");
|
||||
$config_host = $config->{"{$type}_host"};
|
||||
|
||||
// Set the appropriate HTTP host
|
||||
$host = ($config_host !== '') ? $config_host : $_SERVER['HTTP_HOST'];
|
||||
$path = ($config_path !== '') ? $config_path : "";
|
||||
|
||||
return implode("/", ['/', $host, $path]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate full url path from the route path based on config
|
||||
*
|
||||
* @param string $path - (optional) The route path
|
||||
* @param string $type - (optional) The controller (anime or manga), defaults to anime
|
||||
# @param object $config - (optional) Config
|
||||
* @return string
|
||||
*/
|
||||
function full_url($path="", $type="anime", $config=NULL)
|
||||
{
|
||||
if (is_null($config)) global $config;
|
||||
|
||||
$config_path = trim($config->{"{$type}_path"}, "/");
|
||||
$config_host = $config->{"{$type}_host"};
|
||||
$config_default_route = $config->{"default_{$type}_path"};
|
||||
|
||||
// Remove beginning/trailing slashes
|
||||
$config_path = trim($config_path, '/');
|
||||
$path = trim($path, '/');
|
||||
|
||||
// Remove any optional parameters from the route
|
||||
$path = preg_replace('`{/.*?}`i', '', $path);
|
||||
|
||||
// Set the appropriate HTTP host
|
||||
$host = ($config_host !== '') ? $config_host : $_SERVER['HTTP_HOST'];
|
||||
|
||||
// Set the default view
|
||||
if ($path === '')
|
||||
{
|
||||
$path .= trim($config_default_route, '/');
|
||||
if ($config->default_to_list_view) $path .= '/list';
|
||||
}
|
||||
|
||||
// Set an leading folder
|
||||
if ($config_path !== '')
|
||||
{
|
||||
$path = "{$config_path}/{$path}";
|
||||
}
|
||||
|
||||
return "//{$host}/{$path}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last segment of the current url
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function last_segment()
|
||||
{
|
||||
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
|
||||
$segments = explode('/', $path);
|
||||
return end($segments);
|
||||
}
|
||||
|
||||
// End of functions.php
|
@ -1,43 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Functions that need to be included before config
|
||||
*/
|
||||
|
||||
/**
|
||||
* Joins paths together. Variadic to take an
|
||||
* arbitrary number of arguments
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function _dir()
|
||||
{
|
||||
return implode(DIRECTORY_SEPARATOR, func_get_args());
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up autoloaders
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
* @return void
|
||||
*/
|
||||
function _setup_autoloaders()
|
||||
{
|
||||
require _dir(ROOT_DIR, '/vendor/autoload.php');
|
||||
spl_autoload_register(function ($class) {
|
||||
$class_parts = explode('\\', $class);
|
||||
$class = end($class_parts);
|
||||
|
||||
$dirs = ["base", "controllers", "models"];
|
||||
|
||||
foreach($dirs as $dir)
|
||||
{
|
||||
$file = _dir(APP_DIR, $dir, "{$class}.php");
|
||||
if (file_exists($file))
|
||||
{
|
||||
require_once $file;
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
@ -1,57 +1,200 @@
|
||||
<?php
|
||||
<?php declare(strict_types=1);
|
||||
/**
|
||||
* Hummingbird Anime List Client
|
||||
*
|
||||
* An API client for Kitsu to manage anime and manga watch lists
|
||||
*
|
||||
* PHP version 8.1
|
||||
*
|
||||
* @copyright 2015 - 2023 Timothy J. Warren <tim@timshome.page>
|
||||
* @license http://www.opensource.org/licenses/mit-license.html MIT License
|
||||
* @version 5.2
|
||||
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
|
||||
*/
|
||||
|
||||
namespace AnimeClient;
|
||||
namespace Aviat\AnimeClient;
|
||||
|
||||
use \Whoops\Handler\PrettyPageHandler;
|
||||
use \Whoops\Handler\JsonResponseHandler;
|
||||
use \Aura\Web\WebFactory;
|
||||
use \Aura\Router\RouterFactory;
|
||||
use \GuzzleHttp\Client;
|
||||
use \GuzzleHttp\Cookie\CookieJar;
|
||||
use Aura\Html\HelperLocatorFactory;
|
||||
use Aura\Router\RouterContainer;
|
||||
use Aura\Session\SessionFactory;
|
||||
use Aviat\AnimeClient\API\{Anilist, Kitsu};
|
||||
use Aviat\AnimeClient\{Component, Model};
|
||||
use Aviat\Banker\Teller;
|
||||
use Aviat\Ion\Config;
|
||||
use Aviat\Ion\Di\{Container, ContainerInterface};
|
||||
use Laminas\Diactoros\ServerRequestFactory;
|
||||
use Monolog\Formatter\JsonFormatter;
|
||||
use Monolog\Handler\RotatingFileHandler;
|
||||
use Monolog\Logger;
|
||||
use Psr\SimpleCache\CacheInterface;
|
||||
|
||||
use function Aviat\Ion\_dir;
|
||||
|
||||
if ( ! defined('HB_APP_DIR'))
|
||||
{
|
||||
define('HB_APP_DIR', __DIR__);
|
||||
define('ROOT_DIR', dirname(HB_APP_DIR));
|
||||
define('TEMPLATE_DIR', _dir(HB_APP_DIR, 'templates'));
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Setup error handling
|
||||
// Setup DI container
|
||||
// -----------------------------------------------------------------------------
|
||||
$whoops = new \Whoops\Run();
|
||||
return static function (array $configArray = []): Container {
|
||||
$container = new Container();
|
||||
|
||||
// Set up default handler for general errors
|
||||
$defaultHandler = new PrettyPageHandler();
|
||||
$whoops->pushHandler($defaultHandler);
|
||||
// -------------------------------------------------------------------------
|
||||
// Logging
|
||||
// -------------------------------------------------------------------------
|
||||
$LOG_DIR = _dir(HB_APP_DIR, 'logs');
|
||||
|
||||
// Set up json handler for ajax errors
|
||||
$jsonHandler = new JsonResponseHandler();
|
||||
$jsonHandler->onlyForAjaxRequests(true);
|
||||
$whoops->pushHandler($jsonHandler);
|
||||
$appLogger = new Logger('animeclient');
|
||||
$appLogger->pushHandler(new RotatingFileHandler(_dir($LOG_DIR, 'app.log'), 2, Logger::WARNING));
|
||||
$container->setLogger($appLogger);
|
||||
|
||||
$whoops->register();
|
||||
foreach (['anilist-request', 'kitsu-request', 'kitsu-graphql'] as $channel)
|
||||
{
|
||||
$logger = new Logger($channel);
|
||||
$handler = new RotatingFileHandler(_dir($LOG_DIR, "{$channel}.log"), 2, Logger::WARNING);
|
||||
$handler->setFormatter(new JsonFormatter());
|
||||
$logger->pushHandler($handler);
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Injected Objects
|
||||
// -----------------------------------------------------------------------------
|
||||
$container->setLogger($logger, $channel);
|
||||
}
|
||||
|
||||
// Create Config Object
|
||||
$config = new Config();
|
||||
require _dir(BASE_DIR, '/functions.php');
|
||||
// -------------------------------------------------------------------------
|
||||
// Injected Objects
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
// Create Aura Router Object
|
||||
$router_factory = new RouterFactory();
|
||||
$aura_router = $router_factory->newInstance();
|
||||
// Create Config Object
|
||||
$container->set('config', static fn () => new Config($configArray));
|
||||
|
||||
// Create Request/Response Objects
|
||||
$web_factory = new WebFactory([
|
||||
'_GET' => $_GET,
|
||||
'_POST' => $_POST,
|
||||
'_COOKIE' => $_COOKIE,
|
||||
'_SERVER' => $_SERVER,
|
||||
'_FILES' => $_FILES
|
||||
]);
|
||||
$request = $web_factory->newRequest();
|
||||
$response = $web_factory->newResponse();
|
||||
// Create Cache Object
|
||||
$container->set('cache', static function (ContainerInterface $container): CacheInterface {
|
||||
$logger = $container->getLogger();
|
||||
$config = $container->get('config')->get('cache');
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Router
|
||||
// -----------------------------------------------------------------------------
|
||||
$router = new Router($config, $aura_router, $request, $response);
|
||||
$router->dispatch();
|
||||
return new Teller($config, $logger);
|
||||
});
|
||||
|
||||
// Create Aura Router Object
|
||||
$container->set('aura-router', static fn () => new RouterContainer());
|
||||
|
||||
// Create Html helpers
|
||||
$container->set('html-helper', static function (ContainerInterface $container) {
|
||||
$htmlHelper = (new HelperLocatorFactory())->newInstance();
|
||||
$helpers = [
|
||||
'menu' => Helper\Menu::class,
|
||||
'field' => Helper\Form::class,
|
||||
'picture' => Helper\Picture::class,
|
||||
];
|
||||
|
||||
foreach ($helpers as $name => $class)
|
||||
{
|
||||
$htmlHelper->set($name, static function () use ($class, $container) {
|
||||
$helper = new $class();
|
||||
$helper->setContainer($container);
|
||||
|
||||
return $helper;
|
||||
});
|
||||
}
|
||||
|
||||
return $htmlHelper;
|
||||
});
|
||||
|
||||
// Create Component helpers
|
||||
$container->set('component-helper', static function (ContainerInterface $container) {
|
||||
$helper = (new HelperLocatorFactory())->newInstance();
|
||||
$components = [
|
||||
'animeCover' => Component\AnimeCover::class,
|
||||
'mangaCover' => Component\MangaCover::class,
|
||||
'character' => Component\Character::class,
|
||||
'media' => Component\Media::class,
|
||||
'tabs' => Component\Tabs::class,
|
||||
'verticalTabs' => Component\VerticalTabs::class,
|
||||
];
|
||||
|
||||
foreach ($components as $name => $componentClass)
|
||||
{
|
||||
$helper->set($name, static function () use ($container, $componentClass) {
|
||||
$helper = new $componentClass();
|
||||
$helper->setContainer($container);
|
||||
|
||||
return $helper;
|
||||
});
|
||||
}
|
||||
|
||||
return $helper;
|
||||
});
|
||||
|
||||
// Create Request Object
|
||||
$container->set('request', static fn () => ServerRequestFactory::fromGlobals(
|
||||
$GLOBALS['_SERVER'],
|
||||
$_GET,
|
||||
$_POST,
|
||||
$_COOKIE,
|
||||
$_FILES
|
||||
));
|
||||
|
||||
// Create session Object
|
||||
$container->set('session', static fn () => (new SessionFactory())->newInstance($_COOKIE));
|
||||
|
||||
// Miscellaneous helper methods
|
||||
$container->set('util', static fn ($container) => new Util($container));
|
||||
|
||||
// Models
|
||||
$container->set('kitsu-model', static function (ContainerInterface $container): Kitsu\Model {
|
||||
$requestBuilder = new Kitsu\RequestBuilder($container);
|
||||
$requestBuilder->setLogger($container->getLogger('kitsu-request'));
|
||||
|
||||
$listItem = new Kitsu\ListItem();
|
||||
$listItem->setContainer($container);
|
||||
$listItem->setRequestBuilder($requestBuilder);
|
||||
|
||||
$model = new Kitsu\Model($listItem);
|
||||
$model->setContainer($container);
|
||||
$model->setRequestBuilder($requestBuilder);
|
||||
|
||||
$cache = $container->get('cache');
|
||||
$model->setCache($cache);
|
||||
|
||||
return $model;
|
||||
});
|
||||
$container->set('anilist-model', static function (ContainerInterface $container): Anilist\Model {
|
||||
$requestBuilder = new Anilist\RequestBuilder($container);
|
||||
$requestBuilder->setLogger($container->getLogger('anilist-request'));
|
||||
|
||||
$listItem = new Anilist\ListItem();
|
||||
$listItem->setContainer($container);
|
||||
$listItem->setRequestBuilder($requestBuilder);
|
||||
|
||||
$model = new Anilist\Model($listItem);
|
||||
$model->setContainer($container);
|
||||
$model->setRequestBuilder($requestBuilder);
|
||||
|
||||
return $model;
|
||||
});
|
||||
$container->set('anime-model', static fn ($container) => new Model\Anime($container));
|
||||
$container->set('manga-model', static fn ($container) => new Model\Manga($container));
|
||||
$container->set('anime-collection-model', static fn ($container) => new Model\AnimeCollection($container));
|
||||
$container->set('manga-collection-model', static fn ($container) => new Model\MangaCollection($container));
|
||||
$container->set('settings-model', static function ($container) {
|
||||
$model = new Model\Settings($container->get('config'));
|
||||
$model->setContainer($container);
|
||||
|
||||
return $model;
|
||||
});
|
||||
|
||||
// Miscellaneous Classes
|
||||
$container->set('auth', static fn ($container) => new Kitsu\Auth($container));
|
||||
$container->set('url-generator', static fn ($container) => new UrlGenerator($container));
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Dispatcher
|
||||
// -------------------------------------------------------------------------
|
||||
$container->set('dispatcher', static fn ($container) => new Dispatcher($container));
|
||||
|
||||
return $container;
|
||||
};
|
||||
|
||||
// End of bootstrap.php
|
6
app/config/anilist.toml.example
Normal file
6
app/config/anilist.toml.example
Normal file
@ -0,0 +1,6 @@
|
||||
################################################################################
|
||||
# Anilist API #
|
||||
################################################################################
|
||||
client_id = "your_client_id"
|
||||
client_secret = "your_client_secret"
|
||||
username = "user123"
|
@ -1,15 +0,0 @@
|
||||
<?php
|
||||
// ----------------------------------------------------------------------------
|
||||
// Lower level configuration
|
||||
//
|
||||
// You shouldn't generally need to change anything below this line
|
||||
// ----------------------------------------------------------------------------
|
||||
$base_config = [
|
||||
// Cache paths
|
||||
'data_cache_path' => _dir(APP_DIR, 'cache'),
|
||||
'img_cache_path' => _dir(ROOT_DIR, 'public/images'),
|
||||
|
||||
// Included config files
|
||||
'routes' => require _dir(CONF_DIR, 'routes.php'),
|
||||
'database' => require _dir(CONF_DIR, 'database.php'),
|
||||
];
|
22
app/config/cache.toml.example
Normal file
22
app/config/cache.toml.example
Normal file
@ -0,0 +1,22 @@
|
||||
################################################################################
|
||||
# Cache Setup #
|
||||
################################################################################
|
||||
|
||||
# See https://git.timshomepage.net/aviat/banker for more information
|
||||
|
||||
# Available drivers are memcached, redis or null
|
||||
# Null cache driver means no caching
|
||||
driver = "redis"
|
||||
|
||||
[connection]
|
||||
# Host or socket to connect to
|
||||
host = "127.0.0.1"
|
||||
|
||||
# Connection port
|
||||
#port = 6379
|
||||
|
||||
# Connection password
|
||||
#password = ""
|
||||
|
||||
# Database number
|
||||
database = 2
|
@ -1,40 +0,0 @@
|
||||
<?php
|
||||
$config = [
|
||||
// ----------------------------------------------------------------------------
|
||||
// Username for anime and manga lists
|
||||
// ----------------------------------------------------------------------------
|
||||
'hummingbird_username' => 'timw4mail',
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// General config
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// do you wish to show the anime collection tab?
|
||||
'show_anime_collection' => TRUE,
|
||||
|
||||
// path to public directory
|
||||
'asset_path' => '//' . $_SERVER['HTTP_HOST'] . '/public',
|
||||
|
||||
// path to public directory on the server
|
||||
'asset_dir' => __DIR__ . '/../../public',
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Routing
|
||||
//
|
||||
// Route by path, or route by domain. To route by path, set the _host suffixed
|
||||
// options to an empty string. To route by host, set the _path suffixed options
|
||||
// to an empty string
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
'anime_host' => 'anime.timshomepage.net',
|
||||
'manga_host' => 'manga.timshomepage.net',
|
||||
'anime_path' => '',
|
||||
'manga_path' => '',
|
||||
|
||||
// Default pages for anime/manga
|
||||
'default_anime_path' => '/watching',
|
||||
'default_manga_path' => '/all',
|
||||
|
||||
// Default to list view?
|
||||
'default_to_list_view' => FALSE,
|
||||
];
|
39
app/config/config.toml.example
Normal file
39
app/config/config.toml.example
Normal file
@ -0,0 +1,39 @@
|
||||
################################################################################
|
||||
# Main User Configuration #
|
||||
################################################################################
|
||||
|
||||
# Username for anime and manga lists
|
||||
kitsu_username = "johnsmith"
|
||||
|
||||
# Whose list is it?
|
||||
whose_list = "Someone"
|
||||
|
||||
# do you wish to show the anime collection?
|
||||
show_anime_collection = true
|
||||
|
||||
# do you wish to show the manga collection?
|
||||
show_manga_collection = false
|
||||
|
||||
# what theme would you like to use? light, dark, or auto
|
||||
theme = "auto"
|
||||
|
||||
################################################################################
|
||||
# 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
|
@ -1,13 +0,0 @@
|
||||
<?php
|
||||
return [
|
||||
'collection' => [
|
||||
'type' => 'sqlite',
|
||||
'host' => '',
|
||||
'user' => '',
|
||||
'pass' => '',
|
||||
'port' => '',
|
||||
'name' => 'default',
|
||||
'database' => '',
|
||||
'file' => __DIR__ . '/../../anime_collection.sqlite',
|
||||
]
|
||||
];
|
11
app/config/database.toml.example
Normal file
11
app/config/database.toml.example
Normal file
@ -0,0 +1,11 @@
|
||||
################################################################################
|
||||
# Database Configuration #
|
||||
################################################################################
|
||||
|
||||
type = "sqlite"
|
||||
host = ""
|
||||
user = ""
|
||||
pass = ""
|
||||
port = ""
|
||||
database = ""
|
||||
file = "anime_collection.sqlite3"
|
@ -1,60 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Easy Min
|
||||
*
|
||||
* Simple minification for better website performance
|
||||
*
|
||||
* @author Timothy J. Warren
|
||||
* @copyright Copyright (c) 2012
|
||||
* @link https://github.com/aviat4ion/Easy-Min
|
||||
* @license http://philsturgeon.co.uk/code/dbad-license
|
||||
*/
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
/* $config = */require 'config.php';
|
||||
|
||||
$config = (object)$config;
|
||||
|
||||
// Should we use myth to preprocess?
|
||||
$use_myth = FALSE;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| CSS Folder
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The folder where css files exist, in relation to the document root
|
||||
|
|
||||
*/
|
||||
$css_root = $config->asset_dir. '/css/';
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Path from
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Path fragment to rewrite in css files
|
||||
|
|
||||
*/
|
||||
$path_from = '';
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Path to
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The path fragment replacement for the css files
|
||||
|
|
||||
*/
|
||||
$path_to = '';
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| JS Folder
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The folder where javascript files exist, in relation to the document root
|
||||
|
|
||||
*/
|
||||
$js_root = $config->asset_dir. '/js/';
|
@ -1,36 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Easy Min
|
||||
*
|
||||
* Simple minification for better website performance
|
||||
*
|
||||
* @author Timothy J. Warren
|
||||
* @copyright Copyright (c) 2012
|
||||
* @link https://github.com/aviat4ion/Easy-Min
|
||||
* @license http://philsturgeon.co.uk/code/dbad-license
|
||||
*/
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* This is the config array for css files to concatenate and minify
|
||||
*/
|
||||
return [
|
||||
/*-----
|
||||
Css
|
||||
-----*/
|
||||
|
||||
/*
|
||||
For each group create an array like so
|
||||
|
||||
'my_group' => array(
|
||||
'path/to/css/file1.css',
|
||||
'path/to/css/file2.css'
|
||||
),
|
||||
*/
|
||||
'base' => [
|
||||
'marx.css',
|
||||
'base.css'
|
||||
]
|
||||
];
|
||||
// End of css_groups.php
|
@ -1,40 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Easy Min
|
||||
*
|
||||
* Simple minification for better website performance
|
||||
*
|
||||
* @author Timothy J. Warren
|
||||
* @copyright Copyright (c) 2012
|
||||
* @link https://github.com/aviat4ion/Easy-Min
|
||||
* @license http://philsturgeon.co.uk/code/dbad-license
|
||||
*/
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* This is the config array for javascript files to concatenate and minify
|
||||
*/
|
||||
return [
|
||||
/*
|
||||
For each group create an array like so
|
||||
|
||||
'my_group' => array(
|
||||
'path/to/js/file1.js',
|
||||
'path/to/js/file2.js'
|
||||
),
|
||||
*/
|
||||
'table' => [
|
||||
'lib/jquery.min.js',
|
||||
'lib/table_sorter/jquery.tablesorter.min.js',
|
||||
'sort_tables.js'
|
||||
],
|
||||
'edit' => [
|
||||
'lib/jquery.min.js',
|
||||
'show_message.js',
|
||||
'anime_edit.js',
|
||||
'manga_edit.js'
|
||||
]
|
||||
];
|
||||
|
||||
// End of js_groups.php
|
@ -1,188 +0,0 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
// Routes on all controllers
|
||||
'common' => [
|
||||
'update' => [
|
||||
'path' => '/update',
|
||||
'action' => ['update'],
|
||||
'verb' => 'post'
|
||||
],
|
||||
'login_form' => [
|
||||
'path' => '/login',
|
||||
'action' => ['login'],
|
||||
'verb' => 'get'
|
||||
],
|
||||
'login_action' => [
|
||||
'path' => '/login',
|
||||
'action' => ['login_action'],
|
||||
'verb' => 'post'
|
||||
],
|
||||
'logout' => [
|
||||
'path' => '/logout',
|
||||
'action' => ['logout']
|
||||
],
|
||||
],
|
||||
// Routes on anime controller
|
||||
'anime' => [
|
||||
'index' => [
|
||||
'path' => '/',
|
||||
'action' => ['redirect'],
|
||||
'params' => [
|
||||
'url' => '', // Determined by config
|
||||
'code' => '301'
|
||||
]
|
||||
],
|
||||
'all' => [
|
||||
'path' => '/all{/view}',
|
||||
'action' => ['anime_list'],
|
||||
'params' => [
|
||||
'type' => 'all',
|
||||
'title' => WHOSE . " Anime List · All"
|
||||
],
|
||||
'tokens' => [
|
||||
'view' => '[a-z_]+'
|
||||
]
|
||||
],
|
||||
'watching' => [
|
||||
'path' => '/watching{/view}',
|
||||
'action' => ['anime_list'],
|
||||
'params' => [
|
||||
'type' => 'currently-watching',
|
||||
'title' => WHOSE . " Anime List · Watching"
|
||||
],
|
||||
'tokens' => [
|
||||
'view' => '[a-z_]+'
|
||||
]
|
||||
],
|
||||
'plan_to_watch' => [
|
||||
'path' => '/plan_to_watch{/view}',
|
||||
'action' => ['anime_list'],
|
||||
'params' => [
|
||||
'type' => 'plan-to-watch',
|
||||
'title' => WHOSE . " Anime List · Plan to Watch"
|
||||
],
|
||||
'tokens' => [
|
||||
'view' => '[a-z_]+'
|
||||
]
|
||||
],
|
||||
'on_hold' => [
|
||||
'path' => '/on_hold{/view}',
|
||||
'action' => ['anime_list'],
|
||||
'params' => [
|
||||
'type' => 'on-hold',
|
||||
'title' => WHOSE . " Anime List · On Hold"
|
||||
],
|
||||
'tokens' => [
|
||||
'view' => '[a-z_]+'
|
||||
]
|
||||
],
|
||||
'dropped' => [
|
||||
'path' => '/dropped{/view}',
|
||||
'action' => ['anime_list'],
|
||||
'params' => [
|
||||
'type' => 'dropped',
|
||||
'title' => WHOSE . " Anime List · Dropped"
|
||||
],
|
||||
'tokens' => [
|
||||
'view' => '[a-z_]+'
|
||||
]
|
||||
],
|
||||
'completed' => [
|
||||
'path' => '/completed{/view}',
|
||||
'action' => ['anime_list'],
|
||||
'params' => [
|
||||
'type' => 'completed',
|
||||
'title' => WHOSE . " Anime List · Completed"
|
||||
],
|
||||
'tokens' => [
|
||||
'view' => '[a-z_]+'
|
||||
]
|
||||
],
|
||||
'collection' => [
|
||||
'path' => '/collection{/view}',
|
||||
'action' => ['collection'],
|
||||
'params' => [],
|
||||
'tokens' => [
|
||||
'view' => '[a-z_]+'
|
||||
]
|
||||
]
|
||||
],
|
||||
'manga' => [
|
||||
'index' => [
|
||||
'path' => '/',
|
||||
'action' => ['redirect'],
|
||||
'params' => [
|
||||
'url' => '', // Determined by config
|
||||
'code' => '301',
|
||||
'type' => 'manga'
|
||||
]
|
||||
],
|
||||
'all' => [
|
||||
'path' => '/all{/view}',
|
||||
'action' => ['manga_list'],
|
||||
'params' => [
|
||||
'type' => 'all',
|
||||
'title' => WHOSE . " Manga List · All"
|
||||
],
|
||||
'tokens' => [
|
||||
'view' => '[a-z_]+'
|
||||
]
|
||||
],
|
||||
'reading' => [
|
||||
'path' => '/reading{/view}',
|
||||
'action' => ['manga_list'],
|
||||
'params' => [
|
||||
'type' => 'Reading',
|
||||
'title' => WHOSE . " Manga List · Reading"
|
||||
],
|
||||
'tokens' => [
|
||||
'view' => '[a-z_]+'
|
||||
]
|
||||
],
|
||||
'plan_to_read' => [
|
||||
'path' => '/plan_to_read{/view}',
|
||||
'action' => ['manga_list'],
|
||||
'params' => [
|
||||
'type' => 'Plan to Read',
|
||||
'title' => WHOSE . " Manga List · Plan to Read"
|
||||
],
|
||||
'tokens' => [
|
||||
'view' => '[a-z_]+'
|
||||
]
|
||||
],
|
||||
'on_hold' => [
|
||||
'path' => '/on_hold{/view}',
|
||||
'action' => ['manga_list'],
|
||||
'params' => [
|
||||
'type' => 'On Hold',
|
||||
'title' => WHOSE . " Manga List · On Hold"
|
||||
],
|
||||
'tokens' => [
|
||||
'view' => '[a-z_]+'
|
||||
]
|
||||
],
|
||||
'dropped' => [
|
||||
'path' => '/dropped{/view}',
|
||||
'action' => ['manga_list'],
|
||||
'params' => [
|
||||
'type' => 'Dropped',
|
||||
'title' => WHOSE . " Manga List · Dropped"
|
||||
],
|
||||
'tokens' => [
|
||||
'view' => '[a-z_]+'
|
||||
]
|
||||
],
|
||||
'completed' => [
|
||||
'path' => '/completed{/view}',
|
||||
'action' => ['manga_list'],
|
||||
'params' => [
|
||||
'type' => 'Completed',
|
||||
'title' => WHOSE . " Manga List · Completed"
|
||||
],
|
||||
'tokens' => [
|
||||
'view' => '[a-z_]+'
|
||||
]
|
||||
]
|
||||
]
|
||||
];
|
@ -1,121 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Anime Controller
|
||||
*/
|
||||
|
||||
namespace AnimeClient;
|
||||
|
||||
/**
|
||||
* Controller for Anime-related pages
|
||||
*/
|
||||
class AnimeController extends BaseController {
|
||||
|
||||
/**
|
||||
* The anime list model
|
||||
* @var object $model
|
||||
*/
|
||||
protected $model;
|
||||
|
||||
/**
|
||||
* The anime collection model
|
||||
* @var object $collection_model
|
||||
*/
|
||||
private $collection_model;
|
||||
|
||||
/**
|
||||
* Data to ve sent to all routes in this controller
|
||||
* @var array $base_data
|
||||
*/
|
||||
protected $base_data;
|
||||
|
||||
/**
|
||||
* Route mapping for main navigation
|
||||
* @var array $nav_routes
|
||||
*/
|
||||
private $nav_routes = [
|
||||
'Watching' => '/watching{/view}',
|
||||
'Plan to Watch' => '/plan_to_watch{/view}',
|
||||
'On Hold' => '/on_hold{/view}',
|
||||
'Dropped' => '/dropped{/view}',
|
||||
'Completed' => '/completed{/view}',
|
||||
'Collection' => '/collection{/view}',
|
||||
'All' => '/all{/view}'
|
||||
];
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct(Config $config, Array $web)
|
||||
{
|
||||
parent::__construct($config, $web);
|
||||
|
||||
if ($this->config->show_anime_collection === FALSE)
|
||||
{
|
||||
unset($this->nav_routes['Collection']);
|
||||
}
|
||||
|
||||
$this->model = new AnimeModel();
|
||||
$this->collection_model = new AnimeCollectionModel();
|
||||
$this->base_data = [
|
||||
'message' => '',
|
||||
'url_type' => 'anime',
|
||||
'other_type' => 'manga',
|
||||
'nav_routes' => $this->nav_routes,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a portion, or all of the anime list
|
||||
*
|
||||
* @param string $type - The section of the list
|
||||
* @param string $title - The title of the page
|
||||
* @return void
|
||||
*/
|
||||
public function anime_list($type, $title, $view)
|
||||
{
|
||||
$view_map = [
|
||||
'' => 'cover',
|
||||
'list' => 'list'
|
||||
];
|
||||
|
||||
$data = ($type != 'all')
|
||||
? $this->model->get_list($type)
|
||||
: $this->model->get_all_lists();
|
||||
|
||||
$this->outputHTML('anime/' . $view_map[$view], [
|
||||
'title' => $title,
|
||||
'sections' => $data
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the anime collection page
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function collection($view)
|
||||
{
|
||||
$view_map = [
|
||||
'' => 'collection',
|
||||
'list' => 'collection_list'
|
||||
];
|
||||
|
||||
$data = $this->collection_model->get_collection();
|
||||
|
||||
$this->outputHTML('anime/' . $view_map[$view], [
|
||||
'title' => WHOSE . " Anime Collection",
|
||||
'sections' => $data
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an anime item
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function update()
|
||||
{
|
||||
print_r($this->model->update($this->request->post->get()));
|
||||
}
|
||||
}
|
||||
// End of AnimeController.php
|
@ -1,87 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Manga Controller
|
||||
*/
|
||||
namespace AnimeClient;
|
||||
|
||||
/**
|
||||
* Controller for manga list
|
||||
*/
|
||||
class MangaController extends BaseController {
|
||||
|
||||
/**
|
||||
* The manga model
|
||||
* @var object $model
|
||||
*/
|
||||
protected $model;
|
||||
|
||||
/**
|
||||
* Data to ve sent to all routes in this controller
|
||||
* @var array $base_data
|
||||
*/
|
||||
protected $base_data;
|
||||
|
||||
|
||||
/**
|
||||
* Route mapping for main navigation
|
||||
* @var array $nav_routes
|
||||
*/
|
||||
private $nav_routes = [
|
||||
'Reading' => '/reading{/view}',
|
||||
'Plan to Read' => '/plan_to_read{/view}',
|
||||
'On Hold' => '/on_hold{/view}',
|
||||
'Dropped' => '/dropped{/view}',
|
||||
'Completed' => '/completed{/view}',
|
||||
'All' => '/all{/view}'
|
||||
];
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct(Config $config, Array $web)
|
||||
{
|
||||
parent::__construct($config, $web);
|
||||
$this->model = new MangaModel();
|
||||
$this->base_data = [
|
||||
'url_type' => 'manga',
|
||||
'other_type' => 'anime',
|
||||
'nav_routes' => $this->nav_routes
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an anime item
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function update()
|
||||
{
|
||||
$this->outputJSON($this->model->update($this->request->post->get()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a section of the manga list
|
||||
*
|
||||
* @param string $status
|
||||
* @param string $title
|
||||
* @param string $view
|
||||
* @return void
|
||||
*/
|
||||
public function manga_list($status, $title, $view)
|
||||
{
|
||||
$view_map = [
|
||||
'' => 'cover',
|
||||
'list' => 'list'
|
||||
];
|
||||
|
||||
$data = ($status !== 'all')
|
||||
? [$status => $this->model->get_list($status)]
|
||||
: $this->model->get_all_lists();
|
||||
|
||||
$this->outputHTML('manga/' . $view_map[$view], [
|
||||
'title' => $title,
|
||||
'sections' => $data
|
||||
]);
|
||||
}
|
||||
}
|
||||
// End of MangaController.php
|
0
app/views/anime/collection_edit.php → app/logs/.gitkeep
Normal file → Executable file
0
app/views/anime/collection_edit.php → app/logs/.gitkeep
Normal file → Executable file
@ -1,206 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Anime Collection DB Model
|
||||
*/
|
||||
|
||||
namespace AnimeClient;
|
||||
|
||||
/**
|
||||
* Model for getting anime collection data
|
||||
*/
|
||||
class AnimeCollectionModel extends BaseDBModel {
|
||||
|
||||
/**
|
||||
* Anime API Model
|
||||
* @var object $anime_model
|
||||
*/
|
||||
private $anime_model;
|
||||
|
||||
/**
|
||||
* Whether the database is valid for querying
|
||||
* @var bool
|
||||
*/
|
||||
private $valid_database = FALSE;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$this->db = \Query($this->db_config['collection']);
|
||||
$this->anime_model = new AnimeModel();
|
||||
|
||||
// Is database valid? If not, set a flag so the
|
||||
// app can be run without a valid database
|
||||
$db_file = file_get_contents($this->db_config['collection']['file']);
|
||||
$this->valid_database = (strpos($db_file, 'SQLite format 3') === 0);
|
||||
|
||||
|
||||
// Do an import if an import file exists
|
||||
$this->json_import();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get collection from the database, and organize by media type
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_collection()
|
||||
{
|
||||
$raw_collection = $this->_get_collection();
|
||||
|
||||
$collection = [];
|
||||
|
||||
foreach($raw_collection as $row)
|
||||
{
|
||||
if (array_key_exists($row['media'], $collection))
|
||||
{
|
||||
$collection[$row['media']][] = $row;
|
||||
}
|
||||
else
|
||||
{
|
||||
$collection[$row['media']] = [$row];
|
||||
}
|
||||
}
|
||||
|
||||
return $collection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get full collection from the database
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function _get_collection()
|
||||
{
|
||||
if ( ! $this->valid_database) return [];
|
||||
|
||||
$query = $this->db->select('hummingbird_id, slug, title, alternate_title, show_type, age_rating, episode_count, episode_length, cover_image, notes, media.type as media')
|
||||
->from('anime_set a')
|
||||
->join('media', 'media.id=a.media_id', 'inner')
|
||||
->order_by('media')
|
||||
->order_by('title')
|
||||
->get();
|
||||
|
||||
return $query->fetchAll(\PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
/**
|
||||
* Import anime into collection from a json file
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function json_import()
|
||||
{
|
||||
if ( ! file_exists('import.json')) return;
|
||||
if ( ! $this->valid_database) return;
|
||||
|
||||
$anime = json_decode(file_get_contents("import.json"));
|
||||
|
||||
foreach($anime as $item)
|
||||
{
|
||||
$this->db->set([
|
||||
'hummingbird_id' => $item->id,
|
||||
'slug' => $item->slug,
|
||||
'title' => $item->title,
|
||||
'alternate_title' => $item->alternate_title,
|
||||
'show_type' => $item->show_type,
|
||||
'age_rating' => $item->age_rating,
|
||||
'cover_image' => $this->get_cached_image($item->cover_image, $item->slug, 'anime'),
|
||||
'episode_count' => $item->episode_count,
|
||||
'episode_length' => $item->episode_length
|
||||
])->insert('anime_set');
|
||||
}
|
||||
|
||||
// Delete the import file
|
||||
unlink('import.json');
|
||||
|
||||
// Update genre info
|
||||
$this->update_genres();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update genre information
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function update_genres()
|
||||
{
|
||||
$genres = [];
|
||||
$flipped_genres = [];
|
||||
|
||||
$links = [];
|
||||
|
||||
// Get existing genres
|
||||
$query = $this->db->select('id, genre')
|
||||
->from('genres')
|
||||
->get();
|
||||
foreach($query->fetchAll(PDO::FETCH_ASSOC) as $genre)
|
||||
{
|
||||
$genres[$genre['id']] = $genre['genre'];
|
||||
}
|
||||
|
||||
// Get existing link table entries
|
||||
$query = $this->db->select('hummingbird_id, genre_id')
|
||||
->from('genre_anime_set_link')
|
||||
->get();
|
||||
foreach($query->fetchAll(PDO::FETCH_ASSOC) as $link)
|
||||
{
|
||||
if (array_key_exists($link['hummingbird_id'], $links))
|
||||
{
|
||||
$links[$link['hummingbird_id']][] = $link['genre_id'];
|
||||
}
|
||||
else
|
||||
{
|
||||
$links[$link['hummingbird_id']] = [$link['genre_id']];
|
||||
}
|
||||
}
|
||||
|
||||
// Get the anime collection
|
||||
$collection = $this->_get_collection();
|
||||
foreach($collection as $anime)
|
||||
{
|
||||
// Get api information
|
||||
$api = $this->anime_model->get_anime($anime['hummingbird_id']);
|
||||
|
||||
|
||||
foreach($api['genres'] as $genre)
|
||||
{
|
||||
// Add genres that don't currently exist
|
||||
if ( ! in_array($genre['name'], $genres))
|
||||
{
|
||||
$this->db->set('genre', $genre['name'])
|
||||
->insert('genres');
|
||||
|
||||
$genres[] = $genre['name'];
|
||||
}
|
||||
|
||||
|
||||
// Update link table
|
||||
|
||||
// Get id of genre to put in link table
|
||||
$flipped_genres = array_flip($genres);
|
||||
|
||||
$insert_array = [
|
||||
'hummingbird_id' => $anime['hummingbird_id'],
|
||||
'genre_id' => $flipped_genres[$genre['name']]
|
||||
];
|
||||
|
||||
if (array_key_exists($anime['hummingbird_id'], $links))
|
||||
{
|
||||
if ( ! in_array($flipped_genres[$genre['name']], $links[$anime['hummingbird_id']]))
|
||||
{
|
||||
$this->db->set($insert_array)->insert('genre_anime_set_link');
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->db->set($insert_array)->insert('genre_anime_set_link');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// End of AnimeCollectionModel.php
|
@ -1,245 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Anime API Model
|
||||
*/
|
||||
|
||||
namespace AnimeClient;
|
||||
|
||||
/**
|
||||
* Model for handling requests dealing with the anime list
|
||||
*/
|
||||
class AnimeModel extends BaseApiModel {
|
||||
/**
|
||||
* The base url for api requests
|
||||
* @var string $base_url
|
||||
*/
|
||||
protected $base_url = "https://hummingbird.me/api/v1/";
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the selected anime
|
||||
*
|
||||
* @param array $data
|
||||
* @return array
|
||||
*/
|
||||
public function update($data)
|
||||
{
|
||||
$data['auth_token'] = $_SESSION['hummingbird_anime_token'];
|
||||
|
||||
$result = $this->client->post("libraries/{$data['id']}", [
|
||||
'body' => $data
|
||||
]);
|
||||
|
||||
return $result->json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the full set of anime lists
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_all_lists()
|
||||
{
|
||||
$output = [
|
||||
'Watching' => [],
|
||||
'Plan to Watch' => [],
|
||||
'On Hold' => [],
|
||||
'Dropped' => [],
|
||||
'Completed' => [],
|
||||
];
|
||||
|
||||
$data = $this->_get_list();
|
||||
|
||||
foreach($data as $datum)
|
||||
{
|
||||
switch($datum['status'])
|
||||
{
|
||||
case "completed":
|
||||
$output['Completed'][] = $datum;
|
||||
break;
|
||||
|
||||
case "plan-to-watch":
|
||||
$output['Plan to Watch'][] = $datum;
|
||||
break;
|
||||
|
||||
case "dropped":
|
||||
$output['Dropped'][] = $datum;
|
||||
break;
|
||||
|
||||
case "on-hold":
|
||||
$output['On Hold'][] = $datum;
|
||||
break;
|
||||
|
||||
case "currently-watching":
|
||||
$output['Watching'][] = $datum;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Sort anime by name
|
||||
foreach($output as &$status_list)
|
||||
{
|
||||
$this->sort_by_name($status_list);
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a category out of the full list
|
||||
*
|
||||
* @param string $status
|
||||
* @return array
|
||||
*/
|
||||
public function get_list($status)
|
||||
{
|
||||
$map = [
|
||||
'currently-watching' => 'Watching',
|
||||
'plan-to-watch' => 'Plan to Watch',
|
||||
'on-hold' => 'On Hold',
|
||||
'dropped' => 'Dropped',
|
||||
'completed' => 'Completed',
|
||||
];
|
||||
|
||||
$data = $this->_get_list($status);
|
||||
$this->sort_by_name($data);
|
||||
|
||||
$output = [];
|
||||
$output[$map[$status]] = $data;
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get information about an anime from its id
|
||||
*
|
||||
* @param string $anime_id
|
||||
* @return array
|
||||
*/
|
||||
public function get_anime($anime_id)
|
||||
{
|
||||
$config = [
|
||||
'query' => [
|
||||
'id' => $anime_id
|
||||
]
|
||||
];
|
||||
|
||||
$response = $this->client->get("anime/{$anime_id}", $config);
|
||||
|
||||
return $response->json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for anime by name
|
||||
*
|
||||
* @param string $name
|
||||
* @return array
|
||||
*/
|
||||
public function search($name)
|
||||
{
|
||||
global $defaultHandler;
|
||||
|
||||
$config = [
|
||||
'query' => [
|
||||
'query' => $name
|
||||
]
|
||||
];
|
||||
|
||||
$response = $this->client->get('search/anime', $config);
|
||||
$defaultHandler->addDataTable('anime_search_response', (array)$response);
|
||||
|
||||
if ($response->getStatusCode() != 200)
|
||||
{
|
||||
throw new Exception($response->getEffectiveUrl());
|
||||
}
|
||||
|
||||
return $response->json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Actually retreive the data from the api
|
||||
*
|
||||
* @param string $status - Status to filter by
|
||||
* @return array
|
||||
*/
|
||||
private function _get_list($status="all")
|
||||
{
|
||||
global $defaultHandler;
|
||||
|
||||
$cache_file = "{$this->config->data_cache_path}/anime-{$status}.json";
|
||||
|
||||
$config = [
|
||||
'allow_redirects' => FALSE
|
||||
];
|
||||
|
||||
if ($status != "all")
|
||||
{
|
||||
$config['query']['status'] = $status;
|
||||
}
|
||||
|
||||
$response = $this->client->get("users/{$this->config->hummingbird_username}/library", $config);
|
||||
|
||||
$defaultHandler->addDataTable('anime_list_response', (array)$response);
|
||||
|
||||
if ($response->getStatusCode() != 200)
|
||||
{
|
||||
if ( ! file_exists($cache_file))
|
||||
{
|
||||
throw new Exception($response->getEffectiveUrl());
|
||||
}
|
||||
else
|
||||
{
|
||||
$output = json_decode(file_get_contents($cache_file), TRUE);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$output = $response->json();
|
||||
$output_json = json_encode($output);
|
||||
|
||||
if (( ! file_exists($cache_file)) || file_get_contents($cache_file) !== $output_json)
|
||||
{
|
||||
// Attempt to create the cache folder if it doesn't exist
|
||||
if ( ! is_dir($this->config->data_cache_path))
|
||||
{
|
||||
mkdir($this->config->data_cache_path);
|
||||
}
|
||||
// Cache the call in case of downtime
|
||||
file_put_contents($cache_file, json_encode($output));
|
||||
}
|
||||
}
|
||||
|
||||
foreach($output as &$row)
|
||||
{
|
||||
$row['anime']['cover_image'] = $this->get_cached_image($row['anime']['cover_image'], $row['anime']['slug'], 'anime');
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort the list by title
|
||||
*
|
||||
* @param array $array
|
||||
* @return void
|
||||
*/
|
||||
private function sort_by_name(&$array)
|
||||
{
|
||||
$sort = array();
|
||||
|
||||
foreach($array as $key => $item)
|
||||
{
|
||||
$sort[$key] = $item['anime']['title'];
|
||||
}
|
||||
|
||||
array_multisort($sort, SORT_ASC, $array);
|
||||
}
|
||||
}
|
||||
// End of AnimeModel.php
|
@ -1,193 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Manga API Model
|
||||
*/
|
||||
namespace AnimeClient;
|
||||
|
||||
/**
|
||||
* Model for handling requests dealing with the manga list
|
||||
*/
|
||||
class MangaModel extends BaseApiModel {
|
||||
|
||||
/**
|
||||
* The base url for api requests
|
||||
* @var string
|
||||
*/
|
||||
protected $base_url = "https://hummingbird.me/";
|
||||
|
||||
|
||||
/**
|
||||
* Update the selected manga
|
||||
*
|
||||
* @param array $data
|
||||
* @return array
|
||||
*/
|
||||
public function update($data)
|
||||
{
|
||||
$id = $data['id'];
|
||||
unset($data['id']);
|
||||
|
||||
$result = $this->client->put("manga_library_entries/{$id}", [
|
||||
'cookies' => ['token' => $_SESSION['hummingbird_anime_token']],
|
||||
'json' => ['manga_library_entry' => $data]
|
||||
]);
|
||||
|
||||
return $result->json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the full set of anime lists
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_all_lists()
|
||||
{
|
||||
$data = $this->_get_list();
|
||||
|
||||
foreach ($data as $key => &$val)
|
||||
{
|
||||
$this->sort_by_name($val);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a category out of the full list
|
||||
*
|
||||
* @param string $status
|
||||
* @return array
|
||||
*/
|
||||
public function get_list($status)
|
||||
{
|
||||
$data = $this->_get_list($status);
|
||||
|
||||
$this->sort_by_name($data);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Massage the list of manga entries into something more usable
|
||||
*
|
||||
* @param string $status
|
||||
* @return array
|
||||
*/
|
||||
private function _get_list($status="all")
|
||||
{
|
||||
global $defaultHandler;
|
||||
|
||||
$cache_file = _dir($this->config->data_cache_path, 'manga.json');
|
||||
|
||||
$config = [
|
||||
'query' => [
|
||||
'user_id' => $this->config->hummingbird_username
|
||||
],
|
||||
'allow_redirects' => FALSE
|
||||
];
|
||||
|
||||
$response = $this->client->get('manga_library_entries', $config);
|
||||
|
||||
$defaultHandler->addDataTable('response', (array)$response);
|
||||
|
||||
if ($response->getStatusCode() != 200)
|
||||
{
|
||||
if ( ! file_exists($cache_file))
|
||||
{
|
||||
throw new Exception($response->getEffectiveUrl());
|
||||
}
|
||||
else
|
||||
{
|
||||
$raw_data = json_decode(file_get_contents($cache_file), TRUE);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Reorganize data to be more usable
|
||||
$raw_data = $response->json();
|
||||
|
||||
// Attempt to create the cache dir if it doesn't exist
|
||||
if ( ! is_dir($this->config->data_cache_path))
|
||||
{
|
||||
mkdir($this->config->data_cache_path);
|
||||
}
|
||||
|
||||
// Cache data in case of downtime
|
||||
file_put_contents($cache_file, json_encode($raw_data));
|
||||
}
|
||||
|
||||
// Bail out early if there isn't any manga data
|
||||
if (empty($raw_data)) return [];
|
||||
|
||||
$data = [
|
||||
'Reading' => [],
|
||||
'Plan to Read' => [],
|
||||
'On Hold' => [],
|
||||
'Dropped' => [],
|
||||
'Completed' => [],
|
||||
];
|
||||
$manga_data = [];
|
||||
|
||||
// Massage the two lists into one
|
||||
foreach($raw_data['manga'] as $manga)
|
||||
{
|
||||
$manga_data[$manga['id']] = $manga;
|
||||
}
|
||||
|
||||
// Filter data by status
|
||||
foreach($raw_data['manga_library_entries'] as &$entry)
|
||||
{
|
||||
$entry['manga'] = $manga_data[$entry['manga_id']];
|
||||
|
||||
// Cache poster images
|
||||
$entry['manga']['poster_image'] = $this->get_cached_image($entry['manga']['poster_image'], $entry['manga_id'], 'manga');
|
||||
|
||||
switch($entry['status'])
|
||||
{
|
||||
case "Plan to Read":
|
||||
$data['Plan to Read'][] = $entry;
|
||||
break;
|
||||
|
||||
case "Dropped":
|
||||
$data['Dropped'][] = $entry;
|
||||
break;
|
||||
|
||||
case "On Hold":
|
||||
$data['On Hold'][] = $entry;
|
||||
break;
|
||||
|
||||
case "Currently Reading":
|
||||
$data['Reading'][] = $entry;
|
||||
break;
|
||||
|
||||
case "Completed":
|
||||
default:
|
||||
$data['Completed'][] = $entry;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
//file_put_contents(_dir($this->config->data_cache_path, "manga-processed.json"), json_encode($data, JSON_PRETTY_PRINT));
|
||||
|
||||
return (array_key_exists($status, $data)) ? $data[$status] : $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort the manga entries by their title
|
||||
*
|
||||
* @param array $array
|
||||
* @return void
|
||||
*/
|
||||
private function sort_by_name(&$array)
|
||||
{
|
||||
$sort = array();
|
||||
|
||||
foreach($array as $key => $item)
|
||||
{
|
||||
$sort[$key] = $item['manga']['romaji_title'];
|
||||
}
|
||||
|
||||
array_multisort($sort, SORT_ASC, $array);
|
||||
}
|
||||
}
|
||||
// End of MangaModel.php
|
100
app/templates/anime-cover.php
Normal file
100
app/templates/anime-cover.php
Normal file
@ -0,0 +1,100 @@
|
||||
<article
|
||||
class="media"
|
||||
data-kitsu-id="<?= $item['id'] ?>"
|
||||
data-anilist-id="<?= $item['anilist_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->img($item['anime']['cover_image'], ['width' => 220, 'loading' => 'lazy']) ?>
|
||||
|
||||
<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 (isset($item['private']) || isset($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">
|
||||
<?php if ($item['rewatched'] == 1): ?>
|
||||
<div>Rewatched once</div>
|
||||
<?php elseif ($item['rewatched'] == 2): ?>
|
||||
<div>Rewatched twice</div>
|
||||
<?php elseif ($item['rewatched'] == 3): ?>
|
||||
<div>Rewatched thrice</div>
|
||||
<?php else: ?>
|
||||
<div>Rewatched <?= $item['rewatched'] ?> times</div>
|
||||
<?php endif ?>
|
||||
</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->img("/public/images/{$link['meta']['image']}", [
|
||||
'class' => 'streaming-logo',
|
||||
'width' => 20,
|
||||
'height' => 20,
|
||||
'alt' => "{$link['meta']['name']} logo",
|
||||
]); ?>
|
||||
</a>
|
||||
<?php else: ?>
|
||||
<?= $helper->img("/public/images/{$link['meta']['image']}", [
|
||||
'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>
|
6
app/templates/character.php
Normal file
6
app/templates/character.php
Normal file
@ -0,0 +1,6 @@
|
||||
<article class="<?= $className ?>">
|
||||
<div class="name">
|
||||
<a href="<?= $link ?>"><?= $name ?></a>
|
||||
</div>
|
||||
<a href="<?= $link ?>"><?= $picture ?></a>
|
||||
</article>
|
68
app/templates/manga-cover.php
Normal file
68
app/templates/manga-cover.php
Normal file
@ -0,0 +1,68 @@
|
||||
<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>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
<?= $helper->img($item['manga']['image'], ['width' => 220, 'loading' => 'lazy']) ?>
|
||||
<div class="name">
|
||||
<a href="<?= $url->generate('manga.details', ['id' => $item['manga']['slug']]) ?>">
|
||||
<?= $escape->html($item['manga']['title']) ?>
|
||||
<?php foreach($item['manga']['titles'] as $title): ?>
|
||||
<br /><small><?= $title ?></small>
|
||||
<?php endforeach ?>
|
||||
</a>
|
||||
</div>
|
||||
<div class="table">
|
||||
<?php if ($auth->isAuthenticated()): ?>
|
||||
<div class="row">
|
||||
<span class="edit">
|
||||
<a class="bracketed"
|
||||
title="Edit information about this manga"
|
||||
href="<?= $url->generate('edit', [
|
||||
'controller' => 'manga',
|
||||
'id' => $item['id'],
|
||||
'status' => $name
|
||||
]) ?>">
|
||||
Edit
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
<div class="row">
|
||||
<div><?= $item['manga']['type'] ?></div>
|
||||
<div class="user-rating">Rating: <?= $item['user_rating'] ?> / 10</div>
|
||||
</div>
|
||||
|
||||
<?php if ($item['rereading']): ?>
|
||||
<div class="row">
|
||||
<?php foreach(['rereading'] as $attr): ?>
|
||||
<?php if($item[$attr]): ?>
|
||||
<span class="item-<?= $attr ?>"><?= ucfirst($attr) ?></span>
|
||||
<?php endif ?>
|
||||
<?php endforeach ?>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
|
||||
<?php if ($item['reread'] > 0): ?>
|
||||
<div class="row">
|
||||
<?php if ($item['reread'] == 1): ?>
|
||||
<div>Reread once</div>
|
||||
<?php elseif ($item['reread'] == 2): ?>
|
||||
<div>Reread twice</div>
|
||||
<?php elseif ($item['reread'] == 3): ?>
|
||||
<div>Reread thrice</div>
|
||||
<?php else: ?>
|
||||
<div>Reread <?= $item['reread'] ?> times</div>
|
||||
<?php endif ?>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
|
||||
<div class="row">
|
||||
<div class="chapter_completion">
|
||||
Chapters: <span class="chapters_read"><?= $item['chapters']['read'] ?></span> /
|
||||
<span class="chapter_count"><?= $item['chapters']['total'] ?></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
12
app/templates/media.php
Normal file
12
app/templates/media.php
Normal file
@ -0,0 +1,12 @@
|
||||
<article class="<?= $className ?>">
|
||||
<a href="<?= $link ?>"><?= $picture ?></a>
|
||||
<div class="name">
|
||||
<a href="<?= $link ?>">
|
||||
<?= array_shift($titles) ?>
|
||||
<?php foreach ($titles as $title): ?>
|
||||
<br />
|
||||
<small><?= $title ?></small>
|
||||
<?php endforeach ?>
|
||||
</a>
|
||||
</div>
|
||||
</article>
|
5
app/templates/single-tab.php
Normal file
5
app/templates/single-tab.php
Normal file
@ -0,0 +1,5 @@
|
||||
<section class="<?= $className ?>">
|
||||
<?php foreach ($data as $tabName => $tabData): ?>
|
||||
<?= $callback($tabData, $tabName) ?>
|
||||
<?php endforeach ?>
|
||||
</section>
|
32
app/templates/tabs.php
Normal file
32
app/templates/tabs.php
Normal file
@ -0,0 +1,32 @@
|
||||
<div class="tabs">
|
||||
<?php $i = 0; foreach ($data as $tabName => $tabData): ?>
|
||||
<?php if ( ! empty($tabData)): ?>
|
||||
<?php $id = "{$name}-{$i}"; ?>
|
||||
<input
|
||||
role='tab'
|
||||
aria-controls="_<?= $id ?>"
|
||||
type="radio"
|
||||
name="<?= $name ?>"
|
||||
id="<?= $id ?>"
|
||||
<?= ($i === 0) ? 'checked="checked"' : '' ?>
|
||||
/>
|
||||
<label for="<?= $id ?>"><?= ucfirst($tabName) ?></label>
|
||||
|
||||
<?php if ($hasSectionWrapper): ?>
|
||||
<div class="content full-height">
|
||||
<?php endif ?>
|
||||
|
||||
<section
|
||||
id="_<?= $id ?>"
|
||||
role="tabpanel"
|
||||
class="<?= $className ?>"
|
||||
>
|
||||
<?= $callback($tabData, $tabName) ?>
|
||||
</section>
|
||||
|
||||
<?php if ($hasSectionWrapper): ?>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
<?php endif ?>
|
||||
<?php $i++; endforeach ?>
|
||||
</div>
|
25
app/templates/vertical-tabs.php
Normal file
25
app/templates/vertical-tabs.php
Normal file
@ -0,0 +1,25 @@
|
||||
<div class="vertical-tabs">
|
||||
<?php $i = 0; ?>
|
||||
<?php foreach ($data as $tabName => $tabData): ?>
|
||||
<?php $id = "{$name}-{$i}" ?>
|
||||
<div class="tab">
|
||||
<input
|
||||
type="radio"
|
||||
role='tab'
|
||||
aria-controls="_<?= $id ?>"
|
||||
name="<?= $name ?>"
|
||||
id="<?= $id ?>"
|
||||
<?= $i === 0 ? 'checked="checked"' : '' ?>
|
||||
/>
|
||||
<label for="<?= $id ?>"><?= $tabName ?></label>
|
||||
<section
|
||||
id='_<?= $id ?>'
|
||||
role="tabpanel"
|
||||
class="<?= $className ?>"
|
||||
>
|
||||
<?= $callback($tabData, $tabName) ?>
|
||||
</section>
|
||||
</div>
|
||||
<?php $i++; ?>
|
||||
<?php endforeach ?>
|
||||
</div>
|
@ -1,7 +1,6 @@
|
||||
<body>
|
||||
<main>
|
||||
<h1>404</h1>
|
||||
<h2>Page Not Found</h2>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
<main>
|
||||
<h1>404</h1>
|
||||
<h2><?= $message ?></h2>
|
||||
<pre>(╯°□°)╯︵ ┻━┻
|
||||
┬─┬ノ( º _ ºノ)</pre>
|
||||
</main>
|
||||
|
40
app/views/anime/add.php
Normal file
40
app/views/anime/add.php
Normal file
@ -0,0 +1,40 @@
|
||||
<?php if ($auth->isAuthenticated()): ?>
|
||||
<main>
|
||||
<h2>Add Anime to your List</h2>
|
||||
<form action="<?= $action_url ?>" method="post">
|
||||
<?php include realpath(__DIR__ . '/../js-warning.php') ?>
|
||||
<section>
|
||||
<div class="cssload-loader" hidden="hidden">
|
||||
<div class="cssload-inner cssload-one"></div>
|
||||
<div class="cssload-inner cssload-two"></div>
|
||||
<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>
|
||||
</section>
|
||||
<br />
|
||||
<table class="invisible form">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><label for="status">Watching Status</label></td>
|
||||
<td>
|
||||
<select name="status" id="status">
|
||||
<?php foreach($status_list as $status_key => $status_title): ?>
|
||||
<option value="<?= $status_key ?>"><?= $status_title ?></option>
|
||||
<?php endforeach ?>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td> </td>
|
||||
<td>
|
||||
<input type="hidden" name="type" value="anime" />
|
||||
<button type="submit">Save</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</form>
|
||||
</main>
|
||||
<?php endif ?>
|
@ -1,28 +0,0 @@
|
||||
<main>
|
||||
<?php foreach ($sections as $name => $items): ?>
|
||||
<section class="status">
|
||||
<h2><?= $name ?></h2>
|
||||
<section class="media-wrap">
|
||||
<?php foreach($items as $item): ?>
|
||||
<a href="https://hummingbird.me/anime/<?= $item['slug'] ?>">
|
||||
<article class="media" id="a-<?= $item['hummingbird_id'] ?>">
|
||||
<img src="<?= $item['cover_image'] ?>" />
|
||||
<div class="name">
|
||||
<?= $item['title'] ?>
|
||||
<?= ($item['alternate_title'] != "") ? "<br />({$item['alternate_title']})" : ""; ?>
|
||||
</div>
|
||||
<div class="table">
|
||||
<div class="row">
|
||||
<div class="completion">Episodes: <?= $item['episode_count'] ?></div>
|
||||
<div class="media_type"><?= $item['show_type'] ?></div>
|
||||
<div class="age_rating"><?= $item['age_rating'] ?></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</article>
|
||||
</a>
|
||||
<?php endforeach ?>
|
||||
</section>
|
||||
</section>
|
||||
<?php endforeach ?>
|
||||
</main>
|
@ -1,43 +0,0 @@
|
||||
<main>
|
||||
<?php foreach ($sections as $name => $items): ?>
|
||||
<h2><?= $name ?></h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th>Alternate Title</th>
|
||||
<th>Episode Count</th>
|
||||
<th>Episode Length</th>
|
||||
<th>Show Type</th>
|
||||
<th>Age Rating</th>
|
||||
<th>Notes</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach($items as $item): ?>
|
||||
<tr>
|
||||
<td class="align_left">
|
||||
<a href="https://hummingbird.me/anime/<?= $item['slug'] ?>">
|
||||
<?= $item['title'] ?>
|
||||
</a>
|
||||
</td>
|
||||
<td class="align_left"><?= $item['alternate_title'] ?></td>
|
||||
<td><?= $item['episode_count'] ?></td>
|
||||
<td><?= $item['episode_length'] ?></td>
|
||||
<td><?= $item['show_type'] ?></td>
|
||||
<td><?= $item['age_rating'] ?></td>
|
||||
<td class="align_left"><?= $item['notes'] ?></td>
|
||||
</tr>
|
||||
<?php endforeach ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<br />
|
||||
<?php endforeach ?>
|
||||
</main>
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
|
||||
<script src="/public/js/table_sorter/jquery.tablesorter.min.js"></script>
|
||||
<script>
|
||||
$(function() {
|
||||
$('table').tablesorter();
|
||||
});
|
||||
</script>
|
@ -1,40 +1,30 @@
|
||||
<main>
|
||||
<main class="media-list">
|
||||
<?php if ($auth->isAuthenticated()): ?>
|
||||
<a class="bracketed" href="<?= $url->generate('anime.add.get') ?>">Add Item</a>
|
||||
<?php endif ?>
|
||||
<?php if (empty($sections)): ?>
|
||||
<h3>There's nothing here!</h3>
|
||||
<?php else: ?>
|
||||
<br />
|
||||
<label>Filter: <input type='text' class='media-filter' /></label>
|
||||
<br />
|
||||
<?php foreach ($sections as $name => $items): ?>
|
||||
<?php if (empty($items)): ?>
|
||||
<section class="status">
|
||||
<h2><?= $name ?></h2>
|
||||
<h2><?= $escape->html($name) ?></h2>
|
||||
<h3>There's nothing here!</h3>
|
||||
</section>
|
||||
<?php else: ?>
|
||||
<section class="status">
|
||||
<h2><?= $escape->html($name) ?></h2>
|
||||
<section class="media-wrap">
|
||||
<?php foreach($items as $item): ?>
|
||||
<article class="media" id="a-<?= $item['anime']['id'] ?>">
|
||||
<?php if (is_logged_in()): ?>
|
||||
<button class="plus_one" hidden>+1 Episode</button>
|
||||
<?php endif ?>
|
||||
<img src="<?= $item['anime']['cover_image'] ?>" />
|
||||
<div class="name">
|
||||
<a href="<?= $item['anime']['url'] ?>">
|
||||
<?= $item['anime']['title'] ?>
|
||||
<?= ($item['anime']['alternate_title'] != "") ? "<br />({$item['anime']['alternate_title']})" : ""; ?>
|
||||
</a>
|
||||
</div>
|
||||
<div class="table">
|
||||
<div class="row">
|
||||
<div class="user_rating">Rating: <?= ($item['rating']['value'] > 0) ? (int)($item['rating']['value'] * 2) : " - " ?> / 10</div>
|
||||
<div class="completion">Episodes:
|
||||
<span class="completed_number"><?= $item['episodes_watched'] ?></span> /
|
||||
<span class="total_number"><?= ($item['anime']['episode_count'] != 0) ? $item['anime']['episode_count'] : "-" ?></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="media_type"><?= $item['anime']['show_type'] ?></div>
|
||||
<div class="airing_status"><?= $item['anime']['status'] ?></div>
|
||||
<div class="age_rating"><?= $item['anime']['age_rating'] ?></div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
<?php if ($item['private'] && ! $auth->isAuthenticated()) continue; ?>
|
||||
<?= $component->animeCover($item) ?>
|
||||
<?php endforeach ?>
|
||||
</section>
|
||||
</section>
|
||||
<?php endif ?>
|
||||
<?php endforeach ?>
|
||||
</main>
|
||||
<?php if (is_logged_in()): ?>
|
||||
<script src="<?= asset_url('js.php?g=edit') ?>"></script>
|
||||
<?php endif ?>
|
||||
</main>
|
||||
|
205
app/views/anime/details.php
Normal file
205
app/views/anime/details.php
Normal file
@ -0,0 +1,205 @@
|
||||
<?php
|
||||
|
||||
use function Aviat\AnimeClient\friendlyTime;
|
||||
|
||||
?>
|
||||
<main class="details fixed">
|
||||
<section class="flex" unselectable>
|
||||
<aside class="info">
|
||||
<?= $helper->img($data['cover_image'], ['width' => '390']) ?>
|
||||
|
||||
<br />
|
||||
|
||||
<table class="media-details">
|
||||
<tr>
|
||||
<td class="align-right">Airing Status</td>
|
||||
<td><?= $data['status'] ?></td>
|
||||
</tr>
|
||||
|
||||
<?php if ( ! empty($data['airDate'])): ?>
|
||||
<tr>
|
||||
<td>Original Airing</td>
|
||||
<td><?= $data['airDate'] ?></td>
|
||||
</tr>
|
||||
<?php endif ?>
|
||||
|
||||
<tr>
|
||||
<td>Show Type</td>
|
||||
<td><?= (strlen($data['show_type']) > 3) ? ucfirst(strtolower($data['show_type'])) : $data['show_type'] ?></td>
|
||||
</tr>
|
||||
|
||||
<?php if ($data['episode_count'] !== 1): ?>
|
||||
<tr>
|
||||
<td>Episode Count</td>
|
||||
<td><?= $data['episode_count'] ?? '-' ?></td>
|
||||
</tr>
|
||||
<?php endif ?>
|
||||
|
||||
<?php if (( ! empty($data['episode_length'])) && $data['episode_count'] !== 1): ?>
|
||||
<tr>
|
||||
<td>Episode Length</td>
|
||||
<td><?= friendlyTime($data['episode_length']) ?></td>
|
||||
</tr>
|
||||
<?php endif ?>
|
||||
|
||||
<?php if (isset($data['total_length'], $data['episode_count']) && $data['total_length'] > 0): ?>
|
||||
<tr>
|
||||
<td>Total Length</td>
|
||||
<td><?= friendlyTime($data['total_length']) ?></td>
|
||||
</tr>
|
||||
<?php endif ?>
|
||||
|
||||
<?php if ( ! empty($data['age_rating'])): ?>
|
||||
<tr>
|
||||
<td>Age Rating</td>
|
||||
<td><abbr title="<?= $data['age_rating_guide'] ?>"><?= $data['age_rating'] ?></abbr>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endif ?>
|
||||
|
||||
<?php if (count($data['links']) > 0): ?>
|
||||
<tr>
|
||||
<td>External Links</td>
|
||||
<td>
|
||||
<?php foreach ($data['links'] as $urlName => $externalUrl): ?>
|
||||
<a rel='external' href="<?= $externalUrl ?>"><?= $urlName ?></a><br />
|
||||
<?php endforeach ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endif ?>
|
||||
|
||||
<tr>
|
||||
<td>Genres</td>
|
||||
<td>
|
||||
<?= implode(', ', $data['genres']) ?>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
|
||||
<br />
|
||||
|
||||
</aside>
|
||||
<article class="text">
|
||||
<h2 class="toph"><?= $data['title'] ?></h2>
|
||||
<?php foreach ($data['titles_more'] as $title): ?>
|
||||
<h3><?= $title ?></h3>
|
||||
<?php endforeach ?>
|
||||
<br />
|
||||
<div class="description">
|
||||
<p><?= str_replace("\n", '</p><p>', $data['synopsis']) ?></p>
|
||||
</div>
|
||||
<?php if (count($data['streaming_links']) > 0): ?>
|
||||
<hr />
|
||||
<h4>Streaming on:</h4>
|
||||
<table class="full-width invisible streaming-links">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="align-left">Service</th>
|
||||
<th>Subtitles</th>
|
||||
<th>Dubs</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($data['streaming_links'] as $link): ?>
|
||||
<tr>
|
||||
<td class="align-left">
|
||||
<?php if ($link['meta']['link'] !== FALSE): ?>
|
||||
<a
|
||||
href="<?= $link['link'] ?>"
|
||||
title="Stream '<?= $data['title'] ?>' on <?= $link['meta']['name'] ?>"
|
||||
>
|
||||
<?= $helper->img("/public/images/{$link['meta']['image']}", [
|
||||
'class' => 'streaming-logo',
|
||||
'width' => 50,
|
||||
'height' => 50,
|
||||
'alt' => "{$link['meta']['name']} logo",
|
||||
]) ?>
|
||||
<?= $link['meta']['name'] ?>
|
||||
</a>
|
||||
<?php else: ?>
|
||||
<?= $helper->img("/public/images/{$link['meta']['image']}", [
|
||||
'class' => 'streaming-logo',
|
||||
'width' => 50,
|
||||
'height' => 50,
|
||||
'alt' => "{$link['meta']['name']} logo",
|
||||
]) ?>
|
||||
<?= $link['meta']['name'] ?>
|
||||
<?php endif ?>
|
||||
</td>
|
||||
<td><?= implode(', ', array_map(fn ($sub) => Locale::getDisplayLanguage($sub, 'en'), $link['subs'])) ?></td>
|
||||
<td><?= implode(', ', array_map(fn ($dub) => Locale::getDisplayLanguage($dub, 'en'), $link['dubs'])) ?></td>
|
||||
</tr>
|
||||
<?php endforeach ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif ?>
|
||||
<?php if ( ! empty($data['trailer_id'])): ?>
|
||||
<div class="responsive-iframe">
|
||||
<h4>Trailer</h4>
|
||||
<iframe
|
||||
width="560"
|
||||
height="315"
|
||||
role='img'
|
||||
src="https://www.youtube.com/embed/<?= $data['trailer_id'] ?>"
|
||||
allow="autoplay; encrypted-media"
|
||||
allowfullscreen
|
||||
tabindex='0'
|
||||
title="<?= $data['title'] ?> trailer video"
|
||||
></iframe>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<?php if (count($data['characters']) > 0): ?>
|
||||
<section>
|
||||
<h2>Characters</h2>
|
||||
|
||||
<?= $component->tabs('character-types', $data['characters'], static function ($characterList, $role)
|
||||
use ($component, $url, $helper) {
|
||||
$rendered = [];
|
||||
foreach ($characterList as $id => $character):
|
||||
if (empty($character['image']))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
$rendered[] = $component->character(
|
||||
$character['name'],
|
||||
$url->generate('character', ['slug' => $character['slug']]),
|
||||
$helper->img($character['image']),
|
||||
(strtolower($role) !== 'main') ? 'small-character' : 'character'
|
||||
);
|
||||
endforeach;
|
||||
|
||||
return implode('', array_map('mb_trim', $rendered));
|
||||
}) ?>
|
||||
</section>
|
||||
<?php endif ?>
|
||||
|
||||
<?php if (count($data['staff']) > 0): ?>
|
||||
<section>
|
||||
<h2>Staff</h2>
|
||||
|
||||
<?= $component->verticalTabs('staff-role', $data['staff'], static function ($staffList)
|
||||
use ($component, $url, $helper) {
|
||||
$rendered = [];
|
||||
foreach ($staffList as $id => $person):
|
||||
if (empty($person['image']))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
$rendered[] = $component->character(
|
||||
$person['name'],
|
||||
$url->generate('person', ['slug' => $person['slug']]),
|
||||
$helper->img($person['image']),
|
||||
'character small-person',
|
||||
);
|
||||
endforeach;
|
||||
|
||||
return implode('', array_map('mb_trim', $rendered));
|
||||
}) ?>
|
||||
</section>
|
||||
<?php endif ?>
|
||||
</main>
|
@ -1,8 +1,112 @@
|
||||
<body>
|
||||
<?php include 'nav.php' ?>
|
||||
<?php if ($auth->isAuthenticated()): ?>
|
||||
<main>
|
||||
<h2>Edit Anime List Item</h2>
|
||||
<form action="<?= $action ?>" method="post">
|
||||
<table class="invisible form">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<h3><?= $escape->html($item['anime']['title']) ?></h3>
|
||||
<?php foreach($item['anime']['titles'] as $title): ?>
|
||||
<h4><?= $escape->html($title) ?></h4>
|
||||
<?php endforeach ?>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td rowspan="9">
|
||||
<?= $helper->img($item['anime']['cover_image']) ?>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for="private">Is Private?</label></td>
|
||||
<td>
|
||||
<input type="checkbox" name="private" id="private"
|
||||
<?php if($item['private']): ?>checked="checked"<?php endif ?>
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for="watching_status">Watching Status</label></td>
|
||||
<td>
|
||||
<select name="watching_status" id="watching_status">
|
||||
<?php foreach($statuses as $status_key => $status_title): ?>
|
||||
<option <?php if(strtolower($item['watching_status']) === $status_key): ?>selected="selected"<?php endif ?>
|
||||
value="<?= $status_key ?>"><?= $status_title ?></option>
|
||||
<?php endforeach ?>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for="series_rating">Rating</label></td>
|
||||
<td>
|
||||
<input type="number" min="0" max="10" maxlength="2" name="user_rating" id="series_rating" value="<?= $item['user_rating'] ?>" id="series_rating" size="2" /> / 10
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for="episodes_watched">Episodes Watched</label></td>
|
||||
<td>
|
||||
<input type="number" min="0" size="4" maxlength="4" value="<?= $item['episodes']['watched'] ?>" name="episodes_watched" id="episodes_watched" />
|
||||
<?php if($item['episodes']['total'] > 0): ?>
|
||||
/ <?= $item['episodes']['total'] ?>
|
||||
<?php endif ?>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for="rewatching_flag">Rewatching?</label></td>
|
||||
<td>
|
||||
<input type="checkbox" name="rewatching" id="rewatching_flag"
|
||||
<?php if($item['rewatching'] === TRUE): ?>checked="checked"<?php endif ?>
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for="rewatched">Rewatch Count</label></td>
|
||||
<td>
|
||||
<input type="number" min="0" id="rewatched" name="rewatched" value="<?= $item['rewatched'] ?>" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for="notes">Notes</label></td>
|
||||
<td>
|
||||
<textarea name="notes" id="notes"><?= $escape->html($item['notes']) ?></textarea>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td> </td>
|
||||
<td>
|
||||
<input type="hidden" value="<?= $item['id'] ?>" name="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>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</form>
|
||||
<form class="js-delete" action="<?= $url->generate('anime.delete') ?>" method="post">
|
||||
<fieldset>
|
||||
<legend>Danger Zone</legend>
|
||||
<table class="form invisible">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="danger">
|
||||
<strong>Permanently</strong> remove this list item and <strong>all</strong> its data?
|
||||
</td>
|
||||
<td>
|
||||
<input type="hidden" value="<?= $item['id'] ?>" name="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>
|
||||
</fieldset>
|
||||
</form>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
<?php endif ?>
|
@ -1,36 +1,113 @@
|
||||
<main>
|
||||
<?php use function Aviat\AnimeClient\colNotEmpty; ?>
|
||||
<main class="media-list">
|
||||
<?php if ($auth->isAuthenticated()): ?>
|
||||
<a class="bracketed" href="<?= $url->generate('anime.add.get') ?>">Add Item</a>
|
||||
<?php endif ?>
|
||||
<?php if (empty($sections)): ?>
|
||||
<h3>There's nothing here!</h3>
|
||||
<?php else: ?>
|
||||
<br />
|
||||
<label>Filter: <input type='text' class='media-filter' /></label>
|
||||
<br />
|
||||
<?php foreach ($sections as $name => $items): ?>
|
||||
<h2><?= $name ?></h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th>Alternate Title</th>
|
||||
<th>Airing Status</th>
|
||||
<th>Score</th>
|
||||
<th>Type</th>
|
||||
<th>Progress</th>
|
||||
<th>Rated</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach($items as $item): ?>
|
||||
<tr id="a-<?= $item['anime']['id'] ?>">
|
||||
<td class="align_left">
|
||||
<a href="<?= $item['anime']['url'] ?>">
|
||||
<?= $item['anime']['title'] ?>
|
||||
</a>
|
||||
</td>
|
||||
<td class="align_left"><?= $item['anime']['alternate_title'] ?></td>
|
||||
<td class="align_left"><?= $item['anime']['status'] ?></td>
|
||||
<td><?= (int)($item['rating']['value'] * 2) ?> / 10 </td>
|
||||
<td><?= $item['anime']['show_type'] ?></td>
|
||||
<td>Episodes: <?= $item['episodes_watched'] ?> / <?= $item['anime']['episode_count'] ?></td>
|
||||
<td><?= $item['anime']['age_rating'] ?></td>
|
||||
</tr>
|
||||
<?php endforeach ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php if (empty($items)): ?>
|
||||
<h3>There's nothing here!</h3>
|
||||
<?php else: ?>
|
||||
<?php
|
||||
$hasNotes = colNotEmpty($items, 'notes');
|
||||
?>
|
||||
<table class='media-wrap'>
|
||||
<thead>
|
||||
<tr>
|
||||
<?php if($auth->isAuthenticated()): ?>
|
||||
<td class="no-border"> </td>
|
||||
<?php endif ?>
|
||||
<th>Title</th>
|
||||
<th>Airing Status</th>
|
||||
<th class='numeric'>Score</th>
|
||||
<th>Type</th>
|
||||
<th class='numeric'>Progress</th>
|
||||
<th class='rating'>Age Rating</th>
|
||||
<th>Attributes</th>
|
||||
<?php if($hasNotes): ?><th>Notes</th><?php endif ?>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach($items as $item): ?>
|
||||
<?php if ($item['private'] && ! $auth->isAuthenticated()) continue; ?>
|
||||
<tr id="a-<?= $item['id'] ?>">
|
||||
<?php if ($auth->isAuthenticated()): ?>
|
||||
<td>
|
||||
<a class="bracketed" href="<?= $url->generate('edit', [
|
||||
'controller' => 'anime',
|
||||
'id' => $item['id'],
|
||||
'status' => $item['watching_status']
|
||||
]) ?>">Edit</a>
|
||||
</td>
|
||||
<?php endif ?>
|
||||
<td class="align-left justify">
|
||||
<a href="<?= $url->generate('anime.details', ['id' => $item['anime']['slug']]) ?>">
|
||||
<?= $item['anime']['title'] ?>
|
||||
</a>
|
||||
<br />
|
||||
<?= implode('<br />', $item['anime']['titles']) ?>
|
||||
</td>
|
||||
<td><?= $item['airing']['status'] ?></td>
|
||||
<td><?= $item['user_rating'] ?> / 10 </td>
|
||||
<td><?= $item['anime']['show_type'] ?></td>
|
||||
<td id="<?= $item['anime']['slug'] ?>">
|
||||
Episodes: <br />
|
||||
<span class="completed_number"><?= $item['episodes']['watched'] ?></span> / <span class="total_number"><?= $item['episodes']['total'] ?></span>
|
||||
</td>
|
||||
<td><?= $item['anime']['age_rating'] ?></td>
|
||||
<td>
|
||||
<?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'] ?>">
|
||||
<?= $helper->img("/public/images/{$link['meta']['image']}", [
|
||||
'class' => 'small-streaming-logo',
|
||||
'width' => 25,
|
||||
'height' => 25,
|
||||
'alt' => "{$link['meta']['name']} logo",
|
||||
]) ?>
|
||||
</a>
|
||||
<?php else: ?>
|
||||
<?= $helper->img("/public/images/{$link['meta']['image']}", [
|
||||
'class' => 'small-streaming-logo',
|
||||
'width' => 25,
|
||||
'height' => 25,
|
||||
'alt' => "{$link['meta']['name']} logo",
|
||||
]) ?>
|
||||
<?php endif ?>
|
||||
<?php endforeach ?>
|
||||
|
||||
<br />
|
||||
|
||||
<ul>
|
||||
<?php if ($item['rewatched'] > 0): ?>
|
||||
<?php if ($item['rewatched'] == 1): ?>
|
||||
<li>Rewatched once</li>
|
||||
<?php elseif ($item['rewatched'] == 2): ?>
|
||||
<li>Rewatched twice</li>
|
||||
<?php elseif ($item['rewatched'] == 3): ?>
|
||||
<li>Rewatched thrice</li>
|
||||
<?php else: ?>
|
||||
<li>Rewatched <?= $item['rewatched'] ?> times</li>
|
||||
<?php endif ?>
|
||||
<?php endif ?>
|
||||
<?php foreach(['private','rewatching'] as $attr): ?>
|
||||
<?php if($item[$attr]): ?><li><?= ucfirst($attr); ?></li><?php endif ?>
|
||||
<?php endforeach ?>
|
||||
</ul>
|
||||
</td>
|
||||
<?php if ($hasNotes): ?><td><p><?= $escape->html($item['notes']) ?></p></td><?php endif ?>
|
||||
</tr>
|
||||
<?php endforeach ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif ?>
|
||||
<?php endforeach ?>
|
||||
<?php endif ?>
|
||||
</main>
|
||||
<script src="<?= asset_url('js.php?g=table') ?>"></script>
|
||||
<script defer="defer" src="<?= $urlGenerator->assetUrl('js/tables.min.js') ?>"></script>
|
3
app/views/blank.php
Normal file
3
app/views/blank.php
Normal file
@ -0,0 +1,3 @@
|
||||
<main>
|
||||
<h1><?= $title ?></h1>
|
||||
</main>
|
162
app/views/character/details.php
Normal file
162
app/views/character/details.php
Normal file
@ -0,0 +1,162 @@
|
||||
<?php
|
||||
|
||||
use function Aviat\AnimeClient\getLocalImg;
|
||||
use Aviat\AnimeClient\Kitsu;
|
||||
|
||||
?>
|
||||
<main class="character-page details fixed">
|
||||
<section class="flex flex-no-wrap">
|
||||
<aside>
|
||||
<?= $helper->img($data['image']) ?>
|
||||
</aside>
|
||||
<div>
|
||||
<h2 class="toph"><?= $data['name'] ?></h2>
|
||||
<?php foreach ($data['names'] as $name): ?>
|
||||
<h3><?= $name ?></h3>
|
||||
<?php endforeach ?>
|
||||
|
||||
<?php if ( ! empty($data['otherNames'])): ?>
|
||||
<h4>Also Known As:</h4>
|
||||
<ul>
|
||||
<?php foreach ($data['otherNames'] as $name): ?>
|
||||
<li><h5><?= $name ?></h5></li>
|
||||
<?php endforeach ?>
|
||||
</ul>
|
||||
<?php endif ?>
|
||||
<br />
|
||||
<hr />
|
||||
<div class="description">
|
||||
<p><?= nl2br($data['description']) ?></p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<?php if ( ! (empty($data['media']['anime']) || empty($data['media']['manga']))): ?>
|
||||
<h3>Media</h3>
|
||||
|
||||
<?= $component->tabs('character-media', $data['media'], static function ($media, $mediaType) use ($url, $component, $helper) {
|
||||
$rendered = [];
|
||||
foreach ($media as $id => $item)
|
||||
{
|
||||
$rendered[] = $component->media(
|
||||
array_merge([$item['title']], $item['titles']),
|
||||
$url->generate("{$mediaType}.details", ['id' => $item['slug']]),
|
||||
$helper->img(Kitsu::getPosterImage($item), ['width' => 220, 'loading' => 'lazy']),
|
||||
);
|
||||
}
|
||||
|
||||
return implode('', array_map('mb_trim', $rendered));
|
||||
}, 'media-wrap content') ?>
|
||||
<?php endif ?>
|
||||
|
||||
<section>
|
||||
<?php if (count($data['castings']) > 0): ?>
|
||||
<h3>Castings</h3>
|
||||
<?php
|
||||
$vas = $data['castings']['Voice Actor'];
|
||||
unset($data['castings']['Voice Actor']);
|
||||
ksort($vas)
|
||||
?>
|
||||
|
||||
<?php foreach ($data['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>
|
||||
<article class="character">
|
||||
<?php
|
||||
$link = $url->generate('person', ['id' => $c['person']['id']]);
|
||||
?>
|
||||
<a href="<?= $link ?>">
|
||||
<?= $helper->img($c['person']['image']) ?>
|
||||
<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->img(Kitsu::getPosterImage($series['attributes'])) ?>
|
||||
</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 if ( ! empty($vas)): ?>
|
||||
<h4>Voice Actors</h4>
|
||||
|
||||
<?= $component->tabs('character-vas', $vas, static function ($casting) use ($url, $component, $helper) {
|
||||
$castings = [];
|
||||
foreach ($casting as $id => $c):
|
||||
$person = $component->character(
|
||||
$c['person']['name'],
|
||||
$url->generate('person', ['slug' => $c['person']['slug']]),
|
||||
$helper->img($c['person']['image']['original']['url']),
|
||||
);
|
||||
$medias = array_map(fn ($series) => $component->media(
|
||||
array_merge([$series['title']], $series['titles']),
|
||||
$url->generate('anime.details', ['id' => $series['slug']]),
|
||||
$helper->img(Kitsu::getPosterImage($series)),
|
||||
), $c['series']);
|
||||
$media = implode('', array_map('mb_trim', $medias));
|
||||
|
||||
$castings[] = <<<HTML
|
||||
<tr>
|
||||
<td>{$person}</td>
|
||||
<td width="75%">
|
||||
<section class="align-left media-wrap-flex">
|
||||
{$media}
|
||||
</section>
|
||||
</td>
|
||||
</tr>
|
||||
HTML;
|
||||
endforeach;
|
||||
|
||||
$languages = implode('', array_map('mb_trim', $castings));
|
||||
|
||||
return <<<HTML
|
||||
<table class="borderless max-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Cast Member</th>
|
||||
<th>Series</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>{$languages}</tbody>
|
||||
</table>
|
||||
HTML;
|
||||
}, 'content') ?>
|
||||
<?php endif ?>
|
||||
<?php endif ?>
|
||||
</section>
|
||||
</main>
|
39
app/views/collection/add.php
Normal file
39
app/views/collection/add.php
Normal file
@ -0,0 +1,39 @@
|
||||
<?php if ($auth->isAuthenticated()): ?>
|
||||
<main>
|
||||
<h2>Add <?= ucfirst($collection_type) ?> to your Collection</h2>
|
||||
<form action="<?= $action_url ?>" method="post">
|
||||
<?php include realpath(__DIR__ . '/../js-warning.php') ?>
|
||||
<section>
|
||||
<div class="cssload-loader" hidden="hidden">
|
||||
<div class="cssload-inner cssload-one"></div>
|
||||
<div class="cssload-inner cssload-two"></div>
|
||||
<div class="cssload-inner cssload-three"></div>
|
||||
</div>
|
||||
<label for="search-anime-collection">Search for <?= $collection_type ?> by name: <input type="search" id="search-anime-collection" name="search" /></label>
|
||||
<section id="series-list" class="media-wrap">
|
||||
</section>
|
||||
</section>
|
||||
<br />
|
||||
<table class="invisible form">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="align-right"><label for="media_id">Media</label></td>
|
||||
<td class='align-left'>
|
||||
<?php include 'media-select-list.php' ?>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for="notes">Notes</label></td>
|
||||
<td><textarea id="notes" name="notes"></textarea></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td> </td>
|
||||
<td>
|
||||
<button type="submit">Save</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</form>
|
||||
</main>
|
||||
<?php endif ?>
|
28
app/views/collection/cover-item.php
Normal file
28
app/views/collection/cover-item.php
Normal file
@ -0,0 +1,28 @@
|
||||
<article class="media" id="a-<?= $item['hummingbird_id'] ?>">
|
||||
<?= $helper->picture("images/anime/{$item['hummingbird_id']}.webp") ?>
|
||||
<div class="name">
|
||||
<a href="<?= $url->generate('anime.details', ['id' => $item['slug']]) ?>">
|
||||
<?= $item['title'] ?>
|
||||
<?= ($item['alternate_title'] != "") ? "<small><br />{$item['alternate_title']}</small>" : ""; ?>
|
||||
</a>
|
||||
</div>
|
||||
<div class="table">
|
||||
<?php if ($auth->isAuthenticated()): ?>
|
||||
<div class="row">
|
||||
<span class="edit">
|
||||
<a class="bracketed"
|
||||
href="<?= $url->generate($collection_type . '.collection.edit.get', [
|
||||
'id' => $item['hummingbird_id']
|
||||
]) ?>">Edit</a>
|
||||
</span>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
<div class="row">
|
||||
<?php if ($item['episode_count'] > 1): ?>
|
||||
<div class="completion">Episodes: <?= $item['episode_count'] ?></div>
|
||||
<?php endif ?>
|
||||
<div class="media_type"><?= $item['show_type'] ?></div>
|
||||
<div class="age-rating"><?= $item['age_rating'] ?></div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
28
app/views/collection/cover.php
Normal file
28
app/views/collection/cover.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php use function Aviat\AnimeClient\renderTemplate; ?>
|
||||
<main class="media-list">
|
||||
<?php if ($auth->isAuthenticated()): ?>
|
||||
<a class="bracketed" href="<?= $url->generate($collection_type . '.collection.add.get') ?>">Add Item</a>
|
||||
<?php endif ?>
|
||||
<?php if (empty($sections)): ?>
|
||||
<h3>There's nothing here!</h3>
|
||||
<?php else: ?>
|
||||
<br />
|
||||
<label>Filter: <input type='text' class='media-filter' /></label>
|
||||
<br />
|
||||
<?= $component->tabs('collection-tab', $sections, static function ($items) use ($auth, $collection_type, $helper, $url, $component) {
|
||||
$rendered = [];
|
||||
foreach ($items as $item)
|
||||
{
|
||||
$rendered[] = renderTemplate(__DIR__ . '/cover-item.php', [
|
||||
'auth' => $auth,
|
||||
'collection_type' => $collection_type,
|
||||
'helper' => $helper,
|
||||
'item' => $item,
|
||||
'url' => $url,
|
||||
]);
|
||||
}
|
||||
|
||||
return implode('', array_map('mb_trim', $rendered));
|
||||
}, 'media-wrap', true) ?>
|
||||
<?php endif ?>
|
||||
</main>
|
66
app/views/collection/edit.php
Normal file
66
app/views/collection/edit.php
Normal file
@ -0,0 +1,66 @@
|
||||
<?php use function Aviat\AnimeClient\renderTemplate ?>
|
||||
<?php if ($auth->isAuthenticated()): ?>
|
||||
<main>
|
||||
<h2>Edit Anime Collection Item</h2>
|
||||
<form action="<?= $action_url ?>" method="post">
|
||||
<table class="invisible form">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td rowspan="6" class="align-center">
|
||||
<?= $helper->picture("images/anime/{$item['hummingbird_id']}-original.webp", "jpg", [], ["width" => "390"]) ?>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="align-right"><label for="title">Title</label></td>
|
||||
<td class="align-left">
|
||||
<input type="text" id="title" name="title" value="<?= $item['title'] ?>" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="align-right"><label for="alternate_title">Alternate Title</label></td>
|
||||
<td class="align-left">
|
||||
<input type="text" id="alternate_title" 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">
|
||||
<?php include 'media-select-list.php' ?>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for="notes">Notes</label></td>
|
||||
<td><textarea id="notes" name="notes"><?= $escape->html($item['notes']) ?></textarea></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td> </td>
|
||||
<td>
|
||||
<?php if($action === 'Edit'): ?>
|
||||
<input type="hidden" name="hummingbird_id" value="<?= $item['hummingbird_id'] ?>" />
|
||||
<?php endif ?>
|
||||
<button type="submit">Save</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</form>
|
||||
<form class="js-delete" action="<?= $url->generate($collection_type . '.collection.delete') ?>" method="post">
|
||||
<fieldset>
|
||||
<legend>Danger Zone</legend>
|
||||
<table class="form invisible">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="danger">
|
||||
<strong>Permanently</strong> remove this list item and <strong>all</strong> its data?
|
||||
</td>
|
||||
<td>
|
||||
<input type="hidden" value="<?= $item['hummingbird_id'] ?>" name="hummingbird_id" />
|
||||
<button type="submit" class="danger">Delete Entry</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</fieldset>
|
||||
</form>
|
||||
</main>
|
||||
<?php endif ?>
|
23
app/views/collection/list-item.php
Normal file
23
app/views/collection/list-item.php
Normal file
@ -0,0 +1,23 @@
|
||||
<tr>
|
||||
<?php if ($auth->isAuthenticated()): ?>
|
||||
<td>
|
||||
<a class="bracketed"
|
||||
href="<?= $url->generate($collection_type . '.collection.edit.get', ['id' => $item['hummingbird_id']]) ?>">Edit</a>
|
||||
</td>
|
||||
<?php endif ?>
|
||||
<td class="align-left">
|
||||
<a href="<?= $url->generate('anime.details', ['id' => $item['slug']]) ?>">
|
||||
<?= $item['title'] ?>
|
||||
</a>
|
||||
<?= ! empty($item['alternate_title']) ? ' <br /><small> ' . $item['alternate_title'] . '</small>' : '' ?>
|
||||
</td>
|
||||
<?php if ($hasMedia): ?>
|
||||
<td><?= implode(', ', $item['media']) ?></td>
|
||||
<?php endif ?>
|
||||
<td><?= ($item['episode_count'] > 1) ? $item['episode_count'] : '-' ?></td>
|
||||
<td><?= $item['episode_length'] ?></td>
|
||||
<td><?= $item['show_type'] ?></td>
|
||||
<td><?= $item['age_rating'] ?></td>
|
||||
<?php if ($hasNotes): ?><td class="align-left"><?= nl2br($item['notes'], TRUE) ?></td><?php endif ?>
|
||||
<td class="align-left"><?= implode(', ', $item['genres']) ?></td>
|
||||
</tr>
|
56
app/views/collection/list.php
Normal file
56
app/views/collection/list.php
Normal file
@ -0,0 +1,56 @@
|
||||
<?php use function Aviat\AnimeClient\{colNotEmpty, renderTemplate}; ?>
|
||||
<main>
|
||||
<?php if ($auth->isAuthenticated()): ?>
|
||||
<a class="bracketed" href="<?= $url->generate($collection_type . '.collection.add.get') ?>">Add Item</a>
|
||||
<?php endif ?>
|
||||
<?php if (empty($sections)): ?>
|
||||
<h3>There's nothing here!</h3>
|
||||
<?php else: ?>
|
||||
<br />
|
||||
<label>Filter: <input type='text' class='media-filter' /></label>
|
||||
<br />
|
||||
<?= $component->tabs('collection-tab', $sections, static function ($items, $section) use ($auth, $helper, $url, $collection_type) {
|
||||
$hasNotes = colNotEmpty($items, 'notes');
|
||||
$hasMedia = $section === 'All';
|
||||
$firstTh = ($auth->isAuthenticated()) ? '<td> </td>' : '';
|
||||
$mediaTh = ($hasMedia) ? '<th>Media</th>' : '';
|
||||
$noteTh = ($hasNotes) ? '<th>Notes</th>' : '';
|
||||
|
||||
$rendered = [];
|
||||
foreach ($items as $item)
|
||||
{
|
||||
$rendered[] = renderTemplate(__DIR__ . '/list-item.php', [
|
||||
'auth' => $auth,
|
||||
'collection_type' => $collection_type,
|
||||
'hasMedia' => $hasMedia,
|
||||
'hasNotes' => $hasNotes,
|
||||
'helper' => $helper,
|
||||
'item' => $item,
|
||||
'url' => $url,
|
||||
]);
|
||||
}
|
||||
$rows = implode('', array_map('mb_trim', $rendered));
|
||||
|
||||
return <<<HTML
|
||||
<table class="full-width media-wrap">
|
||||
<thead>
|
||||
<tr>
|
||||
{$firstTh}
|
||||
<th>Title</th>
|
||||
{$mediaTh}
|
||||
<th class='numeric'>Episode Count</th>
|
||||
<th class='numeric'>Episode Length</th>
|
||||
<th>Show Type</th>
|
||||
<th class='rating'>Age Rating</th>
|
||||
{$noteTh}
|
||||
<th>Genres</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>{$rows}</tbody>
|
||||
</table>
|
||||
HTML;
|
||||
|
||||
}) ?>
|
||||
<?php endif ?>
|
||||
</main>
|
||||
<script defer="defer" src="<?= $urlGenerator->assetUrl('js/tables.min.js') ?>"></script>
|
11
app/views/collection/media-select-list.php
Normal file
11
app/views/collection/media-select-list.php
Normal file
@ -0,0 +1,11 @@
|
||||
<select name="media_id[]" id="media_id" multiple size="13">
|
||||
<?php foreach ($media_items as $group => $items): ?>
|
||||
<optgroup label='<?= $group ?>'>
|
||||
<?php foreach ($items as $id => $name): ?>
|
||||
<option <?= in_array($id, ($item['media_id'] ?? []), FALSE) ? 'selected="selected"' : '' ?> value="<?= $id ?>">
|
||||
<?= $name ?>
|
||||
</option>
|
||||
<?php endforeach ?>
|
||||
</optgroup>
|
||||
<?php endforeach ?>
|
||||
</select>
|
5
app/views/error.php
Normal file
5
app/views/error.php
Normal file
@ -0,0 +1,5 @@
|
||||
<main>
|
||||
<h1><?= $title ?></h1>
|
||||
<h2><?= $message ?></h2>
|
||||
<div><?= $long_message ?></div>
|
||||
</main>
|
@ -1,2 +1,16 @@
|
||||
<section id="loading-shadow" hidden="hidden">
|
||||
<div class="loading-wrapper">
|
||||
<div class="loading-content">
|
||||
<h3>Updating List Item...</h3>
|
||||
<div class="cssload-loader">
|
||||
<div class="cssload-inner cssload-one"></div>
|
||||
<div class="cssload-inner cssload-two"></div>
|
||||
<div class="cssload-inner cssload-three"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<script nomodule="nomodule" src="https://polyfill.io/v3/polyfill.min.js?features=es5%2CObject.assign"></script>
|
||||
<script async="async" defer="defer" src="<?= $urlGenerator->assetUrl('js/scripts.min.js') ?>"></script>
|
||||
</body>
|
||||
</html>
|
@ -1,35 +1,42 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<title><?= $title ?></title>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="stylesheet" href="<?= asset_url('css.php?g=base') ?>" />
|
||||
<script>
|
||||
var BASE_URL = "<?= base_url($url_type) ?>";
|
||||
var CONTROLLER = "<?= $url_type ?>";
|
||||
</script>
|
||||
<meta http-equiv="cache-control" content="no-store" />
|
||||
<meta http-equiv="Content-Security-Policy" content="script-src 'self'" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1, user-scalable=1" />
|
||||
<link rel="stylesheet" href="<?= $urlGenerator->assetUrl('css/' . $config->get('theme') . '.min.css') ?>" />
|
||||
<link rel="<?= $config->get('theme') === 'dark' ? '' : 'alternate ' ?>stylesheet" title="Dark Theme" href="<?= $urlGenerator->assetUrl('css/dark.min.css') ?>" />
|
||||
<link rel="icon" href="<?= $urlGenerator->assetUrl('images/icons/favicon.ico') ?>" />
|
||||
<link rel="apple-touch-icon" sizes="57x57" href="<?= $urlGenerator->assetUrl('images/icons/apple-icon-57x57.png') ?>">
|
||||
<link rel="apple-touch-icon" sizes="60x60" href="<?= $urlGenerator->assetUrl('images/icons/apple-icon-60x60.png') ?>">
|
||||
<link rel="apple-touch-icon" sizes="72x72" href="<?= $urlGenerator->assetUrl('images/icons/apple-icon-72x72.png') ?>">
|
||||
<link rel="apple-touch-icon" sizes="76x76" href="<?= $urlGenerator->assetUrl('images/icons/apple-icon-76x76.png') ?>">
|
||||
<link rel="apple-touch-icon" sizes="114x114" href="<?= $urlGenerator->assetUrl('images/icons/apple-icon-114x114.png') ?>">
|
||||
<link rel="apple-touch-icon" sizes="120x120" href="<?= $urlGenerator->assetUrl('images/icons/apple-icon-120x120.png') ?>">
|
||||
<link rel="apple-touch-icon" sizes="144x144" href="<?= $urlGenerator->assetUrl('images/icons/apple-icon-144x144.png') ?>">
|
||||
<link rel="apple-touch-icon" sizes="152x152" href="<?= $urlGenerator->assetUrl('images/icons/apple-icon-152x152.png') ?>">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="<?= $urlGenerator->assetUrl('images/icons/apple-icon-180x180.png') ?>">
|
||||
<link rel="icon" type="image/png" sizes="192x192" href="<?= $urlGenerator->assetUrl('images/icons/android-icon-192x192.png') ?>">
|
||||
<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') ?>">
|
||||
|
||||
</head>
|
||||
<body class="<?= $url_type ?> list">
|
||||
<h1 class="flex flex-align-end flex-wrap">
|
||||
<span class="flex-no-wrap grow-1"><?= WHOSE ?> <?= ucfirst($url_type) ?> <?= (strpos($route_path, 'collection') !== FALSE) ? 'Collection' : 'List' ?> [<a href="<?= full_url("", $other_type) ?>"><?= ucfirst($other_type) ?> List</a>]</span>
|
||||
<span class="flex-no-wrap small-font">
|
||||
<?php if (is_logged_in()): ?>
|
||||
[<a href="<?= full_url("/logout", $url_type) ?>">Logout</a>]
|
||||
<?php else: ?>
|
||||
[<a href="<?= full_url("/login", $url_type) ?>"><?= WHOSE ?> Login</a>]
|
||||
<?php endif ?>
|
||||
</span>
|
||||
</h1>
|
||||
<nav>
|
||||
<ul>
|
||||
<?php foreach($nav_routes as $title => $nav_path): ?>
|
||||
<li class="<?= is_selected($nav_path, $route_path) ?>"><a href="<?= full_url($nav_path, $url_type) ?>"><?= $title ?></a></li>
|
||||
<?php endforeach ?>
|
||||
</ul>
|
||||
<br />
|
||||
<ul>
|
||||
<li class="<?= is_not_selected('list', last_segment()) ?>"><a href="<?= full_url($route_path, $url_type) ?>">Cover View</a></li>
|
||||
<li class="<?= is_selected('list', last_segment()) ?>"><a href="<?= full_url("{$route_path}/list", $url_type) ?>">List View</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<br />
|
||||
<body class="<?= $escape->attr($url_type) ?> list">
|
||||
<?php include 'setup-check.php' ?>
|
||||
<header>
|
||||
<?php
|
||||
include 'main-menu.php';
|
||||
if(isset($message) && is_array($message))
|
||||
{
|
||||
foreach($message as $m)
|
||||
{
|
||||
$message = $m['message'];
|
||||
$message_type = $m['message_type'];
|
||||
include 'message.php';
|
||||
}
|
||||
}
|
||||
?>
|
||||
</header>
|
47
app/views/history.php
Normal file
47
app/views/history.php
Normal file
@ -0,0 +1,47 @@
|
||||
<main class="details fixed">
|
||||
<?php if (empty($items)): ?>
|
||||
<h3>No recent history.</h3>
|
||||
<?php else: ?>
|
||||
<section>
|
||||
<?php foreach ($items as $name => $item): ?>
|
||||
<article class="flex flex-no-wrap flex-justify-start">
|
||||
<section class="flex-self-center history-img">
|
||||
<a href="<?= $item['url'] ?>">
|
||||
<?= $helper->img(
|
||||
$item['coverImg'],
|
||||
['width' => '110px', 'height' => '156px'],
|
||||
) ?>
|
||||
</a>
|
||||
</section>
|
||||
<section class="flex-self-center">
|
||||
<?= $helper->a($item['url'], $item['title']) ?>
|
||||
<br />
|
||||
<br />
|
||||
<?= $item['action'] ?>
|
||||
<br />
|
||||
<small>
|
||||
<?php if ( ! empty($item['dateRange'])):
|
||||
[$startDate, $endDate] = array_map(
|
||||
fn ($date) => $date->format('l, F d'),
|
||||
$item['dateRange']
|
||||
);
|
||||
[$startTime, $endTime] = array_map(
|
||||
fn ($date) => $date->format('h:i:s A'),
|
||||
$item['dateRange']
|
||||
);
|
||||
?>
|
||||
<?php if ($startDate === $endDate): ?>
|
||||
<?= "{$startDate}, {$startTime} – {$endTime}" ?>
|
||||
<?php else: ?>
|
||||
<?= "{$startDate} {$startTime} – {$endDate} {$endTime}" ?>
|
||||
<?php endif ?>
|
||||
<?php else: ?>
|
||||
<?= $item['updated']->format('l, F d h:i:s A') ?>
|
||||
<?php endif ?>
|
||||
</small>
|
||||
</section>
|
||||
</article>
|
||||
<?php endforeach ?>
|
||||
</section>
|
||||
<?php endif ?>
|
||||
</main>
|
6
app/views/js-warning.php
Normal file
6
app/views/js-warning.php
Normal file
@ -0,0 +1,6 @@
|
||||
<noscript>
|
||||
<div class="message error">
|
||||
<span class="icon"></span>
|
||||
This feature requires Javascript to function :(
|
||||
</div>
|
||||
</noscript>
|
@ -1,17 +1,16 @@
|
||||
<main>
|
||||
<h2><?= $config->get('whose_list'); ?>'s Login</h2>
|
||||
<?= $message ?>
|
||||
<aside>
|
||||
<form method="post" action="<?= full_url('/login', $url_type) ?>">
|
||||
<dl>
|
||||
<dt><label for="username">Username: </label></dt>
|
||||
<dd><input type="text" id="username" name="username" required="required" /></dd>
|
||||
|
||||
<dt><label for="password">Password: </label></dt>
|
||||
<dd><input type="password" id="password" name="password" required="required" /></dd>
|
||||
|
||||
<dt> </dt>
|
||||
<dd><input type="submit" value="Login" /></dd>
|
||||
</dl>
|
||||
</form>
|
||||
</aside>
|
||||
<form method="post" action="<?= $url->generate('login.post') ?>">
|
||||
<table class="form invisible">
|
||||
<tr>
|
||||
<td><label for="password">Password: </label></td>
|
||||
<td><input type="password" id="password" name="password" required="required" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td> </td>
|
||||
<td><button type="submit">Login</button></td>
|
||||
</tr>
|
||||
</table>
|
||||
</form>
|
||||
</main>
|
114
app/views/main-menu.php
Normal file
114
app/views/main-menu.php
Normal file
@ -0,0 +1,114 @@
|
||||
<?php declare(strict_types=1);
|
||||
/**
|
||||
* Hummingbird Anime List Client
|
||||
*
|
||||
* An API client for Kitsu to manage anime and manga watch lists
|
||||
*
|
||||
* PHP version 8.1
|
||||
*
|
||||
* @copyright 2015 - 2023 Timothy J. Warren <tim@timshome.page>
|
||||
* @license http://www.opensource.org/licenses/mit-license.html MIT License
|
||||
* @version 5.2
|
||||
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
|
||||
*/
|
||||
|
||||
namespace Aviat\AnimeClient;
|
||||
|
||||
$whose = $config->get('whose_list') . "'s ";
|
||||
$lastSegment = $urlGenerator->lastSegment();
|
||||
$extraSegment = $lastSegment === 'list' ? '/list' : '';
|
||||
$hasAnime = str_contains($GLOBALS['_SERVER']['REQUEST_URI'], 'anime');
|
||||
$hasManga = str_contains($GLOBALS['_SERVER']['REQUEST_URI'], 'manga');
|
||||
|
||||
?>
|
||||
<div id="main-nav" class="flex flex-align-end flex-wrap">
|
||||
<span class="flex-no-wrap grow-1">
|
||||
<?php if( ! str_contains($route_path, 'collection')): ?>
|
||||
<?= $helper->a(
|
||||
$urlGenerator->defaultUrl($url_type),
|
||||
$whose . ucfirst($url_type) . ' List',
|
||||
['aria-current'=> 'page']
|
||||
) ?>
|
||||
<?php if($config->get("show_{$url_type}_collection")): ?>
|
||||
[<?= $helper->a(
|
||||
$url->generate("{$url_type}.collection.view") . $extraSegment,
|
||||
ucfirst($url_type) . ' Collection'
|
||||
) ?>]
|
||||
<?php endif ?>
|
||||
<?php if($config->get("show_{$other_type}_collection")): ?>
|
||||
[<?= $helper->a(
|
||||
$url->generate("{$other_type}.collection.view") . $extraSegment,
|
||||
ucfirst($other_type) . ' Collection'
|
||||
) ?>]
|
||||
<?php endif ?>
|
||||
[<?= $helper->a(
|
||||
$urlGenerator->defaultUrl($other_type) . $extraSegment,
|
||||
ucfirst($other_type) . ' List'
|
||||
) ?>]
|
||||
<?php else: ?>
|
||||
<?= $helper->a(
|
||||
$url->generate("{$url_type}.collection.view") . $extraSegment,
|
||||
$whose . ucfirst($url_type) . ' Collection',
|
||||
['aria-current'=> 'page']
|
||||
) ?>
|
||||
<?php if($config->get("show_{$other_type}_collection")): ?>
|
||||
[<?= $helper->a(
|
||||
$url->generate("{$other_type}.collection.view") . $extraSegment,
|
||||
ucfirst($other_type) . ' Collection'
|
||||
) ?>]
|
||||
<?php endif ?>
|
||||
[<?= $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('default_user_info'),
|
||||
'About '. $config->get('whose_list')
|
||||
) ?>]</span>
|
||||
|
||||
<?php if ($auth->isAuthenticated()): ?>
|
||||
<span class="flex-no-wrap small-font">
|
||||
<?= $helper->a(
|
||||
$url->generate('settings'),
|
||||
'Settings',
|
||||
['class' => 'bracketed']
|
||||
) ?>
|
||||
</span>
|
||||
<span class="flex-no-wrap small-font">
|
||||
<?= $helper->a(
|
||||
$url->generate('logout'),
|
||||
'Logout',
|
||||
['class' => 'bracketed']
|
||||
) ?>
|
||||
</span>
|
||||
<?php else: ?>
|
||||
<span class="flex-no-wrap small-font">
|
||||
[<?= $helper->a($url->generate('login'), "{$whose} Login") ?>]
|
||||
</span>
|
||||
<?php endif ?>
|
||||
</div>
|
||||
<?php if ($container->get('util')->isViewPage() && ($hasAnime || $hasManga)): ?>
|
||||
<nav>
|
||||
<?= $helper->menu($menu_name) ?>
|
||||
<?php if (stripos($GLOBALS['_SERVER']['REQUEST_URI'], 'history') === FALSE): ?>
|
||||
<br />
|
||||
<ul>
|
||||
<?php $currentView = Util::eq('list', $lastSegment) ? 'list' : 'cover' ?>
|
||||
<li class="<?= Util::isNotSelected('list', $lastSegment) ?>">
|
||||
<a aria-current="<?= Util::ariaCurrent($currentView === 'cover') ?>"
|
||||
href="<?= $urlGenerator->url($route_path) ?>">Cover View</a>
|
||||
</li>
|
||||
<li class="<?= Util::isSelected('list', $lastSegment) ?>">
|
||||
<a aria-current="<?= Util::ariaCurrent($currentView === 'list') ?>"
|
||||
href="<?= $urlGenerator->url("{$route_path}/list") ?>">List View</a>
|
||||
</li>
|
||||
</ul>
|
||||
<?php endif ?>
|
||||
</nav>
|
||||
<?php endif ?>
|
40
app/views/manga/add.php
Normal file
40
app/views/manga/add.php
Normal file
@ -0,0 +1,40 @@
|
||||
<?php if ($auth->isAuthenticated()): ?>
|
||||
<main>
|
||||
<h2>Add Manga to your List</h2>
|
||||
<form action="<?= $action_url ?>" method="post">
|
||||
<?php include realpath(__DIR__ . '/../js-warning.php') ?>
|
||||
<section>
|
||||
<div class="cssload-loader" hidden="hidden">
|
||||
<div class="cssload-inner cssload-one"></div>
|
||||
<div class="cssload-inner cssload-two"></div>
|
||||
<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>
|
||||
</section>
|
||||
<br />
|
||||
<table class="invisible form">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><label for="status">Reading Status</label></td>
|
||||
<td>
|
||||
<select name="status" id="status">
|
||||
<?php foreach($status_list as $status_key => $status_title): ?>
|
||||
<option value="<?= $status_key ?>"><?= $status_title ?></option>
|
||||
<?php endforeach ?>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td> </td>
|
||||
<td>
|
||||
<input type="hidden" name="type" value="manga" />
|
||||
<button type="submit">Save</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</form>
|
||||
</main>
|
||||
<?php endif ?>
|
@ -1,49 +1,29 @@
|
||||
<main>
|
||||
<main class="media-list">
|
||||
<?php if ($auth->isAuthenticated()): ?>
|
||||
<a class="bracketed" href="<?= $url->generate('manga.add.get') ?>">Add Item</a>
|
||||
<?php endif ?>
|
||||
<?php if (empty($sections)): ?>
|
||||
<h3>There's nothing here!</h3>
|
||||
<?php else: ?>
|
||||
<br />
|
||||
<label>Filter: <input type='text' class='media-filter' /></label>
|
||||
<br />
|
||||
<?php foreach ($sections as $name => $items): ?>
|
||||
<?php if (empty($items)): ?>
|
||||
<section class="status">
|
||||
<h2><?= $name ?></h2>
|
||||
<h2><?= $escape->html($name) ?></h2>
|
||||
<h3>There's nothing here!</h3>
|
||||
</section>
|
||||
<?php else: ?>
|
||||
<section class="status">
|
||||
<h2><?= $escape->html($name) ?></h2>
|
||||
<section class="media-wrap">
|
||||
<?php foreach($items as $item): ?>
|
||||
<article class="media" id="manga-<?= $item['id'] ?>">
|
||||
<?php if (is_logged_in()): ?>
|
||||
<div class="edit_buttons" hidden>
|
||||
<button class="plus_one_chapter">+1 Chapter</button>
|
||||
<button class="plus_one_volume">+1 Volume</button>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
<img src="<?= $item['manga']['poster_image'] ?>" />
|
||||
<div class="name">
|
||||
<a href="https://hummingbird.me/manga/<?= $item['manga_id'] ?>">
|
||||
<?= $item['manga']['romaji_title'] ?>
|
||||
<?= (isset($item['manga']['english_title'])) ? "<br />({$item['manga']['english_title']})" : ""; ?>
|
||||
</a>
|
||||
</div>
|
||||
<div class="table">
|
||||
<div class="row">
|
||||
<div class="user_rating">Rating: <?= ($item['rating'] > 0) ? (int)($item['rating'] * 2) : '-' ?> / 10</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="chapter_completion">
|
||||
Chapters: <span class="chapters_read"><?= $item['chapters_read'] ?></span> /
|
||||
<span class="chapter_count"><?= ($item['manga']['chapter_count'] > 0) ? $item['manga']['chapter_count'] : "-" ?></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="volume_completion">
|
||||
Volumes: <span class="volumes_read"><?= $item['volumes_read'] ?></span> /
|
||||
<span class="volume_count"><?= ($item['manga']['volume_count'] > 0) ? $item['manga']['volume_count'] : "-" ?></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php /*<div class="medium_metadata">
|
||||
<div class="media_type"><?= $item['manga']['manga_type'] ?></div>
|
||||
</div> */ ?>
|
||||
</article>
|
||||
<?= $component->mangaCover($item, $name) ?>
|
||||
<?php endforeach ?>
|
||||
</section>
|
||||
</section>
|
||||
<?php endif ?>
|
||||
<?php endforeach ?>
|
||||
</main>
|
||||
<?php if (is_logged_in()): ?>
|
||||
<script src="<?= asset_url('js.php?g=edit') ?>"></script>
|
||||
<?php endif ?>
|
||||
</main>
|
||||
|
105
app/views/manga/details.php
Normal file
105
app/views/manga/details.php
Normal file
@ -0,0 +1,105 @@
|
||||
<main class="details fixed">
|
||||
<section class="flex flex-no-wrap">
|
||||
<aside class="info">
|
||||
<?= $helper->img($data['cover_image'], ['class' => 'cover', 'width' => '350']) ?>
|
||||
|
||||
<br />
|
||||
|
||||
<table class="media-details">
|
||||
<tr>
|
||||
<td class="align-right">Publishing Status</td>
|
||||
<td><?= $data['status'] ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Manga Type</td>
|
||||
<td><?= ucfirst(strtolower($data['manga_type'])) ?></td>
|
||||
</tr>
|
||||
<?php if ( ! empty($data['volume_count'])): ?>
|
||||
<tr>
|
||||
<td>Volume Count</td>
|
||||
<td><?= $data['volume_count'] ?></td>
|
||||
</tr>
|
||||
<?php endif ?>
|
||||
<?php if ( ! empty($data['chapter_count'])): ?>
|
||||
<tr>
|
||||
<td>Chapter Count</td>
|
||||
<td><?= $data['chapter_count'] ?></td>
|
||||
</tr>
|
||||
<?php endif ?>
|
||||
|
||||
<?php if ( ! empty($data['age_rating'])): ?>
|
||||
<tr>
|
||||
<td>Age Rating</td>
|
||||
<td><abbr title="<?= $data['age_rating_guide'] ?>"><?= $data['age_rating'] ?></abbr>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endif ?>
|
||||
|
||||
<?php if (count($data['links']) > 0): ?>
|
||||
<tr>
|
||||
<td>External Links</td>
|
||||
<td>
|
||||
<?php foreach ($data['links'] as $urlName => $externalUrl): ?>
|
||||
<a rel='external' href="<?= $externalUrl ?>"><?= $urlName ?></a><br />
|
||||
<?php endforeach ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endif ?>
|
||||
|
||||
<tr>
|
||||
<td>Genres</td>
|
||||
<td>
|
||||
<?= implode(', ', $data['genres']); ?>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<br />
|
||||
</aside>
|
||||
<article class="text">
|
||||
<h2 class="toph"><?= $data['title'] ?></h2>
|
||||
<?php foreach ($data['titles_more'] as $title): ?>
|
||||
<h3><?= $title ?></h3>
|
||||
<?php endforeach ?>
|
||||
|
||||
<br />
|
||||
<div class="description">
|
||||
<p><?= str_replace("\n", '</p><p>', $data['synopsis']) ?></p>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<?php if (count($data['characters']) > 0): ?>
|
||||
<h2>Characters</h2>
|
||||
|
||||
<?= $component->tabs('manga-characters', $data['characters'], static function($list, $role) use ($component, $helper, $url) {
|
||||
$rendered = [];
|
||||
foreach ($list as $id => $char)
|
||||
{
|
||||
$rendered[] = $component->character(
|
||||
$char['name'],
|
||||
$url->generate('character', ['slug' => $char['slug']]),
|
||||
$helper->img($char['image'], ['loading' => 'lazy']),
|
||||
($role !== 'main') ? 'small-character' : 'character'
|
||||
);
|
||||
}
|
||||
|
||||
return implode('', array_map('mb_trim', $rendered));
|
||||
}) ?>
|
||||
<?php endif ?>
|
||||
|
||||
<?php if (count($data['staff']) > 0): ?>
|
||||
<h2>Staff</h2>
|
||||
|
||||
<?= $component->verticalTabs('manga-staff', $data['staff'],
|
||||
fn($people) => implode('', array_map(
|
||||
fn ($person) => $component->character(
|
||||
$person['name'],
|
||||
$url->generate('person', ['slug' => $person['slug']]),
|
||||
$helper->img($person['image']),
|
||||
),
|
||||
$people
|
||||
))
|
||||
) ?>
|
||||
<?php endif ?>
|
||||
</main>
|
104
app/views/manga/edit.php
Normal file
104
app/views/manga/edit.php
Normal file
@ -0,0 +1,104 @@
|
||||
<?php if ($auth->isAuthenticated()): ?>
|
||||
<main>
|
||||
<h2>
|
||||
Edit Manga List Item
|
||||
</h2>
|
||||
<form action="<?= $action ?>" method="post">
|
||||
<table class="invisible form">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<h3><?= $escape->html($item['manga']['title']) ?></h3>
|
||||
<?php foreach ($item['manga']['titles'] as $title): ?>
|
||||
<h4><?= $escape->html($title) ?></h4>
|
||||
<?php endforeach ?>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td rowspan="9">
|
||||
<?= $helper->img($item['manga']['image']) ?>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for="status">Reading Status</label></td>
|
||||
<td>
|
||||
<select name="status" id="status">
|
||||
<?php foreach ($status_list as $val => $status): ?>
|
||||
<option <?php if ($item['reading_status'] === $val): ?>selected="selected"<?php endif ?>
|
||||
value="<?= $val ?>"><?= $status ?></option>
|
||||
<?php endforeach ?>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for="series_rating">Rating</label></td>
|
||||
<td>
|
||||
<input type="number" min="0" max="10" maxlength="2" name="new_rating"
|
||||
value="<?= $item['user_rating'] ?>" id="series_rating" size="2"/> / 10
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for="chapters_read">Chapters Read</label></td>
|
||||
<td>
|
||||
<input type="number" min="0" name="chapters_read" id="chapters_read"
|
||||
value="<?= $item['chapters']['read'] ?>"/> / <?= $item['chapters']['total'] ?>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for="rereading_flag">Rereading?</label></td>
|
||||
<td>
|
||||
<input type="checkbox" name="rereading" id="rereading_flag"
|
||||
<?php if ($item['rereading'] === TRUE): ?>checked="checked"<?php endif ?>
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for="reread_count">Reread Count</label></td>
|
||||
<td>
|
||||
<input type="number" min="0" id="reread_count" name="reread_count"
|
||||
value="<?= $item['reread'] ?>"/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for="notes">Notes</label></td>
|
||||
<td>
|
||||
<textarea name="notes" id="notes"><?= $escape->html($item['notes']) ?></textarea>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td> </td>
|
||||
<td>
|
||||
<input type="hidden" value="<?= $item['id'] ?>" name="id"/>
|
||||
<input type="hidden" value="<?= $item['mal_id'] ?>" name="mal_id"/>
|
||||
<input type="hidden" value="<?= $item['manga']['slug'] ?>" name="manga_id"/>
|
||||
<input type="hidden" value="<?= $item['user_rating'] ?>" name="old_rating"/>
|
||||
<input type="hidden" value="true" name="edit"/>
|
||||
<button type="submit">Submit</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</form>
|
||||
<fieldset>
|
||||
<legend>Danger Zone</legend>
|
||||
<form class="js-delete" action="<?= $url->generate('manga.delete') ?>" method="post">
|
||||
<table class="form invisible">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="danger">
|
||||
<strong>Permanently</strong> remove this list item and <strong>all</strong> its data?
|
||||
</td>
|
||||
<td>
|
||||
<input type="hidden" value="<?= $item['id'] ?>" name="id"/>
|
||||
<input type="hidden" value="<?= $item['mal_id'] ?>" name="mal_id"/>
|
||||
<button type="submit" class="danger">Delete Entry</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</form>
|
||||
</fieldset>
|
||||
</main>
|
||||
<?php endif ?>
|
@ -1,33 +1,78 @@
|
||||
<main>
|
||||
<main class="media-list">
|
||||
<?php if ($auth->isAuthenticated()): ?>
|
||||
<a class="bracketed" href="<?= $url->generate('manga.add.get') ?>">Add Item</a>
|
||||
<?php endif ?>
|
||||
<?php if (empty($sections)): ?>
|
||||
<h3>There's nothing here!</h3>
|
||||
<?php else: ?>
|
||||
<br />
|
||||
<label>Filter: <input type='text' class='media-filter' /></label>
|
||||
<br />
|
||||
<?php foreach ($sections as $name => $items): ?>
|
||||
<h2><?= $name ?></h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th>Rating</th>
|
||||
<th>Chapters</th>
|
||||
<th>Volumes</th>
|
||||
<th>Type</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach($items as $item): ?>
|
||||
<tr id="manga-<?= $item['manga']['id'] ?>">
|
||||
<td class="align_left">
|
||||
<a href="https://hummingbird.me/manga/<?= $item['manga']['id'] ?>">
|
||||
<?= $item['manga']['romaji_title'] ?>
|
||||
</a>
|
||||
<?= (array_key_exists('english_title', $item['manga'])) ? " · " . $item['manga']['english_title'] : "" ?>
|
||||
</td>
|
||||
<td><?= ($item['rating'] > 0) ? (int)($item['rating'] * 2) : '-' ?> / 10</td>
|
||||
<td><?= $item['chapters_read'] ?> / <?= ($item['manga']['chapter_count'] > 0) ? $item['manga']['chapter_count'] : "-" ?></td>
|
||||
<td><?= $item['volumes_read'] ?> / <?= ($item['manga']['volume_count'] > 0) ? $item['manga']['volume_count'] : "-" ?></td>
|
||||
<td><?= $item['manga']['manga_type'] ?></td>
|
||||
</tr>
|
||||
<?php endforeach ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php if (empty($items)): ?>
|
||||
<h3>There's nothing here!</h3>
|
||||
<?php else: ?>
|
||||
<table class='media-wrap'>
|
||||
<thead>
|
||||
<tr>
|
||||
<?php if ($auth->isAuthenticated()): ?>
|
||||
<td> </td>
|
||||
<?php endif ?>
|
||||
<th>Title</th>
|
||||
<th class='numeric'>Score</th>
|
||||
<th class='numeric'>Completed Chapters</th>
|
||||
<th>Attributes</th>
|
||||
<th>Type</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach($items as $item): ?>
|
||||
<tr id="manga-<?= $item['id'] ?>">
|
||||
<?php if($auth->isAuthenticated()): ?>
|
||||
<td>
|
||||
<a class="bracketed" href="<?= $url->generate('edit', [
|
||||
'controller' => 'manga',
|
||||
'id' => $item['id'],
|
||||
'status' => $name
|
||||
]) ?>">Edit</a>
|
||||
</td>
|
||||
<?php endif ?>
|
||||
<td class="align-left">
|
||||
<a href="<?= $url->generate('manga.details', ['id' => $item['manga']['slug']]) ?>">
|
||||
<?= $item['manga']['title'] ?>
|
||||
</a>
|
||||
<?php foreach($item['manga']['titles'] as $title): ?>
|
||||
<br /><?= $title ?>
|
||||
<?php endforeach ?>
|
||||
</td>
|
||||
<td><?= $item['user_rating'] ?> / 10</td>
|
||||
<td><?= $item['chapters']['read'] ?> / <?= $item['chapters']['total'] ?></td>
|
||||
<td>
|
||||
<ul>
|
||||
<?php if ($item['reread'] == 1): ?>
|
||||
<li>Reread once</li>
|
||||
<?php elseif ($item['reread'] == 2): ?>
|
||||
<li>Reread twice</li>
|
||||
<?php elseif ($item['reread'] == 3): ?>
|
||||
<li>Reread thrice</li>
|
||||
<?php elseif ($item['reread'] > 3): ?>
|
||||
<li>Reread <?= $item['reread'] ?> times</li>
|
||||
<?php endif ?>
|
||||
<?php foreach(['rereading'] as $attr): ?>
|
||||
<?php if($item[$attr]): ?>
|
||||
<li><?= ucfirst($attr); ?></li>
|
||||
<?php endif ?>
|
||||
<?php endforeach ?>
|
||||
</ul>
|
||||
</td>
|
||||
<td><?= $item['manga']['type'] ?></td>
|
||||
</tr>
|
||||
<?php endforeach ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif ?>
|
||||
<?php endforeach ?>
|
||||
<?php endif ?>
|
||||
</main>
|
||||
<script src="<?= asset_url('js.php?g=table') ?>"></script>
|
||||
<script defer="defer" src="<?= $urlGenerator->assetUrl('js/tables.min.js') ?>"></script>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<div class="message <?= $stat_class ?>">
|
||||
<div class="message <?= $escape->attr($message_type) ?>">
|
||||
<span class="icon"></span>
|
||||
<?= $message ?>
|
||||
<span class="close" onclick="this.parentElement.style.display='none'">x</span>
|
||||
<?= $escape->html($message) ?>
|
||||
<span class="close"></span>
|
||||
</div>
|
104
app/views/person/details.php
Normal file
104
app/views/person/details.php
Normal file
@ -0,0 +1,104 @@
|
||||
<main class="details fixed">
|
||||
<section class="flex flex-no-wrap">
|
||||
<div>
|
||||
<?= $helper->img($data['image'], ['class' => 'cover' ]) ?>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="toph"><?= $data['name'] ?></h2>
|
||||
<?php foreach ($data['names'] as $name): ?>
|
||||
<h3><?= $name ?></h3>
|
||||
<?php endforeach ?>
|
||||
<?php if ( ! empty($data['birthday'])): ?>
|
||||
<h4><?= $data['birthday'] ?></h4>
|
||||
<?php endif ?>
|
||||
<br />
|
||||
<hr />
|
||||
<div class="description">
|
||||
<p><?= str_replace("\n", '</p><p>', $data['description']) ?></p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<?php if ( ! empty($data['staff'])): ?>
|
||||
<section>
|
||||
<h3>Castings</h3>
|
||||
|
||||
<div class="vertical-tabs">
|
||||
<?php $i = 0 ?>
|
||||
<?php foreach ($data['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 (isset($entries['manga'], $entries['anime'])): ?>
|
||||
<h4><?= ucfirst($type) ?></h4>
|
||||
<?php endif ?>
|
||||
<section class="content media-wrap flex flex-wrap flex-justify-start">
|
||||
<?php foreach ($casting as $sid => $series): ?>
|
||||
<?php $mediaType = in_array($type, ['anime', 'manga'], TRUE) ? $type : 'anime'; ?>
|
||||
<?= $component->media(
|
||||
$series['titles'],
|
||||
$url->generate("{$mediaType}.details", ['id' => $series['slug']]),
|
||||
$helper->img($series['image'], ['width' => 220, 'loading' => 'lazy'])
|
||||
) ?>
|
||||
<?php endforeach; ?>
|
||||
</section>
|
||||
<?php endforeach ?>
|
||||
</div>
|
||||
<?php $i++ ?>
|
||||
<?php endforeach ?>
|
||||
</div>
|
||||
</section>
|
||||
<?php endif ?>
|
||||
|
||||
<?php if ( ! empty($data['characters'])): ?>
|
||||
<section>
|
||||
<h3>Voice Acting Roles</h3>
|
||||
<?= $component->tabs('voice-acting-roles', $data['characters'], static function ($characterList) use ($component, $helper, $url) {
|
||||
$voiceRoles = [];
|
||||
foreach ($characterList as $cid => $item):
|
||||
$character = $component->character(
|
||||
$item['character']['canonicalName'],
|
||||
$url->generate('character', ['slug' => $item['character']['slug']]),
|
||||
$helper->img($item['character']['image'], ['loading' => 'lazy']),
|
||||
);
|
||||
$medias = [];
|
||||
foreach ($item['media'] as $sid => $series)
|
||||
{
|
||||
$medias[] = $component->media(
|
||||
$series['titles'],
|
||||
$url->generate('anime.details', ['id' => $series['slug']]),
|
||||
$helper->img($series['image'], ['width' => 220, 'loading' => 'lazy'])
|
||||
);
|
||||
}
|
||||
$media = implode('', array_map('mb_trim', $medias));
|
||||
|
||||
$voiceRoles[] = <<<HTML
|
||||
<tr>
|
||||
<td>{$character}</td>
|
||||
<td>
|
||||
<section class="align-left media-wrap">{$media}</section>
|
||||
</td>
|
||||
</tr>
|
||||
HTML;
|
||||
endforeach;
|
||||
|
||||
$roles = implode('', array_map('mb_trim', $voiceRoles));
|
||||
|
||||
return <<<HTML
|
||||
<table class="borderless max-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Character</th>
|
||||
<th>Series</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>{$roles}</tbody>
|
||||
</table>
|
||||
HTML;
|
||||
|
||||
}) ?>
|
||||
</section>
|
||||
<?php endif ?>
|
||||
</main>
|
24
app/views/settings/_anilist.php
Normal file
24
app/views/settings/_anilist.php
Normal file
@ -0,0 +1,24 @@
|
||||
<?php if ( ! $hasRequiredAnilistConfig): ?>
|
||||
<p class="static-message info">See the <a href="https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient/wiki/anilist">wiki</a> to learn how to set up Anilist integration. </p>
|
||||
<?php else: ?>
|
||||
<?php $auth = $anilistModel->checkAuth(); ?>
|
||||
<?php if (array_key_exists('errors', $auth)): ?>
|
||||
<p class="static-message error">Anilist API Client is Not Authorized.</p>
|
||||
<?= $helper->a(
|
||||
$url->generate('anilist-redirect'),
|
||||
'Link Anilist Account',
|
||||
['class' => 'bracketed user-btn']
|
||||
) ?>
|
||||
<?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>
|
||||
<?php require __DIR__ . '/_form.php' ?>
|
||||
<?= $helper->a(
|
||||
$url->generate('anilist-redirect'),
|
||||
'Update Access Token',
|
||||
['class' => 'bracketed user-btn']
|
||||
) ?>
|
||||
<?php endif ?>
|
||||
<?php endif ?>
|
5
app/views/settings/_field.php
Normal file
5
app/views/settings/_field.php
Normal file
@ -0,0 +1,5 @@
|
||||
<article>
|
||||
<label for="<?= $fieldName ?>"><?= $field['title'] ?></label><br />
|
||||
<small><?= $field['description'] ?></small><br />
|
||||
<?= $helper->field($fieldName, $field); ?>
|
||||
</article>
|
24
app/views/settings/_form.php
Normal file
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 '_subfield.php'; ?>
|
||||
</section>
|
||||
<?php elseif ( ! empty($field['display'])): ?>
|
||||
<?php include '_field.php' ?>
|
||||
<?php else: ?>
|
||||
<?php $hiddenFields[] = $helper->field($fieldName, $field); ?>
|
||||
<?php endif ?>
|
||||
<?php endforeach ?>
|
20
app/views/settings/_subfield.php
Normal file
20
app/views/settings/_subfield.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
// Higher scoped variables:
|
||||
// $field
|
||||
// $fields
|
||||
// $hiddenFields
|
||||
// $nestedPrefix
|
||||
?>
|
||||
|
||||
<?php foreach ($field['fields'] as $name => $field): ?>
|
||||
<?php
|
||||
$fieldName = ($section === 'config' || $nestedPrefix !== 'config')
|
||||
? "{$nestedPrefix}[{$name}]"
|
||||
: "{$nestedPrefix}[{$section}][{$name}]";
|
||||
?>
|
||||
<?php if ( ! empty($field['display'])): ?>
|
||||
<?php include '_field.php' ?>
|
||||
<?php else: ?>
|
||||
<?php $hiddenFields[] = $helper->field($fieldName, $field); ?>
|
||||
<?php endif ?>
|
||||
<?php endforeach ?>
|
46
app/views/settings/settings.php
Normal file
46
app/views/settings/settings.php
Normal file
@ -0,0 +1,46 @@
|
||||
<?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
|
||||
($section === 'anilist')
|
||||
? require __DIR__ . '/_anilist.php'
|
||||
: require __DIR__ . '/_form.php'
|
||||
?>
|
||||
</section>
|
||||
<?php $i++; ?>
|
||||
<?php endforeach ?>
|
||||
</div>
|
||||
<br />
|
||||
<?php foreach ($hiddenFields as $field): ?>
|
||||
<?= $field->__toString() ?>
|
||||
<?php endforeach ?>
|
||||
<button type="submit">Save Changes</button>
|
||||
</main>
|
||||
</form>
|
30
app/views/setup-check.php
Normal file
30
app/views/setup-check.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
use function Aviat\AnimeClient\checkFolderPermissions;
|
||||
|
||||
$setupErrors = 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 ?>
|
118
app/views/user/details.php
Normal file
118
app/views/user/details.php
Normal file
@ -0,0 +1,118 @@
|
||||
<?php
|
||||
use Aviat\AnimeClient\Kitsu;
|
||||
?>
|
||||
<main class="user-page details">
|
||||
<h2 class="toph">
|
||||
About
|
||||
<?= $helper->a(
|
||||
"https://kitsu.io/users/{$data['slug']}",
|
||||
$data['name'], [
|
||||
'title' => 'View profile on Kitsu'
|
||||
])
|
||||
?>
|
||||
</h2>
|
||||
|
||||
<section class="flex flex-no-wrap">
|
||||
<aside class="info">
|
||||
<table class="media-details invisible">
|
||||
<tr>
|
||||
<?php if($data['avatar'] !== null): ?>
|
||||
<td><?= $helper->img($data['avatar'], ['alt' => '', 'width' => '225']); ?></td>
|
||||
<?php endif ?>
|
||||
<td><?= $escape->html($data['about']) ?></td>
|
||||
</tr>
|
||||
</table>
|
||||
<br />
|
||||
<table class="media-details">
|
||||
<?php foreach ([
|
||||
'joinDate' => 'Joined',
|
||||
'birthday' => 'Birthday',
|
||||
'gender' => 'Gender',
|
||||
'location' => 'Location'
|
||||
] as $key => $label): ?>
|
||||
<?php if ($data[$key] !== null): ?>
|
||||
<tr>
|
||||
<td><?= $label ?></td>
|
||||
<td><?= $data[$key] ?></td>
|
||||
</tr>
|
||||
<?php endif ?>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<?php if ($data['website'] !== null): ?>
|
||||
<tr>
|
||||
<td>Website</td>
|
||||
<td><?= $helper->a($data['website'], $data['website']) ?></td>
|
||||
</tr>
|
||||
<?php endif ?>
|
||||
|
||||
<?php if ($data['waifu']['character'] !== null): ?>
|
||||
<tr>
|
||||
<td><?= $escape->html($data['waifu']['label']) ?></td>
|
||||
<td>
|
||||
<?php
|
||||
$character = $data['waifu']['character'];
|
||||
echo $component->character(
|
||||
$character['names']['canonical'],
|
||||
$url->generate('character', ['slug' => $character['slug']]),
|
||||
$helper->img(Kitsu::getImage($character))
|
||||
);
|
||||
?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endif ?>
|
||||
</table>
|
||||
|
||||
<h3>User Stats</h3><br />
|
||||
<table class="media-details">
|
||||
<?php foreach($data['stats'] as $label => $stat): ?>
|
||||
<tr>
|
||||
<td><?= $label ?></td>
|
||||
<td><?= $stat ?></td>
|
||||
</tr>
|
||||
<?php endforeach ?>
|
||||
</table>
|
||||
</aside>
|
||||
<article>
|
||||
<?php if ( ! empty($data['favorites'])): ?>
|
||||
<h3>Favorites</h3>
|
||||
<?= $component->tabs('user-favorites', $data['favorites'], static function ($items, $type) use ($component, $helper, $url) {
|
||||
$rendered = [];
|
||||
if ($type === 'character')
|
||||
{
|
||||
uasort($items, fn ($a, $b) => $a['names']['canonical'] <=> $b['names']['canonical']);
|
||||
}
|
||||
else
|
||||
{
|
||||
uasort($items, fn ($a, $b) => $a['titles']['canonical'] <=> $b['titles']['canonical']);
|
||||
}
|
||||
|
||||
foreach ($items as $id => $item)
|
||||
{
|
||||
if ($type === 'character')
|
||||
{
|
||||
$rendered[] = $component->character(
|
||||
$item['names']['canonical'],
|
||||
$url->generate('character', ['slug' => $item['slug']]),
|
||||
$helper->img(Kitsu::getImage($item))
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
$rendered[] = $component->media(
|
||||
array_merge(
|
||||
[$item['titles']['canonical']],
|
||||
Kitsu::getFilteredTitles($item['titles']),
|
||||
),
|
||||
$url->generate("{$type}.details", ['id' => $item['slug']]),
|
||||
$helper->img(Kitsu::getPosterImage($item), ['width' => 220]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return implode('', array_map('mb_trim', $rendered));
|
||||
|
||||
}, 'content full-width media-wrap') ?>
|
||||
<?php endif ?>
|
||||
</article>
|
||||
</section>
|
||||
</main>
|
84
build/phpcs.xml
Normal file
84
build/phpcs.xml
Normal file
@ -0,0 +1,84 @@
|
||||
<?xml version="1.0"?>
|
||||
<ruleset name="Tim's Coding Standard">
|
||||
<description>A variation of the CodeIgniter standard</description>
|
||||
|
||||
<file>../src/</file>
|
||||
|
||||
<encoding>utf-8</encoding>
|
||||
|
||||
<rule ref="Generic.Files.LineEndings">
|
||||
<properties>
|
||||
<property name="eolChar" value="\n"/>
|
||||
</properties>
|
||||
</rule>
|
||||
|
||||
<!-- PHP files should OMIT the closing PHP tag -->
|
||||
<rule ref="Zend.Files.ClosingTag"/>
|
||||
<!-- Always use full PHP opening tags -->
|
||||
<rule ref="Generic.PHP.DisallowShortOpenTag"/>
|
||||
|
||||
<!-- Constants should always be fully uppercase -->
|
||||
<rule ref="Generic.NamingConventions.UpperCaseConstantName"/>
|
||||
<!-- TRUE, FALSE, and NULL keywords should always be fully uppercase -->
|
||||
<rule ref="Generic.PHP.UpperCaseConstant"/>
|
||||
|
||||
<!-- One statement per line -->
|
||||
<rule ref="Generic.Formatting.DisallowMultipleStatements"/>
|
||||
|
||||
|
||||
|
||||
<!-- Classes and functions should be commented -->
|
||||
<rule ref="PEAR.Commenting.ClassComment">
|
||||
<exclude name="PEAR.Commenting.ClassComment.MissingCategoryTag" />
|
||||
<exclude name="PEAR.Commenting.ClassComment.MissingPackageTag" />
|
||||
<exclude name="PEAR.Commenting.ClassComment.MissingAuthorTag" />
|
||||
<exclude name="PEAR.Commenting.ClassComment.MissingLicenseTag" />
|
||||
<exclude name="PEAR.Commenting.ClassComment.MissingLinkTag" />
|
||||
</rule>
|
||||
<rule ref="PEAR.Commenting.FunctionComment">
|
||||
<!-- Exclude this sniff because it doesn't understand multiple types -->
|
||||
<exclude name="PEAR.Commenting.FunctionComment.MissingParamComment" />
|
||||
<exclude name="PEAR.Commenting.FunctionComment.SpacingAfterParamType" />
|
||||
<exclude name="PEAR.Commenting.FunctionComment.SpacingAfterParamName" />
|
||||
</rule>
|
||||
|
||||
<!-- Use warnings for docblock comments for files and variables, since nothing is clearly explained -->
|
||||
<rule ref="PEAR.Commenting.FileComment">
|
||||
<exclude name="PEAR.Commenting.FileComment.InvalidVersion" />
|
||||
<exclude name="PEAR.Commenting.FileComment.MissingCategoryTag" />
|
||||
<properties>
|
||||
<property name="error" value="false"/>
|
||||
</properties>
|
||||
</rule>
|
||||
|
||||
<rule ref="Squiz.Commenting.FunctionCommentThrowTag"/>
|
||||
<rule ref="Squiz.Commenting.VariableComment">
|
||||
<properties>
|
||||
<property name="error" value="false"/>
|
||||
</properties>
|
||||
</rule>
|
||||
|
||||
<!-- Use Allman style indenting. With the exception of Class declarations,
|
||||
braces are always placed on a line by themselves, and indented at the same level as the control statement that "owns" them. -->
|
||||
<rule ref="Generic.Functions.OpeningFunctionBraceBsdAllman"/>
|
||||
<rule ref="PEAR.WhiteSpace.ScopeClosingBrace">
|
||||
<exclude name="PEAR.WhiteSpace.ScopeClosingBrace.BreakIndent" />
|
||||
</rule>
|
||||
<rule ref="Generic.Functions.FunctionCallArgumentSpacing"/>
|
||||
|
||||
<!-- Use only short array syntax -->
|
||||
<rule ref="Generic.Arrays.DisallowLongArraySyntax" />
|
||||
|
||||
<rule ref="Generic.PHP.ForbiddenFunctions">
|
||||
<properties>
|
||||
<property name="forbiddenFunctions" type="array" value="create_function=>null,eval=>null" />
|
||||
</properties>
|
||||
</rule>
|
||||
|
||||
<!-- Inherit CodeIgniter Rules -->
|
||||
<rule ref="./CodeIgniter">
|
||||
<properties>
|
||||
<property name="error" value="false" />
|
||||
</properties>
|
||||
</rule>
|
||||
</ruleset>
|
32
build/phpunit.xml
Normal file
32
build/phpunit.xml
Normal file
@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" colors="true" stopOnFailure="false" bootstrap="../tests/bootstrap.php" beStrictAboutTestsThatDoNotTestAnything="true" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.1/phpunit.xsd">
|
||||
<coverage>
|
||||
<report>
|
||||
<clover outputFile="logs/clover.xml"/>
|
||||
<html outputDirectory="../coverage"/>
|
||||
</report>
|
||||
</coverage>
|
||||
<testsuites>
|
||||
<testsuite name="AnimeClient">
|
||||
<directory>../tests/AnimeClient</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Ion">
|
||||
<directory>../tests/Ion</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<logging>
|
||||
<junit outputFile="logs/junit.xml"/>
|
||||
</logging>
|
||||
<php>
|
||||
<server name="HTTP_USER_AGENT" value="Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:38.0) Gecko/20100101 Firefox/38.0"/>
|
||||
<server name="HTTP_HOST" value="localhost"/>
|
||||
<server name="SERVER_NAME" value="localhost"/>
|
||||
<server name="REQUEST_URI" value="/"/>
|
||||
<server name="REQUEST_METHOD" value="GET"/>
|
||||
</php>
|
||||
<source>
|
||||
<include>
|
||||
<directory suffix=".php">../src</directory>
|
||||
</include>
|
||||
</source>
|
||||
</phpunit>
|
@ -1,11 +1,78 @@
|
||||
{
|
||||
"require": {
|
||||
"guzzlehttp/guzzle": "5.3.*",
|
||||
"filp/whoops": "1.1.*",
|
||||
"aura/router": "2.2.*",
|
||||
"aura/web": "2.0.*",
|
||||
"aviat4ion/query": "2.0.*",
|
||||
"robmorgan/phinx": "*",
|
||||
"abeautifulsite/simpleimage": "*"
|
||||
"name": "aviat/hummingbird-anime-client",
|
||||
"description": "A self-hosted anime/manga client for Kitsu.",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Timothy J. Warren",
|
||||
"email": "tim@timshomepage.net",
|
||||
"homepage": "https://timshomepage.net",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"autoload": {
|
||||
"files": [
|
||||
"src/Ion/functions.php",
|
||||
"src/AnimeClient.php",
|
||||
"src/AnimeClient/constants.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Aviat\\": "src/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Aviat\\AnimeClient\\Tests\\": "tests/AnimeClient",
|
||||
"Aviat\\Ion\\Tests\\": "tests/Ion"
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"lock": false
|
||||
},
|
||||
"require": {
|
||||
"amphp/http-client": "^4.5.0",
|
||||
"aura/html": "^2.5.0",
|
||||
"aura/router": "^3.1.0",
|
||||
"aura/session": "^2.1.0",
|
||||
"aviat/banker": "^4.1.2",
|
||||
"aviat/query": "^4.1.0",
|
||||
"ext-dom": "*",
|
||||
"ext-gd": "*",
|
||||
"ext-intl": "*",
|
||||
"ext-json": "*",
|
||||
"ext-mbstring": "*",
|
||||
"ext-pdo": "*",
|
||||
"laminas/laminas-diactoros": "^3.0.0",
|
||||
"laminas/laminas-httphandlerrunner": "^2.6.1",
|
||||
"maximebf/consolekit": "^1.0.3",
|
||||
"monolog/monolog": "^3.0.0",
|
||||
"php": ">= 8.2.0",
|
||||
"psr/http-message": "^1.0.1",
|
||||
"symfony/polyfill-mbstring": "^1.0.0",
|
||||
"symfony/polyfill-util": "^1.0.0",
|
||||
"tracy/tracy": "^2.8.0",
|
||||
"yosymfony/toml": "^1.0.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpstan/phpstan": "^1.2.0",
|
||||
"phpunit/phpunit": "^10.0.0",
|
||||
"roave/security-advisories": "dev-master",
|
||||
"spatie/phpunit-snapshot-assertions": "^5.0.1"
|
||||
},
|
||||
"scripts": {
|
||||
"build:css": "cd public && npm run build:css && cd ..",
|
||||
"build:js": "cd public && npm run build:js && cd ..",
|
||||
"coverage": "php -dpcov.enabled=1 -dpcov.directory=. -dpcov.exclude=\"~vendor~\" ./vendor/bin/phpunit -c build",
|
||||
"phpstan": "phpstan analyse -c phpstan.neon",
|
||||
"watch:css": "cd public && npm run watch:css",
|
||||
"watch:js": "cd public && npm run watch:js",
|
||||
"test": "vendor/bin/phpunit -c build --no-coverage",
|
||||
"test-update": "vendor/bin/phpunit -c build --no-coverage -d --update-snapshots"
|
||||
},
|
||||
"scripts-descriptions": {
|
||||
"build:css": "Generate browser css",
|
||||
"coverage": "Generate a test coverage report",
|
||||
"phpstan": "Run PHP Static analysis",
|
||||
"test": "Run the unit tests"
|
||||
}
|
||||
}
|
33
console
Executable file
33
console
Executable file
@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env php
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
// Set up autoloader for third-party dependencies
|
||||
require_once __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
use Aviat\AnimeClient\Command;
|
||||
use ConsoleKit\Console;
|
||||
|
||||
$GLOBALS['_SERVER']['HTTP_HOST'] = 'localhost';
|
||||
|
||||
const APP_DIR = __DIR__ . '/app';
|
||||
const TEMPLATE_DIR = APP_DIR . '/templates';
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Start console script
|
||||
// -----------------------------------------------------------------------------
|
||||
try
|
||||
{
|
||||
(new Console([
|
||||
'clear:cache' => Command\CacheClear::class,
|
||||
'clear:thumbnails' => Command\ClearThumbnails::class,
|
||||
'refresh:cache' => Command\CachePrime::class,
|
||||
'refresh:thumbnails' => Command\UpdateThumbnails::class,
|
||||
'lists:sync' => Command\SyncLists::class,
|
||||
'sync:lists' => Command\SyncLists::class
|
||||
]))->run();
|
||||
}
|
||||
catch (Throwable)
|
||||
{
|
||||
|
||||
}
|
||||
|
70
frontEndSrc/css.js
Normal file
70
frontEndSrc/css.js
Normal file
@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Script for optimizing css
|
||||
*/
|
||||
const fs = require('fs');
|
||||
const postcss = require('postcss');
|
||||
const atImport = require('postcss-import');
|
||||
const cssNext = require('postcss-preset-env');
|
||||
const cssNano = require('cssnano');
|
||||
|
||||
const lightCss = fs.readFileSync('css/light.css', 'utf-8');
|
||||
const darkCss = fs.readFileSync('css/src/dark-override.css', 'utf-8');
|
||||
const fullDarkCss = fs.readFileSync('css/dark.css', 'utf-8');
|
||||
|
||||
const minOptions = {
|
||||
autoprefixer: false,
|
||||
colormin: false,
|
||||
minifyFontValues: false,
|
||||
options: {
|
||||
sourcemap: false
|
||||
}
|
||||
};
|
||||
|
||||
const processOptions = {
|
||||
browser: '> 0.5%',
|
||||
features: {
|
||||
'custom-properties': true,
|
||||
},
|
||||
stage: 0,
|
||||
};
|
||||
|
||||
try {
|
||||
(async () => {
|
||||
// Basic theme
|
||||
const lightMin = await postcss()
|
||||
.use(atImport())
|
||||
.use(cssNext(processOptions))
|
||||
.use(cssNano(minOptions))
|
||||
.process(lightCss, {
|
||||
from: 'css/light.css',
|
||||
to: '/public/css/light.min.css',
|
||||
}).catch(console.error);
|
||||
fs.writeFileSync('../public/css/light.min.css', lightMin.css);
|
||||
|
||||
// Dark theme
|
||||
const darkFullMin = await postcss()
|
||||
.use(atImport())
|
||||
.use(cssNext(processOptions))
|
||||
.use(cssNano(minOptions))
|
||||
.process(fullDarkCss, {
|
||||
from: 'css/dark.css',
|
||||
to: '/public/css/dark.min.css',
|
||||
});
|
||||
fs.writeFileSync('../public/css/dark.min.css', darkFullMin.css);
|
||||
|
||||
// Dark override
|
||||
const darkMin = await postcss()
|
||||
.use(atImport())
|
||||
.use(cssNext(processOptions))
|
||||
.use(cssNano(minOptions))
|
||||
.process(darkCss, {
|
||||
from: 'css/dark-override.css',
|
||||
to: '/public/css/dark.min.css',
|
||||
}).catch(console.error);
|
||||
const autoDarkCss = `${lightMin} @media (prefers-color-scheme: dark) { ${darkMin.css} }`
|
||||
fs.writeFileSync('../public/css/auto.min.css', autoDarkCss)
|
||||
|
||||
})();
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
3
frontEndSrc/css/auto.css
Normal file
3
frontEndSrc/css/auto.css
Normal file
@ -0,0 +1,3 @@
|
||||
@media (prefers-color-scheme: dark) {
|
||||
@import "src/dark-override.css";
|
||||
}
|
5
frontEndSrc/css/dark.css
Normal file
5
frontEndSrc/css/dark.css
Normal file
@ -0,0 +1,5 @@
|
||||
@import "src/-marx-.css";
|
||||
@import "src/general.css";
|
||||
@import "src/components.css";
|
||||
@import "src/responsive.css";
|
||||
@import "src/dark-override.css";
|
4
frontEndSrc/css/light.css
Normal file
4
frontEndSrc/css/light.css
Normal file
@ -0,0 +1,4 @@
|
||||
@import "src/-marx-.css";
|
||||
@import "src/general.css";
|
||||
@import "src/components.css";
|
||||
@import "src/responsive.css";
|
531
frontEndSrc/css/src/-marx-.css
Normal file
531
frontEndSrc/css/src/-marx-.css
Normal file
@ -0,0 +1,531 @@
|
||||
:root {
|
||||
--default-font-list: system-ui,sans-serif;
|
||||
--monospace-font-list:'Anonymous Pro','Fira Code',Menlo,Monaco,Consolas,'Courier New',monospace;
|
||||
--serif-font-list:Georgia,Times,'Times New Roman',serif;
|
||||
-ms-text-size-adjust:100%;
|
||||
-webkit-text-size-adjust:100%;
|
||||
box-sizing:border-box;
|
||||
cursor:default;
|
||||
font-family:var(--default-font-list);
|
||||
line-height:1.4;
|
||||
overflow-y:scroll;
|
||||
text-size-adjust:100%;
|
||||
scroll-behavior:smooth;
|
||||
}
|
||||
|
||||
audio:not([controls]) {
|
||||
display:none;
|
||||
}
|
||||
|
||||
details {
|
||||
display:block;
|
||||
}
|
||||
|
||||
input[type=search] {
|
||||
-webkit-appearance:textfield;
|
||||
}
|
||||
|
||||
input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration {
|
||||
-webkit-appearance:none;
|
||||
}
|
||||
|
||||
main {
|
||||
display:block;
|
||||
margin:0 auto;
|
||||
padding:0 1.6em 1.6em;
|
||||
padding:0 1.6rem 1.6rem;
|
||||
}
|
||||
|
||||
summary {
|
||||
display:block;
|
||||
}
|
||||
|
||||
pre {
|
||||
background:#efefef;
|
||||
color:#444;
|
||||
display:block;
|
||||
font-family:var(--monospace-font-list);
|
||||
font-size:1.4em;
|
||||
font-size:1.4rem;
|
||||
margin:1.6em 0;
|
||||
margin:1.6rem 0;
|
||||
overflow:auto;
|
||||
padding:1.6em;
|
||||
padding:1.6rem;
|
||||
word-break:break-all;
|
||||
word-wrap:break-word;
|
||||
}
|
||||
|
||||
progress {
|
||||
display:inline-block;
|
||||
}
|
||||
|
||||
small {
|
||||
color:#777;
|
||||
font-size:75%;
|
||||
}
|
||||
|
||||
big {
|
||||
font-size:125%;
|
||||
}
|
||||
|
||||
template {
|
||||
display:none;
|
||||
}
|
||||
|
||||
textarea {
|
||||
border:.1rem solid #ccc;
|
||||
border-radius:0;
|
||||
display:block;
|
||||
margin-bottom:.8rem;
|
||||
overflow:auto;
|
||||
padding:.8rem;
|
||||
resize:vertical;
|
||||
vertical-align:middle;
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display:none;
|
||||
}
|
||||
|
||||
[unselectable] {
|
||||
-moz-user-select:none;
|
||||
-ms-user-select:none;
|
||||
-webkit-user-select:none;
|
||||
user-select:none;
|
||||
}
|
||||
|
||||
*,::before,::after {
|
||||
/* border-style:solid;
|
||||
border-width:0; */
|
||||
box-sizing:inherit;
|
||||
}
|
||||
|
||||
* {
|
||||
font-size:inherit;
|
||||
line-height:inherit;
|
||||
margin:0;
|
||||
padding:0;
|
||||
}
|
||||
|
||||
::before,::after {
|
||||
text-decoration:inherit;
|
||||
vertical-align:inherit;
|
||||
}
|
||||
|
||||
a {
|
||||
-webkit-transition:.25s ease;
|
||||
color:#1271db;
|
||||
text-decoration:none;
|
||||
transition:.25s ease;
|
||||
}
|
||||
|
||||
audio,canvas,iframe,img,svg,video {
|
||||
vertical-align:middle;
|
||||
}
|
||||
|
||||
input,/*select*/,textarea {
|
||||
border:.1rem solid #ccc;
|
||||
color:inherit;
|
||||
font-family:inherit;
|
||||
font-style:inherit;
|
||||
font-weight:inherit;
|
||||
min-height:1.4em;
|
||||
}
|
||||
|
||||
code,kbd,pre,samp {
|
||||
font-family:var(--monospace-font-list);
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse:collapse;
|
||||
border-spacing:0;
|
||||
margin-bottom:1.6rem;
|
||||
}
|
||||
|
||||
::-moz-selection {
|
||||
background-color:#b3d4fc;
|
||||
text-shadow:none;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background-color:#b3d4fc;
|
||||
text-shadow:none;
|
||||
}
|
||||
|
||||
button::-moz-focus-inner {
|
||||
border:0;
|
||||
}
|
||||
|
||||
body {
|
||||
color:#444;
|
||||
font-family:var(--default-font-list);
|
||||
font-size:1.6rem;
|
||||
font-style:normal;
|
||||
font-weight:400;
|
||||
padding:0;
|
||||
}
|
||||
|
||||
p {
|
||||
margin:0 0 1.6rem;
|
||||
}
|
||||
|
||||
h1,h2,h3,h4,h5,h6 {
|
||||
font-family:var(--default-font-list);
|
||||
margin:2em 0 1.6em;
|
||||
margin:2rem 0 1.6rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
border-bottom:.1rem solid rgba(0,0,0,0.2);
|
||||
font-size:3.6em;
|
||||
font-size:3.6rem;
|
||||
font-style:normal;
|
||||
font-weight:500;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size:3em;
|
||||
font-size:3rem;
|
||||
font-style:normal;
|
||||
font-weight:500;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size:2.4em;
|
||||
font-size:2.4rem;
|
||||
font-style:normal;
|
||||
font-weight:500;
|
||||
margin:1.6rem 0 .4rem;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size:1.8em;
|
||||
font-size:1.8rem;
|
||||
font-style:normal;
|
||||
font-weight:600;
|
||||
margin:1.6rem 0 .4rem;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size:1.6em;
|
||||
font-size:1.6rem;
|
||||
font-style:normal;
|
||||
font-weight:600;
|
||||
margin:1.6rem 0 .4rem;
|
||||
}
|
||||
|
||||
h6 {
|
||||
color:#777;
|
||||
font-size:1.4em;
|
||||
font-size:1.4rem;
|
||||
font-style:normal;
|
||||
font-weight:600;
|
||||
margin:1.6rem 0 .4rem;
|
||||
}
|
||||
|
||||
code {
|
||||
background:#efefef;
|
||||
color:#444;
|
||||
font-family:var(--monospace-font-list);
|
||||
font-size:1.4rem;
|
||||
word-break:break-all;
|
||||
word-wrap:break-word;
|
||||
}
|
||||
|
||||
a:hover,a:focus {
|
||||
text-decoration:none;
|
||||
}
|
||||
|
||||
dl {
|
||||
margin-bottom:1.6rem;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin-left:4rem;
|
||||
}
|
||||
|
||||
ul,ol {
|
||||
margin-bottom:.8rem;
|
||||
padding-left:2rem;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left:.2rem solid #1271db;
|
||||
font-family:var(--serif-font-list);
|
||||
font-style:italic;
|
||||
margin:1.6rem 0;
|
||||
padding-left:1.6rem;
|
||||
}
|
||||
|
||||
figcaption {
|
||||
font-family:var(--serif-font-list);
|
||||
}
|
||||
|
||||
html {
|
||||
font-size:62.5%;
|
||||
}
|
||||
|
||||
main,header,footer,article,section,aside,details,summary {
|
||||
display:block;
|
||||
height:auto;
|
||||
margin:0 auto;
|
||||
width:100%;
|
||||
}
|
||||
|
||||
footer {
|
||||
border-top:.1rem solid rgba(0,0,0,0.2);
|
||||
clear:both;
|
||||
display:inline-block;
|
||||
float:left;
|
||||
max-width:100%;
|
||||
padding:1rem 0;
|
||||
text-align:center;
|
||||
}
|
||||
|
||||
hr {
|
||||
border-top:.1rem solid rgba(0,0,0,0.2);
|
||||
display:block;
|
||||
margin-bottom:1.6rem;
|
||||
width:100%;
|
||||
}
|
||||
|
||||
img {
|
||||
height:auto;
|
||||
/* max-width:100%; */
|
||||
vertical-align:baseline;
|
||||
}
|
||||
|
||||
input[type=text],input[type=password],input[type=email],input[type=url],input[type=date],input[type=month],input[type=time],input[type=datetime],input[type=datetime-local],input[type=week],input[type=number],input[type=search],input[type=tel],input[type=color]/*,select */ {
|
||||
border:.1rem solid #ccc;
|
||||
border-radius:0;
|
||||
display:inline-block;
|
||||
padding:.8rem;
|
||||
vertical-align:middle;
|
||||
}
|
||||
|
||||
input:not([type]) {
|
||||
-webkit-appearance:none;
|
||||
background-clip:padding-box;
|
||||
background-color:#fff;
|
||||
border:.1rem solid #ccc;
|
||||
border-radius:0;
|
||||
color:#444;
|
||||
display:inline-block;
|
||||
padding:.8rem;
|
||||
text-align:left;
|
||||
}
|
||||
|
||||
input[type=color] {
|
||||
padding:.8rem 1.6rem;
|
||||
}
|
||||
|
||||
input[type=text]:focus,input[type=password]:focus,input[type=email]:focus,input[type=url]:focus,input[type=date]:focus,input[type=month]:focus,input[type=time]:focus,input[type=datetime]:focus,input[type=datetime-local]:focus,input[type=week]:focus,input[type=number]:focus,input[type=search]:focus,input[type=tel]:focus,input[type=color]:focus,/* select:focus */,textarea:focus {
|
||||
border-color:#b3d4fc;
|
||||
}
|
||||
|
||||
input:not([type]):focus {
|
||||
border-color:#b3d4fc;
|
||||
}
|
||||
|
||||
input[type=radio],input[type=checkbox] {
|
||||
vertical-align:middle;
|
||||
}
|
||||
|
||||
input[type=file]:focus,input[type=radio]:focus,input[type=checkbox]:focus {
|
||||
outline:.1rem solid thin #444;
|
||||
}
|
||||
|
||||
input[type=text][disabled],input[type=password][disabled],input[type=email][disabled],input[type=url][disabled],input[type=date][disabled],input[type=month][disabled],input[type=time][disabled],input[type=datetime][disabled],input[type=datetime-local][disabled],input[type=week][disabled],input[type=number][disabled],input[type=search][disabled],input[type=tel][disabled],input[type=color][disabled],/*select[disabled]*/,textarea[disabled] {
|
||||
background-color:#efefef;
|
||||
color:#777;
|
||||
cursor:not-allowed;
|
||||
}
|
||||
|
||||
input:not([type])[disabled] {
|
||||
background-color:#efefef;
|
||||
color:#777;
|
||||
cursor:not-allowed;
|
||||
}
|
||||
|
||||
input[readonly],/*select[readonly]*/,textarea[readonly] {
|
||||
background-color:#efefef;
|
||||
border-color:#ccc;
|
||||
color:#777;
|
||||
}
|
||||
|
||||
input:focus:invalid,textarea:focus:invalid/*,select:focus:invalid*/ {
|
||||
border-color:#e9322d;
|
||||
color:#b94a48;
|
||||
}
|
||||
|
||||
input[type=file]:focus:invalid:focus,input[type=radio]:focus:invalid:focus,input[type=checkbox]:focus:invalid:focus {
|
||||
outline-color:#ff4136;
|
||||
}
|
||||
|
||||
/* select {
|
||||
background-color:#fff;
|
||||
border:.1rem solid #ccc;
|
||||
}*/
|
||||
|
||||
select[multiple] {
|
||||
height:auto;
|
||||
}
|
||||
|
||||
label {
|
||||
line-height:2;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
border:0;
|
||||
margin:0;
|
||||
padding:.8rem 0;
|
||||
}
|
||||
|
||||
legend {
|
||||
border-bottom:.1rem solid #ccc;
|
||||
color:#444;
|
||||
display:block;
|
||||
margin-bottom:.8rem;
|
||||
padding:.8rem 0;
|
||||
width:100%;
|
||||
}
|
||||
|
||||
input[type=submit],button {
|
||||
-moz-user-select:none;
|
||||
-ms-user-select:none;
|
||||
-webkit-transition:.25s ease;
|
||||
-webkit-user-drag:none;
|
||||
-webkit-user-select:none;
|
||||
border:.2rem solid #444;
|
||||
border-radius:0;
|
||||
color:#444;
|
||||
cursor:pointer;
|
||||
display:inline-block;
|
||||
margin-bottom:.8rem;
|
||||
margin-right:.4rem;
|
||||
padding:.8rem 1.6rem;
|
||||
text-align:center;
|
||||
text-decoration:none;
|
||||
text-transform:uppercase;
|
||||
transition:.25s ease;
|
||||
user-select:none;
|
||||
vertical-align:baseline;
|
||||
}
|
||||
|
||||
input[type=submit] a,button a {
|
||||
color:#444;
|
||||
}
|
||||
|
||||
input[type=submit]::-moz-focus-inner,button::-moz-focus-inner {
|
||||
padding:0;
|
||||
}
|
||||
|
||||
input[type=submit]:hover,button:hover {
|
||||
background:#444;
|
||||
border-color:#444;
|
||||
color:#fff;
|
||||
}
|
||||
|
||||
input[type=submit]:hover a,button:hover a {
|
||||
color:#fff;
|
||||
}
|
||||
|
||||
input[type=submit]:active,button:active {
|
||||
background:#6a6a6a;
|
||||
border-color:#6a6a6a;
|
||||
color:#fff;
|
||||
}
|
||||
|
||||
input[type=submit]:active a,button:active a {
|
||||
color:#fff;
|
||||
}
|
||||
|
||||
input[type=submit]:disabled,button:disabled {
|
||||
box-shadow:none;
|
||||
cursor:not-allowed;
|
||||
opacity:.4;
|
||||
}
|
||||
|
||||
nav ul {
|
||||
list-style:none;
|
||||
margin:0;
|
||||
padding:0;
|
||||
text-align:center;
|
||||
}
|
||||
|
||||
nav ul li {
|
||||
display:inline;
|
||||
}
|
||||
|
||||
nav a {
|
||||
-webkit-transition:.25s ease;
|
||||
border-bottom:.2rem solid transparent;
|
||||
color:#444;
|
||||
padding:.8rem 1.6rem;
|
||||
text-decoration:none;
|
||||
transition:.25s ease;
|
||||
}
|
||||
|
||||
nav a:hover,nav li.selected a {
|
||||
border-color:rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
nav a:active {
|
||||
border-color:rgba(0,0,0,0.56);
|
||||
}
|
||||
|
||||
caption {
|
||||
padding:.8rem 0;
|
||||
}
|
||||
|
||||
thead th {
|
||||
background:#efefef;
|
||||
color:#444;
|
||||
}
|
||||
|
||||
tr {
|
||||
background:#fff;
|
||||
margin-bottom:.8rem;
|
||||
}
|
||||
|
||||
th,td {
|
||||
border:.1rem solid #ccc;
|
||||
padding:.8rem 1.6rem;
|
||||
text-align:center;
|
||||
vertical-align:inherit;
|
||||
}
|
||||
|
||||
tfoot tr {
|
||||
background:none;
|
||||
}
|
||||
|
||||
tfoot td {
|
||||
color:#efefef;
|
||||
font-size:.8rem;
|
||||
font-style:italic;
|
||||
padding:1.6rem .4rem;
|
||||
}
|
||||
|
||||
@media screen {
|
||||
[hidden~=screen] {
|
||||
display:inherit;
|
||||
}
|
||||
|
||||
[hidden~=screen]:not(:active):not(:focus):not(:target) {
|
||||
clip:rect(0000)!important;
|
||||
position:absolute!important;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and max-width 40rem {
|
||||
article,section,aside {
|
||||
clear:both;
|
||||
display:block;
|
||||
max-width:100%;
|
||||
}
|
||||
|
||||
img {
|
||||
margin-right:1.6rem;
|
||||
}
|
||||
}
|
271
frontEndSrc/css/src/components.css
Normal file
271
frontEndSrc/css/src/components.css
Normal file
@ -0,0 +1,271 @@
|
||||
/* -----------------------------------------------------------------------------
|
||||
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, .single-tab {
|
||||
display: none;
|
||||
max-height: 950px;
|
||||
border: 1px solid #e5e5e5;
|
||||
border-top: 0;
|
||||
padding: 15px;
|
||||
background: #fff;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.single-tab {
|
||||
display: block;
|
||||
border: 1px solid #e5e5e5;
|
||||
box-shadow: 0 48px 80px -32px rgba(0, 0, 0, 0.3);
|
||||
margin-top: 1.5em;
|
||||
}
|
||||
|
||||
.tabs .content.full-height, .single-tab.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;
|
||||
}
|
||||
|
172
frontEndSrc/css/src/dark-override.css
Normal file
172
frontEndSrc/css/src/dark-override.css
Normal file
@ -0,0 +1,172 @@
|
||||
a {
|
||||
color: rgb(25, 120, 226);
|
||||
text-shadow: var(--link-shadow);
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #9e34fd;
|
||||
}
|
||||
|
||||
body,
|
||||
legend,
|
||||
nav ul li a {
|
||||
background: #333;
|
||||
color: #eee;
|
||||
}
|
||||
|
||||
nav a:hover, nav li.selected a {
|
||||
border-color: #fff;
|
||||
}
|
||||
|
||||
header button {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
table {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
td, th {
|
||||
border-color: #111;
|
||||
}
|
||||
|
||||
thead td,
|
||||
thead th {
|
||||
background: #333;
|
||||
color: #eee;
|
||||
}
|
||||
|
||||
tbody > tr:nth-child(2n) {
|
||||
background: #555;
|
||||
color: #eee;
|
||||
}
|
||||
tbody > tr:nth-child(2n+1) {
|
||||
background: #333;
|
||||
}
|
||||
|
||||
footer, legend, hr {
|
||||
border-color: #ddd;
|
||||
}
|
||||
|
||||
small {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
input, input[type], select, textarea {
|
||||
border-color: #bbb;
|
||||
color: #bbb;
|
||||
background: #333;
|
||||
padding:.8em;
|
||||
}
|
||||
|
||||
button {
|
||||
background: #444;
|
||||
background: linear-gradient(#666, #555, #444, #555, #666);
|
||||
border-radius: 0.5em;
|
||||
margin: 0;
|
||||
text-transform: none;
|
||||
border-color: #ddd;
|
||||
color: #ddd;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: #222;
|
||||
background: linear-gradient(#444, #333, #222, #333, #444);
|
||||
border-color: #ddd;
|
||||
color: #ddd;
|
||||
}
|
||||
|
||||
button:active {
|
||||
background: #333;
|
||||
background: linear-gradient(#333, #333);
|
||||
}
|
||||
|
||||
.media:hover button {
|
||||
background: linear-gradient(#666, #555, #444, #555, #666);
|
||||
}
|
||||
|
||||
.media:hover button:hover {
|
||||
background: linear-gradient(#444, #555, #666, #555, #444);
|
||||
}
|
||||
|
||||
.message, .static-message {
|
||||
text-shadow: var(--white-link-shadow);
|
||||
}
|
||||
|
||||
.message.success, .static-message.success {
|
||||
background: #1f8454;
|
||||
border-color: #70dda9;
|
||||
}
|
||||
.message.error, .static-message.error {
|
||||
border-color:#f3e6e6;
|
||||
background: #924949;
|
||||
}
|
||||
.message.info, .static-message.info {
|
||||
border-color: #FFFFCC;
|
||||
background: #bfbe3a;
|
||||
}
|
||||
|
||||
.invisible tr,
|
||||
.invisible td,
|
||||
.invisible th,
|
||||
.invisible tbody > tr:nth-child(2n),
|
||||
.invisible tbody > tr:nth-child(2n+1) {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
#main-nav {
|
||||
border-bottom: .1rem solid #ddd;
|
||||
}
|
||||
|
||||
.tabs,
|
||||
.vertical-tabs{
|
||||
background: #333;
|
||||
}
|
||||
|
||||
.tabs > label,
|
||||
.vertical-tabs .tab label {
|
||||
background: #222;
|
||||
border: 0;
|
||||
color: #eee;
|
||||
}
|
||||
|
||||
.vertical-tabs .tab label {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tabs > label:hover,
|
||||
.vertical-tabs .tab > label:hover {
|
||||
background: #888;
|
||||
}
|
||||
|
||||
.tabs > label:active,
|
||||
.vertical-tabs .tab > label:active {
|
||||
background: #999;
|
||||
}
|
||||
|
||||
.tabs > [type="radio"]:checked + label,
|
||||
.tabs > [type="radio"]:checked + label + .content,
|
||||
.vertical-tabs [type="radio"]:checked + label,
|
||||
.vertical-tabs [type="radio"]:checked ~ .content,
|
||||
.single-tab {
|
||||
/* border-color: #333; */
|
||||
border: 0;
|
||||
background: #666;
|
||||
color: #eee;
|
||||
}
|
||||
|
||||
.vertical-tabs {
|
||||
background: #222;
|
||||
border: 1px solid #444;
|
||||
}
|
||||
|
||||
.vertical-tabs .tab {
|
||||
background: #666;
|
||||
border-bottom: 1px solid #444;
|
||||
}
|
||||
|
||||
.streaming-logo {
|
||||
-webkit-filter: drop-shadow(0 0 2px #fff);
|
||||
filter: drop-shadow(0 0 2px #fff);
|
||||
}
|
||||
|
931
frontEndSrc/css/src/general.css
Normal file
931
frontEndSrc/css/src/general.css
Normal file
@ -0,0 +1,931 @@
|
||||
:root {
|
||||
--blue-link: rgb(18, 113, 219);
|
||||
--link-shadow: 1px 1px 1px #000;
|
||||
--white-link-shadow: 1px 1px 1px #fff;
|
||||
--shadow: 2px 2px 2px #000;
|
||||
--title-overlay: rgba(0, 0, 0, 0.45);
|
||||
--title-overlay-fallback: #000;
|
||||
--text-color: #ffffff;
|
||||
--normal-padding: 0.25em 0.125em;
|
||||
--link-hover-color: #7d12db;
|
||||
--edit-link-hover-color: #db7d12;
|
||||
--edit-link-color: #12db18;
|
||||
--radius: 5px;
|
||||
}
|
||||
|
||||
template, [hidden="hidden"], .media[hidden] {
|
||||
display: none
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0.5em;
|
||||
}
|
||||
|
||||
|
||||
button {
|
||||
background: #fff;
|
||||
background: linear-gradient(#ddd, #eee, #fff, #eee, #ddd);
|
||||
border-radius: 0.5em;
|
||||
margin: 0;
|
||||
text-transform: none;
|
||||
border-color: #555;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: #bbb;
|
||||
background: linear-gradient(#cfcfcf, #dfdfdf, #efefef, #dfdfdf, #cfcfcf);
|
||||
border-color: #555;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
button:active {
|
||||
background: #ddd;
|
||||
background: linear-gradient(#ddd, #ddd);
|
||||
}
|
||||
|
||||
.media:hover button {
|
||||
background: linear-gradient(#bbb, #ccc, #ddd, #ccc, #bbb);
|
||||
}
|
||||
|
||||
.media:hover button:hover {
|
||||
background: linear-gradient(#afafaf, #bfbfbf, #cfcfcf, #bfbfbf, #afafaf);
|
||||
}
|
||||
|
||||
table {
|
||||
/* min-width: 85%; */
|
||||
box-shadow: 0 48px 80px -32px rgba(0, 0, 0, 0.3);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 1em;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
thead td, thead th {
|
||||
padding: 0.5em;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
input[type=number] {
|
||||
min-width: 0;
|
||||
width: 4.5em;
|
||||
}
|
||||
|
||||
input[type=checkbox], input[type=radio] {
|
||||
min-width: auto;
|
||||
vertical-align: inherit;
|
||||
}
|
||||
|
||||
input, textarea {
|
||||
min-width: 30em;
|
||||
min-width: 30rem;
|
||||
}
|
||||
|
||||
tbody > tr:nth-child(odd) {
|
||||
background: #ddd;
|
||||
}
|
||||
|
||||
a:hover, a:active {
|
||||
color: var(--link-hover-color)
|
||||
}
|
||||
|
||||
iframe {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------------------------------
|
||||
Utility classes
|
||||
------------------------------------------------------------------------------*/
|
||||
|
||||
.bracketed {
|
||||
color: var(--edit-link-color);
|
||||
}
|
||||
|
||||
.bracketed, #main-nav a {
|
||||
text-shadow: var(--link-shadow);
|
||||
}
|
||||
|
||||
.bracketed:before {
|
||||
content: '[\00a0'
|
||||
}
|
||||
|
||||
.bracketed:after {
|
||||
content: '\00a0]'
|
||||
}
|
||||
|
||||
.bracketed:hover, .bracketed:active {
|
||||
color: var(--edit-link-hover-color)
|
||||
}
|
||||
|
||||
.grow-1 {
|
||||
flex-grow: 1
|
||||
}
|
||||
|
||||
.flex-wrap {
|
||||
flex-wrap: wrap
|
||||
}
|
||||
|
||||
.flex-no-wrap {
|
||||
flex-wrap: nowrap
|
||||
}
|
||||
|
||||
.flex-align-start {
|
||||
align-content: flex-start;
|
||||
}
|
||||
|
||||
.flex-align-end {
|
||||
align-items: flex-end
|
||||
}
|
||||
|
||||
.flex-align-space-around {
|
||||
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
|
||||
}
|
||||
|
||||
.small-font {
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
|
||||
.justify {
|
||||
text-align: justify
|
||||
}
|
||||
|
||||
.align-center {
|
||||
text-align: center !important
|
||||
}
|
||||
|
||||
.align-left {
|
||||
text-align: left !important
|
||||
}
|
||||
|
||||
.align-right {
|
||||
text-align: right !important
|
||||
}
|
||||
|
||||
.valign-top {
|
||||
vertical-align: top
|
||||
}
|
||||
|
||||
.no-border {
|
||||
border: none
|
||||
}
|
||||
|
||||
.media-wrap {
|
||||
text-align: center;
|
||||
margin: 0 auto;
|
||||
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;
|
||||
color: #924949;
|
||||
/* color: #fff; */
|
||||
}
|
||||
|
||||
.danger:hover, .danger:active {
|
||||
background-color: #924949;
|
||||
border-color: #ff4136;
|
||||
color: #ff4136;
|
||||
/* color: #fff; */
|
||||
}
|
||||
|
||||
td.danger, td.danger:hover, td.danger:active {
|
||||
background-color: transparent;
|
||||
color: #924949;
|
||||
}
|
||||
|
||||
.user-btn {
|
||||
background: transparent;
|
||||
border-color: var(--edit-link-color);
|
||||
color: var(--edit-link-color);
|
||||
text-shadow: var(--link-shadow);
|
||||
padding: 0 0.5em;
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
|
||||
.user-btn:hover, .user-btn:active {
|
||||
background: transparent;
|
||||
border-color: var(--edit-link-hover-color);
|
||||
color: var(--edit-link-hover-color);
|
||||
}
|
||||
|
||||
.user-btn:active {
|
||||
background: var(--edit-link-hover-color);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.full-height {
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
.toph {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------------------------------
|
||||
Main Nav
|
||||
------------------------------------------------------------------------------*/
|
||||
#main-nav {
|
||||
font-family: var(--default-font-list);
|
||||
margin: 2em 0 1.6em;
|
||||
margin: 2rem 0 1.6rem;
|
||||
border-bottom: .1rem solid rgba(0, 0, 0, 0.2);
|
||||
font-size: 3.6em;
|
||||
font-size: 3.6rem;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------------------------------
|
||||
Table sorting and form styles
|
||||
------------------------------------------------------------------------------*/
|
||||
.sorting,
|
||||
.sorting-asc,
|
||||
.sorting-desc {
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
|
||||
.sorting::before {
|
||||
content: " ↕\00a0";
|
||||
}
|
||||
|
||||
.sorting-asc::before {
|
||||
content: " ↑\00a0";
|
||||
}
|
||||
|
||||
.sorting-desc::before {
|
||||
content: " ↓\00a0";
|
||||
}
|
||||
|
||||
.form {
|
||||
/* width: 100%; */
|
||||
}
|
||||
|
||||
.form thead th, .form thead tr {
|
||||
background: inherit;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.form tr > td:nth-child(odd) {
|
||||
text-align: right;
|
||||
min-width: 25px;
|
||||
max-width: 30%;
|
||||
}
|
||||
|
||||
.form tr > td:nth-child(even) {
|
||||
text-align: left;
|
||||
/* width: 70%; */
|
||||
}
|
||||
|
||||
.invisible tbody > tr:nth-child(odd) {
|
||||
background: inherit;
|
||||
}
|
||||
|
||||
.borderless,
|
||||
.borderless tr,
|
||||
.borderless td,
|
||||
.borderless th,
|
||||
.invisible tr,
|
||||
.invisible td,
|
||||
.invisible th,
|
||||
table.invisible {
|
||||
box-shadow: none;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------------------------------
|
||||
Message boxes
|
||||
------------------------------------------------------------------------------*/
|
||||
|
||||
.message, .static-message {
|
||||
position: relative;
|
||||
margin: 0.5em auto;
|
||||
padding: 0.5em;
|
||||
width: 95%;
|
||||
}
|
||||
|
||||
.message .close {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
position: absolute;
|
||||
right: 0.5em;
|
||||
top: 0.5em;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
line-height: 1em;
|
||||
}
|
||||
|
||||
.message:hover .close:after {
|
||||
content: '☒';
|
||||
}
|
||||
|
||||
.message:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.message .icon {
|
||||
left: 0.5em;
|
||||
top: 0.5em;
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
.message.error, .static-message.error {
|
||||
border: 1px solid #924949;
|
||||
background: #f3e6e6;
|
||||
}
|
||||
|
||||
.message.error .icon::after {
|
||||
content: '✘';
|
||||
}
|
||||
|
||||
.message.success, .static-message.success {
|
||||
border: 1px solid #1f8454;
|
||||
background: #70dda9;
|
||||
}
|
||||
|
||||
.message.success .icon::after {
|
||||
content: '✔'
|
||||
}
|
||||
|
||||
.message.info, .static-message.info {
|
||||
border: 1px solid #bfbe3a;
|
||||
background: #FFFFCC;
|
||||
}
|
||||
|
||||
.message.info .icon::after {
|
||||
content: '⚠';
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------------------------------
|
||||
Base list styles
|
||||
------------------------------------------------------------------------------*/
|
||||
|
||||
.media, .character, .small-character {
|
||||
position: relative;
|
||||
vertical-align: top;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
width: 220px;
|
||||
height: 312px;
|
||||
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 {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.media .edit-buttons > button {
|
||||
margin: 0.5em auto;
|
||||
}
|
||||
|
||||
.name,
|
||||
.media-metadata > div,
|
||||
.medium-metadata > div,
|
||||
.row {
|
||||
text-shadow: var(--shadow);
|
||||
color: var(--text-color);
|
||||
padding: var(--normal-padding);
|
||||
text-align: right;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.media-type, .age-rating {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.media > .media-metadata {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.media > .medium-metadata {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.media > .name {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.media > .name a {
|
||||
display: inline-block;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.media .name a::before {
|
||||
/* background: var(--title-overlay-fallback);
|
||||
background: var(--title-overlay); */
|
||||
content: '';
|
||||
display: block;
|
||||
height: 312px;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 220px;
|
||||
z-index: -1; /* Put the pseudo-element behind its parent */
|
||||
}
|
||||
|
||||
.media-list .media:hover .name a::before {
|
||||
/* transition: .25s ease; */
|
||||
background: rgba(0, 0, 0, 0.75);
|
||||
}
|
||||
|
||||
.media > .name span.canonical {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.media > .name small {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.media:hover .name {
|
||||
background: rgba(0, 0, 0, 0.75);
|
||||
}
|
||||
|
||||
.media-list .media > .name a:hover,
|
||||
.media-list .media > .name a:hover small {
|
||||
color: var(--blue-link);
|
||||
}
|
||||
|
||||
.media:hover > button[hidden],
|
||||
.media:hover > .edit-buttons[hidden] {
|
||||
|
||||
transition: .25s ease;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.media:hover {
|
||||
transition: .25s ease;
|
||||
}
|
||||
|
||||
.small-character > .name a,
|
||||
.small-character > .name a small,
|
||||
.character > .name a,
|
||||
.character > .name a small,
|
||||
.media > .name a,
|
||||
.media > .name a small {
|
||||
background: none;
|
||||
color: #fff;
|
||||
text-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------------------------------
|
||||
Anime-list-specific styles
|
||||
------------------------------------------------------------------------------*/
|
||||
.anime .name, .manga .name {
|
||||
background: var(--title-overlay-fallback);
|
||||
background: var(--title-overlay);
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
padding: 0.5em 0.25em;
|
||||
}
|
||||
|
||||
.anime .media-type,
|
||||
.anime .airing-status,
|
||||
.anime .user-rating,
|
||||
.anime .completion,
|
||||
.anime .age-rating,
|
||||
.anime .edit,
|
||||
.anime .delete {
|
||||
background: none;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.anime .table, .manga .table {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.anime .row, .manga .row {
|
||||
width: 100%;
|
||||
display: inline-block;
|
||||
display: flex;
|
||||
align-content: space-around;
|
||||
justify-content: space-around;
|
||||
text-align: center;
|
||||
padding: 0 inherit;
|
||||
}
|
||||
|
||||
.anime .row > span, .manga .row > span {
|
||||
text-align: left;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.anime .row > div, .manga .row > div {
|
||||
font-size: 0.8em;
|
||||
display: inline-block;
|
||||
display: flex-item;
|
||||
align-self: center;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.anime .media > button.plus-one {
|
||||
border-color: hsla(0, 0%, 100%, .65);
|
||||
position: absolute;
|
||||
top: 138px;
|
||||
top: calc(50% - 21.2px);
|
||||
left: 44px;
|
||||
left: calc(50% - 57.8px);
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------------------------------
|
||||
Manga-list-specific styles
|
||||
------------------------------------------------------------------------------*/
|
||||
.manga .row {
|
||||
padding: 1px;
|
||||
}
|
||||
|
||||
.manga .media {
|
||||
/* border: 1px solid #ddd; */
|
||||
height: 310px;
|
||||
margin: 0.25em;
|
||||
}
|
||||
|
||||
.manga .media > .edit-buttons {
|
||||
position: absolute;
|
||||
top: 86px;
|
||||
/* top: calc(50% - 58.5px); */
|
||||
top: calc(50% - 21.2px);
|
||||
left: 43.5px;
|
||||
left: calc(50% - 57.8px);
|
||||
z-index: 40;
|
||||
}
|
||||
|
||||
.manga .media > .edit-buttons button {
|
||||
border-color: hsla(0, 0%, 100%, .65);
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------------------------------
|
||||
Search page styles
|
||||
------------------------------------------------------------------------------*/
|
||||
.media.search > .name {
|
||||
background-color: #555;
|
||||
background-color: rgba(000, 000, 000, 0.35);
|
||||
background-size: cover;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
/* There are two .name elements, just darken them both in this case! */
|
||||
.media.search.disabled .name {
|
||||
background-color: #000;
|
||||
background-color: rgba(0, 0, 0, 0.75);
|
||||
background-size: cover;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.media.search > .row {
|
||||
z-index: 6;
|
||||
}
|
||||
|
||||
.big-check, .mal-check {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.big-check:checked + label {
|
||||
transition: .25s ease;
|
||||
background: rgba(0, 0, 0, 0.75);
|
||||
}
|
||||
|
||||
.big-check:checked + label:after {
|
||||
content: '✓';
|
||||
font-size: 15em;
|
||||
font-size: 15rem;
|
||||
text-align: center;
|
||||
color: greenyellow;
|
||||
position: absolute;
|
||||
top: 147px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
#series-list article.media {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#series-list .name, #series-list .name label {
|
||||
position: absolute;
|
||||
display: block;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
vertical-align: middle;
|
||||
line-height: 1.25em;
|
||||
}
|
||||
|
||||
#series-list .name small {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
Details page styles
|
||||
-----------------------------------------------------------------------------*/
|
||||
.details {
|
||||
margin: 1.5rem auto 0 auto;
|
||||
padding: 1rem;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
/* .description {
|
||||
max-width: 80rem;
|
||||
columns: 4 28rem;
|
||||
columns: 4 28em;
|
||||
|
||||
margin-bottom: 1.6em;
|
||||
margin-bottom: 1.6rem;
|
||||
}
|
||||
|
||||
p.description br + br {
|
||||
page-break-before: avoid;
|
||||
page-break-after: auto;
|
||||
page-break-inside: avoid;
|
||||
break-inside: avoid;
|
||||
break-after: auto;
|
||||
break-before: avoid;
|
||||
} */
|
||||
|
||||
.fixed {
|
||||
max-width: 115em;
|
||||
max-width: 115rem;
|
||||
/* max-width: 80%; */
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.details .cover {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.details .flex > * {
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
.details .media-details td {
|
||||
padding: 0 1.5rem;
|
||||
}
|
||||
|
||||
.details p {
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
.details .media-details td:nth-child(odd) {
|
||||
width: 1%;
|
||||
white-space: nowrap;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.details .media-details td:nth-child(even) {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.details a h1,
|
||||
.details a h2 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.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 {
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
.small-character a {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.small-character .name,
|
||||
.character .name {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.small-character img,
|
||||
.character img,
|
||||
.small-character picture,
|
||||
.character picture,
|
||||
.person img,
|
||||
.person picture {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 5;
|
||||
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 {
|
||||
min-width: 0;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.max-table {
|
||||
min-width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
aside.info {
|
||||
/* max-width: 390px; */
|
||||
max-width: 33%;
|
||||
}
|
||||
|
||||
.fixed aside {
|
||||
max-width: 390px;
|
||||
}
|
||||
|
||||
aside picture, aside img {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
User page styles
|
||||
-----------------------------------------------------------------------------*/
|
||||
.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;
|
||||
}
|
||||
|
||||
.media a {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
Images / Logos
|
||||
-----------------------------------------------------------------------------*/
|
||||
.streaming-logo {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.small-streaming-logo {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.cover-streaming-link {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.media:hover .cover-streaming-link {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.cover-streaming-link .streaming-logo {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
-webkit-filter: drop-shadow(0 -1px 4px #fff);
|
||||
filter: drop-shadow(0 -1px 4px #fff);
|
||||
}
|
||||
|
||||
.history-img {
|
||||
width: 110px;
|
||||
height: 156px;
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
Settings Form
|
||||
-----------------------------------------------------------------------------*/
|
||||
.settings.form .content article {
|
||||
margin: 1em;
|
||||
display: inline-block;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
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;
|
||||
}
|
137
frontEndSrc/css/src/responsive.css
Normal file
137
frontEndSrc/css/src/responsive.css
Normal file
@ -0,0 +1,137 @@
|
||||
/* ----------------------------------------------------------------------------
|
||||
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-left: auto;
|
||||
margin-right: auto;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
table td {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
table tbody,
|
||||
table.media-details {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
table.media-details td {
|
||||
display: block;
|
||||
text-align: left !important;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
3
frontEndSrc/cssfilter.js
Normal file
3
frontEndSrc/cssfilter.js
Normal file
@ -0,0 +1,3 @@
|
||||
module.exports = function filter(filename) {
|
||||
return ! String(filename).includes('min');
|
||||
}
|
353
frontEndSrc/js/anime-client.js
Normal file
353
frontEndSrc/js/anime-client.js
Normal file
@ -0,0 +1,353 @@
|
||||
// -------------------------------------------------------------------------
|
||||
// ! Base
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
const matches = (elm, selector) => {
|
||||
let m = (elm.document || elm.ownerDocument).querySelectorAll(selector);
|
||||
let i = matches.length;
|
||||
while (--i >= 0 && m.item(i) !== elm) {};
|
||||
return i > -1;
|
||||
}
|
||||
|
||||
const AnimeClient = {
|
||||
/**
|
||||
* Placeholder function
|
||||
*/
|
||||
noop: () => {},
|
||||
/**
|
||||
* DOM selector
|
||||
*
|
||||
* @param {string} selector - The dom selector string
|
||||
* @param {Element} [context]
|
||||
* @return 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 () {
|
||||
const el = AnimeClient.$('header')[0];
|
||||
el.scrollIntoView(true);
|
||||
},
|
||||
/**
|
||||
* Hide the selected element
|
||||
*
|
||||
* @param {string|Element|Element[]} sel - the selector of the element to hide
|
||||
* @return {void}
|
||||
*/
|
||||
hide (sel) {
|
||||
if (typeof sel === 'string') {
|
||||
sel = AnimeClient.$(sel);
|
||||
}
|
||||
|
||||
if (Array.isArray(sel)) {
|
||||
sel.forEach(el => el.setAttribute('hidden', 'hidden'));
|
||||
} else {
|
||||
sel.setAttribute('hidden', 'hidden');
|
||||
}
|
||||
},
|
||||
/**
|
||||
* UnHide the selected element
|
||||
*
|
||||
* @param {string|Element|Element[]} sel - the selector of the element to hide
|
||||
* @return {void}
|
||||
*/
|
||||
show (sel) {
|
||||
if (typeof sel === 'string') {
|
||||
sel = AnimeClient.$(sel);
|
||||
}
|
||||
|
||||
if (Array.isArray(sel)) {
|
||||
sel.forEach(el => el.removeAttribute('hidden'));
|
||||
} else {
|
||||
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 {Element} current - the current Element
|
||||
* @param {string} parentSelector - selector for the parent element
|
||||
* @return {Element|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|Element} sel - the parent selector to bind to
|
||||
* @param {string} event - event name(s) to bind
|
||||
* @param {string|Element|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 {XMLHttpRequest}
|
||||
*/
|
||||
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);
|
||||
|
||||
if (method === 'GET') {
|
||||
request.send(null);
|
||||
} else {
|
||||
request.send(config.data);
|
||||
}
|
||||
|
||||
return request
|
||||
};
|
||||
|
||||
/**
|
||||
* Do a get request
|
||||
*
|
||||
* @param {string} url
|
||||
* @param {object|function} data
|
||||
* @param {function} [callback]
|
||||
* @return {XMLHttpRequest}
|
||||
*/
|
||||
AnimeClient.get = (url, data, callback = null) => {
|
||||
if (callback === null) {
|
||||
callback = data;
|
||||
data = {};
|
||||
}
|
||||
|
||||
return AnimeClient.ajax(url, {
|
||||
data,
|
||||
success: callback
|
||||
});
|
||||
};
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Export
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
export default AnimeClient;
|
128
frontEndSrc/js/anime.js
Normal file
128
frontEndSrc/js/anime.js
Normal file
@ -0,0 +1,128 @@
|
||||
import _ from './anime-client.js'
|
||||
import { renderSearchResults } from './template-helpers.js'
|
||||
import { getNestedProperty, hasNestedProperty } from "./fns";
|
||||
|
||||
const search = (query, isCollection = false) => {
|
||||
// Show the loader
|
||||
_.show('.cssload-loader');
|
||||
|
||||
// Do the api search
|
||||
return _.get(_.url('/anime-collection/search'), { query }, (searchResults, status) => {
|
||||
searchResults = JSON.parse(searchResults);
|
||||
|
||||
// Hide the loader
|
||||
_.hide('.cssload-loader');
|
||||
|
||||
// Show the results
|
||||
_.$('#series-list')[ 0 ].innerHTML = renderSearchResults('anime', searchResults, isCollection);
|
||||
});
|
||||
};
|
||||
|
||||
// Anime list search
|
||||
if (_.hasElement('.anime #search')) {
|
||||
let prevRequest = null;
|
||||
|
||||
_.on('#search', 'input', _.throttle(250, (e) => {
|
||||
const query = encodeURIComponent(e.target.value);
|
||||
if (query === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (prevRequest !== null) {
|
||||
prevRequest.abort();
|
||||
}
|
||||
|
||||
prevRequest = search(query);
|
||||
}));
|
||||
}
|
||||
|
||||
// Anime collection search
|
||||
if (_.hasElement('#search-anime-collection')) {
|
||||
let prevRequest = null;
|
||||
|
||||
_.on('#search-anime-collection', 'input', _.throttle(250, (e) => {
|
||||
const query = encodeURIComponent(e.target.value);
|
||||
if (query === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (prevRequest !== null) {
|
||||
prevRequest.abort();
|
||||
}
|
||||
|
||||
prevRequest = search(query, true);
|
||||
}));
|
||||
}
|
||||
|
||||
// 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,
|
||||
anilist_id: parentSel.dataset.anilistId,
|
||||
mal_id: parentSel.dataset.malId,
|
||||
data: {
|
||||
progress: watchedCount + 1
|
||||
}
|
||||
};
|
||||
|
||||
const displayMessage = (type, message) => {
|
||||
_.hide('#loading-shadow');
|
||||
_.showMessage(type, `${message} ${title}`);
|
||||
_.scrollToTop();
|
||||
}
|
||||
|
||||
const showError = () => displayMessage('error', 'Failed to update');
|
||||
|
||||
// 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');
|
||||
|
||||
// okay, lets actually make some changes!
|
||||
_.ajax(_.url('/anime/increment'), {
|
||||
data,
|
||||
dataType: 'json',
|
||||
type: 'POST',
|
||||
success: (res) => {
|
||||
try {
|
||||
const resData = JSON.parse(res);
|
||||
|
||||
// Do a rough sanity check for weird errors
|
||||
let updatedProgress = getNestedProperty(resData, 'data.libraryEntry.update.libraryEntry.progress');
|
||||
if (hasNestedProperty(resData, 'error') || updatedProgress !== data.data.progress) {
|
||||
showError();
|
||||
return;
|
||||
}
|
||||
|
||||
// We've completed the series
|
||||
if (getNestedProperty(resData, 'data.libraryEntry.update.libraryEntry.status') === 'COMPLETED') {
|
||||
_.hide(parentSel);
|
||||
displayMessage('success', 'Completed')
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Just a normal update
|
||||
_.$('.completed_number', parentSel)[ 0 ].textContent = ++watchedCount;
|
||||
displayMessage('success', 'Updated');
|
||||
} catch (_) {
|
||||
showError();
|
||||
}
|
||||
},
|
||||
error: showError,
|
||||
});
|
||||
});
|
83
frontEndSrc/js/base/sort-tables.js
Normal file
83
frontEndSrc/js/base/sort-tables.js
Normal file
@ -0,0 +1,83 @@
|
||||
const LightTableSorter = (() => {
|
||||
let th = null;
|
||||
let cellIndex = null;
|
||||
let order = '';
|
||||
const text = (row) => row.cells.item(cellIndex).textContent.toLowerCase();
|
||||
const sort = (a, b) => {
|
||||
let textA = text(a);
|
||||
let textB = text(b);
|
||||
console.log("Comparing " + textA + " and " + textB)
|
||||
|
||||
if(th.classList.contains("numeric")){
|
||||
let arrayA = textA.replace('episodes: ','').replace('-',0).split("/");
|
||||
let arrayB = textB.replace('episodes: ','').replace('-',0).split("/");
|
||||
if(arrayA.length > 1) {
|
||||
textA = parseInt(arrayA[0],10) / parseInt(arrayA[1],10);
|
||||
textB = parseInt(arrayB[0],10) / parseInt(arrayB[1],10);
|
||||
}
|
||||
else{
|
||||
textA = parseInt(arrayA[0],10);
|
||||
textB = parseInt(arrayB[0],10);
|
||||
}
|
||||
}
|
||||
else if (parseInt(textA, 10)) {
|
||||
textA = parseInt(textA, 10);
|
||||
textB = parseInt(textB, 10);
|
||||
}
|
||||
if (textA > textB) {
|
||||
return 1;
|
||||
}
|
||||
if (textA < textB) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
const toggle = () => {
|
||||
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.add('sorting');
|
||||
return order = '';
|
||||
};
|
||||
const onClickEvent = (e) => {
|
||||
if (th && (cellIndex !== e.target.cellIndex)) {
|
||||
reset();
|
||||
}
|
||||
th = e.target;
|
||||
if (th.nodeName.toLowerCase() === 'th') {
|
||||
cellIndex = th.cellIndex;
|
||||
const tbody = th.offsetParent.getElementsByTagName('tbody')[0];
|
||||
let rows = Array.from(tbody.rows);
|
||||
if (rows) {
|
||||
rows.sort(sort);
|
||||
if (order === 'sorting-asc') {
|
||||
rows.reverse();
|
||||
}
|
||||
toggle();
|
||||
tbody.innerHtml = '';
|
||||
|
||||
rows.forEach(row => {
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
return {
|
||||
init: () => {
|
||||
let ths = document.getElementsByTagName('th');
|
||||
let results = [];
|
||||
for (let i = 0, len = ths.length; i < len; i++) {
|
||||
let th = ths[i];
|
||||
th.classList.add('sorting');
|
||||
th.classList.add('testing');
|
||||
results.push(th.onclick = onClickEvent);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
LightTableSorter.init();
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user