This commit is contained in:
Pig Fang 2018-08-21 09:13:43 +08:00
parent ce11b56444
commit 7103d52a6e
15 changed files with 9 additions and 2749 deletions

View File

@ -1,4 +0,0 @@
# http://editorconfig.org
[*.js]
indent_size = 2

File diff suppressed because it is too large Load Diff

View File

@ -1,244 +0,0 @@
/* eslint no-unused-vars: "off" */
const $ = require('jquery');
window.jQuery = window.$ = $;
describe('tests for "i18n" module', () => {
const modulePath = '../common/i18n';
it('load locales', () => {
window.isEmpty = obj => !obj; // Just for test
const loadLocales = require(modulePath).loadLocales;
$.locales = {
en: { text: 'text', nested: { sth: ':sth here!' } }
};
loadLocales();
expect($.currentLocale).toEqual({ text: 'text', nested: { sth: ':sth here!' } });
});
it('get translated text', () => {
const trans = require(modulePath).trans;
$.currentLocale = undefined; // Should load locale automatically
$.locales = {
en: { text: 'text', nested: { sth: ':sth here!' } }
};
expect(trans('text')).toBe('text');
expect(trans('text.nothing')).toBe('text.nothing');
expect(trans('nested.sth')).toBe(':sth here!');
expect(trans('nested.sth', { sth: 'abc' })).toBe('abc here!');
});
});
jest.useFakeTimers();
describe('tests for "logout" module', () => {
const modulePath = '../common/logout';
it('logout', async () => {
const swal = jest.fn()
.mockReturnValueOnce(Promise.reject())
.mockReturnValueOnce(Promise.resolve());
const trans = jest.fn(key => key);
const fetch = jest.fn()
.mockReturnValueOnce(Promise.resolve({ msg: 'success' }))
.mockReturnValueOnce(Promise.reject());
const showAjaxError = jest.fn();
window.swal = swal;
window.trans = trans;
window.fetch = fetch;
window.url = jest.fn(path => path);
window.showAjaxError = showAjaxError;
document.body.innerHTML = '<button id="logout-button"></button>';
require(modulePath);
await $('button').click();
expect(fetch).not.toBeCalled();
await $('button').click();
expect(swal).toBeCalledWith({
text: 'general.confirmLogout',
type: 'warning',
showCancelButton: true,
confirmButtonText: 'general.confirm',
cancelButtonText: 'general.cancel'
});
expect(fetch).toBeCalledWith({
type: 'POST',
url: 'auth/logout',
dataType: 'json'
});
jest.runAllTimers();
expect(url).toBeCalled();
await $('button').click();
expect(swal).toBeCalledWith({ type: 'success', html: 'success' });
await $('button').click();
expect(showAjaxError).toBeCalled();
});
});
jest.useRealTimers();
describe('tests for "notify" module', () => {
const modulePath = '../common/notify';
it('show message', () => {
document.body.innerHTML = '<div id="msg" class="a-class"></div>';
const showMsg = require(modulePath).showMsg;
showMsg('msg1');
expect($('div').hasClass('a-class')).toBe(false);
expect($('div').hasClass('callout')).toBe(true);
expect($('div').hasClass('callout-info')).toBe(true);
expect($('div').html()).toBe('msg1');
showMsg('msg2', 'warning');
expect($('div').hasClass('callout-info')).toBe(false);
expect($('div').hasClass('callout')).toBe(true);
expect($('div').hasClass('callout-warning')).toBe(true);
expect($('div').html()).toBe('msg2');
});
it('show ajax error', () => {
window.trans = jest.fn(key => key);
$.fn.modal = jest.fn();
const showAjaxError = require(modulePath).showAjaxError;
showAjaxError({ responseText: 'error' });
expect(window.trans).toBeCalledWith('general.fatalError');
});
it('show modal dialog', () => {
const modal = jest.fn();
$.fn.modal = modal;
const showModal = require(modulePath).showModal;
showModal('');
expect(modal).toBeCalled();
});
});
describe('tests for "polyfill" module', () => {
const modulePath = '../common/polyfill';
String.prototype.includes = undefined;
String.prototype.endsWith = undefined;
require(modulePath);
it('String#includes', () => {
expect('blessing-skin'.includes('skin')).toBe(true);
expect('blessing-skin'.includes('server')).toBe(false);
expect('blessing-skin'.includes('skin', 9)).toBe(true);
expect('blessing-skin'.includes('blessing', 9)).toBe(false);
});
it('String#endsWith', () => {
expect('blessing-skin'.endsWith('skin')).toBe(true);
expect('blessing-skin'.endsWith('server')).toBe(false);
expect('blessing-skin'.endsWith('blessing', 8)).toBe(true);
});
});
describe('tests for "utils" module', () => {
const modulePath = '../common/utils';
window.blessing = {
version: ''
};
it('check a variable if it is empty', () => {
const isEmpty = require(modulePath).isEmpty;
expect(isEmpty()).toBe(true);
expect(isEmpty(null)).toBe(true);
expect(isEmpty(undefined)).toBe(true);
expect(isEmpty(0)).toBe(false);
expect(isEmpty(false)).toBe(false);
expect(isEmpty('')).toBe(true);
expect(isEmpty({})).toBe(true);
expect(isEmpty({ sth: '' })).toBe(false);
expect(isEmpty([])).toBe(true);
expect(isEmpty([1])).toBe(false);
expect(isEmpty('something')).toBe(false);
expect(isEmpty(Symbol())).toBe(true);
});
it('fake fetch', () => {
$.ajax = jest.fn();
const fetch = require(modulePath).fetch;
const xhr = fetch({ type: 'GET' });
expect($.ajax).toBeCalledWith({ type: 'GET' });
expect(Object.getPrototypeOf(xhr)).toBe(Promise.prototype);
});
it('make a debounced function', done => {
const func = jest.fn();
const debounce = require(modulePath).debounce;
const debounced = debounce(func, 100);
debounced();
debounced();
setTimeout(() => {
expect(func.mock.calls.length).toBe(1);
done();
}, 100);
jest.runAllTimers();
expect(() => debounce(func, 'string')).toThrow();
expect(() => debounce('not a function', 100)).toThrow();
});
it('get a absolute url', () => {
window.blessing = { base_url: 'http://localhost' };
const url = require(modulePath).url;
expect(url()).toBe('http://localhost/');
expect(url('test')).toBe('http://localhost/test');
expect(url('/test')).toBe('http://localhost/test');
});
it('get argument from query string', () => {
const { getQueryString } = require(modulePath);
Object.defineProperty(window.location, 'search', {
value: '?key1=val1',
writable: true
});
expect(getQueryString('key1')).toBe('val1');
expect(getQueryString('key2')).toBeUndefined();
expect(getQueryString('key2', 'val2')).toBe('val2');
window.location.search = '?key1=val1&key2=val2';
expect(getQueryString('key2', 'val2')).toBe('val2');
});
it('test `isMobileBrowserScrolling` function', () => {
const { isMobileBrowserScrolling } = require(modulePath);
expect(isMobileBrowserScrolling()).toBe(false);
$.cachedWindowWidth = 50;
$.cachedWindowHeight = 50;
expect(isMobileBrowserScrolling()).toBe(false);
$.cachedWindowWidth = 60;
$.cachedWindowHeight = 0;
expect(isMobileBrowserScrolling()).toBe(false);
$.cachedWindowWidth = 0;
$.cachedWindowHeight = 50;
expect(isMobileBrowserScrolling()).toBe(true);
expect(isMobileBrowserScrolling()).toBe(true); // For the second condition
$.lastWindowHeight = 60;
expect(isMobileBrowserScrolling()).toBe(false);
});
});

