Greasy Fork tweaks

opens pages of scripts from lists in a new tab and makes the user interface more compact, informative and interactive

当前为 2020-06-02 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Greasy Fork tweaks
  3. // @namespace almaceleste
  4. // @version 0.5.1
  5. // @description opens pages of scripts from lists in a new tab and makes the user interface more compact, informative and interactive
  6. // @description:ru открывает страницы скриптов из списков в новой вкладке и делает пользовательский интерфейс более компактным, информативным и интерактивным
  7. // @author (ɔ) almaceleste (https://almaceleste.github.io)
  8. // @license AGPL-3.0-or-later; http://www.gnu.org/licenses/agpl.txt
  9. // @icon https://greasyfork.org/assets/blacklogo16-bc64b9f7afdc9be4cbfa58bdd5fc2e5c098ad4bca3ad513a27b15602083fd5bc.png
  10. // @icon64 https://greasyfork.org/assets/blacklogo96-e0c2c76180916332b7516ad47e1e206b42d131d36ff4afe98da3b1ba61fd5d6c.png
  11.  
  12. // @homepageURL https://greasyfork.org/en/users/174037-almaceleste
  13. // @homepageURL https://openuserjs.org/users/almaceleste
  14. // @homepageURL https://github.com/almaceleste/userscripts
  15. // @supportURL https://github.com/almaceleste/userscripts/issues
  16.  
  17. // @require https://code.jquery.com/jquery-3.3.1.js
  18. // @require https://code.jquery.com/ui/1.12.1/jquery-ui.js
  19. // @require https://openuserjs.org/src/libs/sizzle/GM_config.js
  20. // @grant GM_getValue
  21. // @grant GM_setValue
  22. // @grant GM_registerMenuCommand
  23. // @grant GM_openInTab
  24. // @grant GM_getResourceText
  25.  
  26. // @resource css https://github.com/almaceleste/userscripts/raw/master/css/default.css
  27.  
  28. // @match https://greasyfork.org/*/users/*
  29. // @match https://greasyfork.org/*/scripts*
  30. // ==/UserScript==
  31.  
  32. // ==OpenUserJS==
  33. // @author almaceleste
  34. // ==/OpenUserJS==
  35.  
  36. const route = {};
  37. route.userpage = /^\/.*\/users\/.*/;
  38. route.scriptpage = /^\/.*\/scripts\/.*/;
  39. route.searchpage = /^\/.*\/scripts$/;
  40.  
  41.  
  42. const listitem = '.script-list > li';
  43. const separator = '.name-description-separator';
  44. const scriptversion = 'data-script-version';
  45. const scriptrating = 'dd.script-list-ratings';
  46. const scriptstats = '.inline-script-stats';
  47. const dailyinstalls = '.script-list-daily-installs';
  48. const totalinstalls = '.script-list-total-installs';
  49. const createddate = '.script-list-created-date';
  50. const updateddate = '.script-list-updated-date';
  51.  
  52. const scripturl = 'article h2 a';
  53.  
  54. const userprofile = {};
  55. userprofile.path = '#user-profile';
  56. userprofile.header = 'body > div.width-constraint > section:first-child > h2';
  57.  
  58. const sections = {};
  59. sections.controlpanel = '#control-panel';
  60. sections.discussions = '#user-discussions-on-scripts-written';
  61. sections.scriptsets = 'section:has(h3:contains("Script Sets"))';
  62.  
  63. const configId = 'greasyforktweaksCfg';
  64. const iconUrl = GM_info.script.icon64;
  65. const pattern = {};
  66. pattern[`#${configId}`] = /#configId/g;
  67. pattern[`${iconUrl}`] = /iconUrl/g;
  68.  
  69. let css = GM_getResourceText('css');
  70. Object.keys(pattern).forEach((key) => {
  71. css = css.replace(pattern[key], key);
  72. });
  73. const windowcss = css;
  74. const iframecss = `
  75. height: 530px;
  76. width: 435px;
  77. border: 1px solid;
  78. border-radius: 3px;
  79. position: fixed;
  80. z-index: 9999;
  81. `;
  82.  
  83. GM_registerMenuCommand(`${GM_info.script.name} Settings`, () => {
  84. GM_config.open();
  85. GM_config.frame.style = iframecss;
  86. });
  87.  
  88. GM_config.init({
  89. id: `${configId}`,
  90. title: `${GM_info.script.name} ${GM_info.script.version}`,
  91. fields: {
  92. version: {
  93. section: ['', 'Script list options (own and other pages)'],
  94. label: 'add script version number in the list of scripts',
  95. labelPos: 'right',
  96. type: 'checkbox',
  97. default: true,
  98. },
  99. ratingscore: {
  100. label: 'display script rating score',
  101. labelPos: 'right',
  102. type: 'checkbox',
  103. default: true,
  104. },
  105. updates: {
  106. label: 'display update checks information',
  107. labelPos: 'right',
  108. type: 'checkbox',
  109. default: true,
  110. },
  111. updatesperiods: {
  112. type: 'multiselect',
  113. options: {
  114. daily: 1,
  115. weekly: 7,
  116. monthly: 30,
  117. total: 0
  118. },
  119. default: {daily: 1, weekly: 7},
  120. },
  121. compact: {
  122. label: 'compact script information',
  123. labelPos: 'right',
  124. type: 'checkbox',
  125. default: true,
  126. },
  127. userprofile: {
  128. section: ['', 'User page options (own page and other users`)'],
  129. label: 'collapse user profile info on user page',
  130. labelPos: 'right',
  131. type: 'checkbox',
  132. default: true,
  133. },
  134. controlpanel: {
  135. label: 'collapse control panel on user page',
  136. labelPos: 'right',
  137. type: 'checkbox',
  138. default: true,
  139. },
  140. discussions: {
  141. label: 'collapse discussions on user page',
  142. labelPos: 'right',
  143. type: 'checkbox',
  144. default: true,
  145. },
  146. scriptsets: {
  147. label: 'collapse script sets on user page',
  148. labelPos: 'right',
  149. type: 'checkbox',
  150. default: true,
  151. },
  152. newtab: {
  153. section: ['', 'Other options'],
  154. label: 'open script page in new tab',
  155. labelPos: 'right',
  156. type: 'checkbox',
  157. default: true,
  158. },
  159. background: {
  160. label: 'open new tab in background',
  161. labelPos: 'right',
  162. type: 'checkbox',
  163. default: false,
  164. },
  165. insert: {
  166. label: 'insert new tab next to the current instead of the right end',
  167. labelPos: 'right',
  168. type: 'checkbox',
  169. default: true,
  170. },
  171. setParent: {
  172. label: 'return to the current tab after new tab closed',
  173. labelPos: 'right',
  174. type: 'checkbox',
  175. default: true,
  176. },
  177. support: {
  178. section: ['', 'Support'],
  179. label: 'almaceleste.github.io',
  180. title: 'more info on almaceleste.github.io',
  181. type: 'button',
  182. click: () => {
  183. GM_openInTab('https://almaceleste.github.io', {
  184. active: true,
  185. insert: true,
  186. setParent: true
  187. });
  188. }
  189. },
  190. },
  191. types: {
  192. multiselect: {
  193. default: {},
  194. toNode: function() {
  195. let field = this.settings,
  196. value = this.value,
  197. options = field.options,
  198. id = this.id,
  199. configId = this.configId,
  200. labelPos = field.labelPos,
  201. create = this.create;
  202.  
  203. // console.log('toNode:', field, value, options);
  204. function addLabel(pos, labelEl, parentNode, beforeEl) {
  205. if (!beforeEl) beforeEl = parentNode.firstChild;
  206. switch (pos) {
  207. case 'right': case 'below':
  208. if (pos == 'below')
  209. parentNode.appendChild(create('br', {}));
  210. parentNode.appendChild(labelEl);
  211. break;
  212. default:
  213. if (pos == 'above')
  214. parentNode.insertBefore(create('br', {}), beforeEl);
  215. parentNode.insertBefore(labelEl, beforeEl);
  216. }
  217. }
  218.  
  219. let retNode = create('div', {
  220. className: 'config_var multiselect',
  221. id: `${configId}_${id}_var`,
  222. title: field.title || ''
  223. }),
  224. firstProp;
  225. // Retrieve the first prop
  226. for (let i in field) { firstProp = i; break; }
  227. let label = field.label ? create('label', {
  228. className: 'field_label',
  229. id: `${configId}_${id}_field_label`,
  230. for: `${configId}_field_${id}`,
  231. }, field.label) : null;
  232. let wrap = create('ul', {
  233. id: `${configId}_field_${id}`
  234. });
  235. this.node = wrap;
  236.  
  237. for (const key in options) {
  238. // console.log('toNode:', key);
  239. const inputId = `${configId}_${id}_${key}_checkbox`;
  240. const li = wrap.appendChild(create('li', {
  241. }));
  242. li.appendChild(create('input', {
  243. checked: value.hasOwnProperty(key),
  244. id: inputId,
  245. type: 'checkbox',
  246. value: options[key],
  247. }));
  248. li.appendChild(create('label', {
  249. className: 'option_label',
  250. for: inputId,
  251. }, key));
  252. }
  253.  
  254. retNode.appendChild(wrap);
  255.  
  256. if (label) {
  257. // If the label is passed first, insert it before the field
  258. // else insert it after
  259. if (!labelPos)
  260. labelPos = firstProp == "label" ? "left" : "right";
  261. addLabel(labelPos, label, retNode);
  262. }
  263. return retNode;
  264. },
  265. toValue: function() {
  266. let node = this.node,
  267. id = node.id,
  268. options = this.settings.options,
  269. rval = {};
  270.  
  271. // console.log('toValue:', node, options, this);
  272.  
  273. if (!node) return rval;
  274.  
  275. let nodelist = node.querySelectorAll(`#${id} input:checked`);
  276. // console.log('nodelist:', document.querySelectorAll(`#${id} input:checked`), nodelist);
  277. nodelist.forEach((input) => {
  278. // console.log('toValue:', input);
  279. const value = input.value;
  280. const key = Object.keys(options).find((key) => options[key] == value);
  281. rval[key] = value;
  282. });
  283.  
  284. // console.log('toValue:', rval);
  285. return rval;
  286. },
  287. reset: function() {
  288. let node = this.node,
  289. values = this.default;
  290.  
  291. // console.log('reset:', node, values, Object.values(values));
  292. const inputs = node.getElementsByTagName('input');
  293. for (const index in inputs) {
  294. const input = inputs[index];
  295. // console.log('reset:', input.value, Object.values(values).includes(input.value) || Object.values(values).includes(+input.value));
  296. if (Object.values(values).includes(input.value) || Object.values(values).includes(+input.value)) {
  297. if (!input.checked) input.click();
  298. }
  299. else {
  300. if (input.checked) input.click();
  301. }
  302. }
  303. }
  304. }
  305. },
  306. css: windowcss,
  307. events: {
  308. save: function() {
  309. GM_config.close();
  310. }
  311. },
  312. });
  313.  
  314. function arrow(element){
  315. const arrow = $(`
  316. <svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
  317. <style>
  318. .collapsed {
  319. transform: rotate(0deg);
  320. }
  321. .expanded {
  322. transform: rotate(180deg);
  323. }
  324. </style>
  325. <text x='0' y='18'>▼</text>
  326. </svg>
  327. `).css({
  328. fill: 'whitesmoke',
  329. height: '20px',
  330. width: '30px',
  331. });
  332. $(element).append(arrow);
  333. }
  334.  
  335. function collapse(element, header){
  336. $(element).css({
  337. cursor: 'pointer',
  338. });
  339. arrow($(element).find(header));
  340. $(element).accordion({
  341. collapsible: true,
  342. active: false,
  343. beforeActivate: () => {
  344. rotate($(element).find('svg'));
  345. }
  346. });
  347. }
  348.  
  349. function rotate(element){
  350. if ($(element).hasClass('expanded')) {
  351. $(element).animate({
  352. transform: 'rotate(0deg)',
  353. });
  354. }
  355. else {
  356. $(element).animate({
  357. transform: 'rotate(180deg)',
  358. });
  359. }
  360. $(element).toggleClass('expanded');
  361. }
  362.  
  363. function compact(first, second){
  364. $('dt' + first).each(function(){
  365. $(this).css('display','none');
  366. $(this).siblings('dt' + second).find('span').append(' (' + $(this).find('span').text() + ')');
  367. });
  368. $('dd' + first).each(function(){
  369. $(this).css('display','none');
  370. $(this).siblings('dd' + second).find('span').append(' (' + $(this).find('span').text() + ')');
  371. });
  372. }
  373.  
  374. function newtaber(e){
  375. const options = {active: !GM_config.get('background'), insert: GM_config.get('insert'), setParent: GM_config.get('setParent')};
  376. e.preventDefault();
  377. e.stopPropagation();
  378. GM_openInTab(e.target.href, options);
  379. }
  380.  
  381. function getjson(url){
  382. fetch(url).then((response) => {
  383. // console.log('getjson:', response);
  384. response.json().then((json) => {
  385. console.log('getjson:', json);
  386. });
  387. });
  388. }
  389.  
  390. function sumlast(array, number, prop){
  391. if (number != 0) {
  392. array = array.slice(-number);
  393. }
  394. let result = array.reduce((sum, next) => {
  395. return sum + next[prop];
  396. }, 0);
  397. return result;
  398. }
  399.  
  400. function getupdates(url, target){
  401. fetch(url).then((response) => {
  402. response.json().then((json) => {
  403. const data = Object.values(json);
  404.  
  405. const updatesperiods = GM_config.get('updatesperiods');
  406.  
  407. for (const period in updatesperiods) {
  408. const updates = sumlast(data, updatesperiods[period], 'update_checks');
  409. $('<span></span>', {
  410. title: period,
  411. }).text(updates).appendTo(target);
  412. }
  413. });
  414. });
  415. }
  416.  
  417. function doCompact(){
  418. if (GM_config.get('compact')){
  419. $(scriptstats).children().css('width','auto');
  420. compact(totalinstalls, dailyinstalls);
  421. compact(updateddate, createddate);
  422. }
  423. }
  424.  
  425. function doRating(page){
  426. switch (page) {
  427. case 'user':
  428. case 'search':
  429. $(scriptrating).each(function(){
  430. let rating = $(this).attr('data-rating-score');
  431. $(this).children('span').after(` - ${rating}`);
  432. });
  433. break;
  434. case 'script':
  435. $(scriptrating).each(function(){
  436. const author = '#script-stats > .script-show-author > span > a';
  437. const url = `${window.location.origin}${$(author).attr('href')}`;
  438. const scriptId = '#script-content > .script-in-sets > input[name="script_id"]';
  439. const id = $(scriptId).val();
  440.  
  441. fetch(url).then((response) => {
  442. response.text().then((data) => {
  443. const parser = new DOMParser();
  444. const doc = parser.parseFromString(data, 'text/html');
  445. const el = doc.querySelector(`#user-script-list li[data-script-id="${id}"]`);
  446.  
  447. $(this).children('span').after(` - ${el.dataset.scriptRatingScore}`);
  448. });
  449. });
  450. });
  451. break;
  452. default:
  453. break;
  454. }
  455. }
  456.  
  457. function doCollapse(){
  458. Object.keys(sections).forEach((section) => {
  459. if (GM_config.get(section)) {
  460. collapse(sections[section], 'header h3');
  461. }
  462. });
  463. }
  464.  
  465. function doProfile(){
  466. $(userprofile.path).slideUp();
  467. arrow($(userprofile.header));
  468. $(userprofile.header).css({
  469. cursor: 'pointer',
  470. })
  471. .click(function(){
  472. $(userprofile.path).slideToggle();
  473. rotate($(this).find('svg'));
  474. });
  475. }
  476.  
  477. function doList(){
  478. const version = GM_config.get('version');
  479. const newtab = GM_config.get('newtab');
  480. $(listitem).each(function(){
  481. if (version){
  482. $(this).find(separator).before(` ${$(this).attr(scriptversion)}`);
  483. }
  484. if (newtab){
  485. $(this).find(separator).prev('a').click(newtaber);
  486. }
  487. });
  488. }
  489.  
  490. function doUpdates(page){
  491. let list, target, url;
  492. switch (page) {
  493. case 'user':
  494. case 'search':
  495. list = listitem;
  496. target = scriptstats;
  497. break;
  498. case 'script':
  499. list = `#script-meta`;
  500. target = `#script-stats`;
  501. url = `${window.location.href}/stats.json`;
  502. break;
  503. default:
  504. break;
  505. }
  506. $(list).each((index, item) => {
  507. $(item).css({
  508. maxWidth: 'unset',
  509. });
  510. const stats = $(item).find(target);
  511. if (page != 'script') url = `${$(item).find(scripturl).attr('href')}/stats.json`;
  512.  
  513. const updatesperiods = GM_config.get('updatesperiods');
  514. if (Object.keys(updatesperiods).length > 0) {
  515. const dt = $('<dt></dt>', {
  516. class: 'script-list-update-checks',
  517. style: 'cursor: default',
  518. width: 'auto',
  519. });
  520. let text = 'Updates (';
  521. let title = 'Update checks (';
  522. for (const period in updatesperiods) {
  523. text += `${period.charAt(0)}|`;
  524. title +=`${period}|`;
  525. };
  526. text = text.replace(/\|$/, '):');
  527. title = title.replace(/\|$/, ')');
  528. dt.text(text).attr('title', title).append(`
  529. <style>
  530. .inline-script-stats dt,dd,span {
  531. cursor: default;
  532. width: auto !important;
  533. }
  534. .script-list-update-checks span {
  535. padding: 0 5px;
  536. }
  537. .script-list-update-checks span:not(:last-child) {
  538. border-right: 1px dotted whitesmoke;
  539. }
  540. </style>`).appendTo($(stats));
  541.  
  542. const updatechecks = $('<dd></dd>', {
  543. class: 'script-list-update-checks',
  544. });
  545. $(stats).append(updatechecks);
  546.  
  547. getupdates(url, $(updatechecks));
  548. }
  549. });
  550. }
  551.  
  552. function router(path){
  553. const updates = GM_config.get('updates');
  554. const userprofile = GM_config.get('userprofile');
  555. const ratingscore = GM_config.get('ratingscore');
  556.  
  557. switch (true) {
  558. case route.userpage.test(path):
  559. // console.log('router:', 'user', path);
  560. if (userprofile) doProfile();
  561. doCollapse();
  562. doCompact();
  563. if (ratingscore) doRating('user');
  564. doList();
  565. if (updates) doUpdates('user');
  566. break;
  567. case route.searchpage.test(path):
  568. // console.log('router:', 'search', path);
  569. if (ratingscore) doRating('search');
  570. doCompact();
  571. doList();
  572. if (updates) doUpdates('search');
  573. break;
  574. case route.scriptpage.test(path):
  575. // console.log('router:', 'script', path);
  576. if (ratingscore) doRating('script');
  577. if (updates) doUpdates('script');
  578. break;
  579. default:
  580. break;
  581. }
  582. }
  583.  
  584. (function() {
  585. 'use strict';
  586.  
  587. $(document).ready(() => {
  588. router(window.location.pathname);
  589. });
  590. })();