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