PttChrome Add-on (Ptt)

new features for PttChrome (show flags features code by osk2/ptt-comment-flag)

目前為 2018-09-27 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         PttChrome Add-on (Ptt)
// @namespace    https://greasyfork.org/zh-TW/scripts/372391-pttchrome-add-on-ptt
// @description  new features for PttChrome (show flags features code by osk2/ptt-comment-flag)
// @version      1.2.6
// @author       avan
// @match        iamchucky.github.io/PttChrome/*
// @require      https://cdnjs.cloudflare.com/ajax/libs/axios/0.18.0/axios.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/tippy.js/2.5.4/tippy.min.js
// @require      https://greasyfork.org/scripts/372458-flags/code/Flags.js?version=630928
// @require      https://openuserjs.org/src/libs/sizzle/GM_config.js
// @grant        GM_getValue
// @grant        GM_setValue
// ==/UserScript==
let configStatus = false;
const gmc = new GM_configStruct({
	'id': 'PttChromeAddOnConfig', // The id used for this instance of GM_config
	'title': 'PttChrome Add-on Settings', // Panel Title
	'fields': { // Fields object
		'isHideAll': {
			'label': '是否隱藏黑名單推文', // Appears next to field
			'type': 'checkbox', // Makes this setting a checkbox input
			'default': false // Default value if user doesn't change it
		},
		'whenHideAllShowInfo': {
			'label': '當隱藏黑名單推文顯示提示訊息', // Appears next to field
			'type': 'text', // Makes this setting a text input
			'size': 35, // Limit length of input (default is 25)
			'default': '<本文作者已被列黑名單>' // Default value if user doesn't change it
		},
		'isHideViewImg': {
			'label': '是否隱藏黑名單圖片預覽', // Appears next to field
			'type': 'checkbox', // Makes this setting a checkbox input
			'default': true // Default value if user doesn't change it
		},
		'isHideViewVideo': {
			'label': '是否隱藏黑名單影片預覽', // Appears next to field
			'type': 'checkbox', // Makes this setting a checkbox input
			'default': true // Default value if user doesn't change it
		},
		'isReduceHeight': {
			'label': '是否調降黑名單推文高度', // Appears next to field
			'type': 'checkbox', // Makes this setting a checkbox input
			'default': true // Default value if user doesn't change it
		},
		'reduceHeight': {
			'label': '設定高度值(單位em)', // Appears next to field
			'type': 'float', // Makes this setting a text input
			'min': 0, // Optional lower range limit
			'max': 10, // Optional upper range limit
			'default': 0.4 // Default value if user doesn't change it
		},
		'isReduceOpacity': {
			'label': '是否調降黑名單推文透明值', // Appears next to field
			'type': 'checkbox', // Makes this setting a checkbox input
			'default': false // Default value if user doesn't change it
		},
		'reduceOpacity': {
			'label': '設定透明值', // Appears next to field
			'type': 'float', // Makes this setting a text input
			'min': 0, // Optional lower range limit
			'max': 10, // Optional upper range limit
			'default': 0.1 // Default value if user doesn't change it
		},
		'isAddFloorNum': {
			'label': '是否顯示推文樓層', // Appears next to field
			'type': 'checkbox', // Makes this setting a checkbox input
			'default': true // Default value if user doesn't change it
		},
		'isShowFlags': {
			'label': '看板內若有IP(ex.Gossiping),是否依IP顯示國旗', // Appears next to field
			'type': 'checkbox', // Makes this setting a checkbox input
			'default': true // Default value if user doesn't change it
		},
	},
	'events': { // Callback functions object
		//'open': function() {
		'open': () => {
			gmc.frame.setAttribute('style', "border: 1px solid #AAA;color: #999;background-color: #111; width: 23em; height: 30em; position: fixed; top: 2.5em; right: 0.5em; z-index: 9999;");
			configStatus = true;
		},
		'close': () => { configStatus = false;},
	},
	'css': `#PttChromeAddOnConfig * { color: #999 !important;background-color: #111 !important; }
body#PttChromeAddOnConfig { background-color: #111}`
});

const HOST = 'https://osk2.me:9977',
	  ipValidation = /(\d{1,3}\.){3}\d{1,3}/,
	  timerArray = [];

let timestamp = Math.floor(Date.now() / 1000);