View File

@ -1,30 +0,0 @@
/* global current_skin:true */
'use strict';
$('#layout-skins-list [data-skin]').click(function (e) {
e.preventDefault();
const skin_name = $(this).data('skin');
$('body').removeClass(current_skin).addClass(skin_name);
current_skin = skin_name;
});
async function submitColor() {
try {
const { errno, msg } = await fetch({
type: 'POST',
url: url('admin/customize?action=color'),
dataType: 'json',
data: { color_scheme: current_skin }
});
errno === 0 ? toastr.success(msg) : toastr.warning(msg);
} catch (error) {
showAjaxError(error);
}
}
$('#color-submit').click(submitColor);
if (process.env.NODE_ENV === 'test') {
module.exports = submitColor;
}

View File

@ -1,288 +0,0 @@
'use strict';
if ($('#player-table').length === 1) {
$(document).ready(initPlayersTable);
}
function initPlayersTable() {
const specificUid = getQueryString('uid');
const query = specificUid ? `?uid=${specificUid}` : '';
$('#player-table').DataTable({
ajax: url(`admin/player-data${query}`),
scrollY: ($('.content-wrapper').height() - $('.content-header').outerHeight()) * 0.7,
fnDrawCallback: () => $('[data-toggle="tooltip"]').tooltip(),
columnDefs: playersTableColumnDefs
}).on('xhr.dt', handleDataTablesAjaxError);
}
const playersTableColumnDefs = [
{
targets: 0,
data: 'pid',
width: '1%'
},
{
targets: 1,
data: 'uid',
render: (data, type, row) => `<a href="${url('admin/users?uid=' + row.uid)}" title="${trans('admin.inspectHisOwner')}" data-toggle="tooltip" data-placement="right">${data}</span>`
},
{
targets: 2,
data: 'player_name'
},
{
targets: 3,
data: 'preference',
render: data => {
return `
<select class="form-control" onchange="changePreference.call(this)">
<option ${(data === 'default') ? 'selected=selected' : ''} value="default">Default (Steve)</option>
<option ${(data === 'slim') ? 'selected=selected' : ''} value="slim">Slim (Alex)</option>
</select>`;
}
},
{
targets: 4,
searchable: false,
orderable: false,
render: (data, type, row) => ['steve', 'alex', 'cape'].reduce((html, type) => {
const currentTypeTid = row[`tid_${type}`];
const imageId = `${row.pid}-${currentTypeTid}`;
if (currentTypeTid === 0) {
return html + `<img id="${imageId}" width="64" />`;
} else {
return html + `
<a href="${ url('skinlib/show/' + currentTypeTid) }">
<img id="${imageId}" width="64" src="${url('/preview/64/' + currentTypeTid)}.png" />
</a>`;
}
}, '')
},
{
targets: 5,
data: 'last_modified'
},
{
targets: 6,
searchable: false,
orderable: false,
render: (data, type, row) => `
<div class="btn-group">
<button type="button" class="btn btn-sm btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
${ trans('admin.operationsTitle') } <span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li><a style="cursor: pointer" onclick="changeTexture(${row.pid}, '${row.player_name}');">${trans('admin.changeTexture')}</a></li>
<li><a style="cursor: pointer" onclick="changePlayerName(${row.pid}, '${row.player_name}');">${trans('admin.changePlayerName')}</a></li>
<li><a style="cursor: pointer" onclick="changeOwner(${row.pid});">${trans('admin.changeOwner')}</a></li>
</ul>
</div>
<a class="btn btn-danger btn-sm" style="cursor: pointer" onclick="deletePlayer(${row.pid});">${trans('admin.deletePlayer')}</a>`
}
];
async function changePreference() {
try {
const { errno, msg } = await fetch({
type: 'POST',
url: url('admin/players?action=preference'),
dataType: 'json',
data: {
pid: $(this).parent().parent().attr('id'),
preference: $(this).val()
}
});
errno === 0 ? toastr.success(msg) : toastr.warning(msg);
} catch (error) {
showAjaxError(error);
}
}
function changeTexture(pid, playerName) {
const dom = `
<div class="form-group">
<label for="model">${trans('admin.textureType')}</label>
<select class="form-control" id="model">
<option value="steve">${trans('admin.skin', { 'model': 'Steve' })}</option>
<option value="alex">${trans('admin.skin', { 'model': 'Alex' })}</option>
<option value="cape">${trans('admin.cape')}</option>
</select>
</div>
<div class="form-group">
<label for="tid">${trans('admin.pid')}</label>
<input id="tid" class="form-control" type="text" placeholder="${trans('admin.pidNotice')}">
</div>`;
showModal(dom, trans('admin.changePlayerTexture', { 'player': playerName }), 'default', {
callback: `ajaxChangeTexture(${pid})`
});
return;
}
async function ajaxChangeTexture(pid) {
// Remove interference of modal which is hide
$('.modal').each(function () {
if ($(this).css('display') === 'none') $(this).remove();
});
const model = $('#model').val();
const tid = $('#tid').val();
try {
const { errno, msg } = await fetch({
type: 'POST',
url: url('admin/players?action=texture'),
dataType: 'json',
data: { pid: pid, model: model, tid: tid }
});
if (errno === 0) {
$(`#${pid}-${model}`).attr('src', url(`preview/64/${tid}.png`));
$('.modal').modal('hide');
toastr.success(msg);
} else {
toastr.warning(msg);
}
} catch (error) {
showAjaxError(error);
}
}
async function changePlayerName(pid, oldName) {
const dom = $(`tr#${pid} > td:nth-child(3)`);
let newPlayerName;
try {
newPlayerName = await swal({
text: trans('admin.changePlayerNameNotice'),
input: 'text',
inputValue: oldName,
inputValidator: name => (new Promise((resolve, reject) => {
name ? resolve() : reject(trans('admin.emptyPlayerName'));
}))
});
} catch (error) {
return;
}
try {
const { errno, msg } = await fetch({
type: 'POST',
url: url('admin/players?action=name'),
dataType: 'json',
data: { pid: pid, name: newPlayerName }
});
if (errno === 0) {
dom.text(newPlayerName);
toastr.success(msg);
} else {
toastr.warning(msg);
}
} catch (error) {
showAjaxError(error);
}
}
function changeOwner(pid) {
const dom = $(`#${pid} > td:nth-child(2)`);
let owner = 0;
swal({
html: `${trans('admin.changePlayerOwner')}<br><small>&nbsp;</small>`,
input: 'number',
inputValue: dom.text(),
showCancelButton: true
}).then(async uid => {
owner = uid;
try {
const { errno, msg } = await fetch({
type: 'POST',
url: url('admin/players?action=owner'),
dataType: 'json',
data: { pid, uid }
});
if (errno === 0) {
dom.text(owner);
toastr.success(msg);
} else {
toastr.warning(msg);
}
} catch (error) {
showAjaxError(error);
}
});
$('.swal2-input').on('input', debounce(showNicknameInSwal, 350));
}
async function showNicknameInSwal() {
const uid = $('.swal2-input').val();
if (isNaN(uid) || uid <= 0)
return;
try {
const { user } = await fetch({
type: 'GET',
url: url(`admin/user/${uid}`),
dataType: 'json'
});
$('.swal2-content').html(
trans('admin.changePlayerOwner') +
'<small style="display: block; margin-top: .5em;">' +
trans('admin.targetUser', { nickname: user.nickname }) +
'</small>'
);
} catch (error) {
$('.swal2-content').html(`
${trans('admin.changePlayerOwner')}<br>
<small>${trans('admin.noSuchUser')}</small>
`);
}
}
async function deletePlayer(pid) {
try {
await swal({
text: trans('admin.deletePlayerNotice'),
type: 'warning',
showCancelButton: true
});
} catch (error) {
return;
}
try {
const { errno, msg } = await fetch({
type: 'POST',
url: url('admin/players?action=delete'),
dataType: 'json',
data: { pid: pid }
});
if (errno === 0) {
$(`tr#${pid}`).remove();
toastr.success(msg);
} else {
toastr.warning(msg);
}
} catch (error) {
showAjaxError(error);
}
}
if (process.env.NODE_ENV === 'test') {
module.exports = {
initPlayersTable,
changeOwner,
showNicknameInSwal,
deletePlayer,
changeTexture,
changePlayerName,
changePreference,
ajaxChangeTexture,
};
}

