1. // ==UserScript==
  2. // @name Neochan autoplay
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.1
  5. // @description Adds media controls for neochan
  6. // @author You
  7. // @match https://*.neochan.ru/*
  8. // @match https://*.neochan.net/*
  9. // @icon https://www.google.com/s2/favicons?sz=64&domain=neochan.net
  10. // @grant GM_addStyle
  11. // ==/UserScript==
  12. (function() {
  13. 'use strict';
  14. GM_addStyle(
  15. ".ap-arrow a {display: block;}" +
  16. ".ap-arrow {position: fixed; top: 50%; font-size: x-large; cursor: pointer; background-color: rgba(255, 255, 255, 0.2); width: 50px;padding-top: 5px;padding-bottom: 5px;text-align: center; left:0;} " +
  17. ".ap-arrow-right {right: 0; left: unset;}"+
  18. ".ap-media-button { display: block; margin-left: 14px;margin-top: 10px; margin-bottom: 5px; border: 0; background: transparent; box-sizing: border-box;width: 0; height: 35px; border-color: transparent transparent transparent #202020; transition: 100ms all ease; "+
  19. "cursor: pointer; border-style: solid; border-width: 16px 0 16px 23px;}" +
  20. " .ap-media-button.paused { border-style: double; border-width: 0px 0 0px 23px; } .ap-media-button:hover { border-color: transparent transparent transparent #404040; }"
  21. )
  22. const PlayVidLinks = true,
  23. ReplaceGifs = true,
  24. AddCustomSmiles = true,
  25. Smiles = {
  26. "enid_wink": {
  27. 'smileHtml': '<ul class="smile-tab smile-tab-enid_wink" style="list-style: none; text-align: left; display: none;"><div class="smile" style="display: inline"><i class="s42 s42-enid_wink smiles-icon" data-func="AddSmile" data-arg1="enid_wink"></i></div><div class="smile" style="display: inline"><i class="s42 s42-emma_amazed smiles-icon" data-func="AddSmile" data-arg1="emma_amazed"></i></div><div class="smile" style="display: inline"><i class="s42 s42-emma_angry smiles-icon" data-func="AddSmile" data-arg1="emma_angry"></i></div><div class="smile" style="display: inline"><i class="s42 s42-emma_beauty smiles-icon" data-func="AddSmile" data-arg1="emma_beauty"></i></div><div class="smile" style="display: inline"><i class="s42 s42-emma_braces smiles-icon" data-func="AddSmile" data-arg1="emma_braces"></i></div><div class="smile" style="display: inline"><i class="s42 s42-emma_cool smiles-icon" data-func="AddSmile" data-arg1="emma_cool"></i></div><div class="smile" style="display: inline"><i class="s42 s42-emma_crying smiles-icon" data-func="AddSmile" data-arg1="emma_crying"></i></div><div class="smile" style="display: inline"><i class="s42 s42-emma_duck smiles-icon" data-func="AddSmile" data-arg1="emma_duck"></i></div><div class="smile" style="display: inline"><i class="s42 s42-emma_duck2 smiles-icon" data-func="AddSmile" data-arg1="emma_duck2"></i></div><div class="smile" style="display: inline"><i class="s42 s42-emma_scared smiles-icon" data-func="AddSmile" data-arg1="emma_scared"></i></div><div class="smile" style="display: inline"><i class="s42 s42-emma_serious smiles-icon" data-func="AddSmile" data-arg1="emma_serious"></i></div><div class="smile" style="display: inline"><i class="s42 s42-emma_serious2 smiles-icon" data-func="AddSmile" data-arg1="emma_serious2"></i></div><div class="smile" style="display: inline"><i class="s42 s42-emma_smile smiles-icon" data-func="AddSmile" data-arg1="emma_smile"></i></div><div class="smile" style="display: inline"><i class="s42 s42-emma_smile2 smiles-icon" data-func="AddSmile" data-arg1="emma_smile2"></i></div><div class="smile" style="display: inline"><i class="s42 s42-emma_swag smiles-icon" data-func="AddSmile" data-arg1="emma_swag"></i></div><div class="smile" style="display: inline"><i class="s42 s42-emma_tongue smiles-icon" data-func="AddSmile" data-arg1="emma_tongue"></i></div><div class="smile" style="display: inline"><i class="s42 s42-enid_cat smiles-icon" data-func="AddSmile" data-arg1="enid_cat"></i></div><div class="smile" style="display: inline"><i class="s42 s42-enid_cute smiles-icon" data-func="AddSmile" data-arg1="enid_cute"></i></div><div class="smile" style="display: inline"><i class="s42 s42-enid_disgust smiles-icon" data-func="AddSmile" data-arg1="enid_disgust"></i></div><div class="smile" style="display: inline"><i class="s42 s42-enid_eating smiles-icon" data-func="AddSmile" data-arg1="enid_eating"></i></div><div class="smile" style="display: inline"><i class="s42 s42-enid_rage smiles-icon" data-func="AddSmile" data-arg1="enid_rage"></i></div><div class="smile" style="display: inline"><i class="s42 s42-enid_sad smiles-icon" data-func="AddSmile" data-arg1="enid_sad"></i></div><div class="smile" style="display: inline"><i class="s42 s42-enid_scared smiles-icon" data-func="AddSmile" data-arg1="enid_scared"></i></div><div class="smile" style="display: inline"><i class="s42 s42-enid_scared2 smiles-icon" data-func="AddSmile" data-arg1="enid_scared2"></i></div><div class="smile" style="display: inline"><i class="s42 s42-enid_scared3 smiles-icon" data-func="AddSmile" data-arg1="enid_scared3"></i></div><div class="smile" style="display: inline"><i class="s42 s42-enid_smile smiles-icon" data-func="AddSmile" data-arg1="enid_smile"></i></div><div class="smile" style="display: inline"><i class="s42 s42-enid_smile2 smiles-icon" data-func="AddSmile" data-arg1="enid_smile2"></i></div><div class="smile" style="display: inline"><i class="s42 s42-jenna_beaten smiles-icon" data-func="AddSmile" data-arg1="jenna_beaten"></i></div><div class="smile" style="display: inline"><i class="s42 s42-jenna_cringe smiles-icon" data-func="AddSmile" data-arg1="jenna_cringe"></i></div><div class="smile" style="display: inline"><i class="s42 s42-jenna_facepalm smiles-icon" data-func="AddSmile" data-arg1="jenna_facepalm"></i></div><div class="smile" style="display: inline"><i class="s42 s42-jenna_kiss smiles-icon" data-func="AddSmile" data-arg1="jenna_kiss"></i></div><div class="smile" style="display: inline"><i class="s42 s42-jenna_laugh smiles-icon" data-func="AddSmile" data-arg1="jenna_laugh"></i></div><div class="smile" style="display: inline"><i class="s42 s42-jenna_nose smiles-icon" data-func="AddSmile" data-arg1="jenna_nose"></i></div><div class="smile" style="display: inline"><i class="s42 s42-jenna_sick smiles-icon" data-func="AddSmile" data-arg1="jenna_sick"></i></div><div class="smile" style="display: inline"><i class="s42 s42-jenna_smile smiles-icon" data-func="AddSmile" data-arg1="jenna_smile"></i></div><div class="smile" style="display: inline"><i class="s42 s42-jenna_smoke smiles-icon" data-func="AddSmile" data-arg1="jenna_smoke"></i></div><div class="smile" style="display: inline"><i class="s42 s42-jenna_think smiles-icon" data-func="AddSmile" data-arg1="jenna_think"></i></div><div class="smile" style="display: inline"><i class="s42 s42-jenna_what_ smiles-icon" data-func="AddSmile" data-arg1="jenna_what_"></i></div><div class="smile" style="display: inline"><i class="s42 s42-wedn_cold smiles-icon" data-func="AddSmile" data-arg1="wedn_cold"></i></div><div class="smile" style="display: inline"><i class="s42 s42-wedn_crying smiles-icon" data-func="AddSmile" data-arg1="wedn_crying"></i></div><div class="smile" style="display: inline"><i class="s42 s42-wedn_eyes smiles-icon" data-func="AddSmile" data-arg1="wedn_eyes"></i></div><div class="smile" style="display: inline"><i class="s42 s42-wedn_growl smiles-icon" data-func="AddSmile" data-arg1="wedn_growl"></i></div><div class="smile" style="display: inline"><i class="s42 s42-wedn_mad smiles-icon" data-func="AddSmile" data-arg1="wedn_mad"></i></div><div class="smile" style="display: inline"><i class="s42 s42-wedn_sad smiles-icon" data-func="AddSmile" data-arg1="wedn_sad"></i></div><div class="smile" style="display: inline"><i class="s42 s42-wedn_smile smiles-icon" data-func="AddSmile" data-arg1="wedn_smile"></i></div><div class="smile" style="display: inline"><i class="s42 s42-wedn_smile2 smiles-icon" data-func="AddSmile" data-arg1="wedn_smile2"></i></div><div class="smile" style="display: inline"><i class="s42 s42-wedn_tired smiles-icon" data-func="AddSmile" data-arg1="wedn_tired"></i></div><div class="smile" style="display: inline"><i class="s42 s42-puppy smiles-icon" data-func="AddSmile" data-arg1="puppy"></i></div></ul>',
  28. 'smileIcHtml': '<div class="sc-item" data-func="changeSmileTab" data-arg1="enid_wink"><i class="smile-category-image s42-enid_wink"></i></div>'
  29. }
  30. }
  31. let mediaElems = [],
  32. curIndex =0, maxIndex = 0,
  33. plrPause = false, musToVid = false;
  34. function htmlToElements(html) {
  35. var template = document.createElement('template');
  36. template.innerHTML = html;
  37. return template.content;
  38. }
  39. function createArrow(rightSide) {
  40. const arrow = htmlToElements(`<div class="ap-nav ap-arrow ${rightSide ? 'ap-arrow-right' : ''}"><a ><i class="fa fa-chevron-${rightSide ? 'right' : 'left'}"></i></a></div>`);
  41. arrow.querySelector("a").addEventListener('click', (e) => {
  42. if (curIndex == 0 && !rightSide) return;
  43. if (curIndex == maxIndex-1 && rightSide) curIndex = -1;
  44. let elem = "";
  45. if (plrPause){
  46. const fM = rightSide ?
  47. mediaElems.find(e => curIndex < e.getAttribute('ap-index') && e.classList.contains('ap-media')) :
  48. mediaElems.filter(e => curIndex > e.getAttribute('ap-index') && e.classList.contains('ap-media'))?.slice(-1)?.[0];
  49. if (fM){
  50. elem = fM;
  51. curIndex = +fM.getAttribute('ap-index');}
  52. else{
  53. // elem = mediaElems.filter(e => maxIndex > e.getAttribute('ap-index') && e.classList.contains('ap-media'))?.slice(-1)?.[0];
  54. elem = rightSide ? mediaElems.find(e => 0 <= e.getAttribute('ap-index') && e.classList.contains('ap-media')) :
  55. mediaElems.filter(e => maxIndex > e.getAttribute('ap-index') && e.classList.contains('ap-media'))?.slice(-1)?.[0];
  56. }
  57. }else{
  58. const video = document.querySelector('video');
  59. if (video) video.loop = true;
  60. curIndex = curIndex + (rightSide ? 1 : -1);
  61. elem = mediaElems.find(e => curIndex == e.getAttribute('ap-index'));
  62. }
  63. if (!elem) return;
  64. document.querySelector("#close-plyr__audio")?.click();
  65. elem.click();
  66. elem.scrollIntoView({block: "center", inline: "nearest"});
  67. switchPlBut();
  68. });
  69. return arrow;
  70. }
  71. function switchPlBut(val){
  72. const mB = document.querySelector('.ap-media-button')
  73. if (!mB) return;
  74. if (val !== undefined){
  75. mB.style.display = val ? 'block' : 'none';
  76. return;
  77. }
  78. if (document.querySelector('#player-container img')?.tagName == 'IMG') {
  79. mB.style.display = 'none';
  80. } else {
  81. mB.style.display = 'block';
  82. }
  83. }
  84. function render(){
  85. if(window.location.href.match(/usermod\.php\?|mod\.php\?/)){
  86. const art = document.querySelectorAll('article.post'),
  87. controls = document.querySelector('.view-inline-pc');
  88. for (let i = 0; i < art.length; i++) {
  89. const a = art[i];
  90. if(a.querySelector('.view-inline-pc')) continue;
  91. const header = a.querySelector('header'),
  92. postNum = header.querySelector('.post_anchor').name,
  93. contrDup = controls.cloneNode(true),
  94. links = contrDup.querySelectorAll('a');
  95. for (let j = 0; j < links.length; j++) {
  96. const l = links[j];
  97. l.href = l.href.replace(/\/\d+\//, `/${postNum}/`);
  98. }
  99. header.insertBefore(contrDup, header.querySelector('time'));
  100. }
  101. }
  102. const mE = document.querySelectorAll(`.audio-url, img.webm-file, .img.preview${PlayVidLinks ? ', .y-link' : ""}`);
  103. if (mE.length == mediaElems.length) return;
  104. let index = 0;
  105. mediaElems = [...mE];
  106. mE.forEach(e => { //, vimeo-link
  107. const ind = index++, p = e.parentNode;
  108. if(e.hasAttribute('ap-parsed')) return;
  109. e.setAttribute("ap-index", ind);
  110. const classNames = ['webm-file', 'audio-url'];
  111. if (PlayVidLinks) classNames.push('y-link'); //, 'vimeo-link'
  112. if(classNames.some(cN => e.classList.contains(cN))) {
  113. e.classList.add('ap-media');
  114. }
  115. if(e.classList.contains("img")){
  116. if(p.name == "ClipboardImage.png"){
  117. const ext = p.href?.match(/\.\w+$/)?.[0];
  118. if (ext && ext != '.png') p.setAttribute("name", p.name.replace(/\.png$/, ext));
  119. }
  120. if(ReplaceGifs && p.href.endsWith('.gif')){
  121. p.querySelector('.post-gif-marker').remove();
  122. e.src = e.src.replace(/\/thumb\//, '/src/').replace(/\.\w+$/, '.gif');
  123. }
  124. }
  125. e.addEventListener('click', e => {
  126. const t = e.currentTarget;
  127. curIndex = +t.getAttribute('ap-index');
  128. const plClose = document.querySelector("#close-plyr__audio");
  129. if (plClose){
  130. musToVid = true;
  131. plClose.click();
  132. }
  133. if (e.currentTarget.classList.contains('ap-media')) switchPlBut(true);
  134. else switchPlBut(false);
  135. });
  136. e.toggleAttribute('ap-parsed');
  137. })
  138. maxIndex = index;
  139. }
  140. function obsBody(){
  141. const navElems = document.querySelectorAll('.ap-nav'),
  142. player = document.querySelector('video, #player-container img, .plyr'),
  143. smilePan = document.querySelector(".smile-picker"),
  144. navDisplayed = ['none', undefined].includes(navElems[0]?.style?.display)? false : true;
  145. if (AddCustomSmiles && smilePan && !document.querySelector(".user-smiles-added")){
  146. const smList = smilePan.querySelector('.smile-picker-box'),
  147. smCat = smilePan.querySelector('.smile-category'),
  148. smLength = Object.keys(Smiles).length;
  149. for(let key in Smiles){
  150. if(smCat.querySelector(`[data-arg1="${key}"]`)) continue;
  151. smList.insertBefore(htmlToElements(Smiles[key].smileHtml), smList.firstChild);
  152. smCat.insertBefore(htmlToElements(Smiles[key].smileIcHtml), smCat.firstChild);
  153. }
  154. smList.querySelectorAll('.button').forEach((b)=>{
  155. const n = +b.getAttribute('data-arg2') + smLength;
  156. b.id = `btn-more-${n}`;
  157. b.setAttribute('data-arg2', n);
  158. });
  159. smCat.classList.add('user-smiles-added');
  160. }
  161. if (navElems.length && (!player && navDisplayed || player && !navDisplayed)){
  162. if (navDisplayed) { render(); }
  163. navElems.forEach(e => {e.style.display = e.style.display == 'none' ? '' : 'none';});
  164. if (musToVid && !navDisplayed) {
  165. musToVid = false;
  166. } else if (!musToVid){
  167. document.querySelector('.ap-media-button')?.classList.remove("paused");
  168. plrPause = false;
  169. }
  170. } else {
  171. if(player && navElems.length && plrPause) setNextEvent();
  172. if (navDisplayed || !player) return;
  173. render();
  174. createNav();
  175. }
  176. switchPlBut();
  177. }
  178. function createNav(){
  179. const rightArrow = createArrow(true),
  180. leftArrow = createArrow(),
  181. navElem = document.querySelector(".page-nav"),
  182. l = navElem.appendChild(leftArrow),
  183. btn = htmlToElements("<button class='ap-media-button ap-nav'></button>");
  184. navElem.appendChild(rightArrow);
  185. document.querySelector('.ap-arrow-right').appendChild(btn);
  186. document.querySelector('.ap-media-button').addEventListener('click', e => {
  187. plrPause = e.currentTarget.classList.toggle("paused");
  188. setNextEvent();
  189. return false;
  190. });
  191. }
  192. function crMediaObserver(element, checkFunc){
  193. const plInp = document.querySelector('.plyr__progress > input');
  194. if (element && !element.hasAttribute('ap-parsed')) {
  195. const observer = new MutationObserver(() => {
  196. if(!plrPause) {
  197. element.toggleAttribute('ap-parsed');
  198. observer.disconnect();
  199. return;
  200. }
  201. if (checkFunc()){
  202. document.querySelector('.ap-arrow-right a')?.click();
  203. observer.disconnect();
  204. }
  205. });
  206. observer.observe(element, {attributes: true});
  207. element.toggleAttribute('ap-parsed');
  208. };
  209. }
  210. function setNextEvent(){
  211. const video = document.querySelector('video');
  212. if (video){
  213. video?.toggleAttribute('loop');
  214. if(video.hasAttribute('ap-parsed')) return;
  215. video?.addEventListener("ended", (event) => {
  216. document.querySelector('.ap-arrow-right a')?.click();
  217. });
  218. video.toggleAttribute('ap-parsed');
  219. return;
  220. }
  221. const plInp = document.querySelector('.plyr__progress > input');
  222. crMediaObserver(plInp, () => plInp.style.cssText == '--value: 100%;');
  223. if (PlayVidLinks){
  224. const ytPlr = document.querySelector('#youtubevideo');
  225. crMediaObserver(ytPlr, () => ytPlr.classList.contains('plyr--stopped') && ytPlr.classList.contains('plyr__poster-enabled'));
  226. }
  227. }
  228. function waitForElm(selector) {
  229. return new Promise(resolve => {
  230. if (document.querySelector(selector)) {
  231. return resolve(document.querySelector(selector));
  232. }
  233. const observer = new MutationObserver(mutations => {
  234. if (document.querySelector(selector)) {
  235. observer.disconnect();
  236. resolve(document.querySelector(selector));
  237. }
  238. });
  239. observer.observe(document.body, {
  240. childList: true,
  241. subtree: true
  242. });
  243. });
  244. }
  245. render();
  246. const threadObserver = new MutationObserver(render),
  247. bodyObserver = new MutationObserver(obsBody),
  248. article = document.querySelector("article.thread");
  249. if (!article) return;
  250. threadObserver.observe(article, {childList: true});
  251. bodyObserver.observe(document.body, {childList: true});
  252. waitForElm('#player-container').then(() => {
  253. bodyObserver.observe(document.querySelector('#player-container'), {attributes: true})
  254. })
  255. })();