Greasy Fork 支持简体中文。

Pawoo+

Enhance your Pawoo experience with features from Google+

  1. /*
  2.  
  3. Following code belongs to Pawoo+.
  4. Copyright (C) 2018 Jackson Tan
  5. This program is free software: you can redistribute it and/or modify
  6. it under the terms of the GNU General Public License as published by
  7. the Free Software Foundation, either version 3 of the License, or
  8. (at your option) any later version.
  9.  
  10. This program is distributed in the hope that it will be useful,
  11. but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. GNU General Public License for more details.
  14.  
  15. You should have received a copy of the GNU General Public License
  16. along with this program. If not, see <http://www.gnu.org/licenses/>.
  17.  
  18. */
  19.  
  20. // ==UserScript==
  21. // @id PawooPlus
  22. // @name Pawoo+
  23. // @namespace pawoo.plus
  24. // @version 0.1.2
  25. // @description Enhance your Pawoo experience with features from Google+
  26. // @author Jackson Tan
  27. // @match https://pawoo.net/*
  28. // @include https://pawoo.net/*
  29. // @require https://greasyfork.org/scripts/22406-moment-js/code/Momentjs.js?version=142481
  30. // @run-at document-end
  31. // @grant GM_xmlhttpRequest
  32. // ==/UserScript==
  33.  
  34. 'use strict';
  35.  
  36. GM_addStyle = function (css) {
  37. var head = document.getElementsByTagName('head')[0], style = document.createElement('style');
  38. if (!head) { return }
  39. style.type = 'text/css';
  40. try { style.innerHTML = css } catch (x) { style.innerText = css }
  41. head.appendChild(style);
  42. }
  43.  
  44. var styles = '.detailed-comments-container {padding-top: 12px} .detailed-comments a {color: rgba(255,255,255,0.6); text-decoration: none; font-weight: bold;} .status__action-bar-button {display: flex;} .detailed-status__favorites, .detailed-status__reblogs, .detailed-status__replies {display: inline-block;font-weight: 500;font-size: 16px;margin-left: 6px;} .status .icon-button {width: 36.67147px!important;}';
  45.  
  46. function onLoad() {
  47. var host = 'https://pawoo.net';
  48. var statusApi = '/api/v1/statuses/';
  49. var contextApi = '/context';
  50. var favoriteApi = '/favourite';
  51. var unfavoriteApi = '/unfavourite';
  52. var reblogApi = '/reblog';
  53. var unreblogApi = '/unreblog';
  54. var authentication = JSON.parse(document.getElementById('initial-state').innerHTML).meta.access_token;
  55.  
  56. var streamColClass = "item-list";
  57. var streamColSelector = ".item-list";
  58. var postTagName = 'ARTICLE';
  59. var dataIdAttr = 'data-id';
  60. var statusContentSelector = ".status__content";
  61.  
  62. var autoLoadIntervalVal = 30000;
  63.  
  64. moment.locale(document.documentElement.lang);
  65.  
  66. var observer = new MutationObserver(function (mutations) {
  67. mutations.forEach(function (mutation) {
  68. var nodes = Array.prototype.slice.call(mutation.addedNodes);
  69. nodes.forEach(function (node) {
  70. if (node.parentElement && node.parentElement.className === streamColClass) {
  71. batchReplace(node);
  72. } else if (node.parentElement && node.parentElement.tagName === postTagName){
  73. batchReplace(node.parentNode);
  74. }
  75. });
  76. });
  77. });
  78. observer.observe(document.body.querySelectorAll(streamColSelector)[0], {
  79. childList: true,
  80. subtree: true,
  81. attributes: false,
  82. characterData: false,
  83. });
  84.  
  85. var autoLoadInterval = setInterval(function () {
  86. document.getElementsByClassName(streamColClass)[0].childNodes.forEach(function (item) {
  87. batchReplace(item);
  88. })
  89. }, autoLoadIntervalVal);
  90.  
  91. GM_addStyle(styles);
  92.  
  93. function batchReplace(node) {
  94. if (node) {
  95. var dataId = node.getAttribute(dataIdAttr);
  96. getPostStatus(dataId, function (reblogs, favorites) {
  97. if (reblogs !== null && node.querySelector('.fa-retweet')) {
  98. if (node.querySelector('.detailed-status__reblogs__count')) {
  99. var elem = node.querySelector('.detailed-status__reblogs__count');
  100. removeElement(elem);
  101. }
  102. var element = document.createElement('div');
  103. element.setAttribute('class', 'detailed-status__reblogs__count');
  104. element.innerHTML = '<span class="detailed-status__reblogs"><span>' + reblogs + '</span></span>';
  105. node.querySelector('.status__prepend') ? node.querySelectorAll('.fa-retweet')[1].parentNode.appendChild(element) : node.querySelector('.fa-retweet').parentNode.appendChild(element);
  106. }
  107. if (favorites !== null && node.querySelector('.fa-star')) {
  108. if (node.querySelector('.detailed-status__favorites__count')) {
  109. var elem2 = node.querySelector('.detailed-status__favorites__count');
  110. removeElement(elem2);
  111. }
  112. var element2 = document.createElement('div');
  113. element2.setAttribute('class', 'detailed-status__favorites__count');
  114. element2.innerHTML = '<span class="detailed-status__favorites"><span>' + favorites + '</span></span>';
  115. node.querySelector('.fa-star').parentNode.appendChild(element2);
  116. }
  117. }, function (replies) {
  118. if (replies !== null && (node.querySelector('.fa-reply') || node.querySelector('.fa-reply-all'))) {
  119. if (node.querySelector('.detailed-status__replies__count')) {
  120. var elem = node.querySelector('.detailed-status__replies__count');
  121. removeElement(elem);
  122. }
  123. var element = document.createElement('div');
  124. element.setAttribute('class', 'detailed-status__replies__count');
  125. element.innerHTML = '<span class="detailed-status__replies"><span>' + replies + '</span></span>';
  126. node.querySelector('.fa-reply') ? node.querySelector('.fa-reply').parentNode.appendChild(element) : node.querySelector('.fa-reply-all').parentNode.appendChild(element);
  127. }
  128. }, function (posts) {
  129. if (posts) {
  130. var html = '<div class="detailed-comments">';
  131. posts.forEach(function (item, index) {
  132. var favoriteCountHTML = item.favourites_count > 0 ? '<div style="padding-left: 16px;"><span style="color: #db4437;font-weight: bold;">+' + item.favourites_count + '</span></div>' : '';
  133. var postUrl = item.url;
  134. var commentTimeHTML = '<div style="padding-left: 16px;display: flex;flex-direction: column;justify-content: center;"><span style="color: #606984;"><a href=' + postUrl + '>' + moment(item.created_at).fromNow(true) + '</a></span></div>';
  135. var element = document.createElement('div');
  136. element.innerHTML = item.content;
  137. var contentHTML = processDeleteLine(element.childNodes[0], true);
  138. var postId = item.id;
  139. var favorited = item.favourited;
  140. var favoriteHTML = favorited ? '<div class="detailed-comment-unfavorite-button" style="padding-left: 16px; cursor: pointer;"><span style="color: #db4437;font-weight: bold;">+1</span></div>' : '<div class="detailed-comment-favorite-button" style="padding-left: 16px; cursor: pointer;"><span style="color: rgba(255,255,255,0.6);font-weight: bold;">+1</span></div>';
  141. var reblogged = item.reblogged;
  142. var reblogHTML = reblogged ? '<div class="detailed-comment-unreblog-button" style="padding-left: 16px; cursor: pointer;"><span style="color: #2b90d9;font-weight: bold;">Reshared</span></div>' : '<div class="detailed-comment-reblog-button" style="padding-left: 16px; cursor: pointer;"><span style="color: rgba(255,255,255,0.6);font-weight: bold;">Reshare</span></div>';
  143. html += '<div class="detailed-comment" data-id="' + postId + '"><div style="display: flex;"><div><img src="' + item.account.avatar + '" height=24 width=24 style="border-radius: 50%;"></div><div style="display: flex;flex-direction: row;justify-content: center;padding-left: 6px;"><span style="display: flex;flex-direction: column;justify-content: center;"><a href="' + item.account.url + '">' + item.account.display_name + '</a></span><span class="detailed-comment-username" style="display: flex;flex-direction: column;justify-content: center;color: #606984;padding-left: 4px;">@' + item.account.username + '</span>' + commentTimeHTML + '</div></div><div style="padding-left: 28px;display:flex;"><div>' + contentHTML + '</div>' + favoriteCountHTML + '</div><div style="display: flex; padding-top: 2px;"><span class="reply_inline_comment_button" style="cursor: pointer; padding-left: 28px; color: rgba(255,255,255,0.6);">Reply</span>' + reblogHTML + favoriteHTML + '</div><div class="reply_inline_comment_edit_content" style="display: none; padding-left: 28px; flex-direction: column;"><textarea class="reply_inline_comment_textarea" style="resize: none; background: transparent; color: #FFF;" placeholder="コメントを追加..." aria-label="コメントを追加..." role="textbox"></textarea><div style="display: flex; flex-direction: row;"><div class="reply_inline_comment_submit_button" style="padding: 8px; cursor: pointer;">Submit</div><div class="reply_inline_comment_cancel_button" style="padding: 8px; cursor: pointer;">Cancel</div></div></div></div>'
  144. })
  145. html += '</div>'
  146. if (node.querySelector('.status')) {
  147. if (node.querySelector('.detailed-comments-container')) {
  148. var elem = node.querySelector('.detailed-comments-container');
  149. removeElement(elem);
  150. }
  151. var element = document.createElement('div');
  152. element.setAttribute('class', 'detailed-comments-container');
  153. element.innerHTML = html;
  154. element.querySelectorAll('.detailed-comment') !== null && element.querySelectorAll('.detailed-comment').forEach(function (item, index) {
  155. item.querySelector('.reply_inline_comment_button') !== null && item.querySelector('.reply_inline_comment_button').addEventListener('click', function (e) {
  156. var username = e.target.parentNode.parentNode.querySelector('.detailed-comment-username') !== null ? e.target.parentNode.parentNode.querySelector('.detailed-comment-username').textContent + ' ' : '';
  157. if (e.target.parentNode.parentNode.querySelector('.reply_inline_comment_textarea') !== null) {
  158. e.target.parentNode.parentNode.querySelector('.reply_inline_comment_textarea').value = username;
  159. }
  160. clearInterval(autoLoadInterval);
  161. if (e.target.parentNode.parentNode.querySelector('.reply_inline_comment_edit_content') !== null) {
  162. e.target.parentNode.parentNode.querySelector('.reply_inline_comment_edit_content').style.display = 'flex';
  163. }
  164. })
  165. item.querySelector('.reply_inline_comment_submit_button') !== null && item.querySelector('.reply_inline_comment_submit_button').addEventListener('click', function (e) {
  166. var status = e.target.parentNode.parentNode.querySelector('.reply_inline_comment_textarea').value;
  167. var replyToId = e.target.parentNode.parentNode.parentNode.getAttribute('data-id');
  168. if (status.trim() !== '') {
  169. replyPost(status, replyToId, batchReplace, node);
  170. if (e.target.parentNode.parentNode.querySelector('.reply_inline_comment_textarea') !== null) {
  171. e.target.parentNode.parentNode.querySelector('.reply_inline_comment_textarea').value = '';
  172. }
  173. if (e.target.parentNode.parentNode.parentNode.querySelector('.reply_inline_comment_edit_content') !== null) {
  174. e.target.parentNode.parentNode.parentNode.querySelector('.reply_inline_comment_edit_content').style.display = 'none';
  175. }
  176. setNewInterval();
  177. }
  178. })
  179. item.querySelector('.reply_inline_comment_cancel_button') !== null && item.querySelector('.reply_inline_comment_cancel_button').addEventListener('click', function (e) {
  180. if (e.target.parentNode.parentNode.querySelector('.reply_inline_comment_textarea') !== null) {
  181. e.target.parentNode.parentNode.querySelector('.reply_inline_comment_textarea').value = '';
  182. }
  183. if (e.target.parentNode.parentNode.parentNode.querySelector('.reply_inline_comment_edit_content') !== null) {
  184. e.target.parentNode.parentNode.parentNode.querySelector('.reply_inline_comment_edit_content').style.display = 'none';
  185. }
  186. setNewInterval();
  187. })
  188. item.querySelector('.detailed-comment-favorite-button') !== null && item.querySelector('.detailed-comment-favorite-button').addEventListener('click', function (e) {
  189. var replyToId = e.target.parentNode.parentNode.parentNode.getAttribute('data-id');
  190. favoritePost(replyToId, batchReplace, node);
  191. })
  192. item.querySelector('.detailed-comment-unfavorite-button') !== null && item.querySelector('.detailed-comment-unfavorite-button').addEventListener('click', function (e) {
  193. var replyToId = e.target.parentNode.parentNode.parentNode.getAttribute('data-id');
  194. unfavoritePost(replyToId, batchReplace, node);
  195. })
  196. item.querySelector('.detailed-comment-reblog-button') !== null && item.querySelector('.detailed-comment-reblog-button').addEventListener('click', function (e) {
  197. var replyToId = e.target.parentNode.parentNode.parentNode.getAttribute('data-id');
  198. reblogPost(replyToId, batchReplace, node);
  199. })
  200. item.querySelector('.detailed-comment-unreblog-button') !== null && item.querySelector('.detailed-comment-unreblog-button').addEventListener('click', function (e) {
  201. var replyToId = e.target.parentNode.parentNode.parentNode.getAttribute('data-id');
  202. unreblogPost(replyToId, batchReplace, node);
  203. })
  204. })
  205. node.querySelector('.status').appendChild(element);
  206. }
  207. }
  208. })
  209. processDeleteLine(node, false);
  210. }
  211. }
  212.  
  213. function processDeleteLine(node, isComment) {
  214. var stack = [];
  215. var result = [];
  216. var commentHTML = '';
  217. var textNode = isComment ? node.childNodes : (node.querySelector(statusContentSelector) !== null ? node.querySelector(statusContentSelector).childNodes : null);
  218. textNode !== null && textNode.forEach(function (item, index) {
  219. var matches = item.textContent.match(/(-)/mgi);
  220. if (matches && matches.length > 1) {
  221. var indicies = charLocations('-', item.textContent);
  222. if (indicies.length > 1) {
  223. var newText = item.textContent;
  224. var charsToReplace = indicies.length % 2 === 0 ? indicies.length : indicies.length - 1;
  225. for (var i = 0; i < charsToReplace; i++) {
  226. newText = i % 2 === 0 ? newText.replace('-', '<del>') : newText.replace('-', '</del>');
  227. }
  228. var element = document.createElement('span');
  229. element.innerHTML = newText;
  230. if (isComment) {
  231. commentHTML += newText;
  232. } else {
  233. item.parentNode.insertBefore(element, item);
  234. removeElement(item);
  235. }
  236. }
  237. }
  238. else {
  239. if (item.nodeType === 3 && node.textContent.match('-') !== null) {
  240. if (stack.length > 0) {
  241. var start = stack.pop();
  242. result.push({ start, end: index });
  243. } else {
  244. stack.push(index)
  245. }
  246. }
  247. if (item.tagName === 'BR') {
  248. stack = [];
  249. } else if (isComment) {
  250. commentHTML += item.outerHTML ? item.outerHTML : item.textContent
  251. }
  252. }
  253. })
  254.  
  255. result.length > 0 && result.forEach(function (item, index) {
  256. var startIndex = item.start;
  257. var endIndex = item.end;
  258. for (var i = startIndex; i <= endIndex; i++) {
  259. var currentNode = isComment ? node.childNodes[i] : node.querySelector(statusContentSelector).childNodes[i];
  260. if (currentNode.nodeType === 3) {
  261. var element = document.createElement('span');
  262. var newText;
  263. if (i === endIndex) {
  264. newText = currentNode.textContent.replace('-', '</del>');
  265. element.innerHTML = '<del>' + newText;
  266. } else {
  267. newText = currentNode.textContent.replace('-', '<del>');
  268. element.innerHTML = newText + '</del>';
  269. }
  270. if (isComment) {
  271. commentHTML += newText;
  272. } else {
  273. currentNode.parentNode.insertBefore(element, currentNode);
  274. removeElement(currentNode);
  275. }
  276. } else {
  277. var element2 = document.createElement('span');
  278. element2.innerHTML = '<del>' + currentNode.innerHTML + '</del>';
  279. if (isComment) {
  280. commentHTML += newText;
  281. } else {
  282. currentNode.parentNode.insertBefore(element2, currentNode);
  283. removeElement(currentNode);
  284. }
  285. }
  286. }
  287. })
  288. if (isComment) {
  289. return commentHTML !== '' ? commentHTML : node.innerHTML;
  290. }
  291. }
  292.  
  293. function charLocations(substring, string) {
  294. var a = [], i = -1;
  295. while ((i = string.indexOf(substring, i + 1)) >= 0) a.push(i);
  296. return a;
  297. }
  298.  
  299. function removeElement(element) {
  300. element.parentNode.removeChild(element);
  301. }
  302.  
  303. function uuidv4() {
  304. return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
  305. var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
  306. return v.toString(16);
  307. });
  308. }
  309.  
  310. function setNewInterval() {
  311. autoLoadInterval = setInterval(function () {
  312. document.getElementsByClassName(streamColClass)[0].childNodes.forEach(function (item) {
  313. batchReplace(item);
  314. })
  315. }, autoLoadIntervalVal);
  316. }
  317.  
  318. function getPostStatus(id, statsProcessor0, statsProcessor1, postProcessor) {
  319. GM_xmlhttpRequest({
  320. method: "GET",
  321. url: host + statusApi + id,
  322. headers: { "Accept": "application/json" },
  323. onload: function (response) {
  324. var data = JSON.parse(response.responseText);
  325. var reblogs = data.reblogs_count ? data.reblogs_count : 0;
  326. var favorites = data.favourites_count ? data.favourites_count : 0;
  327. statsProcessor0(reblogs, favorites);
  328. },
  329. });
  330. GM_xmlhttpRequest({
  331. method: "GET",
  332. url: host + statusApi + id + contextApi,
  333. headers: { "Accept": "application/json" },
  334. onload: function (response) {
  335. var data = JSON.parse(response.responseText);
  336. var replies = data.descendants ? data.descendants.length : 0;
  337. var posts = data.descendants;
  338. statsProcessor1(replies);
  339. postProcessor(posts);
  340. },
  341. });
  342. }
  343.  
  344. function replyPost(status, replyToId, cb, node) {
  345. var statusData = new FormData();
  346. statusData.append("status", status);
  347. statusData.append("in_reply_to_id", replyToId);
  348. statusData.append("media_ids", []);
  349. statusData.append("sensitive", false);
  350. statusData.append("spoiler_text", "");
  351. statusData.append("visibility", "public");
  352.  
  353. GM_xmlhttpRequest({
  354. method: "POST",
  355. data: statusData,
  356. url: host + statusApi,
  357. headers: { "idempotency-key": uuidv4, "authorization": "Bearer " + authentication },
  358. onload: function (res) {
  359. cb(node);
  360. }
  361. });
  362. }
  363.  
  364. function favoritePost(postId, cb, node) {
  365. GM_xmlhttpRequest({
  366. method: "POST",
  367. url: host + statusApi + postId + favoriteApi,
  368. headers: { "authorization": "Bearer " + authentication },
  369. onload: function (res) {
  370. cb(node);
  371. }
  372. });
  373. }
  374.  
  375. function unfavoritePost(postId, cb, node) {
  376. GM_xmlhttpRequest({
  377. method: "POST",
  378. url: host + statusApi + postId + unfavoriteApi,
  379. headers: { "authorization": "Bearer " + authentication },
  380. onload: function (res) {
  381. cb(node);
  382. }
  383. });
  384. }
  385.  
  386. function reblogPost(postId, cb, node) {
  387. GM_xmlhttpRequest({
  388. method: "POST",
  389. url: host + statusApi + postId + reblogApi,
  390. headers: { "authorization": "Bearer " + authentication },
  391. onload: function (res) {
  392. cb(node);
  393. }
  394. });
  395. }
  396.  
  397. function unreblogPost(postId, cb, node) {
  398. GM_xmlhttpRequest({
  399. method: "POST",
  400. url: host + statusApi + postId + unreblogApi,
  401. headers: { "authorization": "Bearer " + authentication },
  402. onload: function (res) {
  403. cb(node);
  404. }
  405. });
  406. }
  407. window.onpopstate = history.onpushstate = function(e) {
  408. observer.observe(document.body.querySelectorAll(streamColSelector)[0], {
  409. childList: true,
  410. subtree: true,
  411. attributes: false,
  412. characterData: false,
  413. });
  414. }
  415. }
  416.  
  417. window.addEventListener('load', onLoad, false);