View File

@ -1,178 +0,0 @@
'use strict';
if ($('#plugin-table').length === 1) {
$(document).ready(initPluginsTable);
}
function initPluginsTable() {
$.pluginsTable = $('#plugin-table').DataTable({
ajax: url('admin/plugins/data'),
fnDrawCallback: () => $('[data-toggle="tooltip"]').tooltip(),
columnDefs: pluginsTableColumnDefs
}).on('xhr.dt', handleDataTablesAjaxError);
}
const pluginsTableColumnDefs = [
{
targets: 0,
data: 'title'
},
{
targets: 1,
data: 'description',
orderable: false,
width: '35%'
},
{
targets: 2,
data: 'author',
render: data => isEmpty(data.url) ? data.author : `<a href="${data.url}" target="_blank">${data.author}</a>`
},
{
targets: 3,
data: 'version',
orderable: false
},
{
targets: 4,
data: 'dependencies',
searchable: false,
orderable: false,
render: data => {
if (data.requirements.length === 0) {
return `<i>${trans('admin.noDependencies')}</i>`;
}
let result = data.isRequirementsSatisfied ? '' : `<a href="http://t.cn/RrT7SqC" target="_blank" class="label label-primary">${trans('admin.whyDependencies')}</a><br>`;
for (const name in data.requirements) {
const constraint = data.requirements[name];
const color = (name in data.unsatisfiedRequirements) ? 'red' : 'green';
result += `<span class="label bg-${color}">${name}: ${constraint}</span><br>`;
}
return result;
}
},
{
targets: 5,
data: 'status'
},
{
targets: 6,
data: 'operations',
searchable: false,
orderable: false,
render: (data, type, row) => {
let toggleButton, configViewButton;
if (data.enabled) {
toggleButton = `<a class="btn btn-warning btn-sm" onclick="disablePlugin('${row.name}');">${trans('admin.disablePlugin')}</a>`;
} else {
toggleButton = `<a class="btn btn-primary btn-sm" onclick="enablePlugin('${row.name}');">${trans('admin.enablePlugin')}</a>`;
}
if (data.enabled && data.hasConfigView) {
configViewButton = `<a class="btn btn-default btn-sm" href="${url('/')}admin/plugins/config/${row.name}">${trans('admin.configurePlugin')}</a>`;
} else {
configViewButton = `<a class="btn btn-default btn-sm" disabled="disabled" title="${trans('admin.noPluginConfigNotice')}" data-toggle="tooltip" data-placement="top">${trans('admin.configurePlugin')}</a>`;
}
const deletePluginButton = `<a class="btn btn-danger btn-sm" onclick="deletePlugin('${row.name}');">${trans('admin.deletePlugin')}</a>`;
return toggleButton + configViewButton + deletePluginButton;
}
}
];
async function enablePlugin(name) {
try {
const { requirements } = await fetch({
type: 'POST',
url: url(`admin/plugins/manage?action=requirements&name=${name}`),
dataType: 'json'
});
if (requirements.length === 0) {
await swal({
text: trans('admin.noDependenciesNotice'),
type: 'warning',
showCancelButton: true
});
}
const { errno, msg, reason } = await fetch({
type: 'POST',
url: url(`admin/plugins/manage?action=enable&name=${name}`),
dataType: 'json'
});
if (errno === 0) {
toastr.success(msg);
$.pluginsTable.ajax.reload(null, false);
} else {
swal({ type: 'warning', html: `<p>${msg}</p><ul><li>${reason.join('</li><li>')}</li></ul>` });
}
} catch (error) {
showAjaxError(error);
}
}
async function disablePlugin(name) {
try {
const { errno, msg } = await fetch({
type: 'POST',
url: url(`admin/plugins/manage?action=disable&name=${name}`),
dataType: 'json'
});
if (errno === 0) {
toastr.success(msg);
$.pluginsTable.ajax.reload(null, false);
} else {
swal({ type: 'warning', html: msg });
}
} catch (error) {
showAjaxError(error);
}
}
async function deletePlugin(name) {
try {
await swal({
text: trans('admin.confirmDeletion'),
type: 'warning',
showCancelButton: true
});
} catch (error) {
return;
}
try {
const { errno, msg } = await fetch({
type: 'POST',
url: url(`admin/plugins/manage?action=delete&name=${name}`),
dataType: 'json'
});
if (errno === 0) {
toastr.success(msg);
$.pluginsTable.ajax.reload(null, false);
} else {
swal({ type: 'warning', html: msg });
}
} catch (error) {
showAjaxError(error);
}
}
if (process.env.NODE_ENV === 'test') {
module.exports = {
initPluginsTable,
deletePlugin,
enablePlugin,
disablePlugin,
};
}

