미디어위키:Gadget-DB2-SpecialPage.js

리버티게임, 모두가 만들어가는 자유로운 게임
imported>Senouis님의 2020년 12월 29일 (화) 22:01 판 (Senouis의 265317판 편집을 되돌림)

참고: 설정을 저장한 후에 바뀐 점을 확인하기 위해서는 브라우저의 캐시를 새로 고쳐야 합니다.

  • 파이어폭스 / 사파리: Shift 키를 누르면서 새로 고침을 클릭하거나, Ctrl-F5 또는 Ctrl-R을 입력 (Mac에서는 ⌘-R)
  • 구글 크롬: Ctrl-Shift-R키를 입력 (Mac에서는 ⌘-Shift-R)
  • 인터넷 익스플로러 / 엣지: Ctrl 키를 누르면서 새로 고침을 클릭하거나, Ctrl-F5를 입력.
  • 오페라: Ctrl-F5를 입력.
var punycode = require('ext.gadget.punycode');

let hybridStorage = (function(local) {
    var storage, action;
    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() {
	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() {
			OO.ui.confirm('정말로 ' + (name === 'global'? '모든 전역키' : '"' + label + '"의 모든 키') + '를 제거하시겠습니까?').then(function(response) {
				var process;
				if(response) {
					remove.setDisabled(true);
					remove.setFlags('pending');
					
					for(var key in keys) process = hybridStorage.removeItem(keys[key]);
					
					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.setFlags({pending: false});
					}, notifyApiError.bind(null, '항목을 제거하지 못했습니다.', null));
				}
			});
		});
		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([
			$('<h2 />').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(punycode.toASCII(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 punycode.toUnicode(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%" />')
		.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 metadataQueue = {};
		var metaRqTimeout = null;
		
		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));
	}
});