미디어위키:Gadget-DB2-SpecialPage.js: 두 판 사이의 차이

리버티게임, 모두가 만들어가는 자유로운 게임
imported>Senouis
편집 요약 없음
imported>Senouis
(수리 시도 3)
 
(같은 사용자의 중간 판 15개는 보이지 않습니다)
1번째 줄: 1번째 줄:
var punycode = require('ext.gadget.punycode');
/**
* DB2로 저장한 키들을 관리하는 페이지 전용 기능들
* 원 저작자: [[사용자:hsl0|hsl0]]
* 코드 관리: [[사용자:Senouis|Senouis]]
**/


let hybridStorage = (function(local) {
var metadataQueue = {};
    var storage, action;
var metaRqTimeout = null;
    var saveOption = (function() {
        var options = {};
        var deferred = new $.Deferred();
        var timeout = null;
       
        return function saveOption(key, value) {
            if(timeout) clearTimeout(timeout);
            options[key] = value;
            timeout = setTimeout(function() {
                api.saveOptions(options).then(deferred.resolve, deferred.reject);
               
                options = {};
                timeout = null;
                deferred = new $.Deferred();
            }, 100);
            return deferred.promise();
        };
    })();
    if(local) {
        storage = new Proxy(localStorage, {
            get: function(target, prop, receiver) {
                var value = Reflect.get({
                    length: target.length,
                    getItem: target.getItem,
                    key: target.key,
                    removeItem: function removeItem(key) {
                        return Promise.resolve(target.removeItem(key));
                    },
                    setItem: function setItem(key, value) {
                        return Promise.resolve(target.setItem(key, value));
                    },
                    refresh: function refresh() {
                        return Promise.resolve(storage);
                    },
                    needRefresh: false
                }, prop, receiver) || target.getItem(prop);
                return (value === null)? undefined : value;
            },
            set: function(target, prop, value, receiver) {
                return Promise.resolve(target.setItem(prop, value));
            },
            deleteProperty: function(target, prop) {
                return Promise.resolve(target.removeItem(prop));
            }
        });
    } else {
        action = {
            removeItem: function removeItem(key) {
                if(key) return saveOption("userjs-" + key, null);
                else throw new TypeError("Failed to execute 'removeItem' on 'Storage': 1 argument required, but only 0 present.");
            },
            setItem: function setItem(key, value) {
                if(key) return saveOption("userjs-" + key, value);
                else throw new TypeError("Failed to execute 'removeItem' on 'Storage': 1 argument required, but only 0 present.");
            },
            refresh: (function() {
                var deferred = new $.Deferred();
                var timeout = null;
               
                return function refresh() {
                    if(timeout) clearTimeout(timeout);
                    timeout = setTimeout(function() {
                        api.get({
                            action: 'query',
                            meta: 'userinfo',
                            uiprop: 'options'
                        }, {
                            cache: false
                        }).then(function(response) {
                            mw.user.options.values = response.query.userinfo.options;
                            return storage;
                        }).then(deferred.resolve, deferred.reject);
                       
                        timeout = null;
                        deferred = new $.Deferred();
                    }, 100);
                    return deferred.promise();
                };
            })()
        };
        storage = new Proxy(mw.user.options, {
            get: function(target, prop, receiver) {
                function keys() {
                    return Object.keys(target.values).filter(function(key) {
                        return key.startsWith('userjs-');
                    });
                }
               
                return Reflect.get({
                    length: prop === 'length' && keys().length,
                    getItem: function getItem(key) {
                        if(key) return target.get("userjs-" + key);
                        else throw new TypeError("Failed to execute 'getItem' on 'Storage': 1 argument required, but only 0 present.");
                    },
                    key: function key(index) {
                        return keys()[index].slice(7);
                    },
                    removeItem: function removeItem(key) {
                    if(!target.exists("userjs-" + key)) return Promise.resolve();
                        return action.removeItem(key).then(function() {
                        Reflect.deleteProperty(target.values, "userjs-" + key);
                        });
                    },
                    setItem: function setItem(key, value) {
                    if(target.get("userjs-" + key) === value) return Promise.resolve();
                        return action.setItem(key, value).then(function() {
                        target.set("userjs-" + key, value);
                        });
                    },
                    refresh: action.refresh,
                    needRefresh: true
                }, prop, receiver) || Reflect.get(target.values, "userjs-" + prop, receiver);
            },
            set: function(target, prop, value, receiver) {
            if(Reflect.get(target.values, "userjs-" + prop, receiver) === value) return Promise.resolve();
                return action.setItem(prop, value).then(function() {
                Reflect.set(target.values, "userjs-" + prop, value, receiver);
                });
            },
            deleteProperty: function(target, prop) {
            if(!Reflect.has(target.values, "userjs-" + prop)) return Promise.resolve();
                return action.removeItem(prop).then(function() {
                Reflect.deleteProperty(target.values, "userjs-" + prop);
                });
            },
            has: function(target, prop) {
                return Reflect.has(target.values, "userjs-" + prop);
            },
            ownKeys: function(target) {
                return Reflect.ownKeys(target.values).filter(function(key) {
                    return key.startsWith("userjs-");
                }).map(function(key) {
                    return key.slice(7);
                });
            }
        });
    }
    return storage;
})(mw.user.isAnon());