View File

@ -1,346 +0,0 @@
'use strict';
if ($('#user-table').length === 1) {
$(document).ready(initUsersTable);
}
function initUsersTable() {
const specificUid = getQueryString('uid');
const query = specificUid ? `?uid=${specificUid}` : '';
$('#user-table').DataTable({
ajax: url(`admin/user-data${query}`),
scrollY: ($('.content-wrapper').height() - $('.content-header').outerHeight()) * 0.7,
fnDrawCallback: () => $('[data-toggle="tooltip"]').tooltip(),
rowCallback: (row, data) => $(row).attr('id', `user-${data.uid}`),
columnDefs: usersTableColumnDefs
}).on('xhr.dt', handleDataTablesAjaxError);
}
const userPermissions = {
'-1': 'banned',
'0': 'normal',
'1': 'admin',
'2': 'superAdmin'
};
const usersTableColumnDefs = [
{
targets: 0,
data: 'uid',
width: '1%'
},
{
targets: 1,
data: 'email'
},
{
targets: 2,
data: 'nickname'
},
{
targets: 3,
data: 'score',
render: data => `<input type="number" class="form-control score" value="${data}" title="${trans('admin.scoreTip')}" data-toggle="tooltip" data-placement="right">`
},
{
targets: 4,
data: 'players_count',
searchable: false,
orderable: false,
render: (data, type, row) => `<a href="${url('admin/players?uid='+row.uid)}" title="${trans('admin.inspectHisPlayers')}" data-toggle="tooltip" data-placement="right">${data}</span>`
},
{
targets: 5,
data: 'permission',
className: 'status',
render: data => trans('admin.' + userPermissions[data])
},
{
targets: 6,
data: 'register_at'
},
{
targets: 7,
data: 'operations',
searchable: false,
orderable: false,
render: renderUsersTableOperations
}
];
function renderUsersTableOperations(currentUserPermission, type, row) {
let adminOption = '', bannedOption = '', deleteUserButton;
if (row.permission !== 2) {
// Only SUPER admins are allowed to set/unset admins
if (currentUserPermission === 2) {
const adminStatus = row.permission === 1 ? 'admin' : 'normal';
adminOption = `<li class="divider"></li> <li><a id="admin-${row.uid}" data="${adminStatus}" onclick="changeAdminStatus(${row.uid});">
${ adminStatus === 'admin' ? trans('admin.unsetAdmin') : trans('admin.setAdmin') }
</a></li>`;
}
const banStatus = row.permission === -1 ? 'banned' : 'normal';
bannedOption = `<li class="divider"></li> <li><a id="ban-${row.uid}" data="${banStatus}" onclick="changeBanStatus(${row.uid});">
${ banStatus === 'banned' ? trans('admin.unban') : trans('admin.ban') }
</a></li>`;
}
if (currentUserPermission === 2) {
if (row.permission === 2) {
deleteUserButton = `<a class="btn btn-danger btn-sm" disabled="disabled" data-toggle="tooltip" data-placement="bottom" title="${trans('admin.cannotDeleteSuperAdmin')}">${trans('admin.deleteUser')}</a>`;
} else {
deleteUserButton = `<a class="btn btn-danger btn-sm" onclick="deleteUserAccount(${row.uid});">${trans('admin.deleteUser')}</a>`;
}
} else {
if (row.permission === 1 || row.permission === 2) {
deleteUserButton = `<a class="btn btn-danger btn-sm" disabled="disabled" data-toggle="tooltip" data-placement="bottom" title="${trans('admin.cannotDeleteAdmin')}">${trans('admin.deleteUser')}</a>`;
} else {
deleteUserButton = `<a class="btn btn-danger btn-sm" onclick="deleteUserAccount(${row.uid});">${trans('admin.deleteUser')}</a>`;
}
}
return `
<div class="btn-group">
<button type="button" class="btn btn-sm btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
${trans('admin.operationsTitle')} <span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li><a onclick="changeUserEmail(${row.uid});">${trans('admin.changeEmail')}</a></li>
<li><a onclick="changeUserNickName(${row.uid});">${trans('admin.changeNickName')}</a></li>
<li><a onclick="changeUserPwd(${row.uid});">${trans('admin.changePassword')}</a></li>
${adminOption}
${bannedOption}
</ul>
</div>
${deleteUserButton}`;
}
async function changeUserEmail(uid) {
const dom = $(`tr#user-${uid} > td:nth-child(2)`);
let newUserEmail = '';
try {
newUserEmail = await swal({
text: trans('admin.newUserEmail'),
showCancelButton: true,
input: 'text',
inputValue: dom.text(),
inputValidator: value => (new Promise((resolve, reject) => {
value ? resolve() : reject(trans('auth.emptyEmail'));
}))
});
} catch (error) {
return;
}
try {
const { errno, msg } = await fetch({
type: 'POST',
url: url('admin/users?action=email'),
dataType: 'json',
data: { uid: uid, email: newUserEmail }
});
if (errno === 0) {
dom.text(newUserEmail);
toastr.success(msg);
} else {
toastr.warning(msg);
}
} catch (error) {
showAjaxError(error);
}
}
async function changeUserNickName(uid) {
const dom = $(`tr#user-${uid} > td:nth-child(3)`);
let newNickName = '';
try {
newNickName = await swal({
text: trans('admin.newUserNickname'),
showCancelButton: true,
input: 'text',
inputValue: dom.text(),
inputValidator: value => (new Promise((resolve, reject) => {
value ? resolve() : reject(trans('auth.emptyNickname'));
}))
});
} catch (error) {
return;
}
try {
const { errno, msg } = await fetch({
type: 'POST',
url: url('admin/users?action=nickname'),
dataType: 'json',
data: { uid: uid, nickname: newNickName }
});
if (errno === 0) {
dom.text(newNickName);
toastr.success(msg);
} else {
toastr.warning(msg);
}
} catch (error) {
showAjaxError(error);
}
}
async function changeUserPwd(uid) {
let password;
try {
password = await swal({
text: trans('admin.newUserPassword'),
showCancelButton: true,
input: 'password',
});
} catch (error) {
return;
}
try {
const { errno, msg } = await fetch({
type: 'POST',
url: url('admin/users?action=password'),
dataType: 'json',
data: { uid: uid, password: password }
});
errno === 0 ? toastr.success(msg) : toastr.warning(msg);
} catch (error) {
showAjaxError(error);
}
}
async function changeUserScore(uid, score) {
try {
const { errno, msg } = await fetch({
type: 'POST',
url: url('admin/users?action=score'),
dataType: 'json',
// Handle id formatted as '#user-1234'
data: { uid: uid.slice(5), score: score }
});
errno === 0 ? toastr.success(msg) : toastr.warning(msg);
} catch (error) {
showAjaxError(error);
}
}
async function changeBanStatus(uid) {
try {
const { errno, msg, permission } = await fetch({
type: 'POST',
url: url('admin/users?action=ban'),
dataType: 'json',
data: { uid: uid }
});
if (errno === 0) {
const dom = $(`#ban-${uid}`);
if (dom.attr('data') === 'banned') {
dom.text(trans('admin.ban')).attr('data', 'normal');
} else {
dom.text(trans('admin.unban')).attr('data', 'banned');
}
$(`#user-${uid} > td.status`).text(
permission === -1 ? trans('admin.banned') : trans('admin.normal')
);
toastr.success(msg);
} else {
toastr.warning(msg);
}
} catch (error) {
showAjaxError(error);
}
}
async function changeAdminStatus(uid) {
try {
const { errno, msg, permission } = await fetch({
type: 'POST',
url: url('admin/users?action=admin'),
dataType: 'json',
data: { uid: uid }
});
if (errno === 0) {
const dom = $(`#admin-${uid}`);
if (dom.attr('data') === 'admin') {
dom.text(trans('admin.setAdmin')).attr('data', 'normal');
} else {
dom.text(trans('admin.unsetAdmin')).attr('data', 'admin');
}
$(`#user-${uid} > td.status`).text(
(permission === 1) ? trans('admin.admin') : trans('admin.normal')
);
toastr.success(msg);
} else {
toastr.warning(msg);
}
} catch (error) {
showAjaxError(error);
}
}
async function deleteUserAccount(uid) {
try {
await swal({
text: trans('admin.deleteUserNotice'),
type: 'warning',
showCancelButton: true
});
} catch (error) {
return;
}
try {
const { errno, msg } = await fetch({
type: 'POST',
url: url('admin/users?action=delete'),
dataType: 'json',
data: { uid: uid }
});
if (errno === 0) {
$('tr#user-' + uid).remove();
toastr.success(msg);
} else {
toastr.warning(msg);
}
} catch (error) {
showAjaxError(error);
}
}
$('body').on('keypress', '.score', function(event){
// Change score when Enter key is pressed
if (event.which === 13) {
$(this).blur();
changeUserScore($(this).parent().parent().attr('id'), $(this).val());
}
});
if (process.env.NODE_ENV === 'test') {
module.exports = {
initUsersTable,
changeUserPwd,
changeBanStatus,
changeUserEmail,
changeUserScore,
changeAdminStatus,
deleteUserAccount,
changeUserNickName,
};
}

