631 lines
16 KiB
JavaScript
631 lines
16 KiB
JavaScript
(function () {
|
|
'use strict';
|
|
|
|
// -------------------------------------------------------------------------
|
|
// ! Base
|
|
// -------------------------------------------------------------------------
|
|
|
|
const matches = (elm, selector) => {
|
|
let matches = (elm.document || elm.ownerDocument).querySelectorAll(selector),
|
|
i = matches.length;
|
|
while (--i >= 0 && matches.item(i) !== elm) {} return i > -1;
|
|
};
|
|
|
|
const AnimeClient = {
|
|
/**
|
|
* Placeholder function
|
|
*/
|
|
noop: () => {},
|
|
/**
|
|
* DOM selector
|
|
*
|
|
* @param {string} selector - The dom selector string
|
|
* @param {object} [context]
|
|
* @return {[HTMLElement]} - array of dom elements
|
|
*/
|
|
$(selector, context = null) {
|
|
if (typeof selector !== 'string') {
|
|
return selector;
|
|
}
|
|
|
|
context = (context !== null && context.nodeType === 1)
|
|
? context
|
|
: document;
|
|
|
|
let elements = [];
|
|
if (selector.match(/^#([\w]+$)/)) {
|
|
elements.push(document.getElementById(selector.split('#')[1]));
|
|
} else {
|
|
elements = [].slice.apply(context.querySelectorAll(selector));
|
|
}
|
|
|
|
return elements;
|
|
},
|
|
/**
|
|
* Does the selector exist on the current page?
|
|
*
|
|
* @param {string} selector
|
|
* @returns {boolean}
|
|
*/
|
|
hasElement (selector) {
|
|
return AnimeClient.$(selector).length > 0;
|
|
},
|
|
/**
|
|
* Scroll to the top of the Page
|
|
*
|
|
* @return {void}
|
|
*/
|
|
scrollToTop () {
|
|
window.scroll(0,0);
|
|
},
|
|
/**
|
|
* Hide the selected element
|
|
*
|
|
* @param {string|Element} sel - the selector of the element to hide
|
|
* @return {void}
|
|
*/
|
|
hide (sel) {
|
|
sel.setAttribute('hidden', 'hidden');
|
|
},
|
|
/**
|
|
* UnHide the selected element
|
|
*
|
|
* @param {string|Element} sel - the selector of the element to hide
|
|
* @return {void}
|
|
*/
|
|
show (sel) {
|
|
sel.removeAttribute('hidden');
|
|
},
|
|
/**
|
|
* Display a message box
|
|
*
|
|
* @param {string} type - message type: info, error, success
|
|
* @param {string} message - the message itself
|
|
* @return {void}
|
|
*/
|
|
showMessage (type, message) {
|
|
let template =
|
|
`<div class='message ${type}'>
|
|
<span class='icon'></span>
|
|
${message}
|
|
<span class='close'></span>
|
|
</div>`;
|
|
|
|
let sel = AnimeClient.$('.message');
|
|
if (sel[0] !== undefined) {
|
|
sel[0].remove();
|
|
}
|
|
|
|
AnimeClient.$('header')[0].insertAdjacentHTML('beforeend', template);
|
|
},
|
|
/**
|
|
* Finds the closest parent element matching the passed selector
|
|
*
|
|
* @param {HTMLElement} current - the current HTMLElement
|
|
* @param {string} parentSelector - selector for the parent element
|
|
* @return {HTMLElement|null} - the parent element
|
|
*/
|
|
closestParent (current, parentSelector) {
|
|
if (Element.prototype.closest !== undefined) {
|
|
return current.closest(parentSelector);
|
|
}
|
|
|
|
while (current !== document.documentElement) {
|
|
if (matches(current, parentSelector)) {
|
|
return current;
|
|
}
|
|
|
|
current = current.parentElement;
|
|
}
|
|
|
|
return null;
|
|
},
|
|
/**
|
|
* Generate a full url from a relative path
|
|
*
|
|
* @param {string} path - url path
|
|
* @return {string} - full url
|
|
*/
|
|
url (path) {
|
|
let uri = `//${document.location.host}`;
|
|
uri += (path.charAt(0) === '/') ? path : `/${path}`;
|
|
|
|
return uri;
|
|
},
|
|
/**
|
|
* Throttle execution of a function
|
|
*
|
|
* @see https://remysharp.com/2010/07/21/throttling-function-calls
|
|
* @see https://jsfiddle.net/jonathansampson/m7G64/
|
|
* @param {Number} interval - the minimum throttle time in ms
|
|
* @param {Function} fn - the function to throttle
|
|
* @param {Object} [scope] - the 'this' object for the function
|
|
* @return {Function}
|
|
*/
|
|
throttle (interval, fn, scope) {
|
|
let wait = false;
|
|
return function (...args) {
|
|
const context = scope || this;
|
|
|
|
if ( ! wait) {
|
|
fn.apply(context, args);
|
|
wait = true;
|
|
setTimeout(function() {
|
|
wait = false;
|
|
}, interval);
|
|
}
|
|
};
|
|
},
|
|
};
|
|
|
|
// -------------------------------------------------------------------------
|
|
// ! Events
|
|
// -------------------------------------------------------------------------
|
|
|
|
function addEvent(sel, event, listener) {
|
|
// Recurse!
|
|
if (! event.match(/^([\w\-]+)$/)) {
|
|
event.split(' ').forEach((evt) => {
|
|
addEvent(sel, evt, listener);
|
|
});
|
|
}
|
|
|
|
sel.addEventListener(event, listener, false);
|
|
}
|
|
|
|
function delegateEvent(sel, target, event, listener) {
|
|
// Attach the listener to the parent
|
|
addEvent(sel, event, (e) => {
|
|
// Get live version of the target selector
|
|
AnimeClient.$(target, sel).forEach((element) => {
|
|
if(e.target == element) {
|
|
listener.call(element, e);
|
|
e.stopPropagation();
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Add an event listener
|
|
*
|
|
* @param {string|HTMLElement} sel - the parent selector to bind to
|
|
* @param {string} event - event name(s) to bind
|
|
* @param {string|HTMLElement|function} target - the element to directly bind the event to
|
|
* @param {function} [listener] - event listener callback
|
|
* @return {void}
|
|
*/
|
|
AnimeClient.on = (sel, event, target, listener) => {
|
|
if (listener === undefined) {
|
|
listener = target;
|
|
AnimeClient.$(sel).forEach((el) => {
|
|
addEvent(el, event, listener);
|
|
});
|
|
} else {
|
|
AnimeClient.$(sel).forEach((el) => {
|
|
delegateEvent(el, target, event, listener);
|
|
});
|
|
}
|
|
};
|
|
|
|
// -------------------------------------------------------------------------
|
|
// ! Ajax
|
|
// -------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Url encoding for non-get requests
|
|
*
|
|
* @param data
|
|
* @returns {string}
|
|
* @private
|
|
*/
|
|
function ajaxSerialize(data) {
|
|
let pairs = [];
|
|
|
|
Object.keys(data).forEach((name) => {
|
|
let value = data[name].toString();
|
|
|
|
name = encodeURIComponent(name);
|
|
value = encodeURIComponent(value);
|
|
|
|
pairs.push(`${name}=${value}`);
|
|
});
|
|
|
|
return pairs.join('&');
|
|
}
|
|
|
|
/**
|
|
* Make an ajax request
|
|
*
|
|
* Config:{
|
|
* data: // data to send with the request
|
|
* type: // http verb of the request, defaults to GET
|
|
* success: // success callback
|
|
* error: // error callback
|
|
* }
|
|
*
|
|
* @param {string} url - the url to request
|
|
* @param {Object} config - the configuration object
|
|
* @return {void}
|
|
*/
|
|
AnimeClient.ajax = (url, config) => {
|
|
// Set some sane defaults
|
|
const defaultConfig = {
|
|
data: {},
|
|
type: 'GET',
|
|
dataType: '',
|
|
success: AnimeClient.noop,
|
|
mimeType: 'application/x-www-form-urlencoded',
|
|
error: AnimeClient.noop
|
|
};
|
|
|
|
config = {
|
|
...defaultConfig,
|
|
...config,
|
|
};
|
|
|
|
let request = new XMLHttpRequest();
|
|
let method = String(config.type).toUpperCase();
|
|
|
|
if (method === 'GET') {
|
|
url += (url.match(/\?/))
|
|
? ajaxSerialize(config.data)
|
|
: `?${ajaxSerialize(config.data)}`;
|
|
}
|
|
|
|
request.open(method, url);
|
|
|
|
request.onreadystatechange = () => {
|
|
if (request.readyState === 4) {
|
|
let responseText = '';
|
|
|
|
if (request.responseType === 'json') {
|
|
responseText = JSON.parse(request.responseText);
|
|
} else {
|
|
responseText = request.responseText;
|
|
}
|
|
|
|
if (request.status > 299) {
|
|
config.error.call(null, request.status, responseText, request.response);
|
|
} else {
|
|
config.success.call(null, responseText, request.status);
|
|
}
|
|
}
|
|
};
|
|
|
|
if (config.dataType === 'json') {
|
|
config.data = JSON.stringify(config.data);
|
|
config.mimeType = 'application/json';
|
|
} else {
|
|
config.data = ajaxSerialize(config.data);
|
|
}
|
|
|
|
request.setRequestHeader('Content-Type', config.mimeType);
|
|
|
|
switch (method) {
|
|
case 'GET':
|
|
request.send(null);
|
|
break;
|
|
|
|
default:
|
|
request.send(config.data);
|
|
break;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Do a get request
|
|
*
|
|
* @param {string} url
|
|
* @param {object|function} data
|
|
* @param {function} [callback]
|
|
*/
|
|
AnimeClient.get = (url, data, callback = null) => {
|
|
if (callback === null) {
|
|
callback = data;
|
|
data = {};
|
|
}
|
|
|
|
return AnimeClient.ajax(url, {
|
|
data,
|
|
success: callback
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Event handlers
|
|
*/
|
|
// Close event for messages
|
|
AnimeClient.on('header', 'click', '.message', (e) => {
|
|
AnimeClient.hide(e.target);
|
|
});
|
|
|
|
// Confirm deleting of list or library items
|
|
AnimeClient.on('form.js-delete', 'submit', (event) => {
|
|
const proceed = confirm('Are you ABSOLUTELY SURE you want to delete this item?');
|
|
|
|
if (proceed === false) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
}
|
|
});
|
|
|
|
// Clear the api cache
|
|
AnimeClient.on('.js-clear-cache', 'click', () => {
|
|
AnimeClient.get('/cache_purge', () => {
|
|
AnimeClient.showMessage('success', 'Successfully purged api cache');
|
|
});
|
|
});
|
|
|
|
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);
|
|
});
|
|
}
|
|
|
|
// 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) {
|
|
const results = [];
|
|
|
|
data.forEach(x => {
|
|
const item = x.attributes;
|
|
const titles = item.titles.reduce((prev, current) => {
|
|
return prev + `${current}<br />`;
|
|
}, []);
|
|
|
|
results.push(`
|
|
<article class="media search">
|
|
<div class="name">
|
|
<input type="radio" class="mal-check" id="mal_${item.slug}" name="mal_id" value="${x.mal_id}" />
|
|
<input type="radio" class="big-check" id="${item.slug}" name="id" value="${x.id}" />
|
|
<label for="${item.slug}">
|
|
<picture width="220">
|
|
<source srcset="/public/images/anime/${x.id}.webp" type="image/webp" />
|
|
<source srcset="/public/images/anime/${x.id}.jpg" type="image/jpeg" />
|
|
<img src="/public/images/anime/${x.id}.jpg" alt="" width="220" />
|
|
</picture>
|
|
|
|
<span class="name">
|
|
${item.canonicalTitle}<br />
|
|
<small>${titles}</small>
|
|
</span>
|
|
</label>
|
|
</div>
|
|
<div class="table">
|
|
<div class="row">
|
|
<span class="edit">
|
|
<a class="bracketed" href="/anime/details/${item.slug}">Info Page</a>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</article>
|
|
`);
|
|
});
|
|
|
|
return results.join('');
|
|
}
|
|
|
|
function renderMangaSearchResults (data) {
|
|
const results = [];
|
|
|
|
data.forEach(x => {
|
|
const item = x.attributes;
|
|
const titles = item.titles.reduce((prev, current) => {
|
|
return prev + `${current}<br />`;
|
|
}, []);
|
|
|
|
results.push(`
|
|
<article class="media search">
|
|
<div class="name">
|
|
<input type="radio" id="mal_${item.slug}" name="mal_id" value="${x.mal_id}" />
|
|
<input type="radio" class="big-check" id="${item.slug}" name="id" value="${x.id}" />
|
|
<label for="${item.slug}">
|
|
<picture width="220">
|
|
<source srcset="/public/images/manga/${x.id}.webp" type="image/webp" />
|
|
<source srcset="/public/images/manga/${x.id}.jpg" type="image/jpeg" />
|
|
<img src="/public/images/manga/${x.id}.jpg" alt="" width="220" />
|
|
</picture>
|
|
<span class="name">
|
|
${item.canonicalTitle}<br />
|
|
<small>${titles}</small>
|
|
</span>
|
|
</label>
|
|
</div>
|
|
<div class="table">
|
|
<div class="row">
|
|
<span class="edit">
|
|
<a class="bracketed" href="/manga/details/${item.slug}">Info Page</a>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</article>
|
|
`);
|
|
});
|
|
|
|
return results.join('');
|
|
}
|
|
|
|
const search = (query) => {
|
|
// Show the loader
|
|
AnimeClient.$('.cssload-loader')[ 0 ].removeAttribute('hidden');
|
|
|
|
// Do the api search
|
|
AnimeClient.get(AnimeClient.url('/anime-collection/search'), { query }, (searchResults, status) => {
|
|
searchResults = JSON.parse(searchResults);
|
|
|
|
// Hide the loader
|
|
AnimeClient.$('.cssload-loader')[ 0 ].setAttribute('hidden', 'hidden');
|
|
|
|
// Show the results
|
|
AnimeClient.$('#series_list')[ 0 ].innerHTML = renderAnimeSearchResults(searchResults.data);
|
|
});
|
|
};
|
|
|
|
if (AnimeClient.hasElement('.anime #search')) {
|
|
AnimeClient.on('#search', 'keyup', AnimeClient.throttle(250, (e) => {
|
|
const query = encodeURIComponent(e.target.value);
|
|
if (query === '') {
|
|
return;
|
|
}
|
|
|
|
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(AnimeClient.$('#loading-shadow')[ 0 ]);
|
|
|
|
// 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.errors) {
|
|
AnimeClient.hide(AnimeClient.$('#loading-shadow')[ 0 ]);
|
|
AnimeClient.showMessage('error', `Failed to update ${title}. `);
|
|
AnimeClient.scrollToTop();
|
|
return;
|
|
}
|
|
|
|
if (resData.data.attributes.status === 'completed') {
|
|
AnimeClient.hide(parentSel);
|
|
}
|
|
|
|
AnimeClient.hide(AnimeClient.$('#loading-shadow')[ 0 ]);
|
|
|
|
AnimeClient.showMessage('success', `Successfully updated ${title}`);
|
|
AnimeClient.$('.completed_number', parentSel)[ 0 ].textContent = ++watchedCount;
|
|
AnimeClient.scrollToTop();
|
|
},
|
|
error: () => {
|
|
AnimeClient.hide(AnimeClient.$('#loading-shadow')[ 0 ]);
|
|
AnimeClient.showMessage('error', `Failed to update ${title}. `);
|
|
AnimeClient.scrollToTop();
|
|
}
|
|
});
|
|
});
|
|
|
|
const search$1 = (query) => {
|
|
AnimeClient.$('.cssload-loader')[ 0 ].removeAttribute('hidden');
|
|
AnimeClient.get(AnimeClient.url('/manga/search'), { query }, (searchResults, status) => {
|
|
searchResults = JSON.parse(searchResults);
|
|
AnimeClient.$('.cssload-loader')[ 0 ].setAttribute('hidden', 'hidden');
|
|
AnimeClient.$('#series_list')[ 0 ].innerHTML = renderMangaSearchResults(searchResults.data);
|
|
});
|
|
};
|
|
|
|
if (AnimeClient.hasElement('.manga #search')) {
|
|
AnimeClient.on('#search', 'keyup', AnimeClient.throttle(250, (e) => {
|
|
let query = encodeURIComponent(e.target.value);
|
|
if (query === '') {
|
|
return;
|
|
}
|
|
|
|
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(AnimeClient.$('#loading-shadow')[ 0 ]);
|
|
|
|
AnimeClient.ajax(AnimeClient.url('/manga/increment'), {
|
|
data,
|
|
dataType: 'json',
|
|
type: 'POST',
|
|
mimeType: 'application/json',
|
|
success: () => {
|
|
if (data.data.status === 'completed') {
|
|
AnimeClient.hide(parentSel);
|
|
}
|
|
|
|
AnimeClient.hide(AnimeClient.$('#loading-shadow')[ 0 ]);
|
|
|
|
AnimeClient.$(`.${type}s_read`, parentSel)[ 0 ].textContent = completed;
|
|
AnimeClient.showMessage('success', `Successfully updated ${mangaName}`);
|
|
AnimeClient.scrollToTop();
|
|
},
|
|
error: () => {
|
|
AnimeClient.hide(AnimeClient.$('#loading-shadow')[ 0 ]);
|
|
AnimeClient.showMessage('error', `Failed to update ${mangaName}`);
|
|
AnimeClient.scrollToTop();
|
|
}
|
|
});
|
|
});
|
|
|
|
}());
|
|
//# sourceMappingURL=scripts-authed.min.js.map
|