const execInterval = () => {
	if (timerArray.length === 0) {
		excute();
		timerArray.push(setInterval(excute, 3000));
	}
}
const stopInterval = () => {
	while (timerArray.length > 0) {
		clearInterval(timerArray .shift());
	}
}
const css = (elements, styles) => {
	elements = elements.length ? elements : [elements];
	elements.forEach(element => {
		for (var property in styles) {
			element.style[property] = styles[property];
		}
	});
}
const find = (elements, selectors) => {
	let rtnElements = [];
	elements = elements.length ? elements : [elements];
	elements.forEach(element => rtnElements.push.apply(rtnElements, element.querySelectorAll(selectors)));
	return rtnElements;
}
const show = (elements, specifiedDisplay = 'block') => {
	elements = elements.length ? elements : [elements];
	elements.forEach(element => {
		if (!element.style) return;
		element.style.display = specifiedDisplay;
	});
}
const hide = (elements) => {
	elements = elements.length ? elements : [elements];
	elements.forEach(element => {
		if (!element.style) return;
		element.style.display = 'none';
	});
}
const generateImageHTML = (ip, flag) => {
	if (!flag || !flag.imagePath) return;
	const countryCode = flag.imagePath.toLowerCase().replace('assets/','').replace('.png','');
	const imagePath = flag.imagePath ? `${Flags[countryCode]}` : null;
	const imageTitile = `${flag.locationName || 'N/A'}<br><a href='https://www.google.com/search?q=${ip}' target='_blank'>${ip}</a>`;

	if (!imagePath) {
		return;
	}
	return `<div data-flag title="${imageTitile}" style="background-image: url('${imagePath}');background-repeat:no-repeat;background-position:left;float:right;height:0.8em;width:0.8em;cursor:pointer !important;"></div>`;
};
const excute = async () => {
	//console.log("do excute");
	const currentTS = Math.floor(Date.now() / 1000);
	if ((currentTS - timestamp) > 2) {
		stopInterval();
	}

	let authorNode = document.querySelector('span.q2');
	if (authorNode && authorNode.innerHTML.length > 10) authorNode = authorNode.parentNode;
	else return;

	let blackSpan = document.querySelectorAll('span[style="opacity:0.2"]');
	let whenHideAllShowInfoCss = document.querySelector('#whenHideAllShowInfo');
	if (blackSpan.length > 0) {
		if (gmc.get('isHideAll')) {
			if (gmc.get('whenHideAllShowInfo').length > 0) {
				if (!whenHideAllShowInfoCss) {
					const cssLinkEl = document.createElement('link');
					cssLinkEl.setAttribute('rel', 'stylesheet');
					cssLinkEl.setAttribute('id', 'whenHideAllShowInfo');
					cssLinkEl.setAttribute('type', 'text/css');
					cssLinkEl.setAttribute('href', 'data:text/css;charset=UTF-8,' + encodeURIComponent(`
span[type="bbsrow"][style="opacity:0.2"] {opacity:1 !important;visibility: hidden;}
span[type="bbsrow"][style="opacity:0.2"]:before {
visibility: visible;color: darkred;
content: '                 -            ${gmc.get('whenHideAllShowInfo')}';
}
`));
					document.head.appendChild(cssLinkEl);
				}
			} else {
				if (whenHideAllShowInfoCss) {
					whenHideAllShowInfoCss.remove();
				}
				hide(blackSpan);
			}
		} else {
			if (whenHideAllShowInfoCss) {
				whenHideAllShowInfoCss.remove();
			}
			gmc.get('isHideViewImg') && hide(find(blackSpan, 'img:not([style*="display: none"])'));
			gmc.get('isHideViewVideo') && hide(find(blackSpan, '.easyReadingVideo:not([style*="display: none"])'));
			gmc.get('isReduceHeight') && css(blackSpan, {
				'height': gmc.get('reduceHeight') + 'em',
				'font-size': (gmc.get('reduceHeight')/2) + 'em',
				'line-height': gmc.get('reduceHeight') + 'em'
			});
			gmc.get('isReduceOpacity') && css(blackSpan, {'opacity': gmc.get('reduceOpacity')});
		}
	}

	let authorNodes = document.querySelectorAll('span.q2');
	authorNodes = [].filter.call(authorNodes, (element, index) => {
		return !element.innerHTML.match(/data-flag/) && element.innerHTML.match(ipValidation)
	});
	const authorIpList = authorNodes.map(c => {
		const ip = c.innerHTML.match(ipValidation);
		if (ip) {
			return ip[0];
		}
	});
	if (gmc.get('isShowFlags') && authorIpList.length > 0) {
		const authorFlagsResponse = await axios.post(`${HOST}/ip`, { ip: authorIpList}, {
			headers: {
				'Content-Type': 'application/json',
			}
		}),
			  authorFlags = authorFlagsResponse.data;
		authorNodes.forEach((comment, index) => {
			const ip = comment.innerHTML.match(ipValidation);
			if (!ip || !authorFlags) {
				return;
			}
			const imageHTML = generateImageHTML(ip[0], authorFlags[index]);

			if (!imageHTML) return;
			comment.innerHTML = imageHTML + comment.innerHTML.trim();
			timestamp = Math.floor(Date.now() / 1000);
		});
	}

	const firstEl = (element) => {
		if (!element) return;
		element = element.nextElementSibling;
		if (!element) return;
		if (element.classList.toString().startsWith("blu_")) {
			return element;
		} else {
			return firstEl(element);
		}
	}

	let firstIdx = -1, commentNodes = document.querySelectorAll('span[type="bbsrow"][class^="blu_"]');
	commentNodes = [].filter.call(commentNodes, (element, index) => {
		if ((!element.innerHTML.match(/data-flag/) && !element.innerHTML.match(/data-floor/)) && element === firstEl(authorNode)) firstIdx = index;
		if (firstIdx > -1) return firstIdx <= index;
	});

	let hasIP = false;
	const commentIpList = commentNodes.map(c => {
		const ip = c.innerHTML.match(ipValidation);
		if (ip) {
			hasIP = true;
			return ip[0];
		}
	});
	let commentFlags = null;
	if (gmc.get('isShowFlags') && hasIP && commentIpList.length > 0) {
		const commentFlagsResponse = await axios.post(`${HOST}/ip`, { ip: commentIpList }, {
			headers: {
				'Content-Type': 'application/json',
			}
		});
		commentFlags = commentFlagsResponse.data;
	}

	let currentNum = 1
	commentNodes.forEach((comment, index) => {
		if (gmc.get('isAddFloorNum')) {
			let upstairs = null;
			if (index - 1 >= 0) {
				upstairs = find(commentNodes[index -1], 'div[data-floor]');
				if (upstairs) {
					currentNum = Number(upstairs[0].innerHTML) + 1;
				}
			}
			const divCnt = `<div data-floor style="float:left;margin-left: 2.2%;height: 0em;width: 1.5em;font-size: 0.4em;font-weight:bold;text-align: right;">${currentNum}</div>`;
			comment.innerHTML = divCnt + comment.innerHTML.trim();
		}

		if (!gmc.get('isShowFlags') || !hasIP) return;

		const commentIp = comment.innerHTML.match(ipValidation);
		if (!commentIp || !commentFlags) return;

		const imageHTML = generateImageHTML(commentIp[0], commentFlags[index]);
		if (imageHTML) {
			comment.innerHTML = imageHTML + comment.innerHTML.trim();
			timestamp = Math.floor(Date.now() / 1000);
		}
	});

	tippy('[data-flag]', {
		arrow: true,
		size: 'large',
		placement: 'left',
		interactive: true
	});
};