View File

@ -1,56 +0,0 @@
'use strict';
$.locales = Object.create(null);
$.currentLocale = Object.create(null);
/**
* Load current selected language.
*
* @return void
*/
function loadLocales() {
for (const lang in $.locales) {
if (!isEmpty($.locales[lang])) {
$.currentLocale = $.locales[lang] || Object.create(null);
}
}
}
/**
* Translate according to given key.
*
* @param {string} key
* @param {dict} parameters
* @return {string}
*/
function trans(key, parameters = {}) {
if (isEmpty($.currentLocale)) {
loadLocales();
}
const segments = key.split('.');
let temp = $.currentLocale || {};
for (const i in segments) {
if (isEmpty(temp[segments[i]])) {
return key;
} else {
temp = temp[segments[i]];
}
}
for (const i in parameters) {
if (!isEmpty(parameters[i])) {
temp = temp.replace(':'+i, parameters[i]);
}
}
return temp;
}
if (process.env.NODE_ENV === 'test') {
module.exports = {
trans,
loadLocales,
};
}

View File

@ -1,26 +0,0 @@
'use strict';
$.defaultPaginatorConfig = {
visiblePages: 5,
currentPage: 1,
first: '<li><a style="cursor: pointer;">«</a></li>',
prev: '<li><a style="cursor: pointer;"></a></li>',
next: '<li><a style="cursor: pointer;"></a></li>',
last: '<li><a style="cursor: pointer;">»</a></li>',
page: '<li><a style="cursor: pointer;">{{page}}</a></li>',
wrapper: '<ul class="pagination pagination-sm no-margin"></ul>'
};
$(document).ready(() => {
$('input').iCheck({
radioClass: 'iradio_square-blue',
checkboxClass: 'icheckbox_square-blue'
});
$('[data-toggle="tooltip"]').tooltip();
swal.setDefaults({
confirmButtonText: trans('general.confirm'),
cancelButtonText: trans('general.cancel')
});
});

View File

@ -1,36 +0,0 @@
'use strict';
async function confirmLogout() {
try {
await swal({
text: trans('general.confirmLogout'),
type: 'warning',
showCancelButton: true,
confirmButtonText: trans('general.confirm'),
cancelButtonText: trans('general.cancel')
});
} catch (error) {
return;
}
try {
const { msg } = await logout();
setTimeout(() => window.location = url(), 1000);
swal({
type: 'success',
html: msg
});
} catch (error) {
showAjaxError(error);
}
}
function logout() {
return fetch({
type: 'POST',
url: url('auth/logout'),
dataType: 'json'
});
}
$('#logout-button').click(confirmLogout);

View File

@ -1,78 +0,0 @@
'use strict';
/**
* Show message to div#msg with level
*
* @param {string} msg
* @param {string} type
* @return {void}
*/
function showMsg(msg, type = 'info') {
$('[id=msg]').removeClass().addClass('callout').addClass(`callout-${type}`).html(msg);
}
/**
* Show modal if error occured when sending an ajax request.
*
* @param {object} json
* @return {void}
*/
function showAjaxError(json) {
if (typeof json === 'string') {
return console.warn(json);
}
if (! json.responseText) {
return console.warn('Empty Ajax response body.');
}
showModal(json.responseText.replace(/\n/g, '<br />'), trans('general.fatalError'), 'danger');
}
/**
* Show a bootstrap modal.
*
* @param {string} msg Modal content
* @param {string} title Modal title
* @param {string} type Modal type, default|info|success|warning|error
* @param {object} options All $.fn.modal options, plus { btnText, callback, destroyOnClose }
* @return {void}
*/
function showModal(msg, title = 'Message', type = 'default', options = {}) {
const btnType = (type !== 'default') ? 'btn-outline' : 'btn-primary';
const btnText = options.btnText || 'OK';
const onClick = (options.callback === undefined) ? 'data-dismiss="modal"' : `onclick="${options.callback}"`;
const destroyOnClose = (options.destroyOnClose === false) ? false : true;
const dom = `
<div class="modal modal-${type} fade in">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
<h4 class="modal-title">${title}</h4>
</div>
<div class="modal-body">
<p>${msg}</p>
</div>
<div class="modal-footer">
<button type="button" ${onClick} class="btn ${btnType}">${btnText}</button>
</div>
</div>
</div>
</div>`;
$(dom).on('hidden.bs.modal', function () {
destroyOnClose && $(this).remove();
}).modal(options);
}
if (process.env.NODE_ENV === 'test') {
module.exports = {
showMsg,
showModal,
showAjaxError,
};
}

View File

@ -1,22 +0,0 @@
'use strict';
// polyfill of String.prototype.includes
if (!String.prototype.includes) {
String.prototype.includes = function(search) {
// Copied from core-js
return !!~this.indexOf(search, arguments.length > 1 ? arguments[1] : undefined);
};
}
// polyfill of String.prototype.endsWith
if (!String.prototype.endsWith) {
String.prototype.endsWith = function (searchString, position) {
const subjectString = this.toString();
if (typeof position !== 'number' || !isFinite(position) || Math.floor(position) !== position || position > subjectString.length) {
position = subjectString.length;
}
position -= searchString.length;
const lastIndex = subjectString.lastIndexOf(searchString, position);
return lastIndex !== -1 && lastIndex === position;
};
}

View File

@ -1,81 +0,0 @@
/* global skinview3d */
// TODO: Help wanted. This file needs to be tested.
$.msp = {};
$.msp.handles = {};
$.msp.control = null;
$.msp.config = {
domElement: document.getElementById('preview-3d-container'),
slim: false,
width: $('#preview-3d-container').width(),
height: $('#preview-3d-container').height(),
skinUrl: '',
capeUrl: ''
};
function initSkinViewer(cameraPositionZ = 70) {
disposeSkinViewer();
$.msp.viewer = new skinview3d.SkinViewer($.msp.config);
$.msp.viewer.camera.position.z = cameraPositionZ;
$.msp.viewer.animation = new skinview3d.CompositeAnimation();
// Init all available animations and pause them
$.msp.handles.walk = $.msp.viewer.animation.add(skinview3d.WalkingAnimation);
$.msp.handles.run = $.msp.viewer.animation.add(skinview3d.RunningAnimation);
$.msp.handles.rotate = $.msp.viewer.animation.add(skinview3d.RotatingAnimation);
$.msp.handles.run.paused = true;
$.msp.control = skinview3d.createOrbitControls($.msp.viewer);
}
function applySkinViewerConfig(config) {
config = config || $.msp.config;
for (const key in config) {
$.msp.viewer[key] = config[key];
}
}
function disposeSkinViewer() {
if ($.msp.viewer instanceof skinview3d.SkinViewer) {
$.msp.viewer.dispose();
$.msp.handles = {};
$.msp.control = undefined;
}
}
function registerAnimationController() {
$('.fa-pause').click(function () {
$.msp.viewer.animationPaused = !$.msp.viewer.animationPaused;
$(this).toggleClass('fa-pause').toggleClass('fa-play');
});
$('.fa-forward').click(function () {
$.msp.handles.run.paused = !$.msp.handles.run.paused;
$.msp.handles.walk.paused = !$.msp.handles.run.paused;
});
$('.fa-repeat').click(() => ($.msp.handles.rotate.paused = !$.msp.handles.rotate.paused));
$('.fa-stop').click(() => {
initSkinViewer();
// Pause all animations respectively
for (const key in $.msp.handles) {
$.msp.handles[key].paused = true;
}
});
}
function registerWindowResizeHandler() {
$(window).resize(function () {
$.msp.viewer.width = $('#preview-3d-container').width();
$.msp.viewer.height = $('#preview-3d-container').height();
});
}
if (process.env.NODE_ENV === 'test') {
module.exports = { initSkinViewer, applySkinViewerConfig, registerAnimationController, registerWindowResizeHandler };
}

View File

@ -1,158 +0,0 @@
'use strict';
console.log(
`%c Blessing Skin %c v${blessing.version} %c Made with %c<3%c by printempw.%c https://blessing.studio`,
'color:#fadfa3;background:#030307;padding:5px 0;margin:10px 0;',
'background:#fadfa3;padding:5px 0;',
'font-style:italic;',
'color:red;',
'font-style:italic;', ''
);
/**
* Check if given value is empty.
*
* @param {any} obj
* @return {Boolean}
*/
function isEmpty(obj) {
// null and undefined are "empty"
if (obj == null) return true; // eslint-disable-line eqeqeq
if (typeof (obj) === 'number' || typeof (obj) === 'boolean') return false;
// Assume if it has a length property with a non-zero value
// that that property is correct.
if (obj.length > 0) return false;
if (obj.length === 0) return true;
// If it isn't an object at this point
// it is empty, but it can't be anything *but* empty
// Is it empty? Depends on your application.
if (typeof obj !== 'object') return true;
// Otherwise, does it have any properties of its own?
// Note that this doesn't handle
// toString and valueOf enumeration bugs in IE < 9
for (const key in obj) {
if (hasOwnProperty.call(obj, key)) return false;
}
return true;
}
/**
* A fake fetch API. Returns Promises.
*
* @param {array} option Same as options of jQuery.ajax()
* @return {Promise}
*/
function fetch(option) {
return Promise.resolve($.ajax(option));
}
/**
* Get parameters in query string with key.
*
* @param {string} key
* @param {string} defaultValue
* @return {string}
*/
function getQueryString(key, defaultValue) {
const result = location.search.match(new RegExp('[?&]'+key+'=([^&]+)','i'));
if (result === null || result.length < 1){
return defaultValue;
} else {
return result[1];
}
}
/**
* Check if the `resize` event is fired by scrolling on a mobile browser
* whose address bar (e.g. Chrome) will hide automatically when scrolling.
*
* @return {Boolean}
*/
function isMobileBrowserScrolling() {
const currentWindowWidth = $(window).width();
const currentWindowHeight = $(window).height();
if ($.cachedWindowWidth === undefined) {
$.cachedWindowWidth = currentWindowWidth;
}
if ($.cachedWindowHeight === undefined) {
$.cachedWindowHeight = currentWindowHeight;
}
const isWidthChanged = (currentWindowWidth !== $.cachedWindowWidth);
const isHeightChanged = (currentWindowHeight !== $.cachedWindowHeight);
// If the window width & height changes simultaneously, the resize can't be fired by scrolling.
if (isWidthChanged && isHeightChanged) {
return false;
}
// If only width was changed, it also can't be.
if (isWidthChanged) {
return false;
}
// If width didn't change but height changed ?
if (isHeightChanged) {
const last = $.lastWindowHeight;
$.lastWindowHeight = currentWindowHeight;
if (last === undefined || currentWindowHeight === last) {
return true;
}
}
// If both width & height did not change
return false;
}
/**
* Return a debounced function
*
* @param {Function} func
* @param {number} delay
* @param {Array} args
* @param {Object} context
*/
function debounce(func, delay, args = [], context = undefined) {
if (isNaN(delay) || typeof func !== 'function') {
throw new Error('Arguments type of function "debounce" is incorrect!');
}
let timer = null;
return function () {
clearTimeout(timer);
timer = setTimeout(() => {
func.apply(context, args);
}, delay);
};
}
function url(relativeUri = '') {
blessing.base_url = blessing.base_url || '';
if (relativeUri[0] !== '/') {
relativeUri = '/' + relativeUri;
}
return blessing.base_url + relativeUri;
}
if (process.env.NODE_ENV === 'test') {
module.exports = {
url,
fetch,
isEmpty,
debounce,
getQueryString,
isMobileBrowserScrolling,
};
}

View File

@ -3,3 +3,12 @@ import './i18n';
import './net';
import './layout';
import './logout';
console.log(
`%c Blessing Skin %c v${blessing.version} %c Made with %c<3%c by printempw.%c https://blessing.studio`,
'color:#fadfa3;background:#030307;padding:5px 0;margin:10px 0;',
'background:#fadfa3;padding:5px 0;',
'font-style:italic;',
'color:red;',
'font-style:italic;', ''
);