F95zone marker

Marks F95Zone threads for future reference

  1. // ==UserScript==
  2. // @name F95zone marker
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.1.9
  5. // @description Marks F95Zone threads for future reference
  6. // @author agreg
  7. // @license MIT
  8. // @match https://f95zone.to/*
  9. // @icon https://www.google.com/s2/favicons?domain=f95zone.to
  10. // @grant GM_addStyle
  11. // @grant GM_setValue
  12. // @grant GM_getValue
  13. // @grant GM_deleteValue
  14. // @grant GM_listValues
  15. // ==/UserScript==
  16.  
  17. /* jshint esversion: 6 */
  18. /* eslint no-multi-spaces:off */
  19. (function() {
  20. 'use strict';
  21.  
  22. const THREADS = /^(https:\/\/f95zone.to)?\/threads\//;
  23. const OP = '.message-threadStarterPost';
  24. const OP_ICONS = `${OP} header .message-attribution-opposite`;
  25. var bookmarks = GM_getValue('bookmarks', {});
  26. var tags = GM_getValue('tags', {});
  27.  
  28. let urlId = s => (!THREADS.test(s) ? null : s.replace(THREADS, '').match(/^([^\/]+\.)?([0-9]+)/)[2]);
  29. let $find = (s, e=document) => e.querySelector(s);
  30. let $find_ = (s, e=document) => Array.from( e.querySelectorAll(s) );
  31. let $parent = (e, check=()=>true) => (check(e.parentNode) ? e.parentNode : $parent(e.parentNode, check));
  32. let $e = (tag, options={}, ...children) => {var e = Object.assign(document.createElement(tag), options);
  33. children.forEach(x => e.append(x));
  34. return e}
  35. let $assign = (o, vals) => Object.assign(o, ...Object.entries(vals).map(([k, v]) => ({[k]: v||undefined})));
  36. let $bookmark = e => {
  37. e.prepend( $e('i', {className: "bookmarkLink bookmarkLink--highlightable is-bookmarked"}) );
  38. e.classList.add('-marker-bookmark');
  39. };
  40. let $updData = (id, e, data=GM_getValue(id), upd={}) => {if (data) {
  41. GM_setValue(id, $assign(data, upd));
  42. e.classList[data.fade ? 'add' : 'remove']('-marker-fade');
  43. e.classList[data.mark ? 'add' : 'remove']('-marker-mark');
  44. e.title = data.info || "";
  45. } else {
  46. GM_deleteValue(id);
  47. e.classList.remove('-marker-fade', '-marker-mark');
  48. e.title = "";
  49. }};
  50.  
  51. let _$getData = () => Object.fromEntries(GM_listValues().map(k => [k, GM_getValue(k)]));
  52. let $exportData = () => $e('a', {
  53. download: `F95zoneMarker_${new Date().toJSON().match(/(.*)T/)[1]}.json`,
  54. href: "data:application/json;base64," + btoa(JSON.stringify(_$getData(), null, 2) + "\n"),
  55. }).click();
  56. let $importData = () => $e('input', {type: 'file', accept: 'application/json', onchange() {
  57. this.files[0] && Object.assign(new FileReader(), {onload () {
  58. let data = JSON.parse(this.result);
  59. GM_listValues().forEach(GM_deleteValue);
  60. Object.entries(data).forEach(([k, v]) => GM_setValue(k, v));
  61. location.reload();
  62. }}).readAsText(this.files[0]);
  63. }}).click();
  64.  
  65. let $editIcon = (id, e) => {
  66. var icon = $e('i', {className: "fas fa-star", title: "Edit mark"});
  67. var data = GM_getValue(id), edit = null;
  68. let _$updData = (upd={}) => {$updData(id, e, data, upd);
  69. icon.classList.add(`-marker-${data ? "" : "un"}marked`);
  70. icon.classList.remove(`-marker-${data ? "un" : ""}marked`)}
  71. let $deleteMark = () => {if (confirm("Delete mark?")) {
  72. $toggleEdit();
  73. data = null;
  74. _$updData();
  75. }};
  76. let $toggleEdit = () => {if (edit) {
  77. edit.remove();
  78. edit = null;
  79. } else {
  80. data = data || {};
  81. document.body.append(edit = $e('div', {className: '-marker-dialog'},
  82. $e('textarea', {placeholder: "Tooltip", value: data.info||"", oninput () {_$updData({info: this.value})}}),
  83. $e('div', {className: '-marker-row'},
  84. $e('label', {},
  85. $e('input', {type: 'checkbox', checked: data.fade, onchange () {_$updData({fade: this.checked})}}),
  86. $e('span', {innerText: "Fade"})),
  87. $e('label', {},
  88. $e('input', {type: 'checkbox', checked: data.mark, onchange () {_$updData({mark: this.checked})}}),
  89. $e('span', {innerText: "Mark"}))),
  90. $e('div', {className: '-marker-row'},
  91. $e('button', {innerText: "OK", onclick: $toggleEdit}),
  92. $e('button', {innerText: "Delete", onclick: $deleteMark}),
  93. $e('button', {style: "float:right", innerText: "Export Data", onclick() {$toggleEdit(); $exportData()}}),
  94. $e('button', {style: "float:right", innerText: "Import Data", onclick() {$toggleEdit(); $importData()}}))));
  95. }};
  96. _$updData();
  97. return $e('a', {className: '-marker-edit', href: location.href, onclick () {$toggleEdit(); return false}}, icon);
  98. };
  99.  
  100. GM_addStyle(`.-marker-unmarked {opacity: 0.5; color: rgb(147, 152, 160)} .-marker-marked {color: rgb(193, 88, 88)}
  101. .-marker-fade:not(:hover) {opacity: 0.25} .-marker-fade:hover {outline: 2px solid black} .-marker-mark {outline: 2px solid grey}
  102. .-marker-bookmark {font-weight: bold; text-shadow: 0 0, 0 0 7px !important} .-marker-bookmark .bookmarkLink {padding-right: 1ex}
  103. .-marker-dialog {position: fixed; top: 30%; left: 30%; width: 40%; background: rgb(29, 31, 33); border-radius: 5px; z-index:1000}
  104. .-marker-dialog > * {margin: 1em} .-marker-dialog textarea {width: calc(100% - 2em)}
  105. .-marker-dialog .-marker-row > * {margin-left: .5em; margin-right: .5em} .-marker-tag {box-shadow: 0 0 1px 2px !important}`);
  106.  
  107. $find_(".structItem-title").forEach(e => { // forum, similar threads
  108. let _id = urlId(e.getAttribute('uix-data-href')||"");
  109. _id && $updData(_id, $parent(e, x => x.classList.contains('structItem--thread')));
  110. bookmarks[_id] && $bookmark(e);
  111. });
  112.  
  113. if (['/search/', '/tags/'].some(s => location.pathname.startsWith(s))) { // search results
  114. $find_(".contentRow-title a").forEach(e => {
  115. let _id = urlId(e.href||"");
  116. console.log(e.href, _id)
  117. _id && $updData(_id, $parent(e, x => /^li$/i.test(x.tagName)));
  118. bookmarks[_id] && $bookmark(e);
  119. });
  120. }
  121.  
  122. if (['/latest', '/sam/latest_alpha'].some(s => location.pathname.startsWith(s))) { // latest updates
  123. GM_addStyle(`.-marker-mark {display: block;/* Chrome bug */} .-marker-hide :is(.-marker-edit, .-marker-tag) {display: none}
  124. .-marker-dialog {top: 45%; left: calc(30% - 125px - 10px)}
  125. @media only screen and (max-width: 1240px) {.-marker-dialog {left: 30%}}
  126. .-marker-edit {position: absolute; top: 2%; right: 1%; z-index: 10; opacity: 0.7; background: black; border-radius: 5px}
  127. .-marker-edit > * {padding: 5px 3px; opacity: 1} .resource-tile:hover .-marker-tags {display: none}
  128. .-marker-tags {position: absolute; top: 2%; left: 1%; z-index: 10; font-size: smaller; width: calc(97% - 24px)}
  129. .-marker-tags > * {display: inline-block; background: #A44C; border-radius: 3px; padding: 0 5px; margin: 1px}}`);
  130. let _allTagIds = (() => {try {return latestUpdates.tags} catch (e) {}})(); // eslint-disable-line no-undef
  131. let _markedTagIds = Object.fromEntries( Object.entries(_allTagIds||{}).filter(([_, k]) => tags[k]) );
  132. new MutationObserver(mutations => mutations.forEach(m => Array.from(m.addedNodes).filter(e => e.tagName).forEach(node => {
  133. console.debug(node, node.children);
  134. $find_(".resource-tile_tags > span", node).forEach(e => tags[e.innerText] && e.classList.add('-marker-tag'));
  135. $find_("a.resource-tile_link", node).forEach(e => {
  136. let _id = urlId(e.href||"");
  137. let _tags = (e.parentNode.getAttribute('data-tags')||"").split(',').map(id => _markedTagIds[id]).filter(Boolean);
  138. console.log(e.href, _id, _tags);
  139. e.append($editIcon(_id, e));
  140. e.append($e('div', {className: "-marker-tags"}, ..._tags.map(s => $e('span', {className: "-marker-tag", innerText: s}))));
  141. _id && $updData(_id, e);
  142. bookmarks[_id] && $bookmark($find('.resource-tile_info-header_title', e));
  143. });
  144. }))).observe(document.body, {subtree: true, childList: true});
  145. [['keydown', 'add'], ['keyup', 'remove']].forEach(([name, toggle]) => // hide extra elements when holding down Shift
  146. document.addEventListener(name, evt => (evt.key == 'Shift') && document.body.classList[toggle]('-marker-hide')));
  147. document.addEventListener('visibilitychange', () => document.body.classList.remove('-marker-hide')); // unhide when switching tabs
  148. }
  149.  
  150. if (location.pathname === '/account/bookmarks') {
  151. $find_("li.block-row").forEach(e => {
  152. let _id = urlId($find(".contentRow-title a", e).href||"");
  153. if (_id) {
  154. let toolbar = $find(".contentRow-extra", e);
  155. toolbar.insertBefore($editIcon(_id, e), toolbar.firstChild);
  156. bookmarks[_id] = true;
  157. }
  158. });
  159. GM_setValue('bookmarks', bookmarks);
  160. }
  161.  
  162. if (location.pathname.startsWith('/threads/') && $find(OP)) { // first page of a thread
  163. let _icons = $find(OP_ICONS), _id = urlId(location.href), _bookmarked = Boolean($find("a.is-bookmarked", _icons));
  164. if ( $find_('a', _icons).some(e => e.innerText == "#1") ) {
  165. _icons.insertBefore($e('li', {}, $editIcon(_id, $find(OP))),
  166. _icons.firstChild);
  167. }
  168. (_bookmarked || bookmarks[_id]) && GM_setValue('bookmarks', $assign(bookmarks, {[_id]: _bookmarked}));
  169. }
  170.  
  171. if (location.pathname.startsWith('/threads/')) { // thread tags
  172. let _tagList = $find(".js-tagList"), _tags = $find_(".tagItem", _tagList);
  173. let _markTag = (e, id) => e.classList[tags[id] ? 'add' : 'remove']('-marker-tag');
  174. _tags.forEach(e => {
  175. let id = e.innerText;
  176. _markTag(e, id);
  177. e.title = "Middle Click to mark/unmark the tag as important";
  178. e.addEventListener('auxclick', evt => {evt.preventDefault();
  179. GM_setValue('tags', $assign(tags, {[id]: !tags[id]||void 0}));
  180. _markTag(e, id)});
  181. });
  182. }
  183. })();