const CreateMutationObserver = () => {
	const container = document.querySelector('#mainContainer');
	if (!container) {
		setTimeout(CreateMutationObserver, 2000);
		return;
	}
	const observer = new MutationObserver(mutations => {
		mutations.forEach(mutation => {
			const checkNode = document.querySelector('span.q2');
			if (checkNode && checkNode.innerHTML.length > 10) {
				execInterval();
			}
		});
	})
	observer.observe(container, {childList: true,});
}

try {
	window.addEventListener("load", function(event) {
		CreateMutationObserver();
	});
	/* 	(() => {
		MutationObserver();
	})(); */
} catch (ex) {
	console.error(ex);
}

const _button = document.createElement("div");
_button.innerHTML = 'Settings';
_button.onclick = event => {
	if (!configStatus) {
		configStatus = true;
		gmc.open();
	} else if (configStatus) {
		configStatus = false;
		gmc.close();
	}
	event.preventDefault();
	event.stopPropagation();
	return false;
}
_button.style = "border: 1px solid #AAA;color: #999;background-color: #111;position: fixed; top: 0.5em; right: 0.5em; z-index: 9999;cursor:pointer !important;"

document.body.appendChild(_button)

const el = document.createElement('link');
el.rel = 'stylesheet';
el.type = 'text/css';
el.href = "https://cdnjs.cloudflare.com/ajax/libs/tippy.js/2.5.4/tippy.css";
document.head.appendChild(el);