// ==UserScript==
// @name PttChrome+term.ptt.cc Add-on
// @license MIT
// @namespace https://greasyfork.org/zh-TW/scripts/372391-pttchrome-add-on-ptt
// @description new features for PttChrome+term.ptt.cc (show flags features code by osk2/ptt-comment-flag)
// @version 1.7.1
// @author avan
// @match https://iamchucky.github.io/PttChrome/*
// @match term.ptt.cc/*
// @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://cdnjs.cloudflare.com/ajax/libs/lz-string/1.4.4/lz-string.min.js
// @require https://update.greasyfork.icu/scripts/480183/1282331/GM_config_sync.js
// @require https://greasyfork.org/scripts/372760-gm-config-lz-string/code/GM_config_lz-string.js?version=634230
// @require https://greasyfork.org/scripts/372675-flags-css/code/Flags-CSS.js?version=632757
// @grant GM_getValue
// @grant GM_setValue
// @grant unsafeWindow
// @downloadURL https://update.greasyfork.icu/scripts/372391/PttChrome%2Btermpttcc%20Add-on.user.js
// @updateURL https://update.greasyfork.icu/scripts/372391/PttChrome%2Btermpttcc%20Add-on.meta.js
// ==/UserScript==
"use strict";
//===================================
const pageUrl = window.location.href;
const isTerm = pageUrl.match(/term.ptt.cc/);
let configStatus = false, configBlackStatus = false, flagMap = {};
let fields = { // Fields object
'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
},
'whenShowFlagsIgnoreSpecificCountrys': {
'label': '指定國家不顯示 ex.「tw;jp」(ISO 3166-1 alpha-2)', // 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
},
'isHndleAuthor': {
'label': '是否合併相同作者連續留言的ID名稱', // Appears next to field
'type': 'checkbox', // Makes this setting a checkbox input
'default': false // Default value if user doesn't change it
},
'isShowDebug': {
'label': '是否顯示DeBug紀錄', // Appears next to field
'type': 'checkbox', // Makes this setting a checkbox input
'default': false // Default value if user doesn't change it
},
};
if (isTerm) {
fields = Object.assign({
'isAutoLogin': {
'label': '是否自動登入', // Appears next to field
'type': 'checkbox', // Makes this setting a checkbox input
'default': false // Default value if user doesn't change it
},
'autoUser': {
'label': '帳號', // Appears next to field
'type': 'text', // Makes this setting a text input
'size': 25, // Limit length of input (default is 25)
'default': '' // Default value if user doesn't change it
},
'autoPassWord': {
'label': '密碼', // Appears next to field
'type': 'password', // Makes this setting a text input
'size': 25, // Limit length of input (default is 25)
'default': '' // Default value if user doesn't change it
},
'isAutoSkipInfo1': {
'label': '是否自動跳過登入後歡迎畫面', // Appears next to field
'type': 'checkbox', // Makes this setting a checkbox input
'default': false // Default value if user doesn't change it
},
'isAutoToFavorite': {
'label': '是否自動進入 Favorite 我的最愛', // Appears next to field
'type': 'checkbox', // Makes this setting a checkbox input
'default': false // Default value if user doesn't change it
},
'isEnableDeleteDupLogin': {
'label': '當被問到是否刪除其他重複登入的連線,回答:', // Appears next to field
'type': 'select', // Makes this setting a dropdown
'options': ['N/A', 'Y', 'N'], // Possible choices
'default': 'N/A' // Default value if user doesn't change it
},
'Button': {
'label': '編輯黑名單', // Appears on the button
'type': 'button', // Makes this setting a button input
'size': 100, // Control the size of the button (default is 25)
'click': function() { // Function to call when button is clicked
if (configBlackStatus) gmcBlack.close();
else if (!configBlackStatus) gmcBlack.open();
}
},
'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
},
"previewPbsTwimg": {
'label': '是否預覽推特圖片',
'type': 'checkbox',
'default': true
},
"previewMeeeimg": {
'label': '是否預覽Meee圖片',
'type': 'checkbox',
'default': true
},
"previewYoutube": {
'label': '是否預覽Youtube影片',
'type': 'checkbox',
'default': true
},
/*
'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
},
'whenHideAllShowInfoColor': {
'label': '上述提示訊息之顏色', // Appears next to field
'type': 'text', // Makes this setting a text input
'class':'jscolor',
'data-jscolor': '{hash:true}',
'size': 10, // Limit length of input (default is 25)
'default': '#c0c0c0' // 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
'size': 10, // Limit length of input (default is 25)
'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': 1, // Optional upper range limit
'size': 10, // Limit length of input (default is 25)
'default': 0.05 // Default value if user doesn't change it
},
'isDisableClosePrompt': {
'label': '是否停用關閉頁面提示', // Appears next to field
'type': 'checkbox', // Makes this setting a checkbox input
'default': true // Default value if user doesn't change it
},
*/
}, fields);
} else {
fields = Object.assign({
'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
},
'whenHideAllShowInfoColor': {
'label': '上述提示訊息之顏色', // Appears next to field
'type': 'text', // Makes this setting a text input
'class':'jscolor',
'data-jscolor': '{hash:true}',
'size': 10, // Limit length of input (default is 25)
'default': '#c0c0c0' // 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': false // 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
'size': 10, // Limit length of input (default is 25)
'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': 1, // Optional upper range limit
'size': 10, // Limit length of input (default is 25)
'default': 0.05 // Default value if user doesn't change it
},
'isAutoGotoAIDPage': {
'label': '是否自動跳至AID文章,而非開啟www.ptt.cc網站', // Appears next to field
'type': 'checkbox', // Makes this setting a checkbox input
'default': true // Default value if user doesn't change it
},
}, fields);
};
// Convert number to it's base-64 representation, and reverse it. https://gist.github.com/meeDamian/5749143
const Number64 = {
_rixits: "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_",
toHash: (n) => {
if(isNaN(Number(n)) || n === null || n === Number.POSITIVE_INFINITY || n < 0 ) throw "The input(" + n + ") is not valid";
n = Math.floor(n);
let result = '';
do result = Number64._rixits.charAt(n%64) + result;
while(n = Math.floor(n/64));
return result; // String
},
toNumber: (h) => {
let result = 0;
for(let i = 0; i < h.length; i++) result = (result*64) + Number64._rixits.indexOf(h.charAt(i));
return result; // Integer
}
}
//M.timestamp.A.random{0xfff}
//https://www.ptt.cc/man/C_Chat/DE98/DFF5/DB61/M.1419434423.A.DF0.html
unsafeWindow.AID = {
patternEncode: (name) => {
const pattern = /([\w-]*)\/{0,1}M\.(\d+)\.A\.(\w{3})/g;
const match = pattern.exec(name); //['GroupName/M.1234567890.A.DEF', 'GroupName', '1234567890', 'DEF', index: 23, input:...]
return match;
},
patternDecode: (aid) => {
const pattern = /(?;])#([\w-]{6})([\w-]{2})(?: *\(([\w-]+)\)){0,1}/g;
const match = pattern.exec(aid); //['#19bWBItl (GroupName)', '19bWBI', 'tl', 'GroupName', index: 0, input: '#19bWBItl (GroupName)']
return match;
},
patternDecodeAll: (text) => {
const pattern = /(?;])#([\w-]{6})([\w-]{2})(?: *\(([\w-]+)\)){0,1}/g;
const matchs = [...new Set(text.matchAll(pattern))]//[...text.matchAll(pattern)]; //matchAll compatibility support in Chrome 73
return matchs;
},
encode: (name) => {
const match = unsafeWindow.AID.patternEncode(name);
if (!match || match.length < 4) throw "The input(" + name + ") is not valid";
const hash1 = Number64.toHash(match[2])
const hash2 = Number64.toHash(parseInt("0x" + match[3]))
let result = "#" + hash1 + hash2;
if (match[1].length > 0) result += " (" + match[1] + ")"
return result;
},
decode: (decodeMatch, defaultGroup) => {
if (!decodeMatch || decodeMatch.length < 3) throw "The input is not valid";
const timestamp = Number64.toNumber(decodeMatch[1]);
const random = Number64.toNumber(decodeMatch[2]).toString(16).toUpperCase().padStart(3, '0');
let result = "M." + timestamp + ".A." + random;
if (decodeMatch[3] && decodeMatch[3].length > 0) defaultGroup = decodeMatch[3];
result = "https://www.ptt.cc/bbs/" + defaultGroup + "/" + result + ".html";
return result;
},
goto: (aid) => {
if (isTerm) return
if (unsafeWindow.pttchrome.app.view.bbscore.buf.pageState === 3) unsafeWindow.pttchrome.app.conn.send("[D[D")
if (unsafeWindow.pttchrome.app.view.bbscore.buf.pageState === 5) unsafeWindow.pttchrome.app.conn.send("\r")
unsafeWindow.pttchrome.app.buf.cancelPageDownAndResetPrevPageState()
const aidArr = aid.split(" ", 2)
if (aidArr.length > 1) {
aid = aidArr[0]
unsafeWindow.pttchrome.app.conn.send("s")
unsafeWindow.pttchrome.app.conn.send(aidArr[1] + "\r\r[1~")
}
unsafeWindow.pttchrome.app.buf.cancelPageDownAndResetPrevPageState()
if (unsafeWindow.pttchrome.app.view.bbscore.buf.BBSWin.innerText.includes("【看板列表】")) return
unsafeWindow.pttchrome.app.conn.send(aid + "\r\r[1~")
}
}
const queryConfigEl = (configSelectors, selectors, callback) => {
let configEl = document.querySelector(configSelectors);
if (!configEl) {
setTimeout(queryConfigEl.bind(null, configSelectors, selectors, callback), 1000);
return;
}
configEl = configEl.contentWindow.document.querySelector(selectors);
if (!configEl) {
setTimeout(queryConfigEl.bind(null, configSelectors, selectors, callback), 1000);
return;
}
callback(configEl);
};
const addCssLink = (id, cssStr) => {
let checkEl = document.querySelector(`#${id}`);
if (checkEl) {
checkEl.remove();
}
const cssLinkEl = document.createElement('link');
cssLinkEl.setAttribute('rel', 'stylesheet');
cssLinkEl.setAttribute('id', id);
cssLinkEl.setAttribute('type', 'text/css');
cssLinkEl.setAttribute('href', 'data:text/css;charset=UTF-8,' + encodeURIComponent(cssStr));
document.head.appendChild(cssLinkEl);
};
const gmc = new ConfigLzString({
'id': 'PttChromeAddOnConfig', // The id used for this instance of GM_config
'title': 'PttChrome Add-on Settings', // Panel Title
'fields': fields,
'events': { // Callback functions object
'open': function() {
this.frame.setAttribute('style', "border: 1px solid #AAA;color: #999;background-color: #111; width: 23em; height: 35em; position: fixed; top: 2.5em; right: 0.5em; z-index: 900;");
configStatus = true;
},
'close': () => { configStatus = false;},
},
'css': `#PttChromeAddOnConfig * { color: #999 !important;background-color: #111 !important; } body#PttChromeAddOnConfig { background-color: #111}`,
'src':`https://cdnjs.cloudflare.com/ajax/libs/jscolor/2.0.4/jscolor.js`,
});
const gmcDebug = new ConfigLzString({
'id': 'PttChromeAddOnConfigDebug', // The id used for this instance of GM_config
'title': 'PttChrome Add-on DeBugLog', // Panel Title
'fields': { // Fields object
'showLog': {
'label': 'Show log of debug text',
'type': 'textarea',
'default': ''
},
},
'events': { // Callback functions object
'open': () => {
gmcDebug.frame.setAttribute('style', "border: 1px solid #AAA;color: #999;background-color: #111; width: 26em; height: 35em; position: fixed; top: 2.5em; left: 0.5em; z-index: 900;");
},
},
'css': `#PttChromeAddOnConfigDebug * { color: #999 !important;background-color: #111 !important; } body#PttChromeAddOnConfigDebug { background-color: #111} #PttChromeAddOnConfigDebug_field_showLog { width:26em; height: 24em;}`
});
const addBlackStyle = (blackList) => {
if (blackList && blackList.trim().length === 0) return;
blackList = blackList.replace(/\n$/g, '').replace(/\n\n/g, '\n');
let opacityStyle = blackList.replace(/([^\n]+)/g, '.blu_$1').replace(/\n/g, ',');
addCssLink('opacityStyle', `${opacityStyle} {opacity: 0.2;}`);
if (gmc.get('isHideViewImg')) {
let imgStyle = blackList.replace(/([^\n]+)/g, '.blu_$1 + div > .easyReadingImg').replace(/\n/g, ',');
addCssLink('imgStyle', `${imgStyle} {display: none;}`);
}
if (gmc.get('isHideViewVideo')) {
let videoStyle = blackList.replace(/([^\n]+)/g, '.blu_$1 + div > .easyReadingVideo').replace(/\n/g, ',');
addCssLink('videoStyle', `${videoStyle} {display: none;}`);
}
}
const gmcBlack = new ConfigLzString({
'id': 'PttChromeAddOnConfigBlack', // The id used for this instance of GM_config
'title': 'PttChrome Add-on Black List', // Panel Title
'fields': { // Fields object
'blackList': {
'label': 'Black List',
'type': 'textarea',
'default': ''
},
},
'events': { // Callback functions object
'init': function() {
addBlackStyle(this.get('blackList'));
},
'open': function() {
gmcBlack.frame.setAttribute('style', "border: 1px solid #AAA;color: #999;background-color: #111; width: 26em; height: 35em; position: fixed; top: 2.5em; left: 0.5em; z-index: 900;");
configBlackStatus = true;
},
'save': function() {
addBlackStyle(this.get('blackList'));
},
'close': function() { configBlackStatus = false;},
},
'css': `#PttChromeAddOnConfigBlack * { color: #999 !important;background-color: #111 !important; } body#PttChromeAddOnConfigBlack { background-color: #111} #PttChromeAddOnConfigBlack_field_blackList { width:26em; height: 24em;}`
});
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) {
timerArray.push(setInterval(excute, 1000));
}
}
const stopInterval = () => {
while (timerArray.length > 0) {
clearInterval(timerArray .shift());
}
}
let currentNum, currentPage, pageData = {}, currentGroup;
const excute = async () => {
//console.log("do excute");
const css = (elements, styles) => {
elements = elements.length ? elements : [elements];
elements.forEach(element => {
for (var property in styles) {
element.style[property] = styles[property];
}
});
}
const findAll = (elements, selectors) => {
let rtnElements = [];
elements = elements.length ? elements : [elements];
elements.forEach(element => rtnElements.push.apply(rtnElements, element.querySelectorAll(selectors)));
return rtnElements;
}
const innerHTMLAll = (elements) => {
let rtn = "";
elements = elements.length ? elements : [elements];
elements.forEach(element => {element.innerHTML ? rtn += element.innerHTML : ""});
return rtn;
}
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) return;
flag.countryCode = flag.countryCode ? flag.countryCode : "unknown";
const ignoreCountrys = gmc.get('whenShowFlagsIgnoreSpecificCountrys').match(new RegExp(flag.countryCode, 'i'));
if (ignoreCountrys && ignoreCountrys.length > 0) return;
const imageTitile = `${flag.locationName || 'N/A'}
${ip}`;
return `