2204 lines
		
	
	
	
		
			50 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			2204 lines
		
	
	
	
		
			50 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| 'use strict';
 | |
| 
 | |
| var APP = {
 | |
|   POST_DELETED: 1,
 | |
|   FILE_DELETED: 2,
 | |
|   
 | |
|   init: function() {
 | |
|     this.xhrs = [];
 | |
|     this.xhrCount = 0;
 | |
|     this.resultCount = 0;
 | |
|     this.searchResults = [];
 | |
|     this.deletedResults = {};
 | |
|     this.searchAborted = false;
 | |
|     this.groupBy = null;
 | |
|     this.isArchived = false;
 | |
|     
 | |
|     this.preDelDelay = 3000;
 | |
|     
 | |
|     this.banXhr = [];
 | |
|     
 | |
|     this.delErrors = [];
 | |
|     this.delXhrs = [];
 | |
|     this.delXhrCount = 0;
 | |
|     
 | |
|     this.maxResults = null;
 | |
|     this.maxBoardResults = null;
 | |
|     this.maxIPBans = null;
 | |
|     
 | |
|     this.currentParams = null;
 | |
|     
 | |
|     this.banTemplatesCache = null;
 | |
|     this.banTemplatesLoading = false;
 | |
|     
 | |
|     this.savedSearch = [];
 | |
|     
 | |
|     this.clearPartialStatus();
 | |
|     
 | |
|     this.loadSavedSearch();
 | |
|     
 | |
|     this.clickCommands = {
 | |
|       'ban-post': APP.onBanPostClick,
 | |
|       'thread-opts': APP.onThreadOptsClick,
 | |
|       'pre-del-post': APP.onPreDelPostClick,
 | |
|       'del-post': APP.onDelPostClick,
 | |
|       'pre-del-all': APP.onPreDelAllClick,
 | |
|       'del-all': APP.onDelAllClick,
 | |
|       'ban-multi': APP.onMultiBanClick,
 | |
|       'submit-ban': APP.onSubmitBanClick,
 | |
|       'cancel-ban': APP.onCancelBanClick,
 | |
|       'pre-del-grp': APP.onPreDelGrpClick,
 | |
|       'del-grp': APP.onDelGrpClick,
 | |
|       'toggle-grp': APP.onToggleGrpClick,
 | |
|       'toggle-post': APP.onTogglePostClick,
 | |
|       'toggle-ih': APP.onToggleIHClick,
 | |
|       'toggle-dt': APP.onToggleDTClick,
 | |
|       'save-search': APP.onSaveSearchClick,
 | |
|       'run-search': APP.onRunSearchClick,
 | |
|       'del-search': APP.onDelSearchClick,
 | |
|     };
 | |
|     
 | |
|     this.protocol = 'https://';
 | |
|     this.thumbServer = 'i.4cdn.org';
 | |
|     this.imageServer = 'i.4cdn.org';
 | |
|     this.imageServer2 = 'is2.4chan.org';
 | |
|     this.fileDeleted = 's.4cdn.org/image/filedeleted-res.gif';
 | |
|     
 | |
|     this.capcodes = {
 | |
|       mod: 'Moderator',
 | |
|       manager: 'Manager',
 | |
|       founder: 'Founder',
 | |
|       admin: 'Administrator',
 | |
|       developer: 'Developer',
 | |
|       verified: 'Verified'
 | |
|     };
 | |
|     
 | |
|     Tip.init();
 | |
|     
 | |
|     if (localStorage.getItem('dark-theme')) {
 | |
|       $.addClass($.docEl, 'dark-theme');
 | |
|     }
 | |
|     
 | |
|     $.on(window, 'hashchange', APP.onHashChange);
 | |
|     $.on(document, 'click', APP.onClick);
 | |
|     $.on(document, 'DOMContentLoaded', APP.run);
 | |
|     $.on(window, 'storage', APP.syncSavedSearch);
 | |
|   },
 | |
|   
 | |
|   linkToImage: function(board, file, ext) {
 | |
|     return '//' + this.imageServer + '/' + board + '/' + file + ext;
 | |
|   },
 | |
|   
 | |
|   linkToThumb: function(board, file) {
 | |
|     return '//' + this.thumbServer + '/' + board + '/' + file + 's.jpg';
 | |
|   },
 | |
|   
 | |
|   linkToSWF: function(fileName) {
 | |
|     return '//' + this.imageServer + '/f/' + fileName + '.swf';
 | |
|   },
 | |
|   
 | |
|   linkToPost: function(board, pid, tid) {
 | |
|     return this.protocol + 'boards.' + $L.d(board) + '/' + board + '/thread/'
 | |
|       + (+tid !== 0 ? (tid + '#p' + pid) : pid);
 | |
|   },
 | |
|   
 | |
|   run: function() {
 | |
|     $.off(document, 'DOMContentLoaded', APP.run);
 | |
|     
 | |
|     if (!localStorage.getItem('search-no-ih')) {
 | |
|       ImageHover.init();
 | |
|     }
 | |
|     else {
 | |
|       $.id('cfg-cb-ih').checked = false;
 | |
|     }
 | |
|     
 | |
|     if (localStorage.getItem('dark-theme')) {
 | |
|       $.id('cfg-cb-dt').checked = true;
 | |
|     }
 | |
|     
 | |
|     APP.maxResults = +document.body.getAttribute('data-maxres');
 | |
|     APP.maxBoardResults = +document.body.getAttribute('data-maxboardres');
 | |
|     APP.maxIPBans = +document.body.getAttribute('data-maxipbans');
 | |
|     
 | |
|     APP.buildSavedSearchList();
 | |
|     
 | |
|     APP.onHashChange();
 | |
|     
 | |
|     $.on($.id('search-btn'), 'click', APP.onSearchClick);
 | |
|     $.on($.id('reset-btn'), 'click', APP.onSearchReset);
 | |
|     $.on($.id('search-form'), 'submit', APP.onSearchSubmit);
 | |
|     $.on($.id('ban-form'), 'submit', APP.onBanSubmit);
 | |
|     $.on($.id('group-field'), 'change', APP.onGroupChange);
 | |
|     $.on($.id('group-sort-field'), 'change', APP.onGroupSortChange);
 | |
|   },
 | |
|   
 | |
|   onToggleIHClick: function() {
 | |
|     var el = $.id('cfg-cb-ih');
 | |
|     
 | |
|     if (el.checked !== ImageHover.enabled) {
 | |
|       if (el.checked) {
 | |
|         ImageHover.init();
 | |
|         localStorage.removeItem('search-no-ih');
 | |
|       }
 | |
|       else {
 | |
|         ImageHover.disable();
 | |
|         localStorage.setItem('search-no-ih', '1');
 | |
|       }
 | |
|     }
 | |
|   },
 | |
|   
 | |
|   onToggleDTClick: function() {
 | |
|     var el = $.id('cfg-cb-dt');
 | |
|     
 | |
|     if (el.checked !== $.hasClass($.docEl, 'dark-theme')) {
 | |
|       if (el.checked) {
 | |
|         $.addClass($.docEl, 'dark-theme');
 | |
|         localStorage.setItem('dark-theme', '1');
 | |
|       }
 | |
|       else {
 | |
|         $.removeClass($.docEl, 'dark-theme');
 | |
|         localStorage.removeItem('dark-theme');
 | |
|       }
 | |
|     }
 | |
|   },
 | |
|   
 | |
|   hasPartialResults: function() {
 | |
|     return APP.partial.global || APP.partial.error || APP.partial.boards[0];
 | |
|   },
 | |
|   
 | |
|   getPostUID: function(btn) {
 | |
|     var el;
 | |
|     
 | |
|     el = $.cls('post-sel', btn.parentNode)[0];
 | |
|     
 | |
|     if (!el) {
 | |
|       return null;
 | |
|     }
 | |
|     
 | |
|     return {
 | |
|       board: el.getAttribute('data-board'),
 | |
|       pid: el.getAttribute('data-pid')
 | |
|     };
 | |
|   },
 | |
|   
 | |
|   onHashChange: function(e) {
 | |
|     var i, el, nodes, params;
 | |
|     
 | |
|     if (!location.hash || location.hash === '#') {
 | |
|       if (APP.searchResults.length) {
 | |
|         APP.onSearchReset();
 | |
|       }
 | |
|       return;
 | |
|     }
 | |
|     
 | |
|     // Chrome
 | |
|     if (location.hash[1] === '%' || location.hash[2] === '%') {
 | |
|       e && e.preventDefault();
 | |
|       
 | |
|       location.hash = params = decodeURIComponent(location.hash.slice(1));
 | |
|       
 | |
|       if (location.hash[2] !== '%') {
 | |
|         return;
 | |
|       }
 | |
|     }
 | |
|     else {
 | |
|       params = location.hash.slice(1);
 | |
|     }
 | |
|     
 | |
|     try {
 | |
|       params = JSON.parse(params);
 | |
|     }
 | |
|     catch (err) {
 | |
|       Feedback.error('Invalid parameters.');
 | |
|       return;
 | |
|     }
 | |
|     
 | |
|     nodes = $.cls('s-p', $.id('search-form'));
 | |
|     
 | |
|     for (i = 0; el = nodes[i]; ++i) {
 | |
|       if (params.hasOwnProperty(el.name)) {
 | |
|         if (el.type === 'text' || el.type === 'hidden' || el.type === 'select-one') {
 | |
|           el.value = params[el.name];
 | |
|         }
 | |
|         else if (el.type === 'checkbox') {
 | |
|           el.checked = true;
 | |
|         }
 | |
|       }
 | |
|       else {
 | |
|         if (el.type === 'text' || el.type === 'hidden') {
 | |
|           el.value = '';
 | |
|         }
 | |
|         else if (el.type === 'checkbox') {
 | |
|           el.checked = false;
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|     
 | |
|     if (params.boards) {
 | |
|       $.id('boards-field').value = params.boards;
 | |
|     }
 | |
|     
 | |
|     nodes = $.id('group-field').options;
 | |
|     
 | |
|     if (params.group) {
 | |
|       for (i = 0; el = nodes[i]; ++i) {
 | |
|         if (el.value === params.group) {
 | |
|           el.selected = true;
 | |
|         }
 | |
|         else {
 | |
|           el.selected = false;
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|     else {
 | |
|       for (i = 0; el = nodes[i]; ++i) {
 | |
|         el.selected = false;
 | |
|       }
 | |
|       nodes[0].selected = true;
 | |
|     }
 | |
|     
 | |
|     APP.onSearchSubmit();
 | |
|   },
 | |
|   
 | |
|   parseResponse: function(data) {
 | |
|     try {
 | |
|       return JSON.parse(data);
 | |
|     }
 | |
|     catch (e) {
 | |
|       return {
 | |
|         status: 'error',
 | |
|         message: 'Something went wrong.'
 | |
|       };
 | |
|     }
 | |
|   },
 | |
|   
 | |
|   showFileTip: function(t) {
 | |
|     return $.escapeHTML(t.getAttribute('data-meta')
 | |
|       + ', ' + $.prettyBytes(t.getAttribute('data-fsize')));
 | |
|   },
 | |
|   
 | |
|   /**
 | |
|    * Thread options wndow
 | |
|    */
 | |
|   onThreadOptsClick: function(btn) {
 | |
|     var uid;
 | |
|     
 | |
|     uid = APP.getPostUID(btn);
 | |
|     
 | |
|     if (!uid) {
 | |
|       return;
 | |
|     }
 | |
|     
 | |
|     window.open(
 | |
|       'https://sys.4chan.org/' + uid.board
 | |
|         + '/admin?mode=admin&admin=opt&id=' + uid.pid,
 | |
|       '_blank', 'width=400,height=275'
 | |
|     );
 | |
|   },
 | |
|   
 | |
|   /**
 | |
|    * Del
 | |
|    */
 | |
|   onPreDelPostClick: function(btn) {
 | |
|     $.addClass(btn, 'btn-deny');
 | |
|     btn.setAttribute('data-cmd', 'del-post');
 | |
|     Tip.show(btn, '<span id="del-conf-tip">Confirm</div>');
 | |
|     setTimeout(APP.resetDelConfirmBtn, APP.preDelDelay, btn);
 | |
|   },
 | |
|   
 | |
|   resetDelConfirmBtn: function(btn) {
 | |
|     var el;
 | |
|     
 | |
|     if (!btn) {
 | |
|       return;
 | |
|     }
 | |
|     
 | |
|     if (el = $.id('del-conf-tip')) {
 | |
|       if (btn.matches ? btn.matches(':hover') : btn.matchesSelector(':hover')) {
 | |
|         Tip.hide();
 | |
|       }
 | |
|     }
 | |
|     
 | |
|     $.removeClass(btn, 'btn-deny');
 | |
|     btn.setAttribute('data-cmd', 'pre-del-post');
 | |
|   },
 | |
|   
 | |
|   onDelPostClick: function(btn) {
 | |
|     var el, uid;
 | |
|     
 | |
|     if (uid = APP.getPostUID(btn)) {
 | |
|       el = $.id(uid.board + '-' + uid.pid);
 | |
|       
 | |
|       if ($.hasClass(el, 'processing')) {
 | |
|         return;
 | |
|       }
 | |
|       
 | |
|       $.addClass(el, 'processing');
 | |
|       
 | |
|       APP.deletePost(
 | |
|         uid.board,
 | |
|         uid.pid,
 | |
|         btn.hasAttribute('data-fileonly'),
 | |
|         APP.isArchived
 | |
|       );
 | |
|     }
 | |
|   },
 | |
|   
 | |
|   deletePost: function(board, pid, fileOnly, isArchived) {
 | |
|     var path, params = 'mode=' + (isArchived ? 'arcdel' : 'usrdel')
 | |
|       + '&' + pid + '=delete&pwd=janitorise';
 | |
|     
 | |
|     if (fileOnly) {
 | |
|       params += '&onlyimgdel=on';
 | |
|     }
 | |
|     
 | |
|     path = '/imgboard.php';
 | |
|     
 | |
|     $.xhr('POST', 'https://sys.4chan.org/' + board + path,
 | |
|     //$.xhr('GET', '?action=dummy_ok',
 | |
|       {
 | |
|         onload: APP.onPostDeleted,
 | |
|         onerror: APP.onPostDeleteError,
 | |
|         withCredentials: true,
 | |
|         board: board,
 | |
|         pid: pid,
 | |
|         fileOnly: fileOnly,
 | |
|       },
 | |
|       params
 | |
|     );
 | |
|   },
 | |
|   
 | |
|   onPostDeleteError: function() {
 | |
|     var el = $.id(this.board + '-' + this.pid);
 | |
|     
 | |
|     if (!el) {
 | |
|       return;
 | |
|     }
 | |
|     
 | |
|     $.removeClass(el, 'processing');
 | |
|     
 | |
|     console.log("Couldn't delete " + this.board + ' ' + this.pid);
 | |
|   },
 | |
|   
 | |
|   onPostDeleted: function() {
 | |
|     var key, el, resp;
 | |
|     
 | |
|     key = this.board + '-' + this.pid;
 | |
|     
 | |
|     if (!(el = $.id(key))) {
 | |
|       return;
 | |
|     }
 | |
|     
 | |
|     if (/Updating index|Can't find the post/.test(this.responseText)) {
 | |
|       $.removeClass(el, 'processing');
 | |
|       
 | |
|       if (!this.fileOnly) {
 | |
|         $.addClass(el, 'disabled');
 | |
|         
 | |
|         APP.deletedResults[key] = APP.POST_DELETED;
 | |
|         
 | |
|         if (el = $.cls('post-sel', el)[0]) {
 | |
|           el.disabled = true;
 | |
|           el.checked = false;
 | |
|         }
 | |
|       }
 | |
|       else if (el = $.cls('post-thumb', el)[0]) {
 | |
|         APP.deletedResults[key] = APP.FILE_DELETED;
 | |
|         
 | |
|         el.src = '//' + APP.fileDeleted;
 | |
|         $.removeClass(el, 'post-thumb');
 | |
|       }
 | |
|     }
 | |
|     else {
 | |
|       if (resp = this.responseText.match(/"errmsg"[^>]*>(.*?)<\/span/)) {
 | |
|         Feedback.error(resp[1]);
 | |
|       }
 | |
|       
 | |
|       APP.onPostDeleteError.call(this);
 | |
|     }
 | |
|   },
 | |
|   
 | |
|   /**
 | |
|    * Ban window
 | |
|    */
 | |
|   onBanPostClick: function(btn) {
 | |
|     var uid;
 | |
|     
 | |
|     uid = APP.getPostUID(btn);
 | |
|     
 | |
|     if (!uid) {
 | |
|       return;
 | |
|     }
 | |
|     
 | |
|     window.open(
 | |
|       'https://sys.4chan.org/' + uid.board
 | |
|         + '/admin?mode=admin&admin=ban&id=' + uid.pid,
 | |
|       '_blank', 'width=400,height=445'
 | |
|     );
 | |
|   },
 | |
|   
 | |
|   /**
 | |
|    * Multi ban
 | |
|    */
 | |
|   showBanForm: function(btn, ips) {
 | |
|     var el, rect, el2;
 | |
|     
 | |
|     APP.closeBanForm();
 | |
|     
 | |
|     APP.activeMultiBanBtn = btn;
 | |
|     
 | |
|     rect = btn.getBoundingClientRect();
 | |
|     
 | |
|     el = $.id('ban-form-cnt');
 | |
|     
 | |
|     if (ips.length > APP.maxIPBans) {
 | |
|       $.id('js-btn-no-reverse').checked = true;
 | |
|     }
 | |
|     
 | |
|     if (!APP.banTemplatesCache) {
 | |
|       APP.loadBanTemplates();
 | |
|     }
 | |
|     else {
 | |
|       APP.buildBanTemplates();
 | |
|     }
 | |
|     
 | |
|     el2 = $.id('ban-ips-field');
 | |
|     el2.value = JSON.stringify(ips);
 | |
|     el2.setAttribute('data-size', ips.length);
 | |
|     
 | |
|     $.removeClass(el, 'hidden');
 | |
|     
 | |
|     el.style.top = rect.top - el.offsetHeight + window.pageYOffset - 10 + 'px';
 | |
|     el.style.right = ($.docEl.clientWidth - rect.right) + 'px';
 | |
|     
 | |
|     if (el.offsetTop < window.pageYOffset) {
 | |
|       el.scrollIntoView(true);
 | |
|     }
 | |
|   },
 | |
|   
 | |
|   closeBanForm: function() {
 | |
|     APP.activeMultiBanBtn = null;
 | |
|     $.id('ban-ips-field').value = '';
 | |
|     $.addClass($.id('ban-form-cnt'), 'hidden');
 | |
|   },
 | |
|   
 | |
|   onMultiBanClick: function(btn) {
 | |
|     var cnt, ips;
 | |
|     
 | |
|     if (btn.hasAttribute('data-all')) {
 | |
|       cnt = null;
 | |
|     }
 | |
|     else {
 | |
|       cnt = btn.parentNode.parentNode.parentNode;
 | |
|       
 | |
|       if (!$.hasClass(cnt, 'res-cnt')) {
 | |
|         console.log('Container mismatch');
 | |
|         return;
 | |
|       }
 | |
|     }
 | |
|     
 | |
|     ips = APP.getSelectedIPs($.cls('post-sel', cnt));
 | |
|     
 | |
|     if (!ips) {
 | |
|       Feedback.error('Nothing to do.');
 | |
|       return;
 | |
|     }
 | |
|     
 | |
|     APP.showBanForm(btn, ips);
 | |
|   },
 | |
|   
 | |
|   onSubmitBanClick: function() {
 | |
|     $.id('ban-btn-dummy').click();
 | |
|   },
 | |
|   
 | |
|   onCancelBanClick: function() {
 | |
|     APP.closeBanForm();
 | |
|   },
 | |
|   
 | |
|   onBanSubmit: function(e) {
 | |
|     var i, el, nodes, data, size;
 | |
|     
 | |
|     e && e.preventDefault();
 | |
|     
 | |
|     if ($.hasClass(this, 'hidden')) {
 | |
|       return;
 | |
|     }
 | |
|     
 | |
|     size = +$.id('ban-ips-field').getAttribute('data-size');
 | |
|     
 | |
|     if (size > APP.maxIPBans && !$.id('js-btn-no-reverse').checked) {
 | |
|       Feedback.error("Too many IPs to ban. Use the 'No reverse' option to bypass the limit.");
 | |
|       return;
 | |
|     }
 | |
|     
 | |
|     data = {};
 | |
|     
 | |
|     nodes = $.cls('ban-field');
 | |
|     
 | |
|     for (i = 0; el = nodes[i]; ++i) {
 | |
|       if (el.type === 'checkbox') {
 | |
|         data[el.name] = +el.checked;
 | |
|       }
 | |
|       else {
 | |
|         data[el.name] = el.value;
 | |
|       }
 | |
|     }
 | |
|     
 | |
|     if (APP.currentParams) {
 | |
|       data['params'] = JSON.stringify(APP.currentParams);
 | |
|     }
 | |
|     
 | |
|     if ($.id('js-btn-no-reverse').checked) {
 | |
|        data['no_reverse'] = 1;
 | |
|     }
 | |
|     
 | |
|     APP.banIps(data);
 | |
|   },
 | |
|   
 | |
|   banIps: function(data) {
 | |
|     APP.toggleBackDrop(true);
 | |
|     
 | |
|     Feedback.notify('Processing…', false);
 | |
|     
 | |
|     data.action = 'ban';
 | |
|     data._tkn = $.getToken();
 | |
|     
 | |
|     $.xhr('POST', '', {
 | |
|       onload: APP.onMultiBanLoaded,
 | |
|       onerror: APP.onMultiBanError
 | |
|     },
 | |
|     data);
 | |
|   },
 | |
|   
 | |
|   onMultiBanLoaded: function() {
 | |
|     var resp;
 | |
|     
 | |
|     resp = APP.parseResponse(this.responseText);
 | |
|     
 | |
|     if (resp.status === 'success') {
 | |
|       Feedback.hideMessage();
 | |
|       APP.closeBanForm();
 | |
|     }
 | |
|     else {
 | |
|       Feedback.error(resp.message);
 | |
|     }
 | |
|     
 | |
|     console.log(this.responseText);
 | |
|     
 | |
|     APP.toggleBackDrop(false);
 | |
|   },
 | |
|   
 | |
|   onMultiBanError: function() {
 | |
|     Feedback.error('Something went wrong.');
 | |
|     APP.toggleBackDrop(false);
 | |
|   },
 | |
|   
 | |
|   /**
 | |
|    * Multiban templates
 | |
|    */
 | |
|   loadBanTemplates: function() {
 | |
|     if (APP.banTemplatesLoading) {
 | |
|       return;
 | |
|     }
 | |
|     
 | |
|     APP.banTemplatesLoading = true;
 | |
|     
 | |
|     $.xhr('GET', '?action=get_templates', {
 | |
|       onload: APP.onBanTemplatesLoaded,
 | |
|       onerror: APP.onBanTemplatesError
 | |
|     });
 | |
|   },
 | |
|   
 | |
|   onBanTemplatesError: function() {
 | |
|     APP.banTemplatesLoading = false;
 | |
|     Feedback.error("Could't load ban templates");
 | |
|   },
 | |
|   
 | |
|   onBanTemplatesLoaded: function() {
 | |
|     var resp;
 | |
|     
 | |
|     APP.banTemplatesLoading = false;
 | |
|     
 | |
|     resp = APP.parseResponse(this.responseText);
 | |
|     
 | |
|     if (resp.status === 'success') {
 | |
|       APP.banTemplatesCache = resp.data;
 | |
|       APP.buildBanTemplates();
 | |
|     }
 | |
|     else {
 | |
|       Feedback.error(resp.message);
 | |
|     }
 | |
|   },
 | |
|   
 | |
|   buildBanTemplates: function() {
 | |
|     let sel = $.id('ban-templates-sel');
 | |
|     
 | |
|     if (!sel || !APP.activeMultiBanBtn || !APP.banTemplatesCache) {
 | |
|       return;
 | |
|     }
 | |
|     
 | |
|     let cnt = APP.activeMultiBanBtn.parentNode.parentNode.parentNode;
 | |
|     
 | |
|     let postMap = APP.getSelectedPosts($.cls('post-sel', cnt));
 | |
|     
 | |
|     let boards = ['global'];
 | |
|     
 | |
|     if (postMap) {
 | |
|       let keys = Object.keys(postMap);
 | |
|       
 | |
|       if (keys.length === 1) {
 | |
|         boards.unshift(keys[0]);
 | |
|       }
 | |
|     }
 | |
|     
 | |
|     if (sel.childElementCount > 1) {
 | |
|       sel.innerHTML = '<option></option>';
 | |
|     }
 | |
|     else {
 | |
|       sel.children[0].textContent = '';
 | |
|       $.on(sel, 'change', APP.onBanTemplateChanged);
 | |
|     }
 | |
|     
 | |
|     for (let board of boards) {
 | |
|       let templates = APP.banTemplatesCache[board];
 | |
|       
 | |
|       if (!templates) {
 | |
|         continue;
 | |
|       }
 | |
|       
 | |
|       for (let i = 0; i < templates.length; ++i) {
 | |
|         let tpl = templates[i];
 | |
|         
 | |
|         const option = $.el('option');
 | |
|         option.value = `${board}-${i}`;
 | |
|         option.innerHTML = tpl.name;
 | |
|         
 | |
|         sel.appendChild(option);
 | |
|       }
 | |
|     }
 | |
|   },
 | |
|   
 | |
|   onBanTemplateChanged: function() {
 | |
|     let sel = $.id('ban-templates-sel');
 | |
|     
 | |
|     if (!sel) {
 | |
|       return;
 | |
|     }
 | |
|     
 | |
|     if (!APP.banTemplatesCache) {
 | |
|       return;
 | |
|     }
 | |
|     
 | |
|     let val = sel.value;
 | |
|     
 | |
|     if (!val) {
 | |
|       return;
 | |
|     }
 | |
|     
 | |
|     val = val.split('-');
 | |
|     
 | |
|     let board = val[0];
 | |
|     let idx = +val[1];
 | |
|     
 | |
|     let tpl = APP.banTemplatesCache[board][idx];
 | |
|     
 | |
|     $.id('js-ban-reason').value = tpl.reason;
 | |
|     $.id('js-ban-days').value = tpl.days;
 | |
|     $.id('js-btn-global').checked = !!tpl.global;
 | |
|   },
 | |
|   
 | |
|   /**
 | |
|    * Grp del
 | |
|    */
 | |
|   onPreDelGrpClick: function(btn) {
 | |
|     $.removeClass(btn, 'btn-other');
 | |
|     $.addClass(btn, 'btn-deny');
 | |
|     btn.setAttribute('data-cmd', 'del-grp');
 | |
|     Tip.show(btn, '<span id="del-conf-tip">Confirm</div>');
 | |
|     setTimeout(APP.resetGrpDelConfirmBtn, APP.preDelDelay, btn);
 | |
|   },
 | |
|   
 | |
|   resetGrpDelConfirmBtn: function(btn) {
 | |
|     var el;
 | |
|     
 | |
|     if (!btn) {
 | |
|       return;
 | |
|     }
 | |
|     
 | |
|     if (el = $.id('del-conf-tip')) {
 | |
|       if (btn.matches ? btn.matches(':hover') : btn.matchesSelector(':hover')) {
 | |
|         Tip.hide();
 | |
|       }
 | |
|     }
 | |
|     
 | |
|     $.addClass(btn, 'btn-other');
 | |
|     $.removeClass(btn, 'btn-deny');
 | |
|     btn.setAttribute('data-cmd', 'pre-del-grp');
 | |
|   },
 | |
|   
 | |
|   onDelGrpClick: function(btn) {
 | |
|     var cnt, postMap;
 | |
|     
 | |
|     cnt = btn.parentNode.parentNode.parentNode;
 | |
|     
 | |
|     if (!$.hasClass(cnt, 'res-cnt')) {
 | |
|       console.log('Container mismatch');
 | |
|       return;
 | |
|     }
 | |
|     
 | |
|     if (APP.hasSelectedStickies(cnt)) {
 | |
|       Feedback.error('Some threads are stickied. Unsticky them first.');
 | |
|       return;
 | |
|     }
 | |
|     
 | |
|     postMap = APP.getSelectedPosts($.cls('post-sel', cnt));
 | |
|     
 | |
|     if (!postMap) {
 | |
|       Feedback.error('Nothing to do.');
 | |
|       return;
 | |
|     }
 | |
|     
 | |
|     APP.deletePosts(postMap, APP.isArchived);
 | |
|   },
 | |
|   
 | |
|   /**
 | |
|    * Del all
 | |
|    */
 | |
|   onPreDelAllClick: function(btn) {
 | |
|     $.removeClass(btn, 'btn-other');
 | |
|     $.addClass(btn, 'btn-deny');
 | |
|     btn.setAttribute('data-cmd', 'del-all');
 | |
|     Tip.show(btn, '<span id="del-conf-tip">Confirm</div>');
 | |
|     setTimeout(APP.resetDelAllConfirmBtn, APP.preDelDelay, btn);
 | |
|   },
 | |
|   
 | |
|   resetDelAllConfirmBtn: function(btn) {
 | |
|     var el;
 | |
|     
 | |
|     if (!btn) {
 | |
|       return;
 | |
|     }
 | |
|     
 | |
|     if (el = $.id('del-conf-tip')) {
 | |
|       if (btn.matches ? btn.matches(':hover') : btn.matchesSelector(':hover')) {
 | |
|         Tip.hide();
 | |
|       }
 | |
|     }
 | |
|     
 | |
|     $.addClass(btn, 'btn-other');
 | |
|     $.removeClass(btn, 'btn-deny');
 | |
|     btn.setAttribute('data-cmd', 'pre-del-all');
 | |
|   },
 | |
|   
 | |
|   onDelAllClick: function(btn) {
 | |
|     var postMap;
 | |
|     
 | |
|     if (APP.hasSelectedStickies()) {
 | |
|       Feedback.error('Some threads are stickied. Unsticky them first.');
 | |
|       return;
 | |
|     }
 | |
|     
 | |
|     postMap = APP.getSelectedPosts($.cls('post-sel'));
 | |
|     
 | |
|     APP.deletePosts(postMap, APP.isArchived, btn.hasAttribute('data-fileonly'));
 | |
|   },
 | |
|   
 | |
|   deletePosts: function(postMap, isArchived, fileOnly) {
 | |
|     var board, mode, q, callbacks, path, empty;
 | |
|     
 | |
|     if (APP.delXhrCount) {
 | |
|       console.log('Already deleting.');
 | |
|       return;
 | |
|     }
 | |
|     
 | |
|     empty = true;
 | |
|     
 | |
|     for (board in postMap) {
 | |
|       empty = false;
 | |
|       break;
 | |
|     }
 | |
|     
 | |
|     if (empty) {
 | |
|       Feedback.error('Nothing to do.');
 | |
|       return;
 | |
|     }
 | |
|     
 | |
|     APP.delErrors = false;
 | |
|     APP.toggleBackDrop(true);
 | |
|     Feedback.notify('Deleting…', false);
 | |
|     
 | |
|     callbacks = {
 | |
|       onloadend: APP.onMultiDelLoadEnd,
 | |
|       onload: APP.onMultiDelLoad,
 | |
|       onerror: APP.onMultiDelError,
 | |
|       board: null,
 | |
|       pids: null,
 | |
|       withCredentials: true
 | |
|     };
 | |
|     
 | |
|     mode = 'mode=' + (isArchived ? 'arcdel' : 'usrdel') + '&tool=search&';
 | |
|     
 | |
|     for (board in postMap) {
 | |
|       callbacks.board = board;
 | |
|       callbacks.pids = postMap[board];
 | |
|       
 | |
|       path = 'https://sys.4chan.org/' + board + '/imgboard.php';
 | |
|       q = mode + postMap[board].join('=delete&') + '=delete&pwd=janitorise';
 | |
|       
 | |
|       if (fileOnly) {
 | |
|         q += '&onlyimgdel=on';
 | |
|       }
 | |
|       
 | |
|       ++APP.delXhrCount;
 | |
|       
 | |
|       APP.delXhrs.push($.xhr('POST', path, callbacks, q));
 | |
|     }
 | |
|   },
 | |
|   
 | |
|   onMultiDelLoadEnd: function() {
 | |
|     APP.delXhrCount--;
 | |
|     
 | |
|     if (APP.delXhrCount <= 0) {
 | |
|       APP.onPostDeletionDone();
 | |
|     }
 | |
|   },
 | |
|   
 | |
|   onMultiDelLoad: function() {
 | |
|     var resp;
 | |
|     
 | |
|     if (/Updating index|Can't find the post/.test(this.responseText)) {
 | |
|       APP.updatedDeletedPosts(this.board, this.pids);
 | |
|     }
 | |
|     else {
 | |
|       APP.delErrors = true;
 | |
|       
 | |
|       if (resp = this.responseText.match(/"errmsg"[^>]*>(.*?)<\/span/)) {
 | |
|         console.log(resp[1]);
 | |
|       }
 | |
|       else {
 | |
|         console.log('Error.');
 | |
|       }
 | |
|     }
 | |
|   },
 | |
|   
 | |
|   onMultiDelError: function() {
 | |
|     APP.delErrors = true;
 | |
|     console.log('Connection error.');
 | |
|   },
 | |
|   
 | |
|   updatedDeletedPosts: function(board, pids) {
 | |
|     var key, v, cnt, el, i, pid, deletedResults;
 | |
|     
 | |
|     deletedResults = APP.deletedResults;
 | |
|     v = APP.POST_DELETED;
 | |
|     
 | |
|     for (i = 0; pid = pids[i]; ++i) {
 | |
|       key = board + '-' + pid;
 | |
|       
 | |
|       deletedResults[key] = v;
 | |
|       
 | |
|       if (cnt = $.id(key)) {
 | |
|         $.addClass(cnt, 'disabled');
 | |
|         
 | |
|         if (el = $.cls('post-sel', cnt)[0]) {
 | |
|           el.checked = false;
 | |
|           el.disabled = true;
 | |
|         }
 | |
|         
 | |
|         if (el = $.cls('post-thumb', cnt)[0]) {
 | |
|           el.removeAttribute('data-src');
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|   },
 | |
|   
 | |
|   onPostDeletionDone: function() {
 | |
|     if (APP.delErrors) {
 | |
|       Feedback.error('Errors occurred. Not all posts could be deleted.');
 | |
|     }
 | |
|     else {
 | |
|       Feedback.hideMessage();
 | |
|     }
 | |
|     APP.delErrors = false;
 | |
|     APP.toggleBackDrop(false);
 | |
|     APP.delXhrs = [];
 | |
|   },
 | |
|   
 | |
|   toggleBackDrop: function(flag) {
 | |
|     if (flag) {
 | |
|       if (!$.hasClass(document.body, 'has-backdrop')) {
 | |
|         $.addClass(document.body, 'has-backdrop');
 | |
|       }
 | |
|     }
 | |
|     else {
 | |
|       $.removeClass(document.body, 'has-backdrop');
 | |
|     }
 | |
|   },
 | |
|   
 | |
|   getSelectedIPs: function(nodes) {
 | |
|     var i, el, ips, ip, ipMap;
 | |
|     
 | |
|     ipMap = {};
 | |
|     ips = [];
 | |
|     
 | |
|     for (i = 0; el = nodes[i]; ++i) {
 | |
|       if (!el.checked || el.disabled) {
 | |
|         continue;
 | |
|       }
 | |
|       
 | |
|       ip = el.getAttribute('data-ip');
 | |
|       
 | |
|       if (ip && !ipMap[ip]) {
 | |
|         ips.push({
 | |
|           ip: ip,
 | |
|           pwd: el.getAttribute('data-pwd'),
 | |
|           board: el.getAttribute('data-board'),
 | |
|           pid: el.getAttribute('data-pid')
 | |
|         });
 | |
|         
 | |
|         ipMap[ip] = true;
 | |
|       }
 | |
|     }
 | |
|     
 | |
|     if (ips.length) {
 | |
|       return ips;
 | |
|     }
 | |
|     
 | |
|     return null;
 | |
|   },
 | |
|   
 | |
|   hasSelectedStickies: function(root) {
 | |
|     var i, el, nodes;
 | |
|     
 | |
|     nodes = $.cls('opt-sticky', root);
 | |
|     
 | |
|     for (i = 0; el = nodes[i]; ++i) {
 | |
|       if (el = $.cls('post-sel', el.parentNode)[0]) {
 | |
|         if (el.checked) {
 | |
|           return true;
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|     
 | |
|     return false;
 | |
|   },
 | |
|   
 | |
|   getSelectedPosts: function(nodes) {
 | |
|     var i, el, postMap, board, pid;
 | |
|     
 | |
|     postMap = {};
 | |
|     
 | |
|     for (i = 0; el = nodes[i]; ++i) {
 | |
|       if (!el.checked || el.disabled) {
 | |
|         continue;
 | |
|       }
 | |
|       
 | |
|       board = el.getAttribute('data-board');
 | |
|       pid = el.getAttribute('data-pid');
 | |
|       
 | |
|       if (!postMap[board]) {
 | |
|         postMap[board] = [];
 | |
|       }
 | |
|       
 | |
|       postMap[board].push(pid);
 | |
|     }
 | |
|     
 | |
|     for (i in postMap) {
 | |
|       return postMap;
 | |
|     }
 | |
|     
 | |
|     return null;
 | |
|   },
 | |
|   
 | |
|   showResultsCtrl: function() {
 | |
|     var ok, err, el;
 | |
|     
 | |
|     el = $.id('results-ctrl');
 | |
|     
 | |
|     if ($.hasClass(el, 'hidden')) {
 | |
|       $.removeClass(el, 'hidden');
 | |
|     }
 | |
|     
 | |
|     el = $.id('ban-all-btn');
 | |
|     
 | |
|     $.removeClass(el, 'hidden');
 | |
|     
 | |
|     if (APP.isArchived) {
 | |
|       $.addClass(el, 'hidden');
 | |
|     }
 | |
|     
 | |
|     ok = err = '';
 | |
|     
 | |
|     if (APP.hasPartialResults()) {
 | |
|       err = 'Partial results. ';
 | |
|       
 | |
|       if (APP.partial.error) {
 | |
|         err += 'Errors occurred.';
 | |
|       }
 | |
|       else if (APP.partial.boards[0]) {
 | |
|         err += 'Some boards returned too many posts.';
 | |
|       }
 | |
|       else if (APP.partial.global) {
 | |
|         err += 'Maximum number of results reached.';
 | |
|       }
 | |
|     }
 | |
|     
 | |
|     if (APP.resultCount) {
 | |
|       ok = 'Found ' + APP.resultCount
 | |
|         + ' post' + $.pluralise(APP.resultCount)
 | |
|         + ' on ' + APP.searchResults.length
 | |
|         + ' board' + $.pluralise(APP.searchResults.length);
 | |
|     }
 | |
|     
 | |
|     $.id('js-results-ok').textContent = ok;
 | |
|     $.id('js-results-err').textContent = err;
 | |
|   },
 | |
|   
 | |
|   hideResultsCtrl: function() {
 | |
|     var el = $.id('results-ctrl');
 | |
|     
 | |
|     if (!$.hasClass(el, 'hidden')) {
 | |
|       $.addClass(el, 'hidden');
 | |
|     }
 | |
|   },
 | |
|   
 | |
|   onToggleGrpClick: function(btn) {
 | |
|     var i, el, nodes, grpCnt, flag;
 | |
|     
 | |
|     grpCnt = btn.parentNode.parentNode;
 | |
|     
 | |
|     flag = btn.checked;
 | |
|     
 | |
|     nodes = $.cls('post-sel', grpCnt);
 | |
|     
 | |
|     for (i = 0; el = nodes[i]; ++i) {
 | |
|       if (!el.disabled) {
 | |
|         el.checked = flag;
 | |
|       }
 | |
|     }
 | |
|   },
 | |
|   
 | |
|   onTogglePostClick: function(btn) {
 | |
|     var i, el, nodes, grpCnt, grpSel, checkedCount;
 | |
|     
 | |
|     grpCnt = btn.parentNode.parentNode.parentNode;
 | |
|     grpSel = $.cls('grp-sel', grpCnt)[0];
 | |
|     nodes = $.cls('post-sel', grpCnt);
 | |
|     
 | |
|     checkedCount = 0;
 | |
|     
 | |
|     for (i = 0; el = nodes[i]; ++i) {
 | |
|       if (el.checked) {
 | |
|         ++checkedCount;
 | |
|       }
 | |
|     }
 | |
|     
 | |
|     grpSel.checked = checkedCount === nodes.length;
 | |
|     grpSel.indeterminate = checkedCount > 0 && !grpSel.checked;
 | |
|   },
 | |
|   
 | |
|   showNameTip: function(el) {
 | |
|     el = el.parentNode;
 | |
|     
 | |
|     if (el.textContent.length > 25) {
 | |
|       return el.innerHTML;
 | |
|     }
 | |
|     
 | |
|     return null;
 | |
|   },
 | |
|   
 | |
|   showFieldTip: function(el) {
 | |
|     var tip = $.id('tip-' + el.getAttribute('data-type'));
 | |
|     
 | |
|     if (!tip) {
 | |
|       return null;
 | |
|     }
 | |
|     
 | |
|     return tip.innerHTML;
 | |
|   },
 | |
|   
 | |
|   onGroupChange: function() {
 | |
|     var url_frag, grp;
 | |
|     
 | |
|     if (!APP.searchResults.length) {
 | |
|       return;
 | |
|     }
 | |
|     
 | |
|     url_frag = APP.currentParams || {};
 | |
|     
 | |
|     grp = APP.getGroupOptionTag();
 | |
|     
 | |
|     if (grp.previousElementSibling) {
 | |
|       url_frag['group'] = grp.value;
 | |
|     }
 | |
|     else {
 | |
|       delete url_frag['group'];
 | |
|     }
 | |
|     
 | |
|     history.replaceState(null, '', '#' + JSON.stringify(url_frag));
 | |
|     
 | |
|     Feedback.notify('Building…', false);
 | |
|     setTimeout(APP.onSearchResultsReady, 30);
 | |
|   },
 | |
|   
 | |
|   onGroupSortChange: function() {
 | |
|     var url_frag;
 | |
|     
 | |
|     if (!APP.searchResults.length) {
 | |
|       return;
 | |
|     }
 | |
|     
 | |
|     url_frag = APP.currentParams || {};
 | |
|     
 | |
|     if (this.checked) {
 | |
|       url_frag['gss'] = 1;
 | |
|     }
 | |
|     else {
 | |
|       delete url_frag['gss'];
 | |
|     }
 | |
|     
 | |
|     history.replaceState(null, '', '#' + JSON.stringify(url_frag));
 | |
|     
 | |
|     Feedback.notify('Building…', false);
 | |
|     setTimeout(APP.onSearchResultsReady, 30);
 | |
|   },
 | |
|   
 | |
|   onSearchClick: function() {
 | |
|     if (APP.xhrCount !== 0) {
 | |
|       APP.searchAborted = true;
 | |
|       return;
 | |
|     }
 | |
|     $.id('search-btn-dummy').click();
 | |
|   },
 | |
|   
 | |
|   onSearchReset: function() {
 | |
|     if (APP.xhrCount !== 0) {
 | |
|       return;
 | |
|     }
 | |
|     
 | |
|     APP.clearSearchState();
 | |
|     
 | |
|     APP.hideResultsCtrl();
 | |
|     
 | |
|     $.id('search-results').textContent = '';
 | |
|     $.id('search-form').reset();
 | |
|     
 | |
|     if (location.hash) {
 | |
|       location.hash = '';
 | |
|     }
 | |
|   },
 | |
|   
 | |
|   onSearchSubmit: function(e) {
 | |
|     var i, el, nodes, q, b, boards, boards_raw, valid_boards, callbacks, url_frag, grp;
 | |
|     
 | |
|     e && e.preventDefault();
 | |
|     
 | |
|     if (APP.xhrCount !== 0) {
 | |
|       return;
 | |
|     }
 | |
|     
 | |
|     APP.closeBanForm();
 | |
|     APP.clearPartialStatus();
 | |
|     
 | |
|     if (el = $.id('no-results')) {
 | |
|       el.parentNode.removeChild(el);
 | |
|     }
 | |
|     
 | |
|     APP.parseFileUID();
 | |
|     APP.parseThreadURL();
 | |
|     APP.parseFileSize();
 | |
|     APP.parsePassRef();
 | |
|     
 | |
|     APP.isArchived = $.id('arc-field').checked;
 | |
|     
 | |
|     valid_boards = JSON.parse($.id('data-boards').textContent);
 | |
|     
 | |
|     boards = boards_raw = $.id('boards-field').value.toLowerCase();
 | |
|     
 | |
|     if ($.id('js-loc-field').value !== '') {
 | |
|       if (boards === '' || $.id('js-tid-field').value === '') {
 | |
|         Feedback.error($.id('js-err-loc').textContent);
 | |
|         return;
 | |
|       }
 | |
|     }
 | |
|     
 | |
|     if ($.id('js-phash-field').value !== '' && APP.isArchived) {
 | |
|       Feedback.error($.id('js-err-phash').textContent);
 | |
|       return;
 | |
|     }
 | |
|     
 | |
|     if (boards === '') {
 | |
|       boards = valid_boards;
 | |
|     }
 | |
|     else {
 | |
|       let op_not = false;
 | |
|       
 | |
|       if (boards.indexOf('!') !== -1) {
 | |
|         op_not = true;
 | |
|         boards = boards.replaceAll('!', '');
 | |
|       }
 | |
|       
 | |
|       boards = boards.split(/[^a-z0-9!]+/);
 | |
|       
 | |
|       for (i = 0; b = boards[i]; ++i) {
 | |
|         if (valid_boards.indexOf(b) === -1) {
 | |
|           alert('Invalid board ' + b);
 | |
|           return;
 | |
|         }
 | |
|       }
 | |
|       
 | |
|       if (op_not) {
 | |
|         boards = valid_boards.filter(b => !boards.includes(b));
 | |
|       }
 | |
|     }
 | |
|     
 | |
|     el = $.id('spec-field');
 | |
|     
 | |
|     if (el && el.checked) {
 | |
|       $.id('arc-field').checked = false;
 | |
|     }
 | |
|     
 | |
|     nodes = $.cls('s-p', $.id('search-form'));
 | |
|     
 | |
|     q = [];
 | |
|     url_frag = {};
 | |
|     
 | |
|     for (i = 0; el = nodes[i]; ++i) {
 | |
|       if (el.type === 'text' || el.type === 'hidden' || el.type === 'select-one') {
 | |
|         if (el.value !== '') {
 | |
|           q.push(el.name + '=' + encodeURIComponent(el.value));
 | |
|           url_frag[el.name] = el.value;
 | |
|         }
 | |
|       }
 | |
|       else if (el.type === 'checkbox') {
 | |
|         if (el.checked) {
 | |
|           q.push(el.name + '=1');
 | |
|           url_frag[el.name] = 1;
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|     
 | |
|     if (q.length < 1) {
 | |
|       return;
 | |
|     }
 | |
|     
 | |
|     APP.clearSearchState();
 | |
|     APP.lockSearchForm();
 | |
|     APP.updateSearchProgress();
 | |
|     
 | |
|     q = q.join('&');
 | |
|     
 | |
|     grp = APP.getGroupOptionTag();
 | |
|     
 | |
|     if (grp.previousElementSibling) {
 | |
|       url_frag['group'] = grp.value;
 | |
|     }
 | |
|     
 | |
|     if (boards_raw !== '') {
 | |
|       url_frag['boards'] = boards_raw;
 | |
|     }
 | |
|     
 | |
|     APP.currentParams = url_frag;
 | |
|     
 | |
|     history.replaceState(null, '', '#' + JSON.stringify(url_frag));
 | |
|     
 | |
|     q = '?action=search&' + q;
 | |
|     
 | |
|     callbacks = {
 | |
|       onload: APP.onSearchLoad,
 | |
|       onerror: APP.onSearchError,
 | |
|       onloadend: APP.onSearchLoadEnd
 | |
|     };
 | |
|     
 | |
|     for (i = 0; b = boards[i]; ++i) {
 | |
|       ++APP.xhrCount;
 | |
|       APP.xhrs.push($.xhr('GET', q + '&board=' + b, callbacks));
 | |
|     }
 | |
|   },
 | |
|   
 | |
|   onSaveSearchClick: function() {
 | |
|     let params = APP.collectSearchParams();
 | |
|     
 | |
|     if (!Object.keys(params).length) {
 | |
|       return Feedback.error('Nothing to save.');
 | |
|     }
 | |
|     
 | |
|     let label = prompt('Enter a short name');
 | |
|     
 | |
|     if (label === '') {
 | |
|       return Feedback.error('Label cannot be empty.');
 | |
|     }
 | |
|     
 | |
|     if (label === null) {
 | |
|       return;
 | |
|     }
 | |
|     
 | |
|     let data = {
 | |
|       label: label,
 | |
|       params: params
 | |
|     };
 | |
|     
 | |
|     let done = false;
 | |
|     
 | |
|     for (let i = 0; i < APP.savedSearch.length; ++i) {
 | |
|       let entry = APP.savedSearch[i];
 | |
|       
 | |
|       if (entry.label === label) {
 | |
|         APP.savedSearch[i] = data;
 | |
|         done = true;
 | |
|         break;
 | |
|       }
 | |
|     }
 | |
|     
 | |
|     if (!done) {
 | |
|       APP.savedSearch.push(data);
 | |
|     }
 | |
|     
 | |
|     localStorage.setItem('saved-searches', JSON.stringify(APP.savedSearch));
 | |
|     
 | |
|     APP.loadSavedSearch();
 | |
|     APP.buildSavedSearchList();
 | |
|   },
 | |
|   
 | |
|   onRunSearchClick: function(btn) {
 | |
|     let sid = btn.getAttribute('data-sid');
 | |
|     
 | |
|     if (sid === undefined) {
 | |
|       return;
 | |
|     }
 | |
|     
 | |
|     let data = APP.savedSearch[sid];
 | |
|     
 | |
|     if (!data) {
 | |
|       return;
 | |
|     }
 | |
|     
 | |
|     if (APP.restoreSearchParams(data.params)) {
 | |
|       APP.onSearchClick();
 | |
|     }
 | |
|   },
 | |
|   
 | |
|   onDelSearchClick: function(btn) {
 | |
|     let sid = btn.getAttribute('data-sid');
 | |
|     
 | |
|     if (sid === undefined) {
 | |
|       return;
 | |
|     }
 | |
|     
 | |
|     if (!APP.savedSearch[sid]) {
 | |
|       return;
 | |
|     }
 | |
|     
 | |
|     APP.savedSearch.splice(sid, 1);
 | |
|     
 | |
|     if (APP.savedSearch.length === 0) {
 | |
|       localStorage.removeItem('saved-searches');
 | |
|     }
 | |
|     else {
 | |
|       localStorage.setItem('saved-searches', JSON.stringify(APP.savedSearch));
 | |
|     }
 | |
|     
 | |
|     APP.buildSavedSearchList();
 | |
|   },
 | |
|   
 | |
|   syncSavedSearch: function(e) {
 | |
|     if (e.key !== 'saved-searches') {
 | |
|       return;
 | |
|     }
 | |
|     
 | |
|     if (APP.loadSavedSearch()) {
 | |
|       APP.buildSavedSearchList();
 | |
|     }
 | |
|   },
 | |
|   
 | |
|   loadSavedSearch: function() {
 | |
|     APP.savedSearch = [];
 | |
|     
 | |
|     try {
 | |
|       let data = localStorage.getItem('saved-searches');
 | |
|       
 | |
|       if (data) {
 | |
|         APP.savedSearch = JSON.parse(data);
 | |
|       }
 | |
|     }
 | |
|     catch(e) {
 | |
|       console.log(e);
 | |
|       return false;
 | |
|     }
 | |
|     
 | |
|     return true;
 | |
|   },
 | |
|   
 | |
|   buildSavedSearchList: function() {
 | |
|     let cnt = $.id('js-saved-searches');
 | |
|     
 | |
|     cnt.innerHTML = '';
 | |
|     
 | |
|     APP.savedSearch.forEach((data, i) => {
 | |
|       let wrap = $.el('span');
 | |
|       
 | |
|       let btn = $.el('button');
 | |
|       btn.className = 'button btn-other btn-ls';
 | |
|       btn.type = 'button';
 | |
|       btn.setAttribute('data-cmd', 'run-search');
 | |
|       btn.setAttribute('data-sid', i);
 | |
|       btn.textContent = data.label;
 | |
|       wrap.appendChild(btn);
 | |
|       
 | |
|       let x = $.el('button');
 | |
|       x.type = 'button';
 | |
|       x.className = 'button btn-other btn-rs';
 | |
|       x.innerHTML = '×';
 | |
|       x.setAttribute('data-cmd', 'del-search');
 | |
|       x.setAttribute('data-sid', i);
 | |
|       x.setAttribute('data-tip', 'Delete');
 | |
|       wrap.appendChild(x);
 | |
|       
 | |
|       cnt.appendChild(wrap);
 | |
|     });
 | |
|   },
 | |
|   
 | |
|   collectSearchParams: function() {
 | |
|     let nodes = $.cls('s-p', $.id('search-form'));
 | |
|     
 | |
|     let params = {};
 | |
|     
 | |
|     for (let el of nodes) {
 | |
|       if (el.name[0] === '_') {
 | |
|         continue;
 | |
|       }
 | |
|       if (el.type === 'checkbox') {
 | |
|         if (el.checked !== el.hasAttribute('checked')) {
 | |
|           params[el.name] = el.checked;
 | |
|         }
 | |
|       }
 | |
|       else if (el.selectedIndex !== undefined) {
 | |
|         if (el.selectedIndex > 0) {
 | |
|           params[el.name] = el.value;
 | |
|         }
 | |
|       }
 | |
|       else {
 | |
|         if (el.value !== '') {
 | |
|           params[el.name] = el.value;
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|     
 | |
|     let boards = $.id('boards-field');
 | |
|     let group = $.id('group-field');
 | |
|     
 | |
|     if (boards.value !== '') {
 | |
|       params.boards = boards.value;
 | |
|     }
 | |
|     
 | |
|     if (group.selectedIndex > 0) {
 | |
|       params.group = group.value;
 | |
|     }
 | |
|     
 | |
|     return params;
 | |
|   },
 | |
|   
 | |
|   restoreSearchParams: function(params) {
 | |
|     if (APP.xhrCount !== 0) {
 | |
|       return false;
 | |
|     }
 | |
|     
 | |
|     APP.onSearchReset();
 | |
|     
 | |
|     let nodes = $.cls('s-p', $.id('search-form'));
 | |
|     
 | |
|     for (let el of nodes) {
 | |
|       if (params[el.name] === undefined) {
 | |
|         continue;
 | |
|       }
 | |
|       
 | |
|       if (el.type === 'checkbox') {
 | |
|         el.checked = params[el.name];
 | |
|       }
 | |
|       else {
 | |
|         el.value = params[el.name];
 | |
|       }
 | |
|     }
 | |
|     
 | |
|     let boards = $.id('boards-field');
 | |
|     let group = $.id('group-field');
 | |
|     
 | |
|     if (params.boards !== undefined) {
 | |
|       boards.value = params.boards;
 | |
|     }
 | |
|     
 | |
|     if (params.group !== undefined) {
 | |
|       group.value = params.group;
 | |
|     }
 | |
|     
 | |
|     return true;
 | |
|   },
 | |
|   
 | |
|   parseFileUID: function() {
 | |
|     var el, params, val, m, r;
 | |
|     
 | |
|     el = document.forms.search.fileuid;
 | |
|     
 | |
|     val = el.value;
 | |
|     
 | |
|     if (!el || val === '' || val.indexOf('/') === -1) {
 | |
|       return;
 | |
|     }
 | |
|     
 | |
|     r = /\/([0-9]+)s?\./g;
 | |
|     
 | |
|     params = [];
 | |
|     
 | |
|     while ((m = r.exec(val)) !== null) {
 | |
|       params.push(m[1]);
 | |
|     }
 | |
|     
 | |
|     params = params.join(',');
 | |
|     
 | |
|     if (params !== '') {
 | |
|       el.value = params;
 | |
|     }
 | |
|   },
 | |
|   
 | |
|   parseThreadURL: function() {
 | |
|     var el, el2, m;
 | |
|     
 | |
|     el = document.forms.search.thread_id;
 | |
|     el2 = $.id('boards-field');
 | |
|     
 | |
|     if (!el || !el2 || el.value === '' || el.value.indexOf('/') === -1) {
 | |
|       return;
 | |
|     }
 | |
|     
 | |
|     m = /\/([a-z0-9]+)\/thread\/([0-9]+)/.exec(el.value);
 | |
|     
 | |
|     if (m[1] !== '' && m[2] !== '') {
 | |
|       el2.value = m[1];
 | |
|       el.value = m[2];
 | |
|     }
 | |
|   },
 | |
|   
 | |
|   parseFileSize: function() {
 | |
|     let el;
 | |
|     
 | |
|     el = document.forms.search.filesize;
 | |
|     
 | |
|     if (!el || el.value === '' || el.value.toUpperCase().indexOf('B') === -1) {
 | |
|       return;
 | |
|     }
 | |
|     
 | |
|     el.value = el.value.replace(/([0-9]+) ?([KM])B\b/ig, function(m, p1, p2) {
 | |
|       let u = 0;
 | |
|       
 | |
|       if (p2.toUpperCase() === 'K') {
 | |
|         u = 1024;
 | |
|       }
 | |
|       else if (p2.toUpperCase() === 'M') {
 | |
|         u = 1024 * 1024;
 | |
|       }
 | |
|       
 | |
|       return p1 * u;
 | |
|     });
 | |
|   },
 | |
|   
 | |
|   parsePassRef: function() {
 | |
|     var el, m;
 | |
|     
 | |
|     el = document.forms.search.pass_ref;
 | |
|     
 | |
|     if (!el || el.value === '') {
 | |
|       return;
 | |
|     }
 | |
|     
 | |
|     if (el.value.indexOf('/bans?') !== -1) {
 | |
|       m = /id=([0-9]+)/.exec(el.value);
 | |
|       
 | |
|       if (m && m[1]) {
 | |
|         el.value = m[1];
 | |
|       }
 | |
|     }
 | |
|     else if (el.value.indexOf('/thread/') !== -1) {
 | |
|       m = /\/([a-z0-9]+)\/thread\/([0-9]+)[^#]*(?:#[qp]([0-9]+))?$/.exec(el.value);
 | |
|       
 | |
|       if (m && m[1] && m[2]) {
 | |
|         if (m[3]) {
 | |
|           el.value = '/' + m[1] + '/' + m[3];
 | |
|         }
 | |
|         else {
 | |
|           el.value = '/' + m[1] + '/' + m[2];
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|   },
 | |
|   
 | |
|   clearSearchState: function() {
 | |
|     APP.xhrs = [];
 | |
|     APP.xhrCount = 0;
 | |
|     APP.resultCount = 0;
 | |
|     APP.currentParams = null;
 | |
|     APP.searchResults = [];
 | |
|     APP.deletedResults = {};
 | |
|     APP.searchAborted = false;
 | |
|     APP.clearPartialStatus();
 | |
|   },
 | |
|   
 | |
|   clearPartialStatus: function() {
 | |
|     APP.partial = { boards: [] };
 | |
|   },
 | |
|   
 | |
|   updateSearchProgress: function() {
 | |
|     var perc, cur, total;
 | |
|     
 | |
|     cur = APP.xhrCount;
 | |
|     total = APP.xhrs.length;
 | |
|     
 | |
|     if (APP.searchAborted) {
 | |
|       APP.abortSearch();
 | |
|       return;
 | |
|     }
 | |
|     
 | |
|     if (total === 0) {
 | |
|       Feedback.notify('Searching… 0%', false);
 | |
|       $.id('search-fields').disabled = true;
 | |
|       return;
 | |
|     }
 | |
|     
 | |
|     if (cur <= 0) {
 | |
|       Feedback.notify('Building…', false);
 | |
|       setTimeout(APP.onSearchResultsReady, 30);
 | |
|       return;
 | |
|     }
 | |
|     
 | |
|     perc = 100 - (0 | (cur / total * 100 + 0.5));
 | |
|     
 | |
|     Feedback.replaceMessage('Searching… ' + perc + '%');
 | |
|   },
 | |
|   
 | |
|   lockSearchForm: function() {
 | |
|     var fields, btn;
 | |
|     
 | |
|     fields = $.id('search-fields');
 | |
|     
 | |
|     if (fields.disabled) {
 | |
|       return;
 | |
|     }
 | |
|     
 | |
|     btn = $.id('search-btn');
 | |
|     
 | |
|     $.removeClass(btn, 'btn-other');
 | |
|     $.addClass(btn, 'btn-deny');
 | |
|     btn.textContent = 'Abort';
 | |
|     
 | |
|     fields.disabled = true;
 | |
|   },
 | |
|   
 | |
|   unlockSearchForm: function() {
 | |
|     var fields, btn;
 | |
|     
 | |
|     fields = $.id('search-fields');
 | |
|     
 | |
|     if (!fields.disabled) {
 | |
|       return;
 | |
|     }
 | |
|     
 | |
|     btn = $.id('search-btn');
 | |
|     
 | |
|     $.removeClass(btn, 'btn-deny');
 | |
|     $.addClass(btn, 'btn-other');
 | |
|     btn.textContent = 'Search';
 | |
|     
 | |
|     fields.disabled = false;
 | |
|   },
 | |
|   
 | |
|   onSearchResultsReady: function() {
 | |
|     APP.buildResults();
 | |
|     Feedback.hideMessage();
 | |
|     APP.unlockSearchForm();
 | |
|   },
 | |
|   
 | |
|   onSearchLoad: function() {
 | |
|     var resp;
 | |
|     
 | |
|     resp = APP.parseResponse(this.responseText);
 | |
|     
 | |
|     if (resp.status === 'success') {
 | |
|       if (resp.data.partial) {
 | |
|         APP.partial.boards.push(resp.data.board);
 | |
|         console.log('Too many results from /' + resp.data.board + '/');
 | |
|       }
 | |
|       
 | |
|       if (APP.resultCount >= APP.maxResults) {
 | |
|         APP.partial.global = true;
 | |
|         return;
 | |
|       }
 | |
|       
 | |
|       if (resp.data.posts.length) {
 | |
|         APP.resultCount += resp.data.posts.length;
 | |
|         APP.searchResults.push(resp.data);
 | |
|       }
 | |
|     }
 | |
|     else {
 | |
|       if (resp.fatal === true) {
 | |
|         APP.abortSearch();
 | |
|         Feedback.error(resp.message);
 | |
|       }
 | |
|       else {
 | |
|         APP.partial.error = true;
 | |
|         console.log(resp.message);
 | |
|       }
 | |
|     }
 | |
|   },
 | |
|   
 | |
|   onSearchError: function() {
 | |
|     APP.partial.error = true;
 | |
|     console.log('err');
 | |
|   },
 | |
|   
 | |
|   onSearchLoadEnd: function() {
 | |
|     if (APP.xhrCount < 1) {
 | |
|       return;
 | |
|     }
 | |
|     
 | |
|     APP.xhrCount--;
 | |
|     
 | |
|     APP.updateSearchProgress();
 | |
|     
 | |
|     if (APP.xhrCount < 1) {
 | |
|       APP.xhrs = [];
 | |
|     }
 | |
|   },
 | |
|   
 | |
|   abortSearch: function() {
 | |
|     var i, xhr;
 | |
|     
 | |
|     for (i = 0; xhr = APP.xhrs[i]; ++i) {
 | |
|       xhr.abort();
 | |
|     }
 | |
|     
 | |
|     APP.clearSearchState();
 | |
|     APP.unlockSearchForm();
 | |
|     Feedback.hideMessage();
 | |
|   },
 | |
|   
 | |
|   onClick: function(e) {
 | |
|     var t, cmd;
 | |
|     
 | |
|     if (e.which != 1 || e.ctrlKey || e.altKey || e.shiftKey || e.metaKey) {
 | |
|       return;
 | |
|     }
 | |
|     
 | |
|     if ((t = e.target) == document) {
 | |
|       return;
 | |
|     }
 | |
|     
 | |
|     if ((cmd = t.getAttribute('data-cmd')) && (cmd = APP.clickCommands[cmd])) {
 | |
|       e.stopPropagation();
 | |
|       cmd(t, e);
 | |
|     }
 | |
|   },
 | |
|   
 | |
|   getGroupOptionTag: function() {
 | |
|     var el = $.id('group-field');
 | |
|     return el.options[el.selectedIndex];
 | |
|   },
 | |
|   
 | |
|   getGroupedResults: function() {
 | |
|     var i, j, post, el, results, resGroup, groupBy, grp, posts, groupNames,
 | |
|       asInt, groupedResults, sortedGroupNames, partialBoards;
 | |
|     
 | |
|     el = APP.getGroupOptionTag();
 | |
|     
 | |
|     asInt = el.hasAttribute('data-int');
 | |
|     groupBy = el.value;
 | |
|     
 | |
|     APP.groupBy = groupBy;
 | |
|     
 | |
|     groupNames = [];
 | |
|     groupedResults = {};
 | |
|     partialBoards = {};
 | |
|     
 | |
|     results = APP.searchResults;
 | |
|     
 | |
|     if (groupBy === 'board') {
 | |
|       for (i = 0; resGroup = results[i]; ++i) {
 | |
|         if (!resGroup.posts[0]) {
 | |
|           continue;
 | |
|         }
 | |
|         grp = resGroup.posts[0].board;
 | |
|         groupNames.push(grp);
 | |
|         groupedResults[grp] = resGroup.posts;
 | |
|         
 | |
|         if (resGroup.partial) {
 | |
|           partialBoards[grp] = true;
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|     else {
 | |
|       for (i = 0; resGroup = results[i]; ++i) {
 | |
|         if (!resGroup.posts[0]) {
 | |
|           continue;
 | |
|         }
 | |
|         
 | |
|         posts = resGroup.posts;
 | |
|         
 | |
|         for (j = 0; post = posts[j]; ++j) {
 | |
|           grp = post[groupBy];
 | |
|           if (!groupedResults[grp]) {
 | |
|             groupNames.push(grp);
 | |
|             groupedResults[grp] = [ post ];
 | |
|           }
 | |
|           else {
 | |
|             groupedResults[grp].push(post);
 | |
|           }
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|     
 | |
|     el = $.id('group-sort-field');
 | |
|     
 | |
|     if (el && el.checked) {
 | |
|       sortedGroupNames = APP.sortGroupsPostCount(groupNames, groupedResults);
 | |
|     }
 | |
|     else {
 | |
|       sortedGroupNames = APP.sortGroups(groupNames, asInt);
 | |
|     }
 | |
|     
 | |
|     return [ groupedResults, sortedGroupNames, partialBoards ];
 | |
|   },
 | |
|   
 | |
|   sortGroups: function(keys, asInt) {
 | |
|     if (asInt) {
 | |
|       keys.sort(function(a, b) { return a - b; });
 | |
|     }
 | |
|     else {
 | |
|       keys.sort();
 | |
|     }
 | |
|     
 | |
|     return keys;
 | |
|   },
 | |
|   
 | |
|   sortGroupsPostCount: function(keys, groups) {
 | |
|     keys.sort(function(a, b) {
 | |
|       var ac, bc;
 | |
|       
 | |
|       ac = groups[a].length;
 | |
|       bc = groups[b].length;
 | |
|       
 | |
|       if (ac < bc) {
 | |
|         return 1;
 | |
|       }
 | |
|       
 | |
|       if (ac > bc) {
 | |
|         return -1;
 | |
|       }
 | |
|       
 | |
|       return 0;
 | |
|     });
 | |
|     
 | |
|     return keys;
 | |
|   },
 | |
|   
 | |
|   countResultThreads: function(posts) {
 | |
|     var p, count = 0;
 | |
|     
 | |
|     for (p of posts) {
 | |
|       if (p.resto === '0') {
 | |
|         count++;
 | |
|       }
 | |
|     }
 | |
|     
 | |
|     return count;
 | |
|   },
 | |
|   
 | |
|   buildResults: function() {
 | |
|     var i, j, results, sortedGroups, group, posts, post, html,
 | |
|       name, tripcode, ccCls, thumb, hasFile, sub, cnt, frag, resCnt, delStatus,
 | |
|       postDeleted, checked, partialSel, postOpts, hasOpts, partialBoards,
 | |
|       postOptsCls, threadCount, resErrCls;
 | |
|     
 | |
|     resCnt = $.id('search-results');
 | |
|     
 | |
|     results = APP.getGroupedResults();
 | |
|     
 | |
|     partialBoards = results[2];
 | |
|     sortedGroups = results[1];
 | |
|     results = results[0];
 | |
|     
 | |
|     if (!sortedGroups.length) {
 | |
|       APP.hideResultsCtrl();
 | |
|       resCnt.innerHTML = '<div id="no-results">Nothing found</div>';
 | |
|       return;
 | |
|     }
 | |
|     
 | |
|     APP.showResultsCtrl();
 | |
|     
 | |
|     resCnt.textContent = '';
 | |
|     
 | |
|     partialSel = false;
 | |
|     
 | |
|     frag = $.frag();
 | |
|     
 | |
|     for (i = 0; (group = sortedGroups[i]) !== undefined; ++i) {
 | |
|       posts = results[group];
 | |
|       
 | |
|       threadCount = APP.countResultThreads(posts);
 | |
|       
 | |
|       resErrCls = partialBoards[group] ? ' res-count-err' : '';
 | |
|       
 | |
|       html = '<div class="res-head"><input class="grp-sel" '
 | |
|         + 'data-cmd="toggle-grp" checked type="checkbox">'
 | |
|         + '<span class="res-title">' + (group === '' ? 'N/A' : group)
 | |
|         + '</span><span class="res-count st' + resErrCls + '">' + posts.length
 | |
|           + ' post' + $.pluralise(posts.length) + '</span>'
 | |
|         + (threadCount ? ('<span class="res-count st stx' + resErrCls + '">' + threadCount
 | |
|           + ' thread' + $.pluralise(threadCount) + '</span>') : '')
 | |
|         + '<span class="grp-ctrl">'
 | |
|         + '<span data-cmd="pre-del-grp" class="button btn-other">Delete</span>'
 | |
|         + (!APP.isArchived ?
 | |
|           '<span data-cmd="ban-multi" class="button btn-other">Ban</span>'
 | |
|           : '') + '</span></div>';
 | |
|       
 | |
|       for (j = 0; post = posts[j]; ++j) {
 | |
|         postDeleted = false;
 | |
|         hasFile = false;
 | |
|         checked = ' checked';
 | |
|         
 | |
|         if (delStatus = APP.deletedResults[post.board + '-' + post.no]) {
 | |
|           if (delStatus === APP.FILE_DELETED) {
 | |
|             post.filedeleted = '1';
 | |
|           }
 | |
|           else {
 | |
|             postDeleted = true;
 | |
|           }
 | |
|         }
 | |
|         
 | |
|         if (post.capcode) {
 | |
|           name = post.name + ' ## ' + APP.capcodes[post.capcode];
 | |
|           ccCls = ' post-capcode-' + post.capcode;
 | |
|           checked = '';
 | |
|           if (!postDeleted) {
 | |
|             partialSel = true;
 | |
|           }
 | |
|         }
 | |
|         else {
 | |
|           name = post.name;
 | |
|           ccCls = '';
 | |
|         }
 | |
|         
 | |
|         if (post.tripcode) {
 | |
|           tripcode = '<span class="post-trip">' + post.tripcode + '</span>';
 | |
|         }
 | |
|         else {
 | |
|           tripcode = '';
 | |
|         }
 | |
|         
 | |
|         if (post.has_opts) {
 | |
|           if (!postDeleted) {
 | |
|             partialSel = true;
 | |
|           }
 | |
|           checked = '';
 | |
|           postOpts = [];
 | |
|           postOptsCls = '';
 | |
|           hasOpts = true;
 | |
|           
 | |
|           if (post.sticky) {
 | |
|             if (post.undead) {
 | |
|               postOpts.push('Rolling Sticky');
 | |
|               postOptsCls = 'opt-undead-sticky';
 | |
|             }
 | |
|             else {
 | |
|               postOpts.push('Sticky');
 | |
|               postOptsCls = 'opt-sticky';
 | |
|             }
 | |
|           }
 | |
|           
 | |
|           if (post.closed) {
 | |
|             postOpts.push('Closed');
 | |
|             
 | |
|             if (!post.sticky) {
 | |
|               postOptsCls = 'opt-closed';
 | |
|             }
 | |
|           }
 | |
|           
 | |
|           if (post.permasage) {
 | |
|             postOpts.push('Perma-sage');
 | |
|             postOptsCls = 'opt-perma-sage';
 | |
|           }
 | |
|           
 | |
|           if (post.permaage) {
 | |
|             postOpts.push('Perma-age');
 | |
|             postOptsCls = 'opt-perma-age';
 | |
|           }
 | |
|           
 | |
|           if (post.undead) {
 | |
|             postOpts.push('Undead');
 | |
|             
 | |
|             if (!post.sticky) {
 | |
|               postOptsCls = 'opt-undead';
 | |
|             }
 | |
|           }
 | |
|         }
 | |
|         else {
 | |
|           hasOpts = null;
 | |
|         }
 | |
|         
 | |
|         if (post.ext) {
 | |
|           if (post.filedeleted) {
 | |
|             thumb = '<div><img class="post-thumb-deleted" src="//'
 | |
|               + this.fileDeleted + '" alt="File deleted"></div>';
 | |
|           }
 | |
|           else if (post.board === 'f') {
 | |
|             hasFile = true;
 | |
|             thumb = '<a target="_blank" href="'
 | |
|               + this.linkToSWF(post.filename) + '">'
 | |
|               + '<div class="post-swf" title="' + post.filename + '.swf">'
 | |
|                 + post.filename + '</div></a>';
 | |
|           }
 | |
|           else {
 | |
|             hasFile = true;
 | |
|             thumb = '<a class="post-thumb-link" target="_blank" href="'
 | |
|               + this.linkToImage(post.board, post.tim, post.ext) + '">'
 | |
|               + '<img data-tip data-tip-cb="APP.showFileTip" data-meta="'
 | |
|               + post.filename + post.ext + "\n" + post.w + '×' + post.h
 | |
|               + '" data-fsize="' + post.fsize + '" class="post-thumb'
 | |
|               + (post.spoiler ? ' thumb-spoiler' : '')
 | |
|                 + '" src="'
 | |
|                 + this.linkToThumb(post.board, post.tim)
 | |
|                 + '" loading="lazy" data-width="'
 | |
|                   + post.w + '" alt="">'
 | |
|               + '</a>';
 | |
|           }
 | |
|         }
 | |
|         else {
 | |
|           thumb = '';
 | |
|         }
 | |
|         
 | |
|         if (post.sub) {
 | |
|           sub = '<div class="post-subject">' + post.sub + '</div>';
 | |
|         }
 | |
|         else {
 | |
|           sub = '';
 | |
|         }
 | |
|         
 | |
|         html += '<article id="' + post.board + '-' + post.no
 | |
|           + '" class="post' + (postDeleted ? ' disabled' : '')
 | |
|             + '"><div class="post-meta">'
 | |
|           + '<span class="post-board">/' + post.board + '/</span>'
 | |
|           + '<div class="post-author">'
 | |
|           + '<span class="post-name' + ccCls + '" data-tip data-tip-cb="APP.showNameTip">'
 | |
|           + name + '</span>' + tripcode + '</div>'
 | |
|           + '<div class="post-host"><span class="cnt-block">'
 | |
|             + post.host + '</span><span class="post-country">'
 | |
|             + post.country + '</span></div>'
 | |
|           + (hasOpts ? ('<span class="post-opts-icon '
 | |
|             + postOptsCls + '" data-tip="'
 | |
|             + postOpts.join("\n") +'"></span>') : '')
 | |
|           + '</div>'
 | |
|           + '<div class="post-content">' + thumb + sub + post.com + '</div>'
 | |
|           + '<div class="post-controls">'
 | |
|           + '<input data-cmd="toggle-post" class="post-sel" type="checkbox" '
 | |
|             + 'data-ip="' + post.host + '" data-board="'
 | |
|             + post.board + '" data-pid="' + post.no + '"' + '" data-pwd="' + post.pwd + '"'
 | |
|               + (postDeleted ? ' disabled' : '') + checked + '>'
 | |
|           + '<a href="'
 | |
|             + this.linkToPost(post.board, post.no, post.resto)
 | |
|             + '" target="_blank" class="post-link button button-light right">View'
 | |
|             + (post.resto === '0' ? '<span class="post-op">(OP)</span>' : '')
 | |
|           + '</a>'
 | |
|           + (!APP.isArchived ?
 | |
|               ((post.resto === '0' ? '<span data-cmd="thread-opts" data-tip="Thread Options" class="button button-light right">O</span>' : '')
 | |
|               + '<a href="#{"password":"'
 | |
|               + post.pwd + '"}" data-tip="More from this Password" '
 | |
|               + 'target="_blank" class="button button-light right">M+</a>'
 | |
|               + '<a href="#{"ip":"'
 | |
|               + post.host + '"}" data-tip="More from this IP" '
 | |
|               + 'target="_blank" class="button button-light right">M</a>'
 | |
|               + '<span data-cmd="ban-post" class="button button-light right">Ban</span>')
 | |
|               : '')
 | |
|           + (hasFile ?
 | |
|               '<span data-tip="Delete File" data-cmd="pre-del-post" data-fileonly class="button button-light right">File</span>'
 | |
|               : ''
 | |
|             )
 | |
|           + '<span data-cmd="pre-del-post" class="button button-light right">Delete</span>'
 | |
|           + '</div></article>';
 | |
|       }
 | |
|       
 | |
|       cnt = $.el('div');
 | |
|       cnt.className = 'res-cnt';
 | |
|       cnt.innerHTML = html;
 | |
|       
 | |
|       if (partialSel) {
 | |
|         APP.onTogglePostClick($.cls('post-sel', cnt)[0]);
 | |
|       }
 | |
|       
 | |
|       frag.appendChild(cnt);
 | |
|     }
 | |
|     
 | |
|     resCnt.className = 'group-by-' + APP.groupBy;
 | |
|     resCnt.appendChild(frag);
 | |
|   }
 | |
| };
 | |
| 
 | |
| var Feedback = {
 | |
|   messageTimeout: null,
 | |
|   
 | |
|   showMessage: function(msg, type, timeout, onClick) {
 | |
|     var el;
 | |
|     
 | |
|     Feedback.hideMessage();
 | |
|     
 | |
|     el = document.createElement('div');
 | |
|     el.id = 'feedback';
 | |
|     el.title = 'Dismiss';
 | |
|     el.innerHTML = '<span class="feedback feedback-' + type + '">' + msg + '</span>';
 | |
|     
 | |
|     $.on(el, 'click', onClick || Feedback.hideMessage);
 | |
|     
 | |
|     document.body.appendChild(el);
 | |
|     
 | |
|     if (timeout) {
 | |
|       Feedback.messageTimeout = setTimeout(Feedback.hideMessage, timeout);
 | |
|     }
 | |
|   },
 | |
|   
 | |
|   replaceMessage: function(msg) {
 | |
|     var el = $.id('feedback');
 | |
|     
 | |
|     if (el) {
 | |
|       el.firstElementChild.innerHTML = msg;
 | |
|     }
 | |
|   },
 | |
|   
 | |
|   hideMessage: function() {
 | |
|     var el = $.id('feedback');
 | |
|     
 | |
|     if (el) {
 | |
|       if (Feedback.messageTimeout) {
 | |
|         clearTimeout(Feedback.messageTimeout);
 | |
|         Feedback.messageTimeout = null;
 | |
|       }
 | |
|       
 | |
|       $.off(el, 'click', Feedback.hideMessage);
 | |
|       
 | |
|       document.body.removeChild(el);
 | |
|     }
 | |
|   },
 | |
|   
 | |
|   error: function(msg, timeout, onClick) {
 | |
|     if (timeout === undefined) {
 | |
|       timeout = 5000;
 | |
|     }
 | |
|     
 | |
|     Feedback.showMessage(msg, 'error', timeout, onClick);
 | |
|   },
 | |
|   
 | |
|   notify: function(msg, timeout, onClick) {
 | |
|     if (timeout === undefined) {
 | |
|       timeout = 3000;
 | |
|     }
 | |
|     
 | |
|     Feedback.showMessage(msg, 'notify', timeout, onClick);
 | |
|   }
 | |
| };
 | |
| 
 | |
| APP.init();
 |