Full Anilist settings page OAuth flow, ability to run app without manually editing config files. See #7. Resolves #5

This commit is contained in:
Timothy Warren 2018-10-09 18:10:20 -04:00
parent 7a2bb1ba05
commit fec30b7e36
23 changed files with 412 additions and 244 deletions

View File

@ -30,6 +30,14 @@ return array_merge($tomlConfig, [
'asset_dir' => "{$ROOT_DIR}/public",
'base_config_dir' => __DIR__,
'config_dir' => "{$APP_DIR}/config",
// No config defaults
'kitsu_username' => 'timw4mail',
'whose_list' => 'Someone',
'cache' => [
'connection' => [],
'driver' => 'null',
],
// Routing defaults
'asset_path' => '/public',

View File

@ -187,11 +187,13 @@ return [
'path' => '/anilist-redirect',
'action' => 'anilistRedirect',
'controller' => DEFAULT_CONTROLLER,
'verb' => 'get',
],
'anilist-oauth' => [
'anilist-callback' => [
'path' => '/anilist-oauth',
'action' => 'anilistCallback',
'controller' => DEFAULT_CONTROLLER,
'verb' => 'get',
],
'image_proxy' => [
'path' => '/public/images/{type}/{file}',

View File

@ -3,14 +3,6 @@
// $fields
// $hiddenFields
// $nestedPrefix
if ( ! function_exists('subfieldRender'))
{
function subfieldRender ($nestedPrefix, $fields, &$hiddenFields, $helper, $section)
{
include '_form.php';
}
}
?>
<?php foreach ($fields as $name => $field): ?>
@ -18,7 +10,7 @@ if ( ! function_exists('subfieldRender'))
<?php if ($field['type'] === 'subfield'): ?>
<section>
<h4><?= $field['title'] ?></h4>
<?php subfieldRender($fieldname, $field['fields'], $hiddenFields, $helper, $section); ?>
<?php include_once '_form.php'; ?>
</section>
<?php elseif ( ! empty($field['display'])): ?>
<article>

View File

@ -43,7 +43,7 @@ $hasManga = stripos($_SERVER['REQUEST_URI'], 'manga') !== FALSE;
[<?= $helper->a($urlGenerator->defaultUrl('anime') . $extraSegment, 'Anime List') ?>]
[<?= $helper->a($urlGenerator->defaultUrl('manga') . $extraSegment, 'Manga List') ?>]
<?php endif ?>
<?php if ($auth->isAuthenticated()): ?>
<?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>

View File

@ -29,13 +29,34 @@ $nestedPrefix = 'config';
<label for="settings-tab<?= $i ?>"><h3><?= $sectionMapping[$section] ?></h3></label>
<section class="content">
<?php require __DIR__ . '/_form.php' ?>
<?php if ($section === 'anilist'): ?>
<hr />
<?php $auth = $anilistModel->checkAuth(); ?>
<?php if (array_key_exists('errors', $auth)): ?>
<p class="static-message error">Not Authorized.</p>
<?= $helper->a(
$url->generate('anilist-redirect'),
'Link Anilist Account'
) ?>
<?php else: ?>
<?php $expires = $config->get(['anilist', 'access_token_expires']); ?>
<p class="static-message info">
Linked to Anilist. Your access token will expire around <?= date('F j, Y, g:i a T', $expires) ?>
</p>
<?= $helper->a(
$url->generate('anilist-redirect'),
'Update Access Token'
) ?>
<?php endif ?>
<?php endif ?>
</section>
<?php $i++; ?>
<?php endforeach ?>
</div>
<br />
<?php foreach ($hiddenFields as $field): ?>
<?= $field ?>
<?= $field->__toString() ?>
<?php endforeach ?>
<button type="submit">Save Changes</button>
</main>

File diff suppressed because one or more lines are too long

View File

@ -310,7 +310,7 @@ a:hover, a:active {
Message boxes
------------------------------------------------------------------------------*/
.message {
.message, .static-message {
position: relative;
margin: 0.5em auto;
padding: 0.5em;
@ -342,7 +342,7 @@ a:hover, a:active {
margin-right: 1em;
}
.message.error {
.message.error, .static-message.error {
border: 1px solid #924949;
background: #f3e6e6;
}
@ -351,7 +351,7 @@ a:hover, a:active {
content: '✘';
}
.message.success {
.message.success, .static-message.success {
border: 1px solid #1f8454;
background: #70dda9;
}
@ -360,7 +360,7 @@ a:hover, a:active {
content: '✔'
}
.message.info {
.message.info, .static-message.info {
border: 1px solid #bfbe3a;
background: #FFFFCC;
}

View File

@ -30,6 +30,7 @@ use Aviat\AnimeClient\API\Enum\{
*/
final class Anilist {
public const AUTH_URL = 'https://anilist.co/api/v2/oauth/authorize';
public const TOKEN_URL = 'https://anilist.co/api/v2/oauth/token';
public const BASE_URL = 'https://graphql.anilist.co';
public const KITSU_ANILIST_WATCHING_STATUS_MAP = [

View File

@ -16,15 +16,17 @@
namespace Aviat\AnimeClient\API\Anilist;
use const Aviat\AnimeClient\USER_AGENT;
use function Amp\Promise\wait;
use Amp\Artax\Request;
use Amp\Artax\Response;
use function Amp\Promise\wait;
use Aviat\AnimeClient\API\{
Anilist,
HummingbirdClient
};
use const Aviat\AnimeClient\SESSION_SEGMENT;
use Aviat\Ion\Json;
use Aviat\Ion\Di\ContainerAware;
@ -52,7 +54,7 @@ trait AnilistTrait {
'Accept' => 'application/json',
'Accept-Encoding' => 'gzip',
'Content-type' => 'application/json',
'User-Agent' => "Tim's Anime Client/4.0"
'User-Agent' => USER_AGENT,
];
/**
@ -80,13 +82,10 @@ trait AnilistTrait {
$anilistConfig = $config->get('anilist');
$request = $this->requestBuilder->newRequest('POST', $url);
$sessionSegment = $this->getContainer()
->get('session')
->getSegment(SESSION_SEGMENT);
//$authenticated = $sessionSegment->get('auth_token') !== NULL;
//if ($authenticated)
// You can only authenticate the request if you
// actually have an access_token saved
if ($config->has(['anilist', 'access_token']))
{
$request = $request->setAuth('bearer', $anilistConfig['access_token']);
}
@ -245,7 +244,7 @@ trait AnilistTrait {
{
$response = $this->getResponse(Anilist::BASE_URL, $options);
$validResponseCodes = [200, 201];
$logger = NULL;
if ($this->getContainer())
{

View File

@ -0,0 +1,6 @@
query {
Viewer {
id
name
}
}

View File

@ -16,11 +16,15 @@
namespace Aviat\AnimeClient\API\Anilist;
use function Amp\Promise\wait;
use InvalidArgumentException;
use Amp\Artax\Request;
use Aviat\AnimeClient\API\Anilist;
use Aviat\AnimeClient\API\Mapping\{AnimeWatchingStatus, MangaReadingStatus};
use Aviat\AnimeClient\Types\FormItem;
use Aviat\Ion\Json;
/**
* Anilist API Model
@ -47,6 +51,42 @@ final class Model
// ! Generic API calls
// -------------------------------------------------------------------------
/**
* Attempt to get an auth token
*
* @param string $code - The request token
* @param string $redirectUri - The oauth callback url
* @return array
*/
public function authenticate(string $code, string $redirectUri): array
{
$config = $this->getContainer()->get('config');
$request = $this->requestBuilder
->newRequest('POST', Anilist::TOKEN_URL)
->setJsonBody([
'grant_type' => 'authorization_code',
'client_id' => $config->get(['anilist', 'client_id']),
'client_secret' => $config->get(['anilist', 'client_secret']),
'redirect_uri' => $redirectUri,
'code' => $code,
])
->getFullRequest();
$response = $this->getResponseFromRequest($request);
return Json::decode(wait($response->getBody()));
}
/**
* Check auth status with simple API call
*
* @return array
*/
public function checkAuth(): array
{
return $this->runQuery('CheckLogin');
}
/**
* Get user list data for syncing with Kitsu
*

View File

@ -170,9 +170,11 @@ final class Auth {
*/
public function get_auth_token()
{
$now = time();
$token = $this->segment->get('auth_token', FALSE);
$refreshToken = $this->segment->get('refresh_token', FALSE);
$isExpired = time() >= $this->segment->get('auth_token_expires', 0);
$isExpired = time() > $this->segment->get('auth_token_expires', $now + 5000);
// Attempt to re-authenticate with refresh token
/* if ($isExpired && $refreshToken)

View File

View File

View File

@ -56,26 +56,6 @@ function loadToml(string $path): array
return $output;
}
/**
* Load configuration from toml files, keyed by the original file
*
* @param string $path
* @return array
*/
function loadTomlByFile(string $path): array
{
$output = [];
$files = glob("{$path}/*.toml");
foreach ($files as $file)
{
$config = Toml::parseFile($file);
$output[basename($file)] = $config;
}
return $output;
}
/**
* Load config from one specific TOML file
*
@ -179,6 +159,7 @@ function checkFolderPermissions(ConfigInterface $config): array
$publicDir = $config->get('asset_dir');
$pathMap = [
'app/config' => realpath(__DIR__ . '/../app/config'),
'app/logs' => realpath(__DIR__ . '/../app/logs'),
'public/images/avatars' => "{$publicDir}/images/avatars",
'public/images/anime' => "{$publicDir}/images/anime",

View File

@ -70,12 +70,18 @@ class BaseCommand extends Command {
$APP_DIR = realpath(__DIR__ . '/../../app');
$APPCONF_DIR = realpath("{$APP_DIR}/appConf/");
$CONF_DIR = realpath("{$APP_DIR}/config/");
$base_config = require $APPCONF_DIR . '/base_config.php';
$baseConfig = require $APPCONF_DIR . '/base_config.php';
$config = loadToml($CONF_DIR);
$config_array = array_merge($base_config, $config);
$di = function ($config_array) use ($APP_DIR) {
$overrideFile = $CONF_DIR . '/admin-override.toml';
$overrideConfig = file_exists($overrideFile)
? loadTomlFile($overrideFile)
: [];
$configArray = array_replace_recursive($baseConfig, $config, $overrideConfig);
$di = function ($configArray) use ($APP_DIR) {
$container = new Container();
// -------------------------------------------------------------------------
@ -93,8 +99,8 @@ class BaseCommand extends Command {
$container->setLogger($kitsu_request_logger, 'kitsu-request');
// Create Config Object
$container->set('config', function() use ($config_array) {
return new Config($config_array);
$container->set('config', function() use ($configArray) {
return new Config($configArray);
});
// Create Cache Object
@ -148,6 +154,6 @@ class BaseCommand extends Command {
return $container;
};
return $di($config_array);
return $di($configArray);
}
}

View File

@ -60,6 +60,16 @@ final class SyncLists extends BaseCommand {
{
$this->setContainer($this->setupContainer());
$this->setCache($this->container->get('cache'));
$config = $this->container->get('config');
$anilistEnabled = $config->get(['anilist', 'enabled']);
if ( ! $anilistEnabled)
{
$this->echoBox('Anlist API is not enabled. Can not sync.');
return;
}
$this->anilistModel = $this->container->get('anilist-model');
$this->kitsuModel = $this->container->get('kitsu-model');

View File

@ -27,12 +27,21 @@ use Aviat\Ion\View\HtmlView;
* Controller for handling routes that don't fit elsewhere
*/
final class Index extends BaseController {
/**
* @var \Aviat\API\Anilist\Model
*/
private $anilistModel;
/**
* @var \Aviat\AnimeClient\Model\Settings
*/
private $settingsModel;
public function __construct(ContainerInterface $container)
{
parent::__construct($container);
$this->anilistModel = $container->get('anilist-model');
$this->settingsModel = $container->get('settings-model');
}
@ -83,10 +92,11 @@ final class Index extends BaseController {
$redirectUrl = 'https://anilist.co/api/v2/oauth/authorize?' .
http_build_query([
'client_id' => $this->config->get(['anilist', 'client_id']),
'redirect_uri' => $this->urlGenerator->url('/anilist-oauth'),
'response_type' => 'code',
]);
$this->redirect($redirectUrl, 301);
$this->redirect($redirectUrl, 303);
}
/**
@ -94,10 +104,47 @@ final class Index extends BaseController {
*/
public function anilistCallback()
{
dump($_GET);
$this->outputHTML('blank', [
'title' => 'Oauth!'
]);
$query = $this->request->getQueryParams();
$authCode = $query['code'];
$uri = $this->urlGenerator->url('/anilist-oauth');
$authData = $this->anilistModel->authenticate($authCode, $uri);
$settings = $this->settingsModel->getSettings();
if (array_key_exists('error', $authData))
{
$this->errorPage(400, 'Error Linking Account', $authData['hint']);
return;
}
// Update the override config file
$anilistSettings = [
'access_token' => $authData['access_token'],
'access_token_expires' => (time() - 10) + $authData['expires_in'],
'refresh_token' => $authData['refresh_token'],
];
$newSettings = $settings;
$newSettings['anilist'] = array_merge($settings['anilist'], $anilistSettings);
foreach($newSettings['config'] as $key => $value)
{
$newSettings[$key] = $value;
}
unset($newSettings['config']);
$saved = $this->settingsModel->saveSettingsFile($newSettings);
if ($saved)
{
$this->setFlashMessage('Linked Anilist Account', 'success');
}
else
{
$this->setFlashMessage('Error Linking Anilist Account', 'error');
}
$this->redirect($this->url->generate('settings'), 303);
}
/**
@ -165,11 +212,13 @@ final class Index extends BaseController {
$auth = $this->container->get('auth');
$form = $this->settingsModel->getSettingsForm();
// dump($this->session->getFlash('message'));
$hasAnilistLogin = $this->config->has(['anilist','access_token']);
$this->outputHTML('settings', [
'anilistModel' => $this->anilistModel,
'auth' => $auth,
'form' => $form,
'hasAnilistLogin' => $hasAnilistLogin,
'config' => $this->config,
'title' => $this->config->get('whose_list') . "'s Settings",
]);
@ -183,6 +232,7 @@ final class Index extends BaseController {
public function settings_post()
{
$post = $this->request->getParsedBody();
unset($post['settings-tabs']);
// dump($post);
$saved = $this->settingsModel->saveSettingsFile($post);

View File

@ -79,11 +79,6 @@ final class FormGenerator {
switch($type)
{
case 'boolean':
/* $params['type'] = 'checkbox';
$params['attribs']['label'] = $form['description'];
$params['attribs']['value'] = TRUE;
$params['attribs']['value_unchecked'] = '0'; */
$params['type'] = 'radio';
$params['options'] = [
'1' => 'Yes',

View File

@ -16,6 +16,8 @@
namespace Aviat\AnimeClient\Model;
use const Aviat\AnimeClient\SETTINGS_MAP;
use function Aviat\AnimeClient\arrayToToml;
use function Aviat\Ion\_dir;
@ -34,177 +36,6 @@ final class Settings {
private $config;
/**
* Map the config values to types and form fields
*/
private const SETTINGS_MAP = [
'anilist' => [
'enabled' => [
'type' => 'boolean',
'title' => 'Enable Anilist Integration',
'default' => FALSE,
'description' => 'Enable syncing data between Kitsu and Anilist. Requires appropriate API keys to be set in config',
],
'client_id' => [
'type' => 'string',
'title' => 'Anilist API Client ID',
'default' => '',
'description' => 'The client id for your Anilist API application',
],
'client_secret' => [
'type' => 'string',
'title' => 'Anilist API Client Secret',
'default' => '',
'description' => 'The client secret for your Anilist API application',
],
],
'config' => [
'kitsu_username' => [
'type' => 'string',
'title' => 'Kitsu Username',
'default' => '',
'description' => 'Username of the account to pull list data from.',
],
'whose_list' => [
'type' => 'string',
'title' => 'Whose List',
'default' => 'Somebody',
'description' => 'Name of the owner of the list data.',
],
'show_anime_collection' => [
'type' => 'boolean',
'title' => 'Show Anime Collection',
'default' => FALSE,
'description' => 'Should the anime collection be shown?',
],
'show_manga_collection' => [
'type' => 'boolean',
'title' => 'Show Manga Collection',
'default' => FALSE,
'description' => 'Should the manga collection be shown?',
],
'default_list' => [
'type' => 'select',
'title' => 'Default List',
'description' => 'Which list to show by default.',
'options' => [
'Anime' => 'anime',
'Manga' => 'manga',
],
],
'default_anime_list_path' => [ //watching|plan_to_watch|on_hold|dropped|completed|all
'type' => 'select',
'title' => 'Default Anime List Section',
'description' => 'Which part of the anime list to show by default.',
'options' => [
'Watching' => 'watching',
'Plan to Watch' => 'plan_to_watch',
'On Hold' => 'on_hold',
'Dropped' => 'dropped',
'Completed' => 'completed',
'All' => 'all',
]
],
'default_manga_list_path' => [ //reading|plan_to_read|on_hold|dropped|completed|all
'type' => 'select',
'title' => 'Default Manga List Section',
'description' => 'Which part of the manga list to show by default.',
'options' => [
'Reading' => 'reading',
'Plan to Read' => 'plan_to_read',
'On Hold' => 'on_hold',
'Dropped' => 'dropped',
'Completed' => 'completed',
'All' => 'all',
]
]
],
'cache' => [
'driver' => [
'type' => 'select',
'title' => 'Cache Type',
'description' => 'The Cache backend',
'options' => [
'APCu' => 'apcu',
'Memcached' => 'memcached',
'Redis' => 'redis',
'No Cache' => 'null'
],
],
'connection' => [
'type' => 'subfield',
'title' => 'Connection',
'fields' => [
'host' => [
'type' => 'string',
'title' => 'Cache Host',
'description' => 'Host of the cache backend to connect to',
],
'port' => [
'type' => 'string',
'title' => 'Cache Port',
'description' => 'Port of the cache backend to connect to',
'default' => NULL,
],
'password' => [
'type' => 'string',
'title' => 'Cache Password',
'description' => 'Password to connect to cache backend',
'default' => NULL,
],
'database' => [
'type' => 'string',
'title' => 'Cache Database',
'description' => 'Cache database number for Redis',
],
],
],
],
'database' => [
'type' => [
'type' => 'select',
'title' => 'Database Type',
'options' => [
'MySQL' => 'mysql',
'PostgreSQL' => 'pgsql',
'SQLite' => 'sqlite',
],
'description' => 'Type of database to connect to',
],
'host' => [
'type' => 'string',
'title' => 'Host',
'description' => 'The host of the database server',
],
'user' => [
'type' => 'string',
'title' => 'User',
'description' => 'Database connection user',
],
'pass' => [
'type' => 'string',
'title' => 'Password',
'description' => 'Database connection password'
],
'port' => [
'type' => 'string',
'title' => 'Port',
'description' => 'Database connection port',
'default' => NULL,
],
'database' => [
'type' => 'string',
'title' => 'Database Name',
'description' => 'Name of the database/schema to connect to',
],
'file' => [
'type' => 'string',
'title' => 'Database File',
'description' => 'Path to the database file, if required by the current database type.'
],
],
];
public function __construct(ConfigInterface $config)
{
$this->config = $config;
@ -216,7 +47,7 @@ final class Settings {
'config' => [],
];
foreach(static::SETTINGS_MAP as $file => $values)
foreach(SETTINGS_MAP as $file => $values)
{
if ($file === 'config')
{
@ -243,7 +74,9 @@ final class Settings {
foreach($settings as $file => $values)
{
foreach(static::SETTINGS_MAP[$file] as $key => $value)
$values = $values ?? [];
foreach(SETTINGS_MAP[$file] as $key => $value)
{
if ($value['type'] === 'subfield')
{
@ -317,7 +150,7 @@ final class Settings {
$looseConfig[$key] = $val;
}
}
elseif (is_array($val))
elseif (is_array($val) && ! empty($val))
{
foreach($val as $k => $v)
{
@ -357,7 +190,11 @@ final class Settings {
public function saveSettingsFile(array $settings): bool
{
$settings = $settings['config'];
$configWrapped = (count(array_keys($settings)) === 1 && array_key_exists('config', $settings));
if ($configWrapped)
{
$settings = $settings['config'];
}
try
{
@ -365,6 +202,9 @@ final class Settings {
}
catch (UndefinedPropertyException $e)
{
dump($e);
dump($settings);
die();
return FALSE;
}

View File

@ -23,11 +23,10 @@ class Anilist extends AbstractType {
public $client_id;
public $client_secret;
public $redirect_uri;
public $access_token;
public $access_token_expires;
public $refresh_token;
public $user_id;
public $username;
}

View File

@ -88,7 +88,9 @@ class UrlGenerator extends RoutingBase {
}
$path = implode('/', $path_segments);
return "//{$this->host}/{$path}";
$scheme = ($_SERVER['HTTPS'] === 'on') ? 'https:' : '';
return "{$scheme}//{$this->host}/{$path}";
}
/**

View File

@ -27,4 +27,218 @@ const SRC_DIR = __DIR__;
const USER_AGENT = "Tim's Anime Client/4.0";
// Why doesn't this already exist?
const MILLI_FROM_NANO = 1000 * 1000;
const MILLI_FROM_NANO = 1000 * 1000;
/**
* Map config settings to form fields
*/
const SETTINGS_MAP = [
'anilist' => [
'enabled' => [
'type' => 'boolean',
'title' => 'Enable Anilist Integration',
'default' => FALSE,
'description' => 'Enable syncing data between Kitsu and Anilist. Requires appropriate API keys to be set in config',
],
'client_id' => [
'type' => 'string',
'title' => 'Anilist API Client ID',
'default' => '',
'description' => 'The client id for your Anilist API application',
],
'client_secret' => [
'type' => 'string',
'title' => 'Anilist API Client Secret',
'default' => '',
'description' => 'The client secret for your Anilist API application',
],
'username' => [
'type' => 'string',
'title' => 'Anilist Username',
'default' => '',
'readonly' => TRUE,
'description' => 'Login username for Anilist account to integrate with',
],
'access_token' => [
'type' => 'hidden',
'title' => 'API Access Token',
'default' => '',
'description' => 'The Access code for accessing the Anilist API',
'readonly' => TRUE,
],
'access_token_expires' => [
'type' => 'string',
'title' => 'Expiration timestamp of the access token',
'default' => '0',
'description' => 'The unix timestamp of when the access token expires.',
'readonly' => TRUE,
],
'refresh_token' => [
'type' => 'string',
'title' => 'API Refresh Token',
'default' => '',
'description' => 'Token to refresh the access token before it expires',
'readonly' => TRUE,
],
],
'cache' => [
'driver' => [
'type' => 'select',
'title' => 'Cache Type',
'description' => 'The Cache backend',
'options' => [
'APCu' => 'apcu',
'Memcached' => 'memcached',
'Redis' => 'redis',
'No Cache' => 'null'
],
],
'connection' => [
'type' => 'subfield',
'title' => 'Connection',
'fields' => [
'host' => [
'type' => 'string',
'title' => 'Cache Host',
'description' => 'Host of the cache backend to connect to',
],
'port' => [
'type' => 'string',
'title' => 'Cache Port',
'description' => 'Port of the cache backend to connect to',
'default' => NULL,
],
'password' => [
'type' => 'string',
'title' => 'Cache Password',
'description' => 'Password to connect to cache backend',
'default' => NULL,
],
'persistent' => [
'type' => 'boolean',
'title' => 'Persistent Cache Connection',
'description' => 'Whether to have a persistent connection to the cache',
'default' => FALSE,
],
'database' => [
'type' => 'string',
'title' => 'Cache Database',
'default' => '1',
'description' => 'Cache database number for Redis',
],
],
],
/*'options' => [
'type' => 'subfield',
'title' => 'Options',
'fields' => [],
] */
],
'config' => [
'kitsu_username' => [
'type' => 'string',
'title' => 'Kitsu Username',
'default' => '',
'description' => 'Username of the account to pull list data from.',
],
'whose_list' => [
'type' => 'string',
'title' => 'Whose List',
'default' => 'Somebody',
'description' => 'Name of the owner of the list data.',
],
'show_anime_collection' => [
'type' => 'boolean',
'title' => 'Show Anime Collection',
'default' => FALSE,
'description' => 'Should the anime collection be shown?',
],
'show_manga_collection' => [
'type' => 'boolean',
'title' => 'Show Manga Collection',
'default' => FALSE,
'description' => 'Should the manga collection be shown?',
],
'default_list' => [
'type' => 'select',
'title' => 'Default List',
'description' => 'Which list to show by default.',
'options' => [
'Anime' => 'anime',
'Manga' => 'manga',
],
],
'default_anime_list_path' => [ //watching|plan_to_watch|on_hold|dropped|completed|all
'type' => 'select',
'title' => 'Default Anime List Section',
'description' => 'Which part of the anime list to show by default.',
'options' => [
'Watching' => 'watching',
'Plan to Watch' => 'plan_to_watch',
'On Hold' => 'on_hold',
'Dropped' => 'dropped',
'Completed' => 'completed',
'All' => 'all',
]
],
'default_manga_list_path' => [ //reading|plan_to_read|on_hold|dropped|completed|all
'type' => 'select',
'title' => 'Default Manga List Section',
'description' => 'Which part of the manga list to show by default.',
'options' => [
'Reading' => 'reading',
'Plan to Read' => 'plan_to_read',
'On Hold' => 'on_hold',
'Dropped' => 'dropped',
'Completed' => 'completed',
'All' => 'all',
]
]
],
'database' => [
'type' => [
'type' => 'select',
'title' => 'Database Type',
'options' => [
'MySQL' => 'mysql',
'PostgreSQL' => 'pgsql',
'SQLite' => 'sqlite',
],
'default' => 'sqlite',
'description' => 'Type of database to connect to',
],
'host' => [
'type' => 'string',
'title' => 'Host',
'description' => 'The host of the database server',
],
'user' => [
'type' => 'string',
'title' => 'User',
'description' => 'Database connection user',
],
'pass' => [
'type' => 'string',
'title' => 'Password',
'description' => 'Database connection password'
],
'port' => [
'type' => 'string',
'title' => 'Port',
'description' => 'Database connection port',
'default' => NULL,
],
'database' => [
'type' => 'string',
'title' => 'Database Name',
'description' => 'Name of the database/schema to connect to',
],
'file' => [
'type' => 'string',
'title' => 'Database File',
'description' => 'Path to the database file, if required by the current database type.',
'default' => 'anime_collection.sqlite',
],
],
];