function requestMetadata() {
function requestMetadata() {
191번째 줄: 56번째 줄:
flags: 'destructive'
flags: 'destructive'
});
});
remove.$button.click(function() {
$table.find('.remove button').click();
if(name === 'global') {
keys = allkeys.global = {};
page.$element.find('table').remove();
$table = keyList(keys);
page.$element.append($table);
} else {
delete allkeys.local[prefix];
layout.removePages([page]);
}
});
/*
remove.$button.click(function() {
remove.$button.click(function() {
OO.ui.confirm('정말로 ' + (name === 'global'? '모든 전역키' : '"' + label + '"의 모든 키') + '를 제거하시겠습니까?').then(function(response) {
OO.ui.confirm('정말로 ' + (name === 'global'? '모든 전역키' : '"' + label + '"의 모든 키') + '를 제거하시겠습니까?').then(function(response) {
var process;
var process = null;
var Processfunc = function(){
function resolvedProcess(check, length) {
if (check === length) process.resolved();
}
var check = 0;
process = $.Deferred();
for(var key in keys) {
check++;
hybridStorage.removeItem(keys[key]).then(resolvedProcess(check,keys.length),notifyApiError.bind(null, '항목을 제거하지 못했습니다.', null));
}
return process.promise();
};
var postProcessfunc = function() {
if(name === 'global') {
keys = allkeys.global = {};
page.$element.find('table').remove();
$table = keyList(keys);
page.$element.append($table);
} else {
delete allkeys.local[prefix];
layout.removePages([page]);
}
};
if(response) {
if(response) {
remove.setDisabled(true);
remove.setDisabled(true);
remove.setFlags('pending');
remove.setFlags('pending');
for(var key in keys) process = hybridStorage.removeItem(keys[key]);
Processfunc().done(function (){
postProcessfunc();
process.then(function() {
if(name === 'global') {
keys = allkeys.global = {};
page.$element.find('table').remove();
$table = keyList(keys);
page.$element.append($table);
} else {
delete allkeys.local[prefix];
layout.removePages([page]);
}
remove.setDisabled(false);
remove.setDisabled(false);
remove.setFlags({pending: false});
remove.setFlags({pending: false});
}, notifyApiError.bind(null, '항목을 제거하지 못했습니다.', null));
});
}
}
});
});
});
});
*/
var revert = new OO.ui.ButtonInputWidget({
var revert = new OO.ui.ButtonInputWidget({
classes: ['revertall'],
classes: ['revertall'],
229번째 줄: 120번째 줄:
page.$element.empty().append([
page.$element.empty().append([
$('<h2 />').html(name === 'global'? '전역키' : $('<a />', {href: mw.util.getUrl(label), target: '_blank'}).text(label)),
$('<h3 />').html(name === 'global'? '전역키' : $('<a />', {href: mw.util.getUrl(label), target: '_blank'}).text(label)),
$('<div />', {
$('<div />', {
class: 'buttons'
class: 'buttons'
300번째 줄: 191번째 줄:
}
}
function encode(key) {
function encode(key) {
return encodeURIComponent(punycode.toASCII(key))
return encodeURIComponent(key)
.replace(/\./g, '%2E')
.replace(/\./g, '%2E')
.replace(/!/g, '%21')
.replace(/!/g, '%21')
312번째 줄: 203번째 줄:
}
}
function decode(key) {
function decode(key) {
return punycode.toUnicode(decodeURIComponent(key.replace(/_(?=[a-zA-Z0-9]{2})/g, '%').replace(/__/g, '_')));
return decodeURIComponent(key.replace(/_(?=[a-zA-Z0-9]{2})/g, '%').replace(/__/g, '_'));
}
}
function keyList(keys, abouts, locs) {
function keyList(keys, abouts, locs) {
395번째 줄: 286번째 줄:
}).text('(키 없음)')));
}).text('(키 없음)')));
return $('<table class="wikitable sortable" width="100%" />')
return $('<table class="wikitable sortable" width="100%" style="left:auto;"/>')
.append('<thead><tr><th>키</th><th>설명</th><th>값</th>' + (locs && '<th>사용처</th>') + '<th>옵션</th></tr></thead>')
.append('<thead><tr><th>키</th><th>설명</th><th>값</th>' + (locs && '<th>사용처</th>') + '<th>옵션</th></tr></thead>')
.append($('<tbody />').append(rows)).tablesorter();
.append($('<tbody />').append(rows))//.tablesorter();
}
}
var fromKey, toKey;
var fromKey, toKey;
486번째 줄: 377번째 줄:
$(function() {
$(function() {
try {
try {
var metadataQueue = {};
var metaRqTimeout = null;
var reset = new OO.ui.ButtonInputWidget({
var reset = new OO.ui.ButtonInputWidget({
id: 'reset',
id: 'reset',

2020년 12월 31일 (목) 11:55 기준 최신판

/**
 * DB2로 저장한 키들을 관리하는 페이지 전용 기능들
 * 원 저작자: [[사용자:hsl0|hsl0]]
 * 코드 관리: [[사용자:Senouis|Senouis]]
**/

var metadataQueue = {};
var metaRqTimeout = null;

function requestMetadata() {
	var queue = metadataQueue;
	$.get('/w/api.php', {
		action: "query",
		format: "json",
		prop: "revisions",
		titles: Object.keys(queue).join('|'),
		formatversion: 2,
		rvprop: "content",
		rvslots: "main"
	}).then(function(res) {
		res.query.pages.forEach(function(obj) {
			queue[obj.title](JSON.parse(obj.revisions[0].slots.main.content));
		});
	});
	metadataQueue = {};
	metaRqTimeout = null;
}
function newPage(name, label, prefix) {
	function create(res) {
		var $table = keyList(keys, res.about, res.loc);
		
		var reload = new OO.ui.ButtonInputWidget({
			label: '새로고침',
			icon: 'reload'
		});
		reload.$button.click(function() {
			reload.setDisabled(true);
			reload.setFlags('pending');
			
			Promise.all([hybridStorage.refresh(), loadAbout()]).then(function(res) {
				allkeys = getKeys();
				keys = name === 'global'? allkeys.global : allkeys.local[prefix];
				page.$element.find('table').remove();
				$table = keyList(keys, res[1].about, res[1].loc);
				page.$element.append($table);
				
				reload.setDisabled(false);
				reload.setFlags({pending: false});
			}, notifyApiError.bind(null, '항목을 불러오는 데 실패했습니다.', null));
		});
		
		var remove = new OO.ui.ButtonInputWidget({
			classes: ['removeall'],
			label: '모두 제거',
			icon: 'trash',
			flags: 'destructive'
		});
		remove.$button.click(function() {
			$table.find('.remove button').click();
			if(name === 'global') {
				keys = allkeys.global = {};
				page.$element.find('table').remove();
				$table = keyList(keys);
				page.$element.append($table);
			} else {
				delete allkeys.local[prefix];
				layout.removePages([page]);
			}
		});
		/*
		remove.$button.click(function() {
			OO.ui.confirm('정말로 ' + (name === 'global'? '모든 전역키' : '"' + label + '"의 모든 키') + '를 제거하시겠습니까?').then(function(response) {
				var process = null;
				var Processfunc = function(){
					function resolvedProcess(check, length) {
						if (check === length) process.resolved();
					}
					var check = 0;
					process = $.Deferred();
					for(var key in keys) {
						check++;
						hybridStorage.removeItem(keys[key]).then(resolvedProcess(check,keys.length),notifyApiError.bind(null, '항목을 제거하지 못했습니다.', null));
					}
					return process.promise();
				};
				var postProcessfunc = function() {
					if(name === 'global') {
						keys = allkeys.global = {};
						page.$element.find('table').remove();
						$table = keyList(keys);
						page.$element.append($table);
					} else {
						delete allkeys.local[prefix];
						layout.removePages([page]);
					}
				};
				if(response) {
					remove.setDisabled(true);
					remove.setFlags('pending');
					
					Processfunc().done(function (){
						postProcessfunc();
						remove.setDisabled(false);
						remove.setFlags({pending: false});	
					});
				}
			});
		});
		*/
		var revert = new OO.ui.ButtonInputWidget({
			classes: ['revertall'],
			label: '모두 복원',
			icon: 'undo',
			flags: 'progressive'
		});
		revert.$button.click(function() {
			$table.find('.removed .revert button').click();
		});
		
		
		page.$element.empty().append([
			$('<h3 />').html(name === 'global'? '전역키' : $('<a />', {href: mw.util.getUrl(label), target: '_blank'}).text(label)),
			$('<div />', {
				class: 'buttons'
			}).append(new OO.ui.ButtonGroupWidget({
				items: [reload],
				classes: ['buttons-left']
			}).$element).append(new OO.ui.ButtonGroupWidget({
				items: [revert, remove],
				classes: ['buttons-right']
			}).$element),
			$table
		]);
	}
	
	function loadAbout() {
		if(name === 'global') return $.get('/w/index.php?title=틀:DB2/전역키&action=render').then(function(res) {
			function process(key) {
				var $row = $(res).find('tr:has(td:nth-child(1):contains(' + JSON.stringify(key) +'))');
				about[key] = $row.find('td')[1].innerText;
				loc[key] = $row.find('td:nth-child(4)');
				loc[key].find(':not(a)').contents().unwrap();
				loc[key].find('a').filter(function() {
					var href = new URL(this.href, location);
					href.search = '';
					this.href = href;
					this.target = '_blank';
					return href.origin !== location.origin || !href.pathname.startsWith('/w');
				}).contents().unwrap();
				loc[key] = loc[key].html();
			}
			
			var about = {};
			var loc = {};
			
			if(DEFAULT_KEY in keys) process(DEFAULT_KEY);
			for(var key in keys) process(key);
			
			return {
				about: about,
				loc: loc
			};
		});
		else return new Promise(function(resolve) {
			if(metaRqTimeout) clearTimeout(metaRqTimeout);
			metadataQueue[prefix + '/game.json'] = function(res) {
				var about = {};
				if(res.gameDB.default) about[DEFAULT_KEY] = res.gameDB.default.description;
				
				if(res.gameDB.keys) for(var key in res.gameDB.keys) {
					about[key] = res.gameDB.keys[key].description;
				}
				
				resolve({about: about});
			};
			metaRqTimeout = setTimeout(requestMetadata, 100);
		});
	}
	loadAbout().then(create);
	
	var keys = name === 'global'? allkeys.global : allkeys.local[prefix];
	
	var page = new OO.ui.PageLayout(name, {
		content: [new OO.ui.ProgressBarWidget()]
	});
	page.setupOutlineItem = function() {
		this.outlineItem.setLabel(label);
	};
	
	return page;
}
function encode(key) {
	return encodeURIComponent(key)
		.replace(/\./g, '%2E')
		.replace(/!/g, '%21')
		.replace(/~/g, '%7E')
		.replace(/\*/g, '%2A')
		.replace(/'/g, '%27')
		.replace(/\(/g, '%28')
		.replace(/\)/g, '%29')
		.replace(/_/g, '__')
		.replace(/%/g, '_');
}
function decode(key) {
	return decodeURIComponent(key.replace(/_(?=[a-zA-Z0-9]{2})/g, '%').replace(/__/g, '_'));
}
function keyList(keys, abouts, locs) {
	function keyRow(key) {
		var origin = keys[key];
		var val = hybridStorage.getItem(origin);
		var about = abouts && abouts[key];
		var loc = locs && locs[key];
		
		if(val === null) return;
		
		var copy = new OO.ui.ButtonWidget({
			classes: ['copy'],
			label: '복사'
		});
		copy.$button.click(function() {
			navigator.clipboard.writeText(val);
		});
		
		var remove = new OO.ui.ButtonInputWidget({
			classes: ['remove'],
			label: '제거',
			flags: 'destructive'
		});
		remove.$button.click(function() {
			remove.setDisabled(true);
			remove.setFlags('pending');
			
			hybridStorage.removeItem(origin).then(function() {
				$element.addClass('removed');
				$options.data('sort-value', 0);
				
				remove.$element.addClass('hidden');
				revert.$element.removeClass('hidden');
				revert.setDisabled(false);
				remove.setFlags({pending: false});
			}, notifyApiError.bind(null, '항목을 제거하지 못했습니다.', null));
		});
		var revert = new OO.ui.ButtonInputWidget({
			classes: ['revert', 'hidden'],
			label: '복원',
			flags: 'progressive',
			disabled: true
		});
		revert.$button.click(function() {
			revert.setDisabled(true);
			revert.setFlags('pending');
			
			hybridStorage.setItem(origin, val).then(function() {
				keys[key] = origin;
				
				$element.removeClass('removed');
				
				revert.$element.addClass('hidden');
				remove.$element.removeClass('hidden');
				remove.setDisabled(false);
				revert.setFlags({pending: false});
			}, notifyApiError.bind(null, '항목을 복원하지 못했습니다.', null));
		});
		
		var $options = $('<td />', {'data-sort-value': 1}).append(copy.$element).append(remove.$element).append(revert.$element);
		
		var $element = $('<tr />', {id: 'key-' + origin})
			.append($('<td />').append(key === DEFAULT_KEY? $('<span />', {class: 'key key-default'}).text('기본키') : $('<span />', {class: 'key'}).text(key)))
			.append($('<td />', {class: 'about'}).text(about))
			.append($('<td />').html($('<pre />', {class: 'content'}).text(val)))
			.append(loc && $('<td />', {class: 'location'}).html(loc))
			.append($options);
		
		return $element;
	}
	
	var rows = [];
	
	if(DEFAULT_KEY in keys) rows.push(keyRow(DEFAULT_KEY));
	
	for(var key in keys) rows.push(keyRow(key));
	
	if(!rows.length) rows.push($('<tr />').append($('<td />', {
		colspan: locs? 4 : 5,
		class: 'key-empty'
	}).text('(키 없음)')));
	
	return $('<table class="wikitable sortable" width="100%" style="left:auto;"/>')
		.append('<thead><tr><th>키</th><th>설명</th><th>값</th>' + (locs && '<th>사용처</th>') + '<th>옵션</th></tr></thead>')
		.append($('<tbody />').append(rows))//.tablesorter();
}
var fromKey, toKey;
if(mw.user.isAnon()) {
	fromKey = function(key) {
		return key.slice(7);
	};
	toKey = function(key) {
		return 'gamedb-' + key;
	};
} else {
	fromKey = function(key) {
		return decode(key.slice(7));
	};
	toKey = function(key) {
		return 'gamedb-' + encode(key);
	};
}

var layout = new OO.ui.BookletLayout({
	outlined: true
});

var DEFAULT_KEY = Symbol('defaultkey');

var allkey, scopes;

function getKeys() {
	allkey = Object.getOwnPropertyNames(hybridStorage).filter(function(key) {
		return key.startsWith('gamedb-');
	}).map(fromKey);
	
	var keys = {global:{}, local:{}};
	
	scopes = new Set();
	
	allkey.forEach(function(key) {
		key = key.split('/')[0];
		if(!key.startsWith('#') && key.includes(':')) scopes.add(key);
	});
	
	// global
	allkey.filter(function(key) {
		return key.startsWith('#');
	}).forEach(function(key) {
		keys.global[key.slice(1)] = toKey(key);
	});
	
	// local
	scopes.forEach(function(scope) {
		var current = {};
		
		allkey.filter(function(key) {
			return key.startsWith(scope);
		}).forEach(function(key) {
			key = key.length < scope.length + 1? DEFAULT_KEY : key.slice(scope.length + 1);
			current[key] = toKey(key === DEFAULT_KEY? scope : scope + '/' + key);
		});
		
		keys.local[scope] = current;
	});
	
	return keys;
}

var allkeys = getKeys();

layout.addPages([newPage('global', '전역키')]);

layout.addPages(Array.from(scopes).map(function(prefix) {
	return newPage('local-' + prefix, prefix, prefix);
}));

function handleReset(wrong) {
	var rand = Math.floor(Math.random() * 999);
	rand = '0'.repeat(3 - rand.toString().length) + rand;
	
	OO.ui.prompt($('<p>' + (wrong? '잘못 입력하였습니다.' : 'DB2의 모든 데이터가 초기화됩니다.') + ' 계속하시려면 <b>' + rand + '</b>를 입력해 주세요.</p>'), {
		textInput: {
			validate: new RegExp(rand)
		}
	}).then(function(res) {
		if(res === rand) layout.$element.find('.removeall').click();
		else if(res !== null) handleReset(true);
	})
}

$(function() {
	try {
		var reset = new OO.ui.ButtonInputWidget({
			id: 'reset',
			label: '전체 초기화',
			icon: 'trash',
			flags: 'destructive'
		});
		reset.$button.click(handleReset.bind(null, false));
		
		$('#mw-content-text').html(new OO.ui.PanelLayout({
			expanded: false,
			framed: true,
			content: [layout]
		}).$element).prepend($('<div />', {id: 'globalbuttons'}).append(reset.$element));
	} catch(err) {
		$('#mw-content-text').html($('<span />', {
			class: 'error'
		}).text(err));
	}
});