Drastically simplify setup for bundling js files
This commit is contained in:
parent
7839cf1515
commit
4c85c22c30
@ -11,12 +11,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<script nomodule="nomodule" src="https://polyfill.io/v3/polyfill.min.js?features=es5%2CObject.assign"></script>
|
<script nomodule="nomodule" src="https://polyfill.io/v3/polyfill.min.js?features=es5%2CObject.assign"></script>
|
||||||
<?php if ($auth->isAuthenticated()): ?>
|
<script async="async" defer="defer" src="<?= $urlGenerator->assetUrl('js/scripts.min.js') ?>"></script>
|
||||||
<script nomodule='nomodule' async="async" defer="defer" src="<?= $urlGenerator->assetUrl('js/scripts.min.js') ?>"></script>
|
|
||||||
<script type="module" src="<?= $urlGenerator->assetUrl('es/scripts.js') ?>"></script>
|
|
||||||
<?php else: ?>
|
|
||||||
<script nomodule="nomodule" async="async" defer="defer" src="<?= $urlGenerator->assetUrl('js/anon.min.js') ?>"></script>
|
|
||||||
<script type="module" src="<?= $urlGenerator->assetUrl('es/anon.js') ?>"></script>
|
|
||||||
<?php endif ?>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -1,68 +0,0 @@
|
|||||||
import compiler from '@ampproject/rollup-plugin-closure-compiler';
|
|
||||||
|
|
||||||
const plugins = [
|
|
||||||
compiler({
|
|
||||||
assumeFunctionWrapper: true,
|
|
||||||
compilationLevel: 'WHITESPACE_ONLY', //'ADVANCED',
|
|
||||||
createSourceMap: true,
|
|
||||||
env: 'BROWSER',
|
|
||||||
languageIn: 'ECMASCRIPT_2018',
|
|
||||||
languageOut: 'ES3'
|
|
||||||
})
|
|
||||||
];
|
|
||||||
|
|
||||||
const defaultOutput = {
|
|
||||||
format: 'iife',
|
|
||||||
sourcemap: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
const nonModules = [{
|
|
||||||
input: './js/anon.js',
|
|
||||||
output: {
|
|
||||||
...defaultOutput,
|
|
||||||
file: '../public/js/anon.min.js',
|
|
||||||
sourcemapFile: '../public/js/anon.min.js.map',
|
|
||||||
},
|
|
||||||
plugins,
|
|
||||||
}, {
|
|
||||||
input: './js/index.js',
|
|
||||||
output: {
|
|
||||||
...defaultOutput,
|
|
||||||
file: '../public/js/scripts.min.js',
|
|
||||||
sourcemapFile: '../public/js/scripts.min.js.map',
|
|
||||||
},
|
|
||||||
plugins,
|
|
||||||
}, {
|
|
||||||
input: './js/base/sort-tables.js',
|
|
||||||
output: {
|
|
||||||
...defaultOutput,
|
|
||||||
file: '../public/js/tables.min.js',
|
|
||||||
sourcemapFile: '../public/js/tables.min.js.map',
|
|
||||||
},
|
|
||||||
plugins,
|
|
||||||
}];
|
|
||||||
|
|
||||||
const moduleOutput = {
|
|
||||||
format: 'es',
|
|
||||||
sourcemap: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
let modules = [{
|
|
||||||
input: './js/anon.js',
|
|
||||||
output: {
|
|
||||||
...moduleOutput,
|
|
||||||
file: '../public/es/anon.js',
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
input: './js/index.js',
|
|
||||||
output: {
|
|
||||||
...moduleOutput,
|
|
||||||
file: '../public/es/scripts.js',
|
|
||||||
},
|
|
||||||
}];
|
|
||||||
|
|
||||||
// Return the config array for rollup
|
|
||||||
export default [
|
|
||||||
...nonModules,
|
|
||||||
...modules,
|
|
||||||
];
|
|
@ -9,7 +9,7 @@ const matches = (elm, selector) => {
|
|||||||
return i > -1;
|
return i > -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AnimeClient = {
|
const AnimeClient = {
|
||||||
/**
|
/**
|
||||||
* Placeholder function
|
* Placeholder function
|
||||||
*/
|
*/
|
||||||
@ -18,8 +18,8 @@ export const AnimeClient = {
|
|||||||
* DOM selector
|
* DOM selector
|
||||||
*
|
*
|
||||||
* @param {string} selector - The dom selector string
|
* @param {string} selector - The dom selector string
|
||||||
* @param {object} [context]
|
* @param {Element} [context]
|
||||||
* @return {[HTMLElement]} - array of dom elements
|
* @return array of dom elements
|
||||||
*/
|
*/
|
||||||
$(selector, context = null) {
|
$(selector, context = null) {
|
||||||
if (typeof selector !== 'string') {
|
if (typeof selector !== 'string') {
|
||||||
@ -60,7 +60,7 @@ export const AnimeClient = {
|
|||||||
/**
|
/**
|
||||||
* Hide the selected element
|
* Hide the selected element
|
||||||
*
|
*
|
||||||
* @param {string|Element} sel - the selector of the element to hide
|
* @param {string|Element|Element[]} sel - the selector of the element to hide
|
||||||
* @return {void}
|
* @return {void}
|
||||||
*/
|
*/
|
||||||
hide (sel) {
|
hide (sel) {
|
||||||
@ -77,7 +77,7 @@ export const AnimeClient = {
|
|||||||
/**
|
/**
|
||||||
* UnHide the selected element
|
* UnHide the selected element
|
||||||
*
|
*
|
||||||
* @param {string|Element} sel - the selector of the element to hide
|
* @param {string|Element|Element[]} sel - the selector of the element to hide
|
||||||
* @return {void}
|
* @return {void}
|
||||||
*/
|
*/
|
||||||
show (sel) {
|
show (sel) {
|
||||||
@ -116,9 +116,9 @@ export const AnimeClient = {
|
|||||||
/**
|
/**
|
||||||
* Finds the closest parent element matching the passed selector
|
* Finds the closest parent element matching the passed selector
|
||||||
*
|
*
|
||||||
* @param {HTMLElement} current - the current HTMLElement
|
* @param {Element} current - the current Element
|
||||||
* @param {string} parentSelector - selector for the parent element
|
* @param {string} parentSelector - selector for the parent element
|
||||||
* @return {HTMLElement|null} - the parent element
|
* @return {Element|null} - the parent element
|
||||||
*/
|
*/
|
||||||
closestParent (current, parentSelector) {
|
closestParent (current, parentSelector) {
|
||||||
if (Element.prototype.closest !== undefined) {
|
if (Element.prototype.closest !== undefined) {
|
||||||
@ -204,9 +204,9 @@ function delegateEvent(sel, target, event, listener) {
|
|||||||
/**
|
/**
|
||||||
* Add an event listener
|
* Add an event listener
|
||||||
*
|
*
|
||||||
* @param {string|HTMLElement} sel - the parent selector to bind to
|
* @param {string|Element} sel - the parent selector to bind to
|
||||||
* @param {string} event - event name(s) to bind
|
* @param {string} event - event name(s) to bind
|
||||||
* @param {string|HTMLElement|function} target - the element to directly bind the event to
|
* @param {string|Element|function} target - the element to directly bind the event to
|
||||||
* @param {function} [listener] - event listener callback
|
* @param {function} [listener] - event listener callback
|
||||||
* @return {void}
|
* @return {void}
|
||||||
*/
|
*/
|
||||||
|
@ -1,227 +0,0 @@
|
|||||||
/*
|
|
||||||
* classList.js: Cross-browser full element.classList implementation.
|
|
||||||
* 2014-07-23
|
|
||||||
*
|
|
||||||
* By Eli Grey, http://eligrey.com
|
|
||||||
* Public Domain.
|
|
||||||
* NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*global self, document, DOMException */
|
|
||||||
|
|
||||||
/*! @source http://purl.eligrey.com/github/classList.js/blob/master/classList.js*/
|
|
||||||
|
|
||||||
if ("document" in self) {
|
|
||||||
|
|
||||||
// Full polyfill for browsers with no classList support
|
|
||||||
if (!("classList" in document.createElement("_"))) {
|
|
||||||
|
|
||||||
(function(view) {
|
|
||||||
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
if (!('Element' in view)) return;
|
|
||||||
|
|
||||||
var
|
|
||||||
classListProp = "classList",
|
|
||||||
protoProp = "prototype",
|
|
||||||
elemCtrProto = view.Element[protoProp],
|
|
||||||
objCtr = Object,
|
|
||||||
strTrim = String[protoProp].trim || function() {
|
|
||||||
return this.replace(/^\s+|\s+$/g, "");
|
|
||||||
},
|
|
||||||
arrIndexOf = Array[protoProp].indexOf || function(item) {
|
|
||||||
var
|
|
||||||
i = 0,
|
|
||||||
len = this.length;
|
|
||||||
for (; i < len; i++) {
|
|
||||||
if (i in this && this[i] === item) {
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
// Vendors: please allow content code to instantiate DOMExceptions
|
|
||||||
,
|
|
||||||
DOMEx = function(type, message) {
|
|
||||||
this.name = type;
|
|
||||||
this.code = DOMException[type];
|
|
||||||
this.message = message;
|
|
||||||
},
|
|
||||||
checkTokenAndGetIndex = function(classList, token) {
|
|
||||||
if (token === "") {
|
|
||||||
throw new DOMEx(
|
|
||||||
"SYNTAX_ERR", "An invalid or illegal string was specified"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (/\s/.test(token)) {
|
|
||||||
throw new DOMEx(
|
|
||||||
"INVALID_CHARACTER_ERR", "String contains an invalid character"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return arrIndexOf.call(classList, token);
|
|
||||||
},
|
|
||||||
ClassList = function(elem) {
|
|
||||||
var
|
|
||||||
trimmedClasses = strTrim.call(elem.getAttribute("class") || ""),
|
|
||||||
classes = trimmedClasses ? trimmedClasses.split(/\s+/) : [],
|
|
||||||
i = 0,
|
|
||||||
len = classes.length;
|
|
||||||
for (; i < len; i++) {
|
|
||||||
this.push(classes[i]);
|
|
||||||
}
|
|
||||||
this._updateClassName = function() {
|
|
||||||
elem.setAttribute("class", this.toString());
|
|
||||||
};
|
|
||||||
},
|
|
||||||
classListProto = ClassList[protoProp] = [],
|
|
||||||
classListGetter = function() {
|
|
||||||
return new ClassList(this);
|
|
||||||
};
|
|
||||||
// Most DOMException implementations don't allow calling DOMException's toString()
|
|
||||||
// on non-DOMExceptions. Error's toString() is sufficient here.
|
|
||||||
DOMEx[protoProp] = Error[protoProp];
|
|
||||||
classListProto.item = function(i) {
|
|
||||||
return this[i] || null;
|
|
||||||
};
|
|
||||||
classListProto.contains = function(token) {
|
|
||||||
token += "";
|
|
||||||
return checkTokenAndGetIndex(this, token) !== -1;
|
|
||||||
};
|
|
||||||
classListProto.add = function() {
|
|
||||||
var
|
|
||||||
tokens = arguments,
|
|
||||||
i = 0,
|
|
||||||
l = tokens.length,
|
|
||||||
token, updated = false;
|
|
||||||
do {
|
|
||||||
token = tokens[i] + "";
|
|
||||||
if (checkTokenAndGetIndex(this, token) === -1) {
|
|
||||||
this.push(token);
|
|
||||||
updated = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
while (++i < l);
|
|
||||||
|
|
||||||
if (updated) {
|
|
||||||
this._updateClassName();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
classListProto.remove = function() {
|
|
||||||
var
|
|
||||||
tokens = arguments,
|
|
||||||
i = 0,
|
|
||||||
l = tokens.length,
|
|
||||||
token, updated = false,
|
|
||||||
index;
|
|
||||||
do {
|
|
||||||
token = tokens[i] + "";
|
|
||||||
index = checkTokenAndGetIndex(this, token);
|
|
||||||
while (index !== -1) {
|
|
||||||
this.splice(index, 1);
|
|
||||||
updated = true;
|
|
||||||
index = checkTokenAndGetIndex(this, token);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
while (++i < l);
|
|
||||||
|
|
||||||
if (updated) {
|
|
||||||
this._updateClassName();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
classListProto.toggle = function(token, force) {
|
|
||||||
token += "";
|
|
||||||
|
|
||||||
var
|
|
||||||
result = this.contains(token),
|
|
||||||
method = result ?
|
|
||||||
force !== true && "remove" :
|
|
||||||
force !== false && "add";
|
|
||||||
|
|
||||||
if (method) {
|
|
||||||
this[method](token);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (force === true || force === false) {
|
|
||||||
return force;
|
|
||||||
} else {
|
|
||||||
return !result;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
classListProto.toString = function() {
|
|
||||||
return this.join(" ");
|
|
||||||
};
|
|
||||||
|
|
||||||
if (objCtr.defineProperty) {
|
|
||||||
var classListPropDesc = {
|
|
||||||
get: classListGetter,
|
|
||||||
enumerable: true,
|
|
||||||
configurable: true
|
|
||||||
};
|
|
||||||
try {
|
|
||||||
objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc);
|
|
||||||
} catch (ex) { // IE 8 doesn't support enumerable:true
|
|
||||||
if (ex.number === -0x7FF5EC54) {
|
|
||||||
classListPropDesc.enumerable = false;
|
|
||||||
objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (objCtr[protoProp].__defineGetter__) {
|
|
||||||
elemCtrProto.__defineGetter__(classListProp, classListGetter);
|
|
||||||
}
|
|
||||||
|
|
||||||
}(self));
|
|
||||||
|
|
||||||
} else {
|
|
||||||
// There is full or partial native classList support, so just check if we need
|
|
||||||
// to normalize the add/remove and toggle APIs.
|
|
||||||
|
|
||||||
(function() {
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
var testElement = document.createElement("_");
|
|
||||||
|
|
||||||
testElement.classList.add("c1", "c2");
|
|
||||||
|
|
||||||
// Polyfill for IE 10/11 and Firefox <26, where classList.add and
|
|
||||||
// classList.remove exist but support only one argument at a time.
|
|
||||||
if (!testElement.classList.contains("c2")) {
|
|
||||||
var createMethod = function(method) {
|
|
||||||
var original = DOMTokenList.prototype[method];
|
|
||||||
|
|
||||||
DOMTokenList.prototype[method] = function(token) {
|
|
||||||
var i, len = arguments.length;
|
|
||||||
|
|
||||||
for (i = 0; i < len; i++) {
|
|
||||||
token = arguments[i];
|
|
||||||
original.call(this, token);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
createMethod('add');
|
|
||||||
createMethod('remove');
|
|
||||||
}
|
|
||||||
|
|
||||||
testElement.classList.toggle("c3", false);
|
|
||||||
|
|
||||||
// Polyfill for IE 10 and Firefox <24, where classList.toggle does not
|
|
||||||
// support the second argument.
|
|
||||||
if (testElement.classList.contains("c3")) {
|
|
||||||
var _toggle = DOMTokenList.prototype.toggle;
|
|
||||||
|
|
||||||
DOMTokenList.prototype.toggle = function(token, force) {
|
|
||||||
if (1 in arguments && !this.contains(token) === !force) {
|
|
||||||
return force;
|
|
||||||
} else {
|
|
||||||
return _toggle.call(this, token);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
testElement = null;
|
|
||||||
}());
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -16,7 +16,7 @@ _.on('.media-filter', 'input', filterMedia);
|
|||||||
/**
|
/**
|
||||||
* Hide the html element attached to the event
|
* Hide the html element attached to the event
|
||||||
*
|
*
|
||||||
* @param event
|
* @param {MouseEvent} event
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
function hide (event) {
|
function hide (event) {
|
||||||
@ -26,7 +26,7 @@ function hide (event) {
|
|||||||
/**
|
/**
|
||||||
* Confirm deletion of an item
|
* Confirm deletion of an item
|
||||||
*
|
*
|
||||||
* @param event
|
* @param {MouseEvent} event
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
function confirmDelete (event) {
|
function confirmDelete (event) {
|
||||||
@ -52,7 +52,7 @@ function clearAPICache () {
|
|||||||
/**
|
/**
|
||||||
* Scroll to the accordion/vertical tab section just opened
|
* Scroll to the accordion/vertical tab section just opened
|
||||||
*
|
*
|
||||||
* @param event
|
* @param {InputEvent} event
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
function scrollToSection (event) {
|
function scrollToSection (event) {
|
||||||
@ -70,7 +70,7 @@ function scrollToSection (event) {
|
|||||||
/**
|
/**
|
||||||
* Filter an anime or manga list
|
* Filter an anime or manga list
|
||||||
*
|
*
|
||||||
* @param event
|
* @param {InputEvent} event
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
function filterMedia (event) {
|
function filterMedia (event) {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import './anon.js';
|
import './sw.js';
|
||||||
|
import './events.js';
|
||||||
import './session-check.js';
|
import './session-check.js';
|
||||||
import './anime.js';
|
import './anime.js';
|
||||||
import './manga.js';
|
import './manga.js';
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
import _ from './anime-client.js';
|
import _ from './anime-client.js';
|
||||||
|
|
||||||
(() => {
|
(() => {
|
||||||
// Var is intentional
|
let hidden = null;
|
||||||
var hidden = null;
|
let visibilityChange = null;
|
||||||
var visibilityChange = null;
|
|
||||||
|
|
||||||
if (typeof document.hidden !== "undefined") {
|
if (typeof document.hidden !== "undefined") {
|
||||||
hidden = "hidden";
|
hidden = "hidden";
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
import './events.js';
|
// Start the service worker, if you can
|
||||||
|
|
||||||
if ('serviceWorker' in navigator) {
|
if ('serviceWorker' in navigator) {
|
||||||
navigator.serviceWorker.register('/sw.js').then(reg => {
|
navigator.serviceWorker.register('/sw.js').then(reg => {
|
||||||
console.log('Service worker registered', reg.scope);
|
console.log('Service worker registered', reg.scope);
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
console.error('Failed to register service worker', error);
|
console.error('Failed to register service worker', error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -3,19 +3,19 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "npm run build:css && npm run build:js",
|
"build": "npm run build:css && npm run build:js",
|
||||||
"build:css": "node ./css.js",
|
"build:css": "node ./css.js",
|
||||||
"build:js": "rollup -c ./build-js.js",
|
"build:js": "spack",
|
||||||
"watch:css": "watch 'npm run build:css' --filter=./cssfilter.js",
|
"watch:css": "watch 'npm run build:css' --filter=./cssfilter.js",
|
||||||
"watch:js": "watch 'npm run build:js' ./js",
|
"watch:js": "watch 'npm run build:js' ./js",
|
||||||
"watch": "concurrently \"npm:watch:css\" \"npm:watch:js\" --kill-others"
|
"watch": "concurrently \"npm:watch:css\" \"npm:watch:js\" --kill-others"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@ampproject/rollup-plugin-closure-compiler": "^0.26.0",
|
"@swc/cli": "^0.1.39",
|
||||||
"concurrently": "^5.1.0",
|
"@swc/core": "^1.2.54",
|
||||||
"cssnano": "^4.1.10",
|
"concurrently": "^6.0.2",
|
||||||
|
"cssnano": "^5.0.1",
|
||||||
"postcss": "^8.2.6",
|
"postcss": "^8.2.6",
|
||||||
"postcss-import": "^14.0.0",
|
"postcss-import": "^14.0.0",
|
||||||
"postcss-preset-env": "^6.7.0",
|
"postcss-preset-env": "^6.7.0",
|
||||||
"rollup": "^2.4.0",
|
|
||||||
"watch": "^1.0.2"
|
"watch": "^1.0.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
19
frontEndSrc/spack.config.js
Normal file
19
frontEndSrc/spack.config.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
module.exports = {
|
||||||
|
entry: {
|
||||||
|
'scripts.min': __dirname + '/js/index.js',
|
||||||
|
'tables.min': __dirname + '/js/base/sort-tables.js',
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
path: '../public/js',
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
jsc: {
|
||||||
|
target: 'es3',
|
||||||
|
loose: true,
|
||||||
|
},
|
||||||
|
minify: true,
|
||||||
|
module: {
|
||||||
|
type: 'es6'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
@ -1,463 +0,0 @@
|
|||||||
// -------------------------------------------------------------------------
|
|
||||||
// ! 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 {object} [context]
|
|
||||||
* @return {[HTMLElement]} - array of dom elements
|
|
||||||
*/
|
|
||||||
$(selector, context = null) {
|
|
||||||
if (typeof selector !== 'string') {
|
|
||||||
return selector;
|
|
||||||
}
|
|
||||||
|
|
||||||
context = (context !== null && context.nodeType === 1)
|
|
||||||
? context
|
|
||||||
: document;
|
|
||||||
|
|
||||||
let elements = [];
|
|
||||||
if (selector.match(/^#([\w]+$)/)) {
|
|
||||||
elements.push(document.getElementById(selector.split('#')[1]));
|
|
||||||
} else {
|
|
||||||
elements = [].slice.apply(context.querySelectorAll(selector));
|
|
||||||
}
|
|
||||||
|
|
||||||
return elements;
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Does the selector exist on the current page?
|
|
||||||
*
|
|
||||||
* @param {string} selector
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
|
||||||
hasElement (selector) {
|
|
||||||
return AnimeClient.$(selector).length > 0;
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Scroll to the top of the Page
|
|
||||||
*
|
|
||||||
* @return {void}
|
|
||||||
*/
|
|
||||||
scrollToTop () {
|
|
||||||
const el = AnimeClient.$('header')[0];
|
|
||||||
el.scrollIntoView(true);
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Hide the selected element
|
|
||||||
*
|
|
||||||
* @param {string|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} 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 {HTMLElement} current - the current HTMLElement
|
|
||||||
* @param {string} parentSelector - selector for the parent element
|
|
||||||
* @return {HTMLElement|null} - the parent element
|
|
||||||
*/
|
|
||||||
closestParent (current, parentSelector) {
|
|
||||||
if (Element.prototype.closest !== undefined) {
|
|
||||||
return current.closest(parentSelector);
|
|
||||||
}
|
|
||||||
|
|
||||||
while (current !== document.documentElement) {
|
|
||||||
if (matches(current, parentSelector)) {
|
|
||||||
return current;
|
|
||||||
}
|
|
||||||
|
|
||||||
current = current.parentElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Generate a full url from a relative path
|
|
||||||
*
|
|
||||||
* @param {string} path - url path
|
|
||||||
* @return {string} - full url
|
|
||||||
*/
|
|
||||||
url (path) {
|
|
||||||
let uri = `//${document.location.host}`;
|
|
||||||
uri += (path.charAt(0) === '/') ? path : `/${path}`;
|
|
||||||
|
|
||||||
return uri;
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Throttle execution of a function
|
|
||||||
*
|
|
||||||
* @see https://remysharp.com/2010/07/21/throttling-function-calls
|
|
||||||
* @see https://jsfiddle.net/jonathansampson/m7G64/
|
|
||||||
* @param {Number} interval - the minimum throttle time in ms
|
|
||||||
* @param {Function} fn - the function to throttle
|
|
||||||
* @param {Object} [scope] - the 'this' object for the function
|
|
||||||
* @return {Function}
|
|
||||||
*/
|
|
||||||
throttle (interval, fn, scope) {
|
|
||||||
let wait = false;
|
|
||||||
return function (...args) {
|
|
||||||
const context = scope || this;
|
|
||||||
|
|
||||||
if ( ! wait) {
|
|
||||||
fn.apply(context, args);
|
|
||||||
wait = true;
|
|
||||||
setTimeout(function() {
|
|
||||||
wait = false;
|
|
||||||
}, interval);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
// ! Events
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
|
|
||||||
function addEvent(sel, event, listener) {
|
|
||||||
// Recurse!
|
|
||||||
if (! event.match(/^([\w\-]+)$/)) {
|
|
||||||
event.split(' ').forEach((evt) => {
|
|
||||||
addEvent(sel, evt, listener);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
sel.addEventListener(event, listener, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
function delegateEvent(sel, target, event, listener) {
|
|
||||||
// Attach the listener to the parent
|
|
||||||
addEvent(sel, event, (e) => {
|
|
||||||
// Get live version of the target selector
|
|
||||||
AnimeClient.$(target, sel).forEach((element) => {
|
|
||||||
if(e.target == element) {
|
|
||||||
listener.call(element, e);
|
|
||||||
e.stopPropagation();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add an event listener
|
|
||||||
*
|
|
||||||
* @param {string|HTMLElement} sel - the parent selector to bind to
|
|
||||||
* @param {string} event - event name(s) to bind
|
|
||||||
* @param {string|HTMLElement|function} target - the element to directly bind the event to
|
|
||||||
* @param {function} [listener] - event listener callback
|
|
||||||
* @return {void}
|
|
||||||
*/
|
|
||||||
AnimeClient.on = (sel, event, target, listener) => {
|
|
||||||
if (listener === undefined) {
|
|
||||||
listener = target;
|
|
||||||
AnimeClient.$(sel).forEach((el) => {
|
|
||||||
addEvent(el, event, listener);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
AnimeClient.$(sel).forEach((el) => {
|
|
||||||
delegateEvent(el, target, event, listener);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
// ! Ajax
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Url encoding for non-get requests
|
|
||||||
*
|
|
||||||
* @param data
|
|
||||||
* @returns {string}
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
function ajaxSerialize(data) {
|
|
||||||
let pairs = [];
|
|
||||||
|
|
||||||
Object.keys(data).forEach((name) => {
|
|
||||||
let value = data[name].toString();
|
|
||||||
|
|
||||||
name = encodeURIComponent(name);
|
|
||||||
value = encodeURIComponent(value);
|
|
||||||
|
|
||||||
pairs.push(`${name}=${value}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
return pairs.join('&');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Make an ajax request
|
|
||||||
*
|
|
||||||
* Config:{
|
|
||||||
* data: // data to send with the request
|
|
||||||
* type: // http verb of the request, defaults to GET
|
|
||||||
* success: // success callback
|
|
||||||
* error: // error callback
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* @param {string} url - the url to request
|
|
||||||
* @param {Object} config - the configuration object
|
|
||||||
* @return {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
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
|
||||||
// Event subscriptions
|
|
||||||
// ----------------------------------------------------------------------------
|
|
||||||
AnimeClient.on('header', 'click', '.message', hide);
|
|
||||||
AnimeClient.on('form.js-delete', 'submit', confirmDelete);
|
|
||||||
AnimeClient.on('.js-clear-cache', 'click', clearAPICache);
|
|
||||||
AnimeClient.on('.vertical-tabs input', 'change', scrollToSection);
|
|
||||||
AnimeClient.on('.media-filter', 'input', filterMedia);
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
|
||||||
// Handler functions
|
|
||||||
// ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hide the html element attached to the event
|
|
||||||
*
|
|
||||||
* @param event
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
function hide (event) {
|
|
||||||
AnimeClient.hide(event.target);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Confirm deletion of an item
|
|
||||||
*
|
|
||||||
* @param event
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
function confirmDelete (event) {
|
|
||||||
const proceed = confirm('Are you ABSOLUTELY SURE you want to delete this item?');
|
|
||||||
|
|
||||||
if (proceed === false) {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear the API cache, and show a message if the cache is cleared
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
function clearAPICache () {
|
|
||||||
AnimeClient.get('/cache_purge', () => {
|
|
||||||
AnimeClient.showMessage('success', 'Successfully purged api cache');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Scroll to the accordion/vertical tab section just opened
|
|
||||||
*
|
|
||||||
* @param event
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
function scrollToSection (event) {
|
|
||||||
const el = event.currentTarget.parentElement;
|
|
||||||
const rect = el.getBoundingClientRect();
|
|
||||||
|
|
||||||
const top = rect.top + window.pageYOffset;
|
|
||||||
|
|
||||||
window.scrollTo({
|
|
||||||
top,
|
|
||||||
behavior: 'smooth',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Filter an anime or manga list
|
|
||||||
*
|
|
||||||
* @param event
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
function filterMedia (event) {
|
|
||||||
const rawFilter = event.target.value;
|
|
||||||
const filter = new RegExp(rawFilter, 'i');
|
|
||||||
|
|
||||||
// console.log('Filtering items by: ', filter);
|
|
||||||
|
|
||||||
if (rawFilter !== '') {
|
|
||||||
// Filter the cover view
|
|
||||||
AnimeClient.$('article.media').forEach(article => {
|
|
||||||
const titleLink = AnimeClient.$('.name a', article)[0];
|
|
||||||
const title = String(titleLink.textContent).trim();
|
|
||||||
if ( ! filter.test(title)) {
|
|
||||||
AnimeClient.hide(article);
|
|
||||||
} else {
|
|
||||||
AnimeClient.show(article);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Filter the list view
|
|
||||||
AnimeClient.$('table.media-wrap tbody tr').forEach(tr => {
|
|
||||||
const titleCell = AnimeClient.$('td.align-left', tr)[0];
|
|
||||||
const titleLink = AnimeClient.$('a', titleCell)[0];
|
|
||||||
const linkTitle = String(titleLink.textContent).trim();
|
|
||||||
const textTitle = String(titleCell.textContent).trim();
|
|
||||||
if ( ! (filter.test(linkTitle) || filter.test(textTitle))) {
|
|
||||||
AnimeClient.hide(tr);
|
|
||||||
} else {
|
|
||||||
AnimeClient.show(tr);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
AnimeClient.show('article.media');
|
|
||||||
AnimeClient.show('table.media-wrap tbody tr');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('serviceWorker' in navigator) {
|
|
||||||
navigator.serviceWorker.register('/sw.js').then(reg => {
|
|
||||||
console.log('Service worker registered', reg.scope);
|
|
||||||
}).catch(error => {
|
|
||||||
console.error('Failed to register service worker', error);
|
|
||||||
});
|
|
||||||
}
|
|
@ -1,768 +0,0 @@
|
|||||||
// -------------------------------------------------------------------------
|
|
||||||
// ! 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 {object} [context]
|
|
||||||
* @return {[HTMLElement]} - array of dom elements
|
|
||||||
*/
|
|
||||||
$(selector, context = null) {
|
|
||||||
if (typeof selector !== 'string') {
|
|
||||||
return selector;
|
|
||||||
}
|
|
||||||
|
|
||||||
context = (context !== null && context.nodeType === 1)
|
|
||||||
? context
|
|
||||||
: document;
|
|
||||||
|
|
||||||
let elements = [];
|
|
||||||
if (selector.match(/^#([\w]+$)/)) {
|
|
||||||
elements.push(document.getElementById(selector.split('#')[1]));
|
|
||||||
} else {
|
|
||||||
elements = [].slice.apply(context.querySelectorAll(selector));
|
|
||||||
}
|
|
||||||
|
|
||||||
return elements;
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Does the selector exist on the current page?
|
|
||||||
*
|
|
||||||
* @param {string} selector
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
|
||||||
hasElement (selector) {
|
|
||||||
return AnimeClient.$(selector).length > 0;
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Scroll to the top of the Page
|
|
||||||
*
|
|
||||||
* @return {void}
|
|
||||||
*/
|
|
||||||
scrollToTop () {
|
|
||||||
const el = AnimeClient.$('header')[0];
|
|
||||||
el.scrollIntoView(true);
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Hide the selected element
|
|
||||||
*
|
|
||||||
* @param {string|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} 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 {HTMLElement} current - the current HTMLElement
|
|
||||||
* @param {string} parentSelector - selector for the parent element
|
|
||||||
* @return {HTMLElement|null} - the parent element
|
|
||||||
*/
|
|
||||||
closestParent (current, parentSelector) {
|
|
||||||
if (Element.prototype.closest !== undefined) {
|
|
||||||
return current.closest(parentSelector);
|
|
||||||
}
|
|
||||||
|
|
||||||
while (current !== document.documentElement) {
|
|
||||||
if (matches(current, parentSelector)) {
|
|
||||||
return current;
|
|
||||||
}
|
|
||||||
|
|
||||||
current = current.parentElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Generate a full url from a relative path
|
|
||||||
*
|
|
||||||
* @param {string} path - url path
|
|
||||||
* @return {string} - full url
|
|
||||||
*/
|
|
||||||
url (path) {
|
|
||||||
let uri = `//${document.location.host}`;
|
|
||||||
uri += (path.charAt(0) === '/') ? path : `/${path}`;
|
|
||||||
|
|
||||||
return uri;
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Throttle execution of a function
|
|
||||||
*
|
|
||||||
* @see https://remysharp.com/2010/07/21/throttling-function-calls
|
|
||||||
* @see https://jsfiddle.net/jonathansampson/m7G64/
|
|
||||||
* @param {Number} interval - the minimum throttle time in ms
|
|
||||||
* @param {Function} fn - the function to throttle
|
|
||||||
* @param {Object} [scope] - the 'this' object for the function
|
|
||||||
* @return {Function}
|
|
||||||
*/
|
|
||||||
throttle (interval, fn, scope) {
|
|
||||||
let wait = false;
|
|
||||||
return function (...args) {
|
|
||||||
const context = scope || this;
|
|
||||||
|
|
||||||
if ( ! wait) {
|
|
||||||
fn.apply(context, args);
|
|
||||||
wait = true;
|
|
||||||
setTimeout(function() {
|
|
||||||
wait = false;
|
|
||||||
}, interval);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
// ! Events
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
|
|
||||||
function addEvent(sel, event, listener) {
|
|
||||||
// Recurse!
|
|
||||||
if (! event.match(/^([\w\-]+)$/)) {
|
|
||||||
event.split(' ').forEach((evt) => {
|
|
||||||
addEvent(sel, evt, listener);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
sel.addEventListener(event, listener, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
function delegateEvent(sel, target, event, listener) {
|
|
||||||
// Attach the listener to the parent
|
|
||||||
addEvent(sel, event, (e) => {
|
|
||||||
// Get live version of the target selector
|
|
||||||
AnimeClient.$(target, sel).forEach((element) => {
|
|
||||||
if(e.target == element) {
|
|
||||||
listener.call(element, e);
|
|
||||||
e.stopPropagation();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add an event listener
|
|
||||||
*
|
|
||||||
* @param {string|HTMLElement} sel - the parent selector to bind to
|
|
||||||
* @param {string} event - event name(s) to bind
|
|
||||||
* @param {string|HTMLElement|function} target - the element to directly bind the event to
|
|
||||||
* @param {function} [listener] - event listener callback
|
|
||||||
* @return {void}
|
|
||||||
*/
|
|
||||||
AnimeClient.on = (sel, event, target, listener) => {
|
|
||||||
if (listener === undefined) {
|
|
||||||
listener = target;
|
|
||||||
AnimeClient.$(sel).forEach((el) => {
|
|
||||||
addEvent(el, event, listener);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
AnimeClient.$(sel).forEach((el) => {
|
|
||||||
delegateEvent(el, target, event, listener);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
// ! Ajax
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Url encoding for non-get requests
|
|
||||||
*
|
|
||||||
* @param data
|
|
||||||
* @returns {string}
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
function ajaxSerialize(data) {
|
|
||||||
let pairs = [];
|
|
||||||
|
|
||||||
Object.keys(data).forEach((name) => {
|
|
||||||
let value = data[name].toString();
|
|
||||||
|
|
||||||
name = encodeURIComponent(name);
|
|
||||||
value = encodeURIComponent(value);
|
|
||||||
|
|
||||||
pairs.push(`${name}=${value}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
return pairs.join('&');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Make an ajax request
|
|
||||||
*
|
|
||||||
* Config:{
|
|
||||||
* data: // data to send with the request
|
|
||||||
* type: // http verb of the request, defaults to GET
|
|
||||||
* success: // success callback
|
|
||||||
* error: // error callback
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* @param {string} url - the url to request
|
|
||||||
* @param {Object} config - the configuration object
|
|
||||||
* @return {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
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
|
||||||
// Event subscriptions
|
|
||||||
// ----------------------------------------------------------------------------
|
|
||||||
AnimeClient.on('header', 'click', '.message', hide);
|
|
||||||
AnimeClient.on('form.js-delete', 'submit', confirmDelete);
|
|
||||||
AnimeClient.on('.js-clear-cache', 'click', clearAPICache);
|
|
||||||
AnimeClient.on('.vertical-tabs input', 'change', scrollToSection);
|
|
||||||
AnimeClient.on('.media-filter', 'input', filterMedia);
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
|
||||||
// Handler functions
|
|
||||||
// ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hide the html element attached to the event
|
|
||||||
*
|
|
||||||
* @param event
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
function hide (event) {
|
|
||||||
AnimeClient.hide(event.target);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Confirm deletion of an item
|
|
||||||
*
|
|
||||||
* @param event
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
function confirmDelete (event) {
|
|
||||||
const proceed = confirm('Are you ABSOLUTELY SURE you want to delete this item?');
|
|
||||||
|
|
||||||
if (proceed === false) {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear the API cache, and show a message if the cache is cleared
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
function clearAPICache () {
|
|
||||||
AnimeClient.get('/cache_purge', () => {
|
|
||||||
AnimeClient.showMessage('success', 'Successfully purged api cache');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Scroll to the accordion/vertical tab section just opened
|
|
||||||
*
|
|
||||||
* @param event
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
function scrollToSection (event) {
|
|
||||||
const el = event.currentTarget.parentElement;
|
|
||||||
const rect = el.getBoundingClientRect();
|
|
||||||
|
|
||||||
const top = rect.top + window.pageYOffset;
|
|
||||||
|
|
||||||
window.scrollTo({
|
|
||||||
top,
|
|
||||||
behavior: 'smooth',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Filter an anime or manga list
|
|
||||||
*
|
|
||||||
* @param event
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
function filterMedia (event) {
|
|
||||||
const rawFilter = event.target.value;
|
|
||||||
const filter = new RegExp(rawFilter, 'i');
|
|
||||||
|
|
||||||
// console.log('Filtering items by: ', filter);
|
|
||||||
|
|
||||||
if (rawFilter !== '') {
|
|
||||||
// Filter the cover view
|
|
||||||
AnimeClient.$('article.media').forEach(article => {
|
|
||||||
const titleLink = AnimeClient.$('.name a', article)[0];
|
|
||||||
const title = String(titleLink.textContent).trim();
|
|
||||||
if ( ! filter.test(title)) {
|
|
||||||
AnimeClient.hide(article);
|
|
||||||
} else {
|
|
||||||
AnimeClient.show(article);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Filter the list view
|
|
||||||
AnimeClient.$('table.media-wrap tbody tr').forEach(tr => {
|
|
||||||
const titleCell = AnimeClient.$('td.align-left', tr)[0];
|
|
||||||
const titleLink = AnimeClient.$('a', titleCell)[0];
|
|
||||||
const linkTitle = String(titleLink.textContent).trim();
|
|
||||||
const textTitle = String(titleCell.textContent).trim();
|
|
||||||
if ( ! (filter.test(linkTitle) || filter.test(textTitle))) {
|
|
||||||
AnimeClient.hide(tr);
|
|
||||||
} else {
|
|
||||||
AnimeClient.show(tr);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
AnimeClient.show('article.media');
|
|
||||||
AnimeClient.show('table.media-wrap tbody tr');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('serviceWorker' in navigator) {
|
|
||||||
navigator.serviceWorker.register('/sw.js').then(reg => {
|
|
||||||
console.log('Service worker registered', reg.scope);
|
|
||||||
}).catch(error => {
|
|
||||||
console.error('Failed to register service worker', error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
(() => {
|
|
||||||
// Var is intentional
|
|
||||||
var hidden = null;
|
|
||||||
var visibilityChange = null;
|
|
||||||
|
|
||||||
if (typeof document.hidden !== "undefined") {
|
|
||||||
hidden = "hidden";
|
|
||||||
visibilityChange = "visibilitychange";
|
|
||||||
} else if (typeof document.msHidden !== "undefined") {
|
|
||||||
hidden = "msHidden";
|
|
||||||
visibilityChange = "msvisibilitychange";
|
|
||||||
} else if (typeof document.webkitHidden !== "undefined") {
|
|
||||||
hidden = "webkitHidden";
|
|
||||||
visibilityChange = "webkitvisibilitychange";
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleVisibilityChange() {
|
|
||||||
// Check the user's session to see if they are currently logged-in
|
|
||||||
// when the page becomes visible
|
|
||||||
if ( ! document[hidden]) {
|
|
||||||
AnimeClient.get('/heartbeat', (beat) => {
|
|
||||||
const status = JSON.parse(beat);
|
|
||||||
|
|
||||||
// If the session is expired, immediately reload so that
|
|
||||||
// you can't attempt to do an action that requires authentication
|
|
||||||
if (status.hasAuth !== true) {
|
|
||||||
document.removeEventListener(visibilityChange, handleVisibilityChange, false);
|
|
||||||
location.reload();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hidden === null) {
|
|
||||||
console.info('Page visibility API not supported, JS session check will not work');
|
|
||||||
} else {
|
|
||||||
document.addEventListener(visibilityChange, handleVisibilityChange, false);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
// Click on hidden MAL checkbox so
|
|
||||||
// that MAL id is passed
|
|
||||||
AnimeClient.on('main', 'change', '.big-check', (e) => {
|
|
||||||
const id = e.target.id;
|
|
||||||
document.getElementById(`mal_${id}`).checked = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
function renderAnimeSearchResults (data) {
|
|
||||||
return data.map(item => {
|
|
||||||
const titles = item.titles.join('<br />');
|
|
||||||
|
|
||||||
return `
|
|
||||||
<article class="media search">
|
|
||||||
<div class="name">
|
|
||||||
<input type="radio" class="mal-check" id="mal_${item.slug}" name="mal_id" value="${item.mal_id}" />
|
|
||||||
<input type="radio" class="big-check" id="${item.slug}" name="id" value="${item.id}" />
|
|
||||||
<label for="${item.slug}">
|
|
||||||
<picture width="220">
|
|
||||||
<source srcset="/public/images/anime/${item.id}.webp" type="image/webp" />
|
|
||||||
<source srcset="/public/images/anime/${item.id}.jpg" type="image/jpeg" />
|
|
||||||
<img src="/public/images/anime/${item.id}.jpg" alt="" width="220" />
|
|
||||||
</picture>
|
|
||||||
<span class="name">
|
|
||||||
${item.canonicalTitle}<br />
|
|
||||||
<small>${titles}</small>
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="table">
|
|
||||||
<div class="row">
|
|
||||||
<span class="edit">
|
|
||||||
<a class="bracketed" href="/anime/details/${item.slug}">Info Page</a>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
`;
|
|
||||||
}).join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderMangaSearchResults (data) {
|
|
||||||
return data.map(item => {
|
|
||||||
const titles = item.titles.join('<br />');
|
|
||||||
return `
|
|
||||||
<article class="media search">
|
|
||||||
<div class="name">
|
|
||||||
<input type="radio" id="mal_${item.slug}" name="mal_id" value="${item.mal_id}" />
|
|
||||||
<input type="radio" class="big-check" id="${item.slug}" name="id" value="${item.id}" />
|
|
||||||
<label for="${item.slug}">
|
|
||||||
<picture width="220">
|
|
||||||
<source srcset="/public/images/manga/${item.id}.webp" type="image/webp" />
|
|
||||||
<source srcset="/public/images/manga/${item.id}.jpg" type="image/jpeg" />
|
|
||||||
<img src="/public/images/manga/${item.id}.jpg" alt="" width="220" />
|
|
||||||
</picture>
|
|
||||||
<span class="name">
|
|
||||||
${item.canonicalTitle}<br />
|
|
||||||
<small>${titles}</small>
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="table">
|
|
||||||
<div class="row">
|
|
||||||
<span class="edit">
|
|
||||||
<a class="bracketed" href="/manga/details/${item.slug}">Info Page</a>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
`;
|
|
||||||
}).join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
const search = (query) => {
|
|
||||||
// Show the loader
|
|
||||||
AnimeClient.show('.cssload-loader');
|
|
||||||
|
|
||||||
// Do the api search
|
|
||||||
return AnimeClient.get(AnimeClient.url('/anime-collection/search'), { query }, (searchResults, status) => {
|
|
||||||
searchResults = JSON.parse(searchResults);
|
|
||||||
|
|
||||||
// Hide the loader
|
|
||||||
AnimeClient.hide('.cssload-loader');
|
|
||||||
|
|
||||||
// Show the results
|
|
||||||
AnimeClient.$('#series-list')[ 0 ].innerHTML = renderAnimeSearchResults(searchResults);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
if (AnimeClient.hasElement('.anime #search')) {
|
|
||||||
let prevRequest = null;
|
|
||||||
|
|
||||||
AnimeClient.on('#search', 'input', AnimeClient.throttle(250, (e) => {
|
|
||||||
const query = encodeURIComponent(e.target.value);
|
|
||||||
if (query === '') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prevRequest !== null) {
|
|
||||||
prevRequest.abort();
|
|
||||||
}
|
|
||||||
|
|
||||||
prevRequest = search(query);
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Action to increment episode count
|
|
||||||
AnimeClient.on('body.anime.list', 'click', '.plus-one', (e) => {
|
|
||||||
let parentSel = AnimeClient.closestParent(e.target, 'article');
|
|
||||||
let watchedCount = parseInt(AnimeClient.$('.completed_number', parentSel)[ 0 ].textContent, 10) || 0;
|
|
||||||
let totalCount = parseInt(AnimeClient.$('.total_number', parentSel)[ 0 ].textContent, 10);
|
|
||||||
let title = AnimeClient.$('.name a', parentSel)[ 0 ].textContent;
|
|
||||||
|
|
||||||
// Setup the update data
|
|
||||||
let data = {
|
|
||||||
id: parentSel.dataset.kitsuId,
|
|
||||||
mal_id: parentSel.dataset.malId,
|
|
||||||
data: {
|
|
||||||
progress: watchedCount + 1
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// If the episode count is 0, and incremented,
|
|
||||||
// change status to currently watching
|
|
||||||
if (isNaN(watchedCount) || watchedCount === 0) {
|
|
||||||
data.data.status = 'CURRENT';
|
|
||||||
}
|
|
||||||
|
|
||||||
// If you increment at the last episode, mark as completed
|
|
||||||
if ((!isNaN(watchedCount)) && (watchedCount + 1) === totalCount) {
|
|
||||||
data.data.status = 'COMPLETED';
|
|
||||||
}
|
|
||||||
|
|
||||||
AnimeClient.show('#loading-shadow');
|
|
||||||
|
|
||||||
// okay, lets actually make some changes!
|
|
||||||
AnimeClient.ajax(AnimeClient.url('/anime/increment'), {
|
|
||||||
data,
|
|
||||||
dataType: 'json',
|
|
||||||
type: 'POST',
|
|
||||||
success: (res) => {
|
|
||||||
const resData = JSON.parse(res);
|
|
||||||
|
|
||||||
if (resData.error) {
|
|
||||||
AnimeClient.hide('#loading-shadow');
|
|
||||||
AnimeClient.showMessage('error', `Failed to update ${title}. `);
|
|
||||||
AnimeClient.scrollToTop();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (resData.data.libraryEntry.update.libraryEntry.status === 'COMPLETED') {
|
|
||||||
AnimeClient.hide(parentSel);
|
|
||||||
}
|
|
||||||
|
|
||||||
AnimeClient.hide('#loading-shadow');
|
|
||||||
|
|
||||||
AnimeClient.showMessage('success', `Successfully updated ${title}`);
|
|
||||||
AnimeClient.$('.completed_number', parentSel)[ 0 ].textContent = ++watchedCount;
|
|
||||||
AnimeClient.scrollToTop();
|
|
||||||
},
|
|
||||||
error: () => {
|
|
||||||
AnimeClient.hide('#loading-shadow');
|
|
||||||
AnimeClient.showMessage('error', `Failed to update ${title}. `);
|
|
||||||
AnimeClient.scrollToTop();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const search$1 = (query) => {
|
|
||||||
AnimeClient.show('.cssload-loader');
|
|
||||||
return AnimeClient.get(AnimeClient.url('/manga/search'), { query }, (searchResults, status) => {
|
|
||||||
searchResults = JSON.parse(searchResults);
|
|
||||||
AnimeClient.hide('.cssload-loader');
|
|
||||||
AnimeClient.$('#series-list')[ 0 ].innerHTML = renderMangaSearchResults(searchResults);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
if (AnimeClient.hasElement('.manga #search')) {
|
|
||||||
let prevRequest = null;
|
|
||||||
|
|
||||||
AnimeClient.on('#search', 'input', AnimeClient.throttle(250, (e) => {
|
|
||||||
let query = encodeURIComponent(e.target.value);
|
|
||||||
if (query === '') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prevRequest !== null) {
|
|
||||||
prevRequest.abort();
|
|
||||||
}
|
|
||||||
|
|
||||||
prevRequest = search$1(query);
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Javascript for editing manga, if logged in
|
|
||||||
*/
|
|
||||||
AnimeClient.on('.manga.list', 'click', '.edit-buttons button', (e) => {
|
|
||||||
let thisSel = e.target;
|
|
||||||
let parentSel = AnimeClient.closestParent(e.target, 'article');
|
|
||||||
let type = thisSel.classList.contains('plus-one-chapter') ? 'chapter' : 'volume';
|
|
||||||
let completed = parseInt(AnimeClient.$(`.${type}s_read`, parentSel)[ 0 ].textContent, 10) || 0;
|
|
||||||
let total = parseInt(AnimeClient.$(`.${type}_count`, parentSel)[ 0 ].textContent, 10);
|
|
||||||
let mangaName = AnimeClient.$('.name', parentSel)[ 0 ].textContent;
|
|
||||||
|
|
||||||
if (isNaN(completed)) {
|
|
||||||
completed = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Setup the update data
|
|
||||||
let data = {
|
|
||||||
id: parentSel.dataset.kitsuId,
|
|
||||||
mal_id: parentSel.dataset.malId,
|
|
||||||
data: {
|
|
||||||
progress: completed
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// If the episode count is 0, and incremented,
|
|
||||||
// change status to currently reading
|
|
||||||
if (isNaN(completed) || completed === 0) {
|
|
||||||
data.data.status = 'CURRENT';
|
|
||||||
}
|
|
||||||
|
|
||||||
// If you increment at the last chapter, mark as completed
|
|
||||||
if ((!isNaN(completed)) && (completed + 1) === total) {
|
|
||||||
data.data.status = 'COMPLETED';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the total count
|
|
||||||
data.data.progress = ++completed;
|
|
||||||
|
|
||||||
AnimeClient.show('#loading-shadow');
|
|
||||||
|
|
||||||
AnimeClient.ajax(AnimeClient.url('/manga/increment'), {
|
|
||||||
data,
|
|
||||||
dataType: 'json',
|
|
||||||
type: 'POST',
|
|
||||||
mimeType: 'application/json',
|
|
||||||
success: (res) => {
|
|
||||||
const resData = JSON.parse(res);
|
|
||||||
if (resData.error) {
|
|
||||||
AnimeClient.hide('#loading-shadow');
|
|
||||||
AnimeClient.showMessage('error', `Failed to update ${mangaName}. `);
|
|
||||||
AnimeClient.scrollToTop();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (String(data.data.status).toUpperCase() === 'COMPLETED') {
|
|
||||||
AnimeClient.hide(parentSel);
|
|
||||||
}
|
|
||||||
|
|
||||||
AnimeClient.hide('#loading-shadow');
|
|
||||||
|
|
||||||
AnimeClient.$(`.${type}s_read`, parentSel)[ 0 ].textContent = String(completed);
|
|
||||||
AnimeClient.showMessage('success', `Successfully updated ${mangaName}`);
|
|
||||||
AnimeClient.scrollToTop();
|
|
||||||
},
|
|
||||||
error: () => {
|
|
||||||
AnimeClient.hide('#loading-shadow');
|
|
||||||
AnimeClient.showMessage('error', `Failed to update ${mangaName}`);
|
|
||||||
AnimeClient.scrollToTop();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
14
public/js/anon.min.js
vendored
14
public/js/anon.min.js
vendored
@ -1,14 +0,0 @@
|
|||||||
(function(){var matches=function(elm,selector){var m=(elm.document||elm.ownerDocument).querySelectorAll(selector);var i=matches.length;while(--i>=0&&m.item(i)!==elm);return i>-1};var AnimeClient={noop:function(){},$:function(selector,context){context=context===undefined?null:context;if(typeof selector!=="string")return selector;context=context!==null&&context.nodeType===1?context:document;var elements=[];if(selector.match(/^#([\w]+$)/))elements.push(document.getElementById(selector.split("#")[1]));
|
|
||||||
else elements=[].slice.apply(context.querySelectorAll(selector));return elements},hasElement:function(selector){return AnimeClient.$(selector).length>0},scrollToTop:function(){var el=AnimeClient.$("header")[0];el.scrollIntoView(true)},hide:function(sel){if(typeof sel==="string")sel=AnimeClient.$(sel);if(Array.isArray(sel))sel.forEach(function(el){return el.setAttribute("hidden","hidden")});else sel.setAttribute("hidden","hidden")},show:function(sel){if(typeof sel==="string")sel=AnimeClient.$(sel);
|
|
||||||
if(Array.isArray(sel))sel.forEach(function(el){return el.removeAttribute("hidden")});else sel.removeAttribute("hidden")},showMessage:function(type,message){var template="<div class='message "+type+"'>\n\t\t\t\t<span class='icon'></span>\n\t\t\t\t"+message+"\n\t\t\t\t<span class='close'></span>\n\t\t\t</div>";var sel=AnimeClient.$(".message");if(sel[0]!==undefined)sel[0].remove();AnimeClient.$("header")[0].insertAdjacentHTML("beforeend",template)},closestParent:function(current,parentSelector){if(Element.prototype.closest!==
|
|
||||||
undefined)return current.closest(parentSelector);while(current!==document.documentElement){if(matches(current,parentSelector))return current;current=current.parentElement}return null},url:function(path){var uri="//"+document.location.host;uri+=path.charAt(0)==="/"?path:"/"+path;return uri},throttle:function(interval,fn,scope){var wait=false;return function(args){var $jscomp$restParams=[];for(var $jscomp$restIndex=0;$jscomp$restIndex<arguments.length;++$jscomp$restIndex)$jscomp$restParams[$jscomp$restIndex-
|
|
||||||
0]=arguments[$jscomp$restIndex];{var args$0=$jscomp$restParams;var context=scope||this;if(!wait){fn.apply(context,args$0);wait=true;setTimeout(function(){wait=false},interval)}}}}};function addEvent(sel,event,listener){if(!event.match(/^([\w\-]+)$/))event.split(" ").forEach(function(evt){addEvent(sel,evt,listener)});sel.addEventListener(event,listener,false)}function delegateEvent(sel,target,event,listener){addEvent(sel,event,function(e){AnimeClient.$(target,sel).forEach(function(element){if(e.target==
|
|
||||||
element){listener.call(element,e);e.stopPropagation()}})})}AnimeClient.on=function(sel,event,target,listener){if(listener===undefined){listener=target;AnimeClient.$(sel).forEach(function(el){addEvent(el,event,listener)})}else AnimeClient.$(sel).forEach(function(el){delegateEvent(el,target,event,listener)})};function ajaxSerialize(data){var pairs=[];Object.keys(data).forEach(function(name){var value=data[name].toString();name=encodeURIComponent(name);value=encodeURIComponent(value);pairs.push(name+
|
|
||||||
"="+value)});return pairs.join("&")}AnimeClient.ajax=function(url,config){var defaultConfig={data:{},type:"GET",dataType:"",success:AnimeClient.noop,mimeType:"application/x-www-form-urlencoded",error:AnimeClient.noop};config=Object.assign({},defaultConfig,config);var request=new XMLHttpRequest;var method=String(config.type).toUpperCase();if(method==="GET")url+=url.match(/\?/)?ajaxSerialize(config.data):"?"+ajaxSerialize(config.data);request.open(method,url);request.onreadystatechange=function(){if(request.readyState===
|
|
||||||
4){var responseText="";if(request.responseType==="json")responseText=JSON.parse(request.responseText);else responseText=request.responseText;if(request.status>299)config.error.call(null,request.status,responseText,request.response);else config.success.call(null,responseText,request.status)}};if(config.dataType==="json"){config.data=JSON.stringify(config.data);config.mimeType="application/json"}else config.data=ajaxSerialize(config.data);request.setRequestHeader("Content-Type",config.mimeType);if(method===
|
|
||||||
"GET")request.send(null);else request.send(config.data);return request};AnimeClient.get=function(url,data,callback){callback=callback===undefined?null:callback;if(callback===null){callback=data;data={}}return AnimeClient.ajax(url,{data:data,success:callback})};AnimeClient.on("header","click",".message",hide);AnimeClient.on("form.js-delete","submit",confirmDelete);AnimeClient.on(".js-clear-cache","click",clearAPICache);AnimeClient.on(".vertical-tabs input","change",scrollToSection);AnimeClient.on(".media-filter",
|
|
||||||
"input",filterMedia);function hide(event){AnimeClient.hide(event.target)}function confirmDelete(event){var proceed=confirm("Are you ABSOLUTELY SURE you want to delete this item?");if(proceed===false){event.preventDefault();event.stopPropagation()}}function clearAPICache(){AnimeClient.get("/cache_purge",function(){AnimeClient.showMessage("success","Successfully purged api cache")})}function scrollToSection(event){var el=event.currentTarget.parentElement;var rect=el.getBoundingClientRect();var top=
|
|
||||||
rect.top+window.pageYOffset;window.scrollTo({top:top,behavior:"smooth"})}function filterMedia(event){var rawFilter=event.target.value;var filter=new RegExp(rawFilter,"i");if(rawFilter!==""){AnimeClient.$("article.media").forEach(function(article){var titleLink=AnimeClient.$(".name a",article)[0];var title=String(titleLink.textContent).trim();if(!filter.test(title))AnimeClient.hide(article);else AnimeClient.show(article)});AnimeClient.$("table.media-wrap tbody tr").forEach(function(tr){var titleCell=
|
|
||||||
AnimeClient.$("td.align-left",tr)[0];var titleLink=AnimeClient.$("a",titleCell)[0];var linkTitle=String(titleLink.textContent).trim();var textTitle=String(titleCell.textContent).trim();if(!(filter.test(linkTitle)||filter.test(textTitle)))AnimeClient.hide(tr);else AnimeClient.show(tr)})}else{AnimeClient.show("article.media");AnimeClient.show("table.media-wrap tbody tr")}}if("serviceWorker"in navigator)navigator.serviceWorker.register("/sw.js").then(function(reg){console.log("Service worker registered",
|
|
||||||
reg.scope)})["catch"](function(error){console.error("Failed to register service worker",error)})})()
|
|
||||||
//# sourceMappingURL=anon.min.js.map
|
|
File diff suppressed because one or more lines are too long
29
public/js/scripts.min.js
vendored
29
public/js/scripts.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
5
public/js/tables.min.js
vendored
5
public/js/tables.min.js
vendored
@ -1,4 +1 @@
|
|||||||
(function(){var LightTableSorter=function(){var th=null;var cellIndex=null;var order="";var text=function(row){return row.cells.item(cellIndex).textContent.toLowerCase()};var sort=function(a,b){var textA=text(a);var textB=text(b);var n=parseInt(textA,10);if(n){textA=n;textB=parseInt(textB,10)}if(textA>textB)return 1;if(textA<textB)return-1;return 0};var toggle=function(){var c=order!=="sorting-asc"?"sorting-asc":"sorting-desc";th.className=(th.className.replace(order,"")+" "+c).trim();return order=
|
var LightTableSorter=function(){var th=null;var cellIndex=null;var order='';var text=function(row){return row.cells.item(cellIndex).textContent.toLowerCase();};var sort=function(a,b){var textA=text(a);var textB=text(b);var n=parseInt(textA,10);if(n){textA=n;textB=parseInt(textB,10);}if(textA>textB)return 1;if(textA<textB)return -1;return 0;};var toggle=function(){var c=order!=='sorting-asc'?'sorting-asc':'sorting-desc';th.className=(th.className.replace(order,'')+' '+c).trim();return order=c;};var reset=function(){th.classList.remove('sorting-asc','sorting-desc');th.classList.add('sorting');return order='';};var onClickEvent=function(e){if(th&&cellIndex!==e.target.cellIndex)reset();th=e.target;if(th.nodeName.toLowerCase()==='th'){cellIndex=th.cellIndex;var tbody=th.offsetParent.getElementsByTagName('tbody')[0];var rows=Array.from(tbody.rows);if(rows){rows.sort(sort);if(order==='sorting-asc')rows.reverse();toggle();tbody.innerHtml='';rows.forEach(function(row){tbody.appendChild(row);});}}};return {init:function(){var ths=document.getElementsByTagName('th');var results=[];for(var i=0,len=ths.length;i<len;i++){var th1=ths[i];th1.classList.add('sorting');results.push(th1.onclick=onClickEvent);}return results;}};}();LightTableSorter.init();
|
||||||
c};var reset=function(){th.classList.remove("sorting-asc","sorting-desc");th.classList.add("sorting");return order=""};var onClickEvent=function(e){if(th&&cellIndex!==e.target.cellIndex)reset();th=e.target;if(th.nodeName.toLowerCase()==="th"){cellIndex=th.cellIndex;var tbody=th.offsetParent.getElementsByTagName("tbody")[0];var rows=Array.from(tbody.rows);if(rows){rows.sort(sort);if(order==="sorting-asc")rows.reverse();toggle();tbody.innerHtml="";rows.forEach(function(row){tbody.appendChild(row)})}}};
|
|
||||||
return{init:function(){var ths=document.getElementsByTagName("th");var results=[];for(var i=0,len=ths.length;i<len;i++){var th$0=ths[i];th$0.classList.add("sorting");results.push(th$0.onclick=onClickEvent)}return results}}}();LightTableSorter.init()})()
|
|
||||||
//# sourceMappingURL=tables.min.js.map
|
|
@ -1 +1 @@
|
|||||||
{"version":3,"file":"tables.min.js.map","sources":["../../frontEndSrc/js/base/sort-tables.js"],"sourcesContent":["const LightTableSorter = (() => {\n\tlet th = null;\n\tlet cellIndex = null;\n\tlet order = '';\n\tconst text = (row) => row.cells.item(cellIndex).textContent.toLowerCase();\n\tconst sort = (a, b) => {\n\t\tlet textA = text(a);\n\t\tlet textB = text(b);\n\t\tconst n = parseInt(textA, 10);\n\t\tif (n) {\n\t\t\ttextA = n;\n\t\t\ttextB = parseInt(textB, 10);\n\t\t}\n\t\tif (textA > textB) {\n\t\t\treturn 1;\n\t\t}\n\t\tif (textA < textB) {\n\t\t\treturn -1;\n\t\t}\n\t\treturn 0;\n\t};\n\tconst toggle = () => {\n\t\tconst c = order !== 'sorting-asc' ? 'sorting-asc' : 'sorting-desc';\n\t\tth.className = (th.className.replace(order, '') + ' ' + c).trim();\n\t\treturn order = c;\n\t};\n\tconst reset = () => {\n\t\tth.classList.remove('sorting-asc', 'sorting-desc');\n\t\tth.classList.add('sorting');\n\t\treturn order = '';\n\t};\n\tconst onClickEvent = (e) => {\n\t\tif (th && (cellIndex !== e.target.cellIndex)) {\n\t\t\treset();\n\t\t}\n\t\tth = e.target;\n\t\tif (th.nodeName.toLowerCase() === 'th') {\n\t\t\tcellIndex = th.cellIndex;\n\t\t\tconst tbody = th.offsetParent.getElementsByTagName('tbody')[0];\n\t\t\tlet rows = Array.from(tbody.rows);\n\t\t\tif (rows) {\n\t\t\t\trows.sort(sort);\n\t\t\t\tif (order === 'sorting-asc') {\n\t\t\t\t\trows.reverse();\n\t\t\t\t}\n\t\t\t\ttoggle();\n\t\t\t\ttbody.innerHtml = '';\n\n\t\t\t\trows.forEach(row => {\n\t\t\t\t\ttbody.appendChild(row);\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t};\n\treturn {\n\t\tinit: () => {\n\t\t\tlet ths = document.getElementsByTagName('th');\n\t\t\tlet results = [];\n\t\t\tfor (let i = 0, len = ths.length; i < len; i++) {\n\t\t\t\tlet th = ths[i];\n\t\t\t\tth.classList.add('sorting');\n\t\t\t\tresults.push(th.onclick = onClickEvent);\n\t\t\t}\n\t\t\treturn results;\n\t\t}\n\t};\n})();\n\nLightTableSorter.init();"],"names":["th","cellIndex","order","text","row","cells","item","textContent","toLowerCase","sort","a","b","textA","textB","n","parseInt","toggle","c","className","trim","replace","reset","classList","remove","add","onClickEvent","e","target","nodeName","tbody","offsetParent","getElementsByTagName","rows","Array","from","reverse","innerHtml","forEach","appendChild","init","ths","document","results","i","len","length","push","onclick","LightTableSorter"],"mappings":"YAAA,gCACC,IAAIA,GAAK,IACT,KAAIC,UAAY,IAChB,KAAIC,MAAQ,EACZ,KAAMC,KAAOA,QAAA,CAACC,GAAD,CAAS,CAAA,MAAAA,IAAAC,MAAAC,KAAA,CAAeL,SAAf,CAAAM,YAAAC,YAAA,EAAA,CACtB,KAAMC,KAAOA,QAAA,CAACC,CAAD,CAAIC,CAAJ,CAAU,CACtB,IAAIC,MAAQT,IAAA,CAAKO,CAAL,CACZ,KAAIG,MAAQV,IAAA,CAAKQ,CAAL,CACZ,KAAMG,EAAIC,QAAA,CAASH,KAAT,CAAgB,EAAhB,CACV,IAAIE,CAAJ,CAAO,CACNF,KAAA,CAAQE,CACRD,MAAA,CAAQE,QAAA,CAASF,KAAT,CAAgB,EAAhB,CAFF,CAIP,GAAID,KAAJ,CAAYC,KAAZ,CACC,MAAO,EAER,IAAID,KAAJ,CAAYC,KAAZ,CACC,MAAO,EAER,OAAO,EAde,CAgBvB,KAAMG,OAASA,QAAA,EAAM,CACpB,IAAMC,EAAIf,KAAA,GAAU,aAAV,CAA0B,aAA1B,CAA0C,cACpDF,GAAAkB,UAAA,CAAeC,CAACnB,EAAAkB,UAAAE,QAAA,CAAqBlB,KAArB,CAA4B,EAA5B,CAADiB,CAAmC,GAAnCA,CAAyCF,CAAzCE,MAAA,EACf,OAAOjB,MAAP;AAAee,CAHK,CAKrB,KAAMI,MAAQA,QAAA,EAAM,CACnBrB,EAAAsB,UAAAC,OAAA,CAAoB,aAApB,CAAmC,cAAnC,CACAvB,GAAAsB,UAAAE,IAAA,CAAiB,SAAjB,CACA,OAAOtB,MAAP,CAAe,EAHI,CAKpB,KAAMuB,aAAeA,QAAA,CAACC,CAAD,CAAO,CAC3B,GAAI1B,EAAJ,EAAWC,SAAX,GAAyByB,CAAAC,OAAA1B,UAAzB,CACCoB,KAAA,EAEDrB,GAAA,CAAK0B,CAAAC,OACL,IAAI3B,EAAA4B,SAAApB,YAAA,EAAJ,GAAkC,IAAlC,CAAwC,CACvCP,SAAA,CAAYD,EAAAC,UACZ,KAAM4B,MAAQ7B,EAAA8B,aAAAC,qBAAA,CAAqC,OAArC,CAAA,CAA8C,CAA9C,CACd,KAAIC,KAAOC,KAAAC,KAAA,CAAWL,KAAAG,KAAX,CACX,IAAIA,IAAJ,CAAU,CACTA,IAAAvB,KAAA,CAAUA,IAAV,CACA,IAAIP,KAAJ,GAAc,aAAd,CACC8B,IAAAG,QAAA,EAEDnB,OAAA,EACAa,MAAAO,UAAA,CAAkB,EAElBJ,KAAAK,QAAA,CAAa,QAAA,CAAAjC,GAAA,CAAO,CACnByB,KAAAS,YAAA,CAAkBlC,GAAlB,CADmB,CAApB,CARS,CAJ6B,CALb,CAuB5B;MAAO,CACNmC,KAAMA,QAAA,EAAM,CACX,IAAIC,IAAMC,QAAAV,qBAAA,CAA8B,IAA9B,CACV,KAAIW,QAAU,EACd,KAAK,IAAIC,EAAI,CAAR,CAAWC,IAAMJ,GAAAK,OAAtB,CAAkCF,CAAlC,CAAsCC,GAAtC,CAA2CD,CAAA,EAA3C,CAAgD,CAC/C,IAAI3C,KAAKwC,GAAA,CAAIG,CAAJ,CACT3C,KAAAsB,UAAAE,IAAA,CAAiB,SAAjB,CACAkB,QAAAI,KAAA,CAAa9C,IAAA+C,QAAb,CAA0BtB,YAA1B,CAH+C,CAKhD,MAAOiB,QARI,CADN,IAcRM,iBAAAT,KAAA;"}
|
{"version":3,"sources":["/var/www/htdocs/github.timshomepage.net/animeclient/frontEndSrc/js/base/sort-tables.js"],"sourcesContent":["const LightTableSorter = (() => {\n\tlet th = null;\n\tlet cellIndex = null;\n\tlet order = '';\n\tconst text = (row) => row.cells.item(cellIndex).textContent.toLowerCase();\n\tconst sort = (a, b) => {\n\t\tlet textA = text(a);\n\t\tlet textB = text(b);\n\t\tconst n = parseInt(textA, 10);\n\t\tif (n) {\n\t\t\ttextA = n;\n\t\t\ttextB = parseInt(textB, 10);\n\t\t}\n\t\tif (textA > textB) {\n\t\t\treturn 1;\n\t\t}\n\t\tif (textA < textB) {\n\t\t\treturn -1;\n\t\t}\n\t\treturn 0;\n\t};\n\tconst toggle = () => {\n\t\tconst c = order !== 'sorting-asc' ? 'sorting-asc' : 'sorting-desc';\n\t\tth.className = (th.className.replace(order, '') + ' ' + c).trim();\n\t\treturn order = c;\n\t};\n\tconst reset = () => {\n\t\tth.classList.remove('sorting-asc', 'sorting-desc');\n\t\tth.classList.add('sorting');\n\t\treturn order = '';\n\t};\n\tconst onClickEvent = (e) => {\n\t\tif (th && (cellIndex !== e.target.cellIndex)) {\n\t\t\treset();\n\t\t}\n\t\tth = e.target;\n\t\tif (th.nodeName.toLowerCase() === 'th') {\n\t\t\tcellIndex = th.cellIndex;\n\t\t\tconst tbody = th.offsetParent.getElementsByTagName('tbody')[0];\n\t\t\tlet rows = Array.from(tbody.rows);\n\t\t\tif (rows) {\n\t\t\t\trows.sort(sort);\n\t\t\t\tif (order === 'sorting-asc') {\n\t\t\t\t\trows.reverse();\n\t\t\t\t}\n\t\t\t\ttoggle();\n\t\t\t\ttbody.innerHtml = '';\n\n\t\t\t\trows.forEach(row => {\n\t\t\t\t\ttbody.appendChild(row);\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t};\n\treturn {\n\t\tinit: () => {\n\t\t\tlet ths = document.getElementsByTagName('th');\n\t\t\tlet results = [];\n\t\t\tfor (let i = 0, len = ths.length; i < len; i++) {\n\t\t\t\tlet th = ths[i];\n\t\t\t\tth.classList.add('sorting');\n\t\t\t\tresults.push(th.onclick = onClickEvent);\n\t\t\t}\n\t\t\treturn results;\n\t\t}\n\t};\n})();\n\nLightTableSorter.init();"],"names":[],"mappings":"IAAM,gBAAgB,gBACjB,EAAE,CAAG,IAAI,KACT,SAAS,CAAG,IAAI,KAChB,KAAK,QACH,IAAI,UAAI,GAAG,SAAK,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,EAAE,WAAW,CAAC,WAAW,SACjE,IAAI,UAAI,CAAC,CAAE,CAAC,MACb,KAAK,CAAG,IAAI,CAAC,CAAC,MACd,KAAK,CAAG,IAAI,CAAC,CAAC,MACZ,CAAC,CAAG,QAAQ,CAAC,KAAK,CAAE,EAAE,KACxB,CAAC,EACJ,KAAK,CAAG,CAAC,CACT,KAAK,CAAG,QAAQ,CAAC,KAAK,CAAE,EAAE,MAEvB,KAAK,CAAG,KAAK,QACT,CAAC,IAEL,KAAK,CAAG,KAAK,QACT,EAAE,QAEH,CAAC,OAEH,MAAM,gBACL,CAAC,CAAG,KAAK,IAAK,WAAa,GAAG,WAAa,GAAG,YAAc,EAClE,EAAE,CAAC,SAAS,EAAI,EAAE,CAAC,SAAS,CAAC,OAAO,CAAC,KAAK,MAAQ,CAAG,EAAG,CAAC,EAAE,IAAI,UACxD,KAAK,CAAG,CAAC,OAEX,KAAK,YACV,EAAE,CAAC,SAAS,CAAC,MAAM,EAAC,WAAa,GAAE,YAAc,GACjD,EAAE,CAAC,SAAS,CAAC,GAAG,EAAC,OAAS,UACnB,KAAK,UAEP,YAAY,UAAI,CAAC,KAClB,EAAE,EAAK,SAAS,GAAK,CAAC,CAAC,MAAM,CAAC,SAAS,CAC1C,KAAK,GAEN,EAAE,CAAG,CAAC,CAAC,MAAM,IACT,EAAE,CAAC,QAAQ,CAAC,WAAW,MAAO,EAAI,GACrC,SAAS,CAAG,EAAE,CAAC,SAAS,KAClB,KAAK,CAAG,EAAE,CAAC,YAAY,CAAC,oBAAoB,EAAC,KAAO,GAAE,CAAC,MACzD,IAAI,CAAG,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,KAC5B,IAAI,EACP,IAAI,CAAC,IAAI,CAAC,IAAI,KACV,KAAK,IAAK,WAAa,EAC1B,IAAI,CAAC,OAAO,GAEb,MAAM,GACN,KAAK,CAAC,SAAS,IAEf,IAAI,CAAC,OAAO,UAAC,GAAG,EACf,KAAK,CAAC,WAAW,CAAC,GAAG,iBAMxB,IAAI,gBACC,GAAG,CAAG,QAAQ,CAAC,oBAAoB,EAAC,EAAI,OACxC,OAAO,YACF,CAAC,CAAG,CAAC,CAAE,GAAG,CAAG,GAAG,CAAC,MAAM,CAAE,CAAC,CAAG,GAAG,CAAE,CAAC,QACvC,GAAE,CAAG,GAAG,CAAC,CAAC,EACd,GAAE,CAAC,SAAS,CAAC,GAAG,EAAC,OAAS,GAC1B,OAAO,CAAC,IAAI,CAAC,GAAE,CAAC,OAAO,CAAG,YAAY,UAEhC,OAAO,QAKjB,gBAAgB,CAAC,IAAI"}
|
Loading…
Reference in New Issue
Block a user