8chanSS

Userscript to style 8chan

安装此脚本?
作者推荐脚本

您可能也喜欢Fullchan X

安装此脚本
  1. // ==UserScript==
  2. // @name 8chanSS
  3. // @version 1.47.0
  4. // @namespace 8chanss
  5. // @description Userscript to style 8chan
  6. // @author otakudude
  7. // @minGMVer 4.3
  8. // @minFFVer 121
  9. // @license MIT; https://github.com/otacoo/8chanSS/blob/main/LICENSE
  10. // @match *://8chan.moe/*
  11. // @match *://8chan.se/*
  12. // @exclude *://8chan.moe/login.html
  13. // @exclude *://8chan.se/login.html
  14. // @grant GM.getValue
  15. // @grant GM.setValue
  16. // @grant GM.deleteValue
  17. // @grant GM.listValues
  18. // @run-at document-start
  19. // @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAMAAABg3Am1AAAAZlBMVEUAAABdlloAhl758AH58AH58AcAhl758ADj4CYAh14AhV4AhV0Ahl748AcChl4Chl0Ab2H58AIAhl758AD58AAAhl757wL48AD47wL78QL47wcAh1748AF3oFfs5yEAh1/68QDz7BM5qSu8AAAAH3RSTlMA/lg/OYtM8g/onXtoXzAaCdzBsIFzczMeaCXXyrmp9ddA3QAAANpJREFUSMft0tkOgjAQheFjtVCQVVxwnfr+L+kWM5FOC73TxP/6fBedFJwpyx5CtSpqSHXWpns4qYxo1cDtkNp7GoOW9KgSwM4+09KeEhmw4H0IuGJDAbCw79a8nwJYFDQCuO1gT8oLWCiKAXavKA5cZ78I5n/wBx7wfb+1TwOggpD2gxxSpvWBrIbY3AcUPK1lkMNbJ4FV4wd964KsQqBF6oAEwcoh2GAk/QlyjNYx4AeHMicGxxoTOrRvIB5IPtULJJhY+QIFJrd9gCUi0tdZjqgu5yYOGAO5G/kyc3TkciPeAAAAAElFTkSuQmCC
  20. // ==/UserScript==
  21.  
  22. function onReady(fn) {
  23. if (document.readyState === "loading") {
  24. document.addEventListener("DOMContentLoaded", fn, { once: true });
  25. } else {
  26. fn();
  27. }
  28. }
  29. const faviconManager = (() => {
  30. const STYLES = [
  31. "default",
  32. "eight", "eight_dark",
  33. "pixel", "pixel_alt"
  34. ];
  35. const STATES = ["base", "unread", "notif"];
  36. const FAVICON_DATA = {
  37. default: {
  38. base: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAq1BMVEUAAACKtMV2lqSE1PWZ1Oyr1eaqzNp7udPO7vuc3ffh8vmG0/OB0fKv3e+N0e3B4e693+56x+bb6e6YzeOgydl7wd3L3OOwytS35fjZ8fuQ2feB1Pbo9vyy5Pjm9Pmp4PW85fbW7feO1/S55Pbk8/mq3/TY7fbn9Pnf7/bQ6vWS1fDa7fWH0vCb0+rY6/Pc7POBzezA4/GNz+vJ4+662ebH1958wNzD6vqm4Ph4HvJJAAAAN3RSTlMADwXliF4dGP7949HLn5mUhWllVCglJBf6+fn5+Pjo4N3a2NfV1MzIxb28rayinJKOioB7UzEt0Y3/cQAAAIBJREFUGNOVjsUOhVAMRG97BXcePHd3Q/7/y0hhQWDHrDpJT86wIQHo1t/xHGLb8aNvzHnYFC2KFHh+mu1PNTW6GuVB0vlaJvTsTl2xWjgxw5upCOB3ZLEzXvve5IlE8DdJ5K7QL0nts61cEw9Q8p82kmDGt4ZgbTCwrS/0Rw9IBRlvB34XFuslAAAAAElFTkSuQmCC",
  39. unread: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAolBMVEUAAACG2vyC1/iH1vaB1/mC1PSC2PuC2fuD2fyE2fyG3P2I3Pqn3vWB1PWO1fKD2fuE2vqE2vyH3Puk4PaK1vSg2/OP1vJ/0fGB0/SF1vaC1faB1vmF2vyy5PiP2PbF6vnK6vjU7/m/5/ao3fKF0/K55PaW2PKK1PGA0/SP2PWA1/mC2fuE2fyD2fyx4vaT1e+S1/PX8PrO7vqb3fjh9Pu25fiNN6jgAAAAMXRSTlMAHK7Kmf6CYlc/EAnrxOt4OSwV/f3y8OrQv7afI/7+/f379/Pz8OLhvLmRb1pK+/PUqRXqEQAAALhJREFUGNN1j8cSgkAAQ7c3dykiSLFTpaiA/v+vCXrAi7klM5nkgUWrFfjVWgrhqMVDnkQREeuPURBCEevjSaNurrmcsYbo4dQ/NYMA7GxTlhinGj/OZ+QAxc1m9DwrRHad91cJXJIdgsDb+5igyxzImGYowf5r76d9TlwgK7qtbRRat+NwwVwBB1EaGutucFFE9m4aYVu6OYwBaRjj7kzQoSmxYjG9U1+IllQm4XDBUI5o5QTxB/wNI78Nh2SdMuEAAAAASUVORK5CYII=",
  40. notif: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAA4VBMVEUAAACE3vuD1veC1veB2PqB2fuD2/2H2/2I3PqP1vOB1faE2vyN1/WD1PSB2PuE2vzQ7Pak4PaB0/OG0/J/0fGn3vSB1vmC2PqE2vmG2vq54vKf3fWrLzam4PaO1fGD1faP2PWE2fyD2fyD2fzW8PrD5PG11+WXtcW55PVvbnqqOj+XmKaT1e+x3e1+obS4AACBjqGH0/F8vNh5hpuQNj+P2PZyX2+C1/SKEBOC2ftjKjApAAAdAAAJAACD1fXkAADg9Pu05/qb3fi1vsefoq+xhIildHu4PECpMjSkLDDPAQHtACoGAAAAPnRSTlMAEsOumWJVGwnuuT39zIMs/f388erpn3k4I/Tz8/Lmz7laSkH++/v7+vr5+PPv7ezk4M7OzcSdkZFwWTIVCIgxa4YAAADDSURBVBjTdY/VrgJBGIP/cWZm3XE77oLLLm7v/0BAgMANvWuapv3gqlwOboU05wm6esEijClHp1AIwVX49BiSxrFmM8vq0/J8s5yElgDIx/i3Yprl9+zNcUgCiGFj5rqV6n9W+pzUmmBT35DSnRZ/slLhoaZBq9QnkVmcLrK1U6A2NFUadGNSlc/b1UubIdDE80wsP/Df6xeO84cRK0hTYyZpZ9di9pGgQQLP8BUffPfOP+tU4YiJ8XB0wUp4XR/CO+B76XMRs7uqu4cAAAAASUVORK5CYII=",
  41. },
  42. eight: {
  43. base: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAA6lBMVEUAAAAxV2oFSGADP1Y1Wm0zWW1EY3Q1VmgpU2g2VWYkUGUyUWIiTWEAKUcFSmErU2c5W24zVWghUWckTmIKSmEZTmUxUmMwUWP1+fxcc4HL6f7j9f/g7PWi1fWWprFJZXUeSl7Z7v6Myu7d4OOjt8SKrcOSrsFMa30/YnT////C5v/Q6PmHz/nt7/Gw1e+bzu+Dx+7A1+d7wOa+ztqtx9m5v8Sds8KDn7J0lqqJl6GEkZtJfZlRb4E5Zn1Ybnw0YHc3V2kPSWAAQVqY0vZpstqMu9mIutjO0tV5rcyru8dfm7xTmLthh51TfpcZngh2AAAAGHRSTlMA1cx8MBoH8fHm5s3Kv5qUiFhYUFDxmpptBtYcAAAAzUlEQVQY0zWOVRKDQBBEB4+77OLB4+5B43b/64QU8P76VddMQwpTyWGFbEAGXTjoaLxWqmlmSRnF9HUl7bSwKO49fO+vyURQsvCMiDZGY+Xfb1JlQ9iENcJDCMf3SqetOe8tgzDaIz0HTH43GfD8sLfcGCI6VKD7laamtYqNIIhygQHudj6qarDi54aMSRrA1jSfqKkWb5WpFhv/cBYzlyNUc7ClkgnF1+hh2x9pcmomou5cRtpUknYlFhKqznsxux7zNGTUi77LdZg0/QBWixhGoJiKLQAAAABJRU5ErkJggg==",
  44. unread: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAtFBMVEUAAAAAYYUAYYQAXIAAW38AYYMAY4YAXYAAYIQAZIcAY4Udc5MAWX8BZogldJQAWn6L2fIRa4xy0faB2/xt0vqI3Ph+0vBdxO1Wp8c/i6l41fhPwO1Ivu1Lo8dEoMYegaYYcZMEa46X5f1NxvZfyfNZyPOI1OxqwuV3w947rttputpdttldq8porscrmcNRnbtEmLpOmLU9krQFb5MHZIVIueY6tuaAyeJOstp1vNRCp9Auh6qJsLovAAAADnRSTlMAWfLRzujVm5EzHAqLiBrNc7IAAADDSURBVBjTNY9XEsIwEEPd0mFtp4d0eocU+v3vRRic9yeNZldCCtsgIBlGI5aWbMX6JE2lHdoEgRDzh1QZDGGYALzmJ/Y3JhfvyHUCYi1/eWzS2Iv5VAchYLhH4ZpvZvue8yTYEmRrbeT7rjvbx8cwSAxklMuoKA6D43lho9mI7FYl5/3B3cQXoBZCkKW1PuWFn7MJdoYf9T3riM5z/6pasvPiXVWfKKrGknK3SNPVsqWO2mHK8y17lpqFRjCrO2LYSn0BV4wQhK3VgFcAAAAASUVORK5CYII=",
  45. notif: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAABEVBMVEUAAAAAW38AYoUAYoQAYYQAZYgAYYMAZIYAXoEBZom1AwbrAABGAAAALT4AAAAAYYRHAAAAAAAadJTeAAABZohHGymuAABFAACcAAAAAAAAAAAAUnIAZ4oAAAAQao2M2/Qic5Rw0/pKv+1EoscAXoD5AAB31fhfxO0egaYpdpUAa40AWX3/AgK/AAC6AABNAACX5f2B2/xr0fqH2/dNxvZy0fVfyfNZyPN80e+I1OxQv+x7xuA7rttmu9ldttlorsddqsdUpsZLocMrmcNEmLpPm7g+iqkFb5MZcJJYwu1IueY6tuZoweROstp1vNRdsdBCp9Bwt8xAk7U6kbRYnbEuh6oJZYWZdYM1RF0AKDwAHjhOZ3nrAAAAHnRSTlMA0FnyjRzo1psy/v785K2UiVwL+PLu6OPLyL6nm49luDk4AAAA4ElEQVQY0zWP5XLDMBAGT5YpnJRRkiHGOg0zc7lheP8HiTN29t/t7NzMByGCjBlBHFyIiNMWbfRIKryv0KRUojTfImHD2fX6lNm/+R4KBD8p9F0J27RBzj3Ho0FhoPOJw4Iy/x9i4+G30dH3UW+3vgFBXJlFRakY/ffsm3d8Admqfmhat2J0fJFLCIDbNcvVt13l7yyeAJjadCRe15RhPJaNJgHIlzrDkqsVx8/38TQAoFH5fz7fmKZzex27ewXgSLusNmvVJUrnvIeMn6TI6FP9scQIJB8zwRDkzLAsQMAJImsd3S869hIAAAAASUVORK5CYII=",
  46. },
  47. eight_dark: {
  48. base: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAA2FBMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAmKCkAAAADBAQAAAD1+fxMTEzM6v4vLy7i9f/E6P/v8PKMye2frbiFo7aNpLQ1P0b////Z7/+I0PrQ6Pmk1vab0/bi7PXe6fKfz+6w1O2Cxu15veS/1OPY2dq8ydOrwdKHtdK1traZqbSQmqF8kZ+QlJhshJSBhIZ7fH0+YnY9R09GREMOIiwfJivp+f/X7fyYzO3g4OBmrtTMzMt0pcKnsrpWjq1Ii6xUcIEyZX8ENUgYNkYAKD1NaNryAAAADnRSTlMAzPGY5tV8WFAwGge/iAmaI18AAADKSURBVBjTNY7XFoIwEEQ3CR1NQi/23rsC9q7//0fiAe7bzLlndyBHI5ibSIYCVdgMmTM1SZ51tGQplZOZOwpvt9YRv1SmKCukuXWjIubMMf++IpU8a0bLYuQwnt5Du4U/qI4opWs2xKAJq65tG/XDaOa12IYA2TY6fjA26tW91V4KGuBJ85XaY2PgzTlSAULX/YplGhhBSVL09Efc732wSH17IWUT0L12DcNnp7tTskJOzjX32GyskA4ZJHn0e5OtoEKBjOI3Jlqefl82Et7gg4sWAAAAAElFTkSuQmCC",
  49. unread: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAA6lBMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEAAADb3NxEQ0IAAAAAAABISEgAAAAAAAAlKCk9NzGurq6cnJwAAAD+/v73+Pi6u7tNTExGREMwLy7N6v61trY1QEfE6P/t7u+dzu6Mye2rwdKForaNpLTy+f/h9f/Z7//S6vuI0Pqk1vbg6/Sw1O2Cxu2+0uCHtdLKzM2qtbycqrV8kaCPl51shJV8f4I+YnYOIiwfJiud1PaY0vbo6Oh+vuR0vOPg4OBmrtS8ydN0pcLAwMBWjq1Ii6yjpaZUcIEyZX88R08ENUgYNkYAKD0sty6hAAAAFXRSTlMAzZjyWeUa1owv/Oe/roh8UAf26MtVcWhmAAAA10lEQVQY0zWP1ZLDMBRDr+04SbkLpjgMZeZ2mfn/f2ebqas3aTQaHTCyMVEaWXBWhW6rIg40Nv4CrTsdIfijNp22Go22nprzAJ2C1rq7YA5RItZlv92qL7tL5lI25+q4h75X+bT3zHQg5QcBm27GUeT3ewuPc8kw4N0wyYvM7089wSW1gcxuvxhjmZ+VQR0gTNM/x2WFX1zGPGgA7B8mv8RhebTCNa8JAOht8BKGn8k4rFXvrlwA6/A0SJOb4QY15et1+Rwf3u8nsx2tQMPQWmj/Q7BtOP8BG/MW/0ZJlAQAAAAASUVORK5CYII=",
  50. notif: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAABDlBMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAQEAAAAAAAAAAAC2AQHrAAA4AAAAAAAAAABIAgIAAAAAAAAlKCneAABDAACuAABFAACcAAADBAQAAAAAAAC8AABNTE3+AQHN6v72+PmKo7QvLy5GAAD+/v/E6P+Myu6rwtLy+f/h9f/Z7//S6vuI0Pqk1vab0/bg6/Sfz+6w1O2Cxu2+0uHc3d2HtdLKzc61tracqrV8kaBshJV8f4I+YnY5R081QUkOIiwfJiv3AADt7u/r7e6YzO1+vuR0vONmrtS8ydN0pcKtucGnsrqQqLeCordWjq1Ii6yjqaqQmqGOk5i3hYhUcIEyZX8ENUgYNkZGREM1PEEAKD1ovUZyAAAAGnRSTlMAzfLlmliSGtYv/v77v66IfFAH+Ozo4suIXxNuy1MAAADcSURBVBjTNY9VjkJBFERvyxMcxud2Pxfc3cfdffa/ESA09VeVk0oOqBicSo9osEuCTbrO+cDTVd8j78Wi4wRdTzEF2WhMfmU/GJDtkH8rPaBJpXPhbfhCPjMsDVFn2A/k+o/MRm6nfIvppPh4pWCwcSsM7Ur53rcsgRz4tNZ8cmO70vHPLMEMoL36FyLGdrwZMgDzKPo3T9C1H9MpK5kFWFy3f6iJbjjiR34OAMhz9W7+99lszQ72U4c6gLa8qUaX9dqY5IQ4PgUAffly1e5NWQKyylYji2/KDeW5AobCG0Lco88iAAAAAElFTkSuQmCC",
  51. },
  52. pixel: {
  53. base: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAYFBMVEUAAAAAAAAAAABiam00WmxcZWlFTE4jQE1GTU8jQU4yV2g8QUIbNkE/WGRWYWY9WGUAAAAAAADg8vnY6fB5yOyA0fW95fi23fB+z/PQ4Od0weNoeYFWcoBSXmNFS00jP0wFxP2oAAAAEnRSTlMAIRHe3v3w8Nzc/ezs/u7u7EbfERAsAAAAcUlEQVQY05VP1wqAMAw0de82TYf7//9SOhBFELy3G1wuyQcYYw8OVVHBzYJUokzBW3VZg+MYFMhI0ThMMm9zlIXnVmmzbFy0iHtSkmq63mhtYuJQthO8X40OHSHR0DwO4UrsoAziDq84HudcO15P/MAJxpAGt0Xg7i0AAAAASUVORK5CYII=",
  54. unread: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAYFBMVEUAAAAATHcAIGJii6I0gKFFepYjdJVGepYjdJUAXIFeiaEyf6A8dZIbcJJThZ8/f55WhZ89f58AY4YAM2ng8/p/0vbY6/R5zPC95vq23/TQ5O10xelolKxWj6tFepUjdJUJarSxAAAAFHRSTlMAIBHe3vDw3Nwj/f3s7P7+7u7sRkWW9w4AAAB4SURBVBjTlY/ZDoUwCERL3fcNSnsX/f+/tEpjNCYm8nZgMjOoh9FaXxiyJIPTCSKDJoL9lKc5bIx+U3iOmXjoP6asSzSJKjwTWfdrx6lGnFXKVDWds9a1oliImmns/s6Khygq/g69pAQPjiH0kBTPoc7R4/bEi1kBHCgHTIPhydUAAAAASUVORK5CYII=",
  55. notif: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAhFBMVEUAAAAAV34AIDg0gKEkdJQjdJUUAAMAAEAyf6AbcJI7dZA/f55MAABAgaBEeZVljaVUhJ4AZIZpkKYAQmp5y/B+0fVrS0224PPf8vnP4uu85vqA0/fY6/NUIivtAABNHynjAAB0xelolKxWj6tNgZpFepUjdJVOTU9fREtAREvaAABiAADosgfnAAAAFHRSTlMAIBHe8NzcIP3s3v7x7u7e7uzeRqhOt1sAAACESURBVBjTlY9ZDoMwDERxCHvp6mASSKH7dv/71YWoAiEh4b83Ho3H3swIIUYchaswGqwgta1N4ccQyABgr4tjoTfA7KPCfKtrFsp63bGqDJ2vloXy5klUcZKRMfRsO8e9Uslhl12IXp8HZ/SOGE+5bN4NX3EZ6IPr0SvMrs6/x+SJBfMF/doItAPX5RIAAAAASUVORK5CYII=",
  56. },
  57. pixel_alt: {
  58. base: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAY1BMVEUAAAAATHcAIGJii6I0gKFFepYjdJVGepYjdJUAXIFeiaEyf6A8dZIbcJJThZ8/f55WhZ89f58AY4YAQmoAJGjg8/p/0vbY6/R5zPC95vq23/TQ5O10xelolKxWj6tFepUjdJUOfB3LAAAAFXRSTlMAIBHe3vDw3Nwj/f3s7P7+7u7sRkaqQLDYAAAAeUlEQVQY05WP6Q6EMAiES73vYxdKu4e+/1NapTEaExP598FkZlA3o7U+MWRJBocTRAZNBNspT3NYGf2m8Bwz8dB/TFmXaBJVeCay7teO7xpxUilT1XTOWteKYiZqXmP3d1Y8RFHxd+glJXhwDKGHpHgOdfYelycezAI9gweHH0yPzQAAAABJRU5ErkJggg==",
  59. unread: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAhFBMVEUAAAAjR1sKChFJeZBBa4FAZ3tCbIEVFRYABA9IeI9OaXlReI1MTExSeo9WcYBxh5RjfYw4WGt0ipcdKTh7y+6A0vdqbm/f8fi85fl+0PTY6vK23fDt7OzP4OdVWFlOUlS44PPj4+NZdITR4+t3xefa2tpyj55jiZ1Ba4BgZ2tiYF9ATFKjqzLCAAAAFHRSTlMAIBHe8O3c3CD93v7x7u7e7uzeRvBdAgMAAACFSURBVBjTlY9ZDoMwDERxIKylqwOBlATo3t7/fjVpVIGQkPDfG4/GY29hGGMTjqNNFI9WkOpOpzAwhEEIcGyKc9HsgNhHgfn+UpNQ1lvLlZDtvdcklL0XoEh4pqRUr846nqLip0P2aNXnbSjj50jwmnNzM3TFZaAProdVBnZ1/j1mT6yYLxHCCNKMC/PDAAAAAElFTkSuQmCC",
  60. notif: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAdVBMVEUAAAAkSFwKChFAaX9JeZAABBhCbIEVAABIeI9yiJVObnxReI1MAABSeo9WcYBjfYw4WGsdKTiA0vd7y+5rS0233/JSHiPf8vnP4um85fnY6/LtAABZdobjAAB3xedyj55jiZ1Ba4BOTU9gREtAREvaAABiAADFYUIqAAAAEnRSTlMAIRHv3iDc3P3e3v7x7u7u7EYDRb2ZAAAAgklEQVQY05WPWQ7CMAxE67jpSlnsOk1Kyw73PyJJFCEQUqX6741H43G2MEqpH9blptRfK2jMbBoIDAUWoGvbH3tbg+ecibudHb0wjNvIRE7Ok/HCMGXIVGErzsl9jo4rER727UXk8bqFjOio+NSheYYrKYNzSD2iEjjV+fT4e2LFvAFuzge5hY5wYgAAAABJRU5ErkJggg==",
  61. }
  62. };
  63. let currentStyle = "default";
  64. let currentState = "base";
  65. function removeFavicons() {
  66. document.querySelectorAll('link[rel="icon"], link[rel="shortcut icon"]').forEach(link => link.remove());
  67. }
  68. function insertFavicon(href) {
  69. const link = document.createElement('link');
  70. link.rel = 'icon';
  71. link.type = 'image/png';
  72. link.href = href;
  73. document.head.appendChild(link);
  74. }
  75. async function getUserFaviconStyle() {
  76. let style = "default";
  77. try {
  78. style = await getSetting("customFavicon_faviconStyle");
  79. } catch { }
  80. if (!STYLES.includes(style)) style = "default";
  81. return style;
  82. }
  83. async function setFaviconStyle(style, state = "base") {
  84. if (!STYLES.includes(style)) style = "default";
  85. if (!STATES.includes(state)) state = "base";
  86. const url = (FAVICON_DATA?.[style]?.[state]) || FAVICON_DATA.default.base;
  87. removeFavicons();
  88. insertFavicon(url);
  89. currentStyle = style;
  90. currentState = state;
  91. document.dispatchEvent(new CustomEvent("faviconStateChanged", {
  92. detail: { style, state }
  93. }));
  94. }
  95. async function setFavicon(state = "base") {
  96. if (!STATES.includes(state)) state = "base";
  97. const style = await getUserFaviconStyle();
  98. await setFaviconStyle(style, state);
  99. }
  100. async function resetFavicon() {
  101. await setFavicon("base");
  102. }
  103. function getCurrentFaviconState() {
  104. return { style: currentStyle, state: currentState };
  105. }
  106.  
  107. return {
  108. setFavicon,
  109. setFaviconStyle,
  110. resetFavicon,
  111. getCurrentFaviconState,
  112. STYLES,
  113. STATES
  114. };
  115. })();
  116. onReady(async function () {
  117. "use strict";
  118. const divThreads = document.getElementById("divThreads");
  119. const innerOP = document.querySelector(".innerOP");
  120. const divPosts = document.querySelector(".divPosts");
  121. const opHeadTitle = document.querySelector(".opHead.title");
  122. const scriptSettings = {
  123. site: {
  124. _siteTWTitle: { type: "title", label: ":: Thread Watcher" },
  125. _siteSection1: { type: "separator" },
  126. alwaysShowTW: { label: "Pin Thread Watcher", default: false },
  127. autoExpandTW: { label: "Auto Expand Thread Watcher", default: false },
  128. _siteSiteTitle: { type: "title", label: ":: Site" },
  129. _siteSection2: { type: "separator" },
  130. customFavicon: {
  131. label: "Custom Favicon",
  132. default: false,
  133. subOptions: {
  134. faviconStyle: {
  135. label: "Favicon Style",
  136. type: "select",
  137. default: "default",
  138. options: [
  139. { value: "default", label: "Default" },
  140. { value: "pixel", label: "Pixel" },
  141. { value: "pixel_alt", label: "Pixel Alt" },
  142. { value: "eight", label: "Eight" },
  143. { value: "eight_dark", label: "Eight Dark" }
  144. ]
  145. }
  146. }
  147. },
  148. enableBottomHeader: { label: "Bottom Header", default: false },
  149. enableAutoHideHeaderScroll: { label: "Auto-hide Header On Scroll", default: false },
  150. enableHeaderCatalogLinks: {
  151. label: "Header Catalog Links",
  152. default: true,
  153. subOptions: {
  154. openInNewTab: {
  155. label: "Always open in new tab",
  156. default: false,
  157. }
  158. }
  159. },
  160. enableScrollArrows: { label: "Show Up/Down Arrows", default: false },
  161. _siteMediaTitle: { type: "title", label: ":: Media" },
  162. _siteSection3: { type: "separator" },
  163. enableThreadImageHover: { label: "Thread Image Hover", default: true },
  164. blurSpoilers: {
  165. label: "Blur Spoilers",
  166. default: false,
  167. subOptions: {
  168. removeSpoilers: {
  169. label: "Remove Spoilers",
  170. default: false
  171. }
  172. }
  173. },
  174. enableMediaViewer: {
  175. label: "Enable Advanced Media Viewer",
  176. default: false,
  177. subOptions: {
  178. viewerStyle: {
  179. label: "Style",
  180. type: "select",
  181. default: "native",
  182. options: [
  183. { value: "native", label: "Native" },
  184. { value: "topright", label: "Pin Top Right" },
  185. { value: "topleft", label: "Pin Top Left" }
  186. ]
  187. }
  188. }
  189. },
  190. hoverVideoVolume: { label: "Hover Media Volume (0-100%)", default: 50, type: "number", min: 0, max: 100 }
  191. },
  192. threads: {
  193. _threadsNotiTitle: { type: "title", label: ":: Notifications" },
  194. _threadsSection1: { type: "separator" },
  195. beepOnYou: { label: "Beep on (You)", default: false },
  196. notifyOnYou: {
  197. label: "Tab Notification when (You) (!)",
  198. default: true,
  199. subOptions: {
  200. customMessage: {
  201. label: "Custom Text (max: 8 chars.)",
  202. default: "",
  203. type: "text",
  204. maxLength: 9
  205. }
  206. }
  207. },
  208. _threadsNavTitle: { type: "title", label: ":: Navigation & Others" },
  209. _threadsSection3: { type: "separator" },
  210. enableScrollSave: {
  211. label: "Save Scroll Position",
  212. default: true,
  213. subOptions: {
  214. showUnreadLine: {
  215. label: "Show Unread Line",
  216. default: true,
  217. }
  218. }
  219. },
  220. quoteThreading: { label: "Enable Quote Threading", default: false },
  221. enableHashNav: { label: "Hash Navigation", default: false },
  222. threadStatsInHeader: { label: "Thread Stats in Header", default: false },
  223. watchThreadOnReply: { label: "Watch Thread on Reply", default: true },
  224. scrollToBottom: { label: "Don't Scroll to Bottom on Reply", default: true },
  225. deleteSavedName: { label: "Delete Name Checkbox", default: false }
  226. },
  227. catalog: {
  228. enableCatalogImageHover: { label: "Catalog Image Hover", default: true },
  229. enableThreadHiding: { label: "Enable Thread Hiding", default: false },
  230. openCatalogThreadNewTab: { label: "Always Open Threads in New Tab", default: false }
  231. },
  232. styling: {
  233. _stylingSiteTitle: { type: "title", label: ":: Site Styling" },
  234. _stylingSection1: { type: "separator" },
  235. hideAnnouncement: { label: "Hide Announcement", default: false },
  236. hidePanelMessage: { label: "Hide Panel Message", default: false },
  237. hidePostingForm: {
  238. label: "Hide Posting Form",
  239. default: false,
  240. subOptions: {
  241. showCatalogForm: {
  242. label: "Don't Hide in Catalog",
  243. default: false
  244. }
  245. }
  246. },
  247. hideBanner: { label: "Hide Board Banners", default: false },
  248. hideDefaultBL: { label: "Hide Default Board List", default: true },
  249. hideNoCookieLink: { label: "Hide No Cookie? Link", default: false },
  250. hideJannyTools: { label: "Hide Janitor Forms", default: false },
  251. _stylingThreadTitle: { type: "title", label: ":: Thread Styling" },
  252. _stylingSection2: { type: "separator" },
  253. highlightOnYou: { label: "Style (You) posts", default: true },
  254. enableStickyQR: { label: "Enable Sticky Quick Reply", default: false },
  255. fadeQuickReply: { label: "Fade Quick Reply", default: false },
  256. enableFitReplies: { label: "Fit Replies", default: false },
  257. enableSidebar: {
  258. label: "Enable Sidebar",
  259. default: false,
  260. subOptions: {
  261. leftSidebar: {
  262. label: "Sidebar on Left",
  263. default: false
  264. }
  265. }
  266. },
  267. threadHideCloseBtn: { label: "Hide Inline Close Button", default: false },
  268. hideHiddenPostStub: { label: "Hide Stubs of Hidden Posts", default: false, },
  269. hideCheckboxes: { label: "Hide Post Checkbox", default: false }
  270. },
  271. miscel: {
  272. enableShortcuts: { label: "Enable Keyboard Shortcuts", type: "checkbox", default: true },
  273. enhanceYoutube: { label: "Enhanced Youtube Links", type: "checkbox", default: true },
  274. highlightNewIds: {
  275. label: "Highlight New IDs",
  276. default: false,
  277. subOptions: {
  278. idHlStyle: {
  279. label: "Highlight Style",
  280. type: "select",
  281. default: "moetext",
  282. options: [
  283. { value: "moetext", label: "Moe" },
  284. { value: "glow", label: "Glow" },
  285. { value: "dotted", label: "Border" }
  286. ]
  287. }
  288. }
  289. },
  290. enableIdFilters: { label: "Show only posts by ID when ID is clicked", type: "checkbox", default: true },
  291. switchTimeFormat: { label: "Enable 12-hour Clock (AM/PM)", default: false },
  292. truncFilenames: {
  293. label: "Truncate filenames",
  294. default: false,
  295. subOptions: {
  296. customTrunc: {
  297. label: "Max filename length (min: 5, max: 50)",
  298. default: 15,
  299. type: "number",
  300. min: 5,
  301. max: 50
  302. }
  303. }
  304. }
  305. }
  306. };
  307.  
  308. Object.freeze(scriptSettings);
  309. let flatSettings = null;
  310. function flattenSettings() {
  311. if (flatSettings !== null) return flatSettings;
  312. const result = {};
  313. Object.keys(scriptSettings).forEach((category) => {
  314. Object.keys(scriptSettings[category]).forEach((key) => {
  315. if (key.startsWith('_')) return;
  316. result[key] = scriptSettings[category][key];
  317. if (!scriptSettings[category][key].subOptions) return;
  318. Object.keys(scriptSettings[category][key].subOptions).forEach(
  319. (subKey) => {
  320. const fullKey = `${key}_${subKey}`;
  321. result[fullKey] =
  322. scriptSettings[category][key].subOptions[subKey];
  323. }
  324. );
  325. });
  326. });
  327. flatSettings = Object.freeze(result);
  328. return flatSettings;
  329. }
  330. flattenSettings();
  331. async function getSetting(key) {
  332. if (!flatSettings[key]) {
  333. console.warn(`Setting key not found: ${key}`);
  334. return false;
  335. }
  336. let val;
  337. try {
  338. val = await GM.getValue("8chanSS_" + key, null);
  339. } catch (err) {
  340. console.error(`Failed to get setting for key ${key}:`, err);
  341. return flatSettings[key]?.default ?? false;
  342. }
  343. if (val === null) return flatSettings[key].default;
  344. if (flatSettings[key].type === "number") return Number(val);
  345. if (flatSettings[key].type === "text") return String(val).replace(/[<>"']/g, "").slice(0, flatSettings[key].maxLength || 32);
  346. if (flatSettings[key].type === "textarea") return String(val);
  347. if (flatSettings[key].type === "select") return String(val);
  348. return val === "true";
  349. }
  350.  
  351. async function setSetting(key, value) {
  352. try {
  353. await GM.setValue("8chanSS_" + key, String(value));
  354. } catch (err) {
  355. console.error(`Failed to set setting for key ${key}:`, err);
  356. }
  357. }
  358. async function featureCssClassToggles() {
  359. document.documentElement.classList.add("8chanSS");
  360. const enableSidebar = await getSetting("enableSidebar");
  361. const enableSidebar_leftSidebar = await getSetting("enableSidebar_leftSidebar");
  362.  
  363. const classToggles = {
  364. enableFitReplies: "fit-replies",
  365. enableSidebar_leftSidebar: "ss-leftsidebar",
  366. enableStickyQR: "sticky-qr",
  367. fadeQuickReply: "fade-qr",
  368. enableBottomHeader: "bottom-header",
  369. hideHiddenPostStub: "hide-stub",
  370. hideBanner: "disable-banner",
  371. hidePostingForm: "hide-posting-form",
  372. hidePostingForm_showCatalogForm: "show-catalog-form",
  373. hideDefaultBL: "hide-defaultBL",
  374. hidePanelMessage: "hide-panelmessage",
  375. highlightOnYou: "highlight-you",
  376. threadHideCloseBtn: "hide-close-btn",
  377. hideCheckboxes: "hide-checkboxes",
  378. hideNoCookieLink: "hide-nocookie",
  379. autoExpandTW: "auto-expand-tw",
  380. hideJannyTools: "hide-jannytools"
  381. };
  382. if (enableSidebar && !enableSidebar_leftSidebar) {
  383. document.documentElement.classList.add("ss-sidebar");
  384. } else {
  385. document.documentElement.classList.remove("ss-sidebar");
  386. }
  387. const settingKeys = Object.keys(classToggles);
  388. const settingValues = await Promise.all(settingKeys.map(getSetting));
  389. settingKeys.forEach((key, i) => {
  390. const className = classToggles[key];
  391. if (settingValues[i]) {
  392. document.documentElement.classList.add(className);
  393. } else {
  394. document.documentElement.classList.remove(className);
  395. }
  396. });
  397. const path = window.location.pathname.toLowerCase();
  398. const urlClassMap = [
  399. { pattern: /\/catalog\.html$/i, className: "is-catalog" },
  400. { pattern: /\/res\/[^/]+\.html$/i, className: "is-thread" },
  401. { pattern: /\/[^/]+\/$/i, className: "is-index" },
  402. ];
  403.  
  404. urlClassMap.forEach(({ pattern, className }) => {
  405. if (pattern.test(path)) {
  406. document.documentElement.classList.add(className);
  407. } else {
  408. document.documentElement.classList.remove(className);
  409. }
  410. });
  411. }
  412. featureCssClassToggles();
  413. async function featureSidebar() {
  414. const enableSidebar = await getSetting("enableSidebar");
  415. const enableSidebar_leftSidebar = await getSetting("enableSidebar_leftSidebar");
  416.  
  417. const mainPanel = document.getElementById("mainPanel");
  418. if (!mainPanel) return;
  419.  
  420. if (enableSidebar && enableSidebar_leftSidebar) {
  421. mainPanel.style.marginLeft = "19rem";
  422. mainPanel.style.marginRight = "0";
  423. } else if (enableSidebar) {
  424. mainPanel.style.marginRight = "19rem";
  425. mainPanel.style.marginLeft = "0";
  426. } else {
  427. mainPanel.style.marginRight = "0";
  428. mainPanel.style.marginLeft = "0";
  429. }
  430. }
  431. featureSidebar();
  432. const currentPath = window.location.pathname.toLowerCase();
  433. const currentHost = window.location.hostname.toLowerCase();
  434.  
  435. let css = "";
  436.  
  437. if (/^8chan\.(se|moe)$/.test(currentHost)) {
  438. css += ":not(.is-catalog) body{margin:0}#sideCatalogDiv{z-index:200;background:var(--background-gradient)}#navFadeEnd,#navFadeMid,.watchedNotification::before,:root.disable-banner #bannerImage,:root.hide-announcement #dynamicAnnouncement,:root.hide-checkboxes .deletionCheckBox,:root.hide-close-btn .inlineQuote>.innerPost>.postInfo.title>a:first-child,:root.hide-jannytools #actionsForm,:root.hide-jannytools #boardContentLinks,:root.hide-nocookie #captchaBody>table:nth-child(2)>tbody:first-child>tr:nth-child(2),:root.hide-panelmessage #panelMessage,:root.hide-posting-form #postingForm{display:none}:root.hide-defaultBL #navTopBoardsSpan{display:none!important}:root.is-catalog.show-catalog-form #postingForm{display:block!important}footer{visibility:hidden;height:0}nav.navHeader{z-index:300}nav.navHeader>.nav-boards:hover{overflow-x:auto;overflow-y:hidden;scrollbar-width:thin}:not(:root.bottom-header) .navHeader{box-shadow:0 1px 2px rgba(0,0,0,.15)}:root.bottom-header nav.navHeader{top:auto!important;bottom:0!important;box-shadow:0 -1px 2px rgba(0,0,0,.15)}:root.highlight-you .innerOP:has(> .opHead.title > .youName),:root.highlight-you .innerPost:has(> .postInfo.title > .youName){border-left:dashed #68b723 2px}:root.highlight-you .innerPost:has(>.divMessage>.you),:root.highlight-you .innerPost:has(>.divMessage>:not(div)>.you),:root.highlight-you .innerPost:has(>.divMessage>:not(div)>:not(div)>.you){border-left:solid var(--subject-color) 2px}:root.fit-replies :not(.hidden).innerPost{margin-left:10px;display:flow-root}:root.fit-replies :not(.hidden,.inlineQuote).innerPost{margin-left:0}.originalNameLink{display:inline;overflow-wrap:anywhere;white-space:normal}.multipleUploads .uploadCell:not(.expandedCell){max-width:215px}:not(#media-viewer)>.imgExpanded,:not(#media-viewer)>video{max-height:90vh!important;object-fit:contain;width:auto!important}:not(:root.auto-expand-tw) #watchedMenu .floatingContainer{overflow-x:hidden;overflow-wrap:break-word}:root.auto-expand-tw #watchedMenu .floatingContainer{height:fit-content!important;padding-bottom:10px}.watchedCellLabel a::before{content:attr(data-board);color:#aaa;margin-right:4px;font-weight:700}.watchButton.watched-active::before{color:#dd003e!important}#media-viewer,#multiboardMenu,#settingsMenu,#watchedMenu{font-size:smaller;padding:5px!important;box-shadow:-3px 3px 2px 0 rgba(0,0,0,.19)}#watchedMenu,#watchedMenu .floatingContainer{min-width:200px;max-width:100vw}.watchedNotification::before{padding-right:2px}#watchedMenu .floatingContainer{scrollbar-width:thin;scrollbar-color:var(--link-color) var(--contrast-color)}.scroll-arrow-btn{position:fixed;right:50px;width:36px;height:35px;background:#222;color:#fff;border:none;border-radius:50%;box-shadow:0 2px 8px rgba(0,0,0,.18);font-size:22px;cursor:pointer;opacity:.7;z-index:800;display:flex;align-items:center;justify-content:center;transition:opacity .2s,background .2s}:root:not(.is-index,.is-catalog).ss-sidebar .scroll-arrow-btn{right:330px!important}.scroll-arrow-btn:hover{opacity:1;background:#444}#scroll-arrow-up{bottom:80px}#scroll-arrow-down{bottom:32px}.innerUtility.top{margin-top:2em;background-color:transparent!important;color:var(--link-color)!important}.innerUtility.top a{color:var(--link-color)!important}.bumpLockIndicator::after{padding-right:3px}.floatingMenu.focused{z-index:305!important}#quick-reply{padding:0}#media-viewer{padding:20px 0 0!important}#media-viewer.topright{top:26px!important;right:0!important;left:auto!important;max-height:97%!important;max-width:max-content!important}#media-viewer.topleft{top:26px!important;left:0!important;right:auto!important;max-height:97%!important;max-width:max-content!important}#media-viewer.topright::after{pointer-events:none}#media-viewer.topleft::after{pointer-events:none}.ss-chevron{transition:transform .2s;margin-left:6px;font-size:12px;display:inline-block}a.imgLink[data-filemime^='audio/'],a.originalNameLink[href$='.m4a'],a.originalNameLink[href$='.mp3'],a.originalNameLink[href$='.ogg'],a.originalNameLink[href$='.wav']{position:relative}.audio-preview-indicator{display:none;position:absolute;background:rgba(0,0,0,.7);color:#fff;padding:5px;font-size:12px;border-radius:3px;z-index:1000;left:0;top:0;white-space:nowrap;pointer-events:none}a.originalNameLink:hover .audio-preview-indicator,a[data-filemime^='audio/']:hover .audio-preview-indicator{display:block}.yt-icon{width:16px;height:13px;vertical-align:middle;margin-right:2px}.id-glow{box-shadow:0 0 12px var(--subject-color)}.id-dotted{border:2px dotted #fff}";
  439. }
  440. if (/\/res\/[^/]+\.html$/.test(currentPath)) {
  441. css += ":root.sticky-qr #quick-reply{display:block;top:auto!important;bottom:0}:root.sticky-qr.ss-sidebar #quick-reply{left:auto!important;right:0!important}:root.sticky-qr.ss-leftsidebar #quick-reply{left:0!important;right:auto!important}:root.sticky-qr #qrbody{resize:vertical;max-height:50vh;height:130px}#selectedDivQr,:root.sticky-qr #selectedDiv{display:inline-flex;overflow:scroll hidden;max-width:300px}#qrbody{min-width:300px}:root.bottom-header #quick-reply{bottom:28px!important}:root.fade-qr #quick-reply{padding:0;opacity:.7;transition:opacity .3s ease}:root.fade-qr #quick-reply:focus-within,:root.fade-qr #quick-reply:hover{opacity:1}#qrFilesBody{max-width:310px}#quick-reply{box-shadow:-3px 3px 2px 0 rgba(0,0,0,.19)}#unread-line{height:2px;border:none!important;pointer-events:none!important;background-image:linear-gradient(to left,rgba(185,185,185,.2),var(--text-color),rgba(185,185,185,.2));margin:-3px auto 0 auto;width:60%}:root.ss-sidebar #bannerImage{width:19rem;right:0;position:fixed;top:26px}:root.ss-sidebar.bottom-header #bannerImage{top:0!important}:root.ss-leftsidebar #bannerImage{width:19rem;left:0;position:fixed;top:26px}:root.ss-leftsidebar.bottom-header #bannerImage{top:0!important}.quoteTooltip{z-index:999}.nestedQuoteLink{text-decoration:underline dashed!important}:root.hide-stub .unhideButton{display:none}.quoteTooltip .innerPost{overflow:hidden}.inlineQuote .innerPost,.quoteTooltip .innerPost{box-shadow:-1px 1px 2px 0 rgba(0,0,0,.19)}.inlineQuote{margin-top:4px}.postInfo.title>.inlineQuote{margin-left:15px}.postCell.is-hidden-by-filter{display:none}.reply-inlined{opacity:.5;text-decoration:underline dashed!important;text-underline-offset:2px}.quote-inlined{opacity:.5;text-decoration:underline dashed!important;text-underline-offset:2px}.target-highlight{background:var(--marked-color);border-color:var(--marked-border-color);color:var(--marked-text-color)}.statLabel{color:var(--link-color)}.statNumb{color:var(--text-color)}.postCell::before{display:inline!important;height:auto!important}.threadedReplies{border-left:1px solid #ccc;padding-left:15px}";
  442. }
  443. if (/\/catalog\.html$/.test(currentPath)) {
  444. css += "#postingForm{margin:2em auto}#divTools>div:nth-child(5){float:left!important;margin-top:9px!important;margin-right:8px}";
  445. }
  446.  
  447. if (!document.getElementById('8chSS')) {
  448. const style = document.createElement('style');
  449. style.id = '8chSS';
  450. style.textContent = css;
  451. document.head.appendChild(style);
  452. }
  453. const featureMap = [
  454. { key: "enableScrollSave", fn: featureSaveScroll },
  455. { key: "watchThreadOnReply", fn: featureWatchThreadOnReply },
  456. { key: "blurSpoilers", fn: featureBlurSpoilers },
  457. { key: "enableHeaderCatalogLinks", fn: featureHeaderCatalogLinks },
  458. { key: "openCatalogThreadNewTab", fn: catalogThreadsInNewTab },
  459. { key: "deleteSavedName", fn: featureDeleteNameCheckbox },
  460. { key: "enableScrollArrows", fn: featureScrollArrows },
  461. { key: "alwaysShowTW", fn: featureAlwaysShowTW },
  462. { key: "scrollToBottom", fn: preventFooterScrollIntoView },
  463. { key: "enableThreadHiding", fn: featureCatalogHiding },
  464. { key: "switchTimeFormat", fn: featureLabelCreated12h },
  465. { key: "enableIdFilters", fn: enableIdFiltering },
  466. { key: "enhanceYoutube", fn: enhanceYouTubeLinks },
  467. { key: "threadStatsInHeader", fn: threadInfoHeader },
  468. { key: "enableHashNav", fn: hashNavigation },
  469. { key: "hideAnnouncement", fn: featureHideAnnouncement },
  470. { key: "enableAutoHideHeaderScroll", fn: autoHideHeaderOnScroll },
  471. { key: "enableMediaViewer", fn: mediaViewerPositioning },
  472. { key: "customFavicon", fn: enableFavicon },
  473. { key: "highlightNewIds", fn: featureHighlightNewIds },
  474. { key: "quoteThreading", fn: featureQuoteThreading },
  475. ];
  476. for (const { key, fn } of featureMap) {
  477. try {
  478. if (await getSetting(key)) {
  479. fn();
  480. }
  481. } catch (e) {
  482. console.error(`${fn.name || 'Feature'} failed:`, e);
  483. }
  484. }
  485. if (await getSetting("truncFilenames")) {
  486. try {
  487. const filenameLength = await getSetting("truncFilenames_customTrunc");
  488. truncateFilenames(filenameLength);
  489. } catch (e) {
  490. console.error("truncateFilenames failed:", e);
  491. }
  492. }
  493. async function enableFavicon() {
  494. try {
  495. const customFaviconEnabled = await getSetting("customFavicon");
  496. const selectedStyle = await getSetting("customFavicon_faviconStyle");
  497.  
  498. if (customFaviconEnabled) {
  499. if (selectedStyle && typeof selectedStyle === 'string') {
  500. await faviconManager.setFaviconStyle(selectedStyle);
  501. } else {
  502. console.warn("Invalid favicon style:", selectedStyle);
  503. await faviconManager.setFaviconStyle("eight_dark");
  504. }
  505. } else {
  506. await faviconManager.resetFavicon();
  507. }
  508. } catch (e) {
  509. console.error("Error updating favicon:", e);
  510. }
  511. }
  512. const isCatalogPage = /\/catalog\.html$/.test(window.location.pathname.toLowerCase());
  513. let imageHoverEnabled = false;
  514. try {
  515. if (isCatalogPage) {
  516. imageHoverEnabled = await getSetting("enableCatalogImageHover");
  517. } else {
  518. imageHoverEnabled = await getSetting("enableThreadImageHover");
  519. }
  520. if (imageHoverEnabled) {
  521. localStorage.removeItem("hoveringImage");
  522. featureImageHover();
  523. }
  524. } catch (e) {
  525. console.error("featureImageHover failed:", e);
  526. }
  527. async function featureSaveScroll() {
  528. function getDivPosts() {
  529. return document.querySelector(".divPosts");
  530. }
  531. const STORAGE_KEY = "8chanSS_scrollPositions";
  532. const UNREAD_LINE_ID = "unread-line";
  533. const MAX_THREADS = 150;
  534. const threadPagePattern = /^\/[^/]+\/res\/[^/]+\.html$/i;
  535. if (!threadPagePattern.test(window.location.pathname)) return;
  536. function getBoardAndThread() {
  537. const match = window.location.pathname.match(/^\/([^/]+)\/res\/([^/.]+)\.html$/i);
  538. if (!match) return null;
  539. return { board: match[1], thread: match[2] };
  540. }
  541. async function getAllSavedScrollData() {
  542. const saved = await GM.getValue(STORAGE_KEY, null);
  543. if (!saved) return {};
  544. try { return JSON.parse(saved); } catch { return {}; }
  545. }
  546. async function setAllSavedScrollData(data) {
  547. await GM.setValue(STORAGE_KEY, JSON.stringify(data));
  548. }
  549. function getCurrentPostCount() {
  550. const divPosts = getDivPosts();
  551. if (!divPosts) return 0;
  552. return divPosts.querySelectorAll(":scope > .postCell[id]").length;
  553. }
  554. function removeUnreadLineMarker() {
  555. const oldMarker = document.getElementById(UNREAD_LINE_ID);
  556. if (oldMarker && oldMarker.parentNode) {
  557. oldMarker.parentNode.removeChild(oldMarker);
  558. }
  559. }
  560. let lastSeenPostCount = 0;
  561. let unseenCount = 0;
  562. let tabTitleBase = null;
  563. let previousFaviconState = null;
  564. const customFaviconEnabled = await getSetting("customFavicon");
  565. async function updateTabTitle() {
  566. if (window.isNotifying) return;
  567. if (!tabTitleBase) tabTitleBase = document.title.replace(/^\(\d+\)\s*/, "");
  568. document.title = unseenCount > 0 ? `(${unseenCount}) ${tabTitleBase}` : tabTitleBase;
  569. const { style, state } = faviconManager.getCurrentFaviconState();
  570.  
  571. if (unseenCount > 0 && customFaviconEnabled) {
  572. if (state !== "unread") {
  573. previousFaviconState = { style, state };
  574. }
  575. faviconManager.setFaviconStyle(style, "unread");
  576. } else if (unseenCount == 0 && customFaviconEnabled) {
  577. if (state === "unread" && previousFaviconState) {
  578. faviconManager.setFaviconStyle(previousFaviconState.style, previousFaviconState.state);
  579. previousFaviconState = null;
  580. } else if (state === "unread") {
  581. faviconManager.setFavicon("base");
  582. }
  583. }
  584. }
  585. async function updateUnseenCountFromSaved() {
  586. const info = getBoardAndThread();
  587. if (!info) return;
  588. const allData = await getAllSavedScrollData();
  589. const key = `${info.board}/${info.thread}`;
  590. const saved = allData[key];
  591. const currentCount = getCurrentPostCount();
  592. lastSeenPostCount = (saved && typeof saved.lastSeenPostCount === "number") ? saved.lastSeenPostCount : 0;
  593. unseenCount = Math.max(0, currentCount - lastSeenPostCount);
  594. updateTabTitle();
  595. }
  596. let lastScrollY = window.scrollY;
  597. async function onScrollUpdateSeen() {
  598. const info = getBoardAndThread();
  599. if (!info || !(await getSetting("enableScrollSave"))) return;
  600. const posts = Array.from(document.querySelectorAll(".divPosts > .postCell[id]"));
  601. let maxIndex = -1;
  602. for (let i = 0; i < posts.length; ++i) {
  603. const rect = posts[i].getBoundingClientRect();
  604. if (rect.bottom > 0 && rect.top < window.innerHeight) maxIndex = i;
  605. }
  606. const currentCount = getCurrentPostCount();
  607. let newLastSeen = lastSeenPostCount;
  608.  
  609. if (window.scrollY > lastScrollY) {
  610. if (maxIndex >= 0 && currentCount > 0) {
  611. if ((window.innerHeight + window.scrollY) >= (document.body.offsetHeight - 20)) {
  612. newLastSeen = currentCount;
  613. } else {
  614. newLastSeen = Math.max(lastSeenPostCount, maxIndex + 1);
  615. }
  616. }
  617. if (newLastSeen !== lastSeenPostCount) {
  618. lastSeenPostCount = newLastSeen;
  619. let allData = await getAllSavedScrollData();
  620. const key = `${info.board}/${info.thread}`;
  621. if (!allData[key]) allData[key] = {};
  622. allData[key].lastSeenPostCount = lastSeenPostCount;
  623. allData[key].timestamp = Date.now();
  624. if (
  625. typeof allData[key].position !== "number" ||
  626. window.scrollY > allData[key].position
  627. ) {
  628. allData[key].position = window.scrollY;
  629. }
  630. await setAllSavedScrollData(allData);
  631. }
  632. unseenCount = Math.max(0, currentCount - lastSeenPostCount);
  633. updateTabTitle();
  634. }
  635. lastScrollY = window.scrollY;
  636. }
  637. async function saveScrollPosition() {
  638. const info = getBoardAndThread();
  639. if (!info || !(await getSetting("enableScrollSave"))) return;
  640.  
  641. const scrollPosition = window.scrollY;
  642. const timestamp = Date.now();
  643.  
  644. let allData = await getAllSavedScrollData();
  645. const keys = Object.keys(allData);
  646. if (keys.length >= MAX_THREADS) {
  647. keys.sort((a, b) => (allData[a].timestamp || 0) - (allData[b].timestamp || 0));
  648. for (let i = 0; i < keys.length - MAX_THREADS + 1; ++i) delete allData[keys[i]];
  649. }
  650.  
  651. const key = `${info.board}/${info.thread}`;
  652. if (!allData[key]) allData[key] = {};
  653. if (
  654. typeof allData[key].position !== "number" ||
  655. scrollPosition > allData[key].position
  656. ) {
  657. allData[key].position = scrollPosition;
  658. allData[key].timestamp = timestamp;
  659. await setAllSavedScrollData(allData);
  660. }
  661. }
  662. function scrollElementToViewportCenter(el) {
  663. if (!el) return;
  664. const rect = el.getBoundingClientRect();
  665. const elTop = rect.top + window.pageYOffset;
  666. const elHeight = rect.height;
  667. const viewportHeight = window.innerHeight;
  668. const scrollTo = elTop - (viewportHeight / 2) + (elHeight / 2);
  669. window.scrollTo({ top: scrollTo, behavior: "auto" });
  670. }
  671. async function restoreScrollPosition() {
  672. const info = getBoardAndThread();
  673. if (!info || !(await getSetting("enableScrollSave"))) return;
  674.  
  675. const allData = await getAllSavedScrollData();
  676. const key = `${info.board}/${info.thread}`;
  677. const saved = allData[key];
  678. if (!saved || typeof saved.position !== "number") return;
  679.  
  680. const anchor = window.location.hash ? window.location.hash.replace(/^#/, "") : null;
  681. const safeAnchor = anchor && /^[a-zA-Z0-9_-]+$/.test(anchor) ? anchor : null;
  682.  
  683. if (safeAnchor) {
  684. setTimeout(() => {
  685. const post = document.getElementById(safeAnchor);
  686. if (post && post.classList.contains("postCell")) {
  687. scrollElementToViewportCenter(post);
  688. }
  689. addUnreadLineAtSavedScrollPosition(saved.position, false);
  690. }, 25);
  691. return;
  692. }
  693. saved.timestamp = Date.now();
  694. await setAllSavedScrollData(allData);
  695.  
  696. setTimeout(() => addUnreadLineAtSavedScrollPosition(saved.position, true), 100);
  697. }
  698. async function addUnreadLineAtSavedScrollPosition(scrollPosition, centerAfter = false) {
  699. if (!(await getSetting("enableScrollSave_showUnreadLine"))) return;
  700. if (!divPosts) return;
  701. const posts = Array.from(divPosts.querySelectorAll(":scope > .postCell[id]"));
  702. let targetPost = null;
  703. for (let i = 0; i < posts.length; ++i) {
  704. const postTop = posts[i].offsetTop;
  705. if (postTop > scrollPosition) break;
  706. targetPost = posts[i];
  707. }
  708. if (!targetPost) return;
  709. removeUnreadLineMarker();
  710. const marker = document.createElement("hr");
  711. marker.id = UNREAD_LINE_ID;
  712. if (targetPost.nextSibling) {
  713. divPosts.insertBefore(marker, targetPost.nextSibling);
  714. } else {
  715. divPosts.appendChild(marker);
  716. }
  717. if (centerAfter) {
  718. setTimeout(() => {
  719. const markerElem = document.getElementById(UNREAD_LINE_ID);
  720. if (markerElem) {
  721. const rect = markerElem.getBoundingClientRect();
  722. const desiredY = window.innerHeight / 3;
  723. const scrollY = window.scrollY + rect.top - desiredY;
  724. window.scrollTo({ top: scrollY, behavior: "auto" });
  725. }
  726. }, 25);
  727. }
  728. }
  729. function observePostCount() {
  730. if (!divPosts) return;
  731. const observer = new MutationObserver(() => {
  732. updateUnseenCountFromSaved();
  733. });
  734. observer.observe(divPosts, { childList: true, subtree: false });
  735. }
  736. async function removeUnreadLineIfAtBottom() {
  737. if (!(await getSetting("enableScrollSave_showUnreadLine"))) return;
  738. const margin = 10;
  739. if ((window.innerHeight + window.scrollY) >= (document.body.offsetHeight - margin)) {
  740. removeUnreadLineMarker();
  741. }
  742. }
  743. window.addEventListener("beforeunload", () => {
  744. saveScrollPosition();
  745. });
  746.  
  747. document.addEventListener("DOMContentLoaded", () => {
  748. tabTitleBase = document.title.replace(/^\(\d+\)\s*/, "");
  749. updateTabTitle();
  750. });
  751. window.addEventListener("load", async () => {
  752. await restoreScrollPosition();
  753. await updateUnseenCountFromSaved();
  754. observePostCount();
  755. });
  756.  
  757. let scrollTimeout = null;
  758. window.addEventListener("scroll", () => {
  759. if (scrollTimeout) return;
  760. scrollTimeout = setTimeout(async () => {
  761. await onScrollUpdateSeen();
  762. await removeUnreadLineIfAtBottom();
  763. scrollTimeout = null;
  764. }, 100);
  765. });
  766. await restoreScrollPosition();
  767. await updateUnseenCountFromSaved();
  768. observePostCount();
  769. }
  770. async function featureHeaderCatalogLinks() {
  771. function debounce(fn, delay) {
  772. let timeout;
  773. return function (...args) {
  774. clearTimeout(timeout);
  775. timeout = setTimeout(() => fn.apply(this, args), delay);
  776. };
  777. }
  778.  
  779. async function appendCatalogToLinks() {
  780. const navboardsSpan = document.getElementById("navBoardsSpan");
  781. if (navboardsSpan) {
  782. const links = navboardsSpan.getElementsByTagName("a");
  783. const openInNewTab = await getSetting(
  784. "enableHeaderCatalogLinks_openInNewTab"
  785. );
  786.  
  787. for (let link of links) {
  788. if (
  789. link.href &&
  790. !link.href.endsWith("/catalog.html") &&
  791. !link.dataset.catalogLinkProcessed
  792. ) {
  793. link.href += "/catalog.html";
  794. link.dataset.catalogLinkProcessed = "1";
  795. if (openInNewTab) {
  796. link.target = "_blank";
  797. link.rel = "noopener noreferrer";
  798. } else {
  799. link.target = "";
  800. link.rel = "";
  801. }
  802. }
  803. }
  804. }
  805. }
  806. appendCatalogToLinks();
  807. const debouncedAppend = debounce(appendCatalogToLinks, 100);
  808. const config = { childList: true, subtree: true };
  809. const navboardsSpan = document.getElementById("navBoardsSpan");
  810. if (navboardsSpan && !navboardsSpan._catalogLinksObserverAttached) {
  811. const observer = new MutationObserver(debouncedAppend);
  812. observer.observe(navboardsSpan, config);
  813. navboardsSpan._catalogLinksObserverAttached = true;
  814. }
  815. }
  816. function catalogThreadsInNewTab() {
  817. const catalogDiv = document.querySelector('.catalogDiv');
  818. if (!catalogDiv) return;
  819. catalogDiv.querySelectorAll('.catalogCell a.linkThumb').forEach(link => {
  820. if (link.getAttribute('target') !== '_blank') {
  821. link.setAttribute('target', '_blank');
  822. }
  823. });
  824. catalogDiv.addEventListener('click', function (e) {
  825. const link = e.target.closest('.catalogCell a.linkThumb');
  826. if (link && link.getAttribute('target') !== '_blank') {
  827. link.setAttribute('target', '_blank');
  828. }
  829. });
  830. }
  831. function featureImageHover() {
  832. const MEDIA_MAX_WIDTH = "90vw";
  833. const MEDIA_OPACITY_LOADING = "0";
  834. const MEDIA_OPACITY_LOADED = "1";
  835. const MEDIA_OFFSET = 50;
  836. const MEDIA_BOTTOM_MARGIN = 3;
  837. const AUDIO_INDICATOR_TEXT = "▶ Playing audio...";
  838. function getExtensionForMimeType(mime) {
  839. const map = {
  840. "image/jpeg": ".jpg",
  841. "image/jpg": ".jpg",
  842. "image/jxl": ".jxl",
  843. "image/png": ".png",
  844. "image/apng": ".png",
  845. "image/gif": ".gif",
  846. "image/avif": ".avif",
  847. "image/webp": ".webp",
  848. "image/bmp": ".bmp",
  849. "video/mp4": ".mp4",
  850. "video/webm": ".webm",
  851. "video/x-m4v": ".m4v",
  852. "audio/ogg": ".ogg",
  853. "audio/mpeg": ".mp3",
  854. "audio/x-m4a": ".m4a",
  855. "audio/x-wav": ".wav",
  856. };
  857. return map[mime.toLowerCase()] || null;
  858. }
  859. function sanitizeUrl(url) {
  860. try {
  861. const parsed = new URL(url, window.location.origin);
  862. if ((parsed.protocol === "http:" || parsed.protocol === "https:") &&
  863. parsed.origin === window.location.origin) {
  864. return parsed.href;
  865. }
  866. } catch { }
  867. return "";
  868. }
  869. let floatingMedia = null;
  870. let cleanupFns = [];
  871. let currentAudioIndicator = null;
  872. let lastMouseEvent = null;
  873. const docElement = document.documentElement;
  874. const SCROLLBAR_WIDTH = window.innerWidth - docElement.clientWidth;
  875. function clamp(val, min, max) {
  876. return Math.max(min, Math.min(max, val));
  877. }
  878. function positionFloatingMedia(event) {
  879. if (!floatingMedia) return;
  880. const vw = window.innerWidth;
  881. const vh = window.innerHeight;
  882. const mw = floatingMedia.offsetWidth || 0;
  883. const mh = floatingMedia.offsetHeight || 0;
  884.  
  885. const MEDIA_BOTTOM_MARGIN_PX = window.innerHeight * (MEDIA_BOTTOM_MARGIN / 100);
  886.  
  887. let x, y;
  888. let rightX = event.clientX + MEDIA_OFFSET;
  889. let leftX = event.clientX - MEDIA_OFFSET - mw;
  890. if (rightX + mw <= vw - SCROLLBAR_WIDTH) {
  891. x = rightX;
  892. }
  893. else if (leftX >= 0) {
  894. x = leftX;
  895. }
  896. else {
  897. x = clamp(rightX, 0, vw - mw - SCROLLBAR_WIDTH);
  898. }
  899.  
  900. y = event.clientY;
  901. const maxY = vh - mh - MEDIA_BOTTOM_MARGIN_PX;
  902. y = Math.max(0, Math.min(y, maxY));
  903.  
  904. floatingMedia.style.left = `${x}px`;
  905. floatingMedia.style.top = `${y}px`;
  906. }
  907. function cleanupFloatingMedia() {
  908. cleanupFns.forEach(fn => { try { fn(); } catch { } });
  909. cleanupFns = [];
  910. if (floatingMedia) {
  911. if (["VIDEO", "AUDIO"].includes(floatingMedia.tagName)) {
  912. try {
  913. floatingMedia.pause();
  914. floatingMedia.srcObject = null;
  915. URL.revokeObjectURL(floatingMedia.src);
  916. floatingMedia.removeAttribute("src");
  917. floatingMedia.load();
  918. } catch { }
  919. }
  920. floatingMedia.remove();
  921. floatingMedia = null;
  922. }
  923. if (currentAudioIndicator && currentAudioIndicator.parentNode) {
  924. currentAudioIndicator.parentNode.removeChild(currentAudioIndicator);
  925. currentAudioIndicator = null;
  926. }
  927. }
  928. function getFullMediaSrc(thumbNode, filemime) {
  929. const thumbnailSrc = thumbNode.getAttribute("src");
  930. if (!filemime) {
  931. if (
  932. thumbNode.closest('.catalogCell') ||
  933. /^\/\.media\/t?_[a-f0-9]{40,}$/i.test(thumbnailSrc.replace(/\\/g, ''))
  934. ) {
  935. return thumbnailSrc;
  936. }
  937. return null;
  938. }
  939. const parentA = thumbNode.closest("a.linkThumb, a.imgLink");
  940. const fileWidth = parentA ? parseInt(parentA.getAttribute("data-filewidth"), 10) : null;
  941. const fileHeight = parentA ? parseInt(parentA.getAttribute("data-fileheight"), 10) : null;
  942. const isSmallImage = (fileWidth && fileWidth < 220) || (fileHeight && fileHeight < 220);
  943. if (
  944. isSmallImage &&
  945. filemime && filemime.toLowerCase() === "image/png" &&
  946. !/\/t_/.test(thumbnailSrc) &&
  947. !/\.[a-z0-9]+$/i.test(thumbnailSrc)
  948. ) {
  949. return thumbnailSrc;
  950. }
  951. if (isSmallImage && thumbnailSrc.match(/\/\.media\/[^\/]+\.[a-zA-Z0-9]+$/)) {
  952. return thumbnailSrc;
  953. }
  954. if (/\/t_/.test(thumbnailSrc)) {
  955. let base = thumbnailSrc.replace(/\/t_/, "/");
  956. base = base.replace(/\.(jpe?g|jxl|png|apng|gif|avif|webp|webm|mp4|m4v|ogg|mp3|m4a|wav)$/i, "");
  957. if (filemime && (filemime.toLowerCase() === "image/apng" || filemime.toLowerCase() === "video/x-m4v")) {
  958. return base;
  959. }
  960.  
  961. const ext = filemime ? getExtensionForMimeType(filemime) : null;
  962. if (!ext) return null;
  963. return base + ext;
  964. }
  965. if (
  966. thumbnailSrc.match(/^\/\.media\/[a-f0-9]{40,}$/i) &&
  967. !/\.[a-z0-9]+$/i.test(thumbnailSrc)
  968. ) {
  969. if (filemime && (filemime.toLowerCase() === "image/apng" || filemime.toLowerCase() === "video/x-m4v")) {
  970. return thumbnailSrc;
  971. }
  972.  
  973. const ext = filemime ? getExtensionForMimeType(filemime) : null;
  974. if (!ext) {
  975. return thumbnailSrc;
  976. }
  977. return thumbnailSrc + ext;
  978. }
  979.  
  980. if (
  981. /\/spoiler\.png$/i.test(thumbnailSrc) ||
  982. /\/custom\.spoiler$/i.test(thumbnailSrc) ||
  983. /\/audioGenericThumb\.png$/i.test(thumbnailSrc)
  984. ) {
  985. if (parentA && parentA.getAttribute("href")) {
  986. return sanitizeUrl(parentA.getAttribute("href"));
  987. }
  988. return null;
  989. }
  990. return null;
  991. }
  992. async function onThumbEnter(e) {
  993. cleanupFloatingMedia();
  994. lastMouseEvent = e;
  995. const thumb = e.currentTarget;
  996. let filemime = null, fullSrc = null, isVideo = false, isAudio = false;
  997. if (thumb.tagName === "IMG") {
  998. const parentA = thumb.closest("a.linkThumb, a.imgLink");
  999. if (!parentA) return;
  1000. const href = parentA.getAttribute("href");
  1001. if (!href) return;
  1002. const ext = href.split(".").pop().toLowerCase();
  1003. filemime =
  1004. parentA.getAttribute("data-filemime") ||
  1005. {
  1006. jpg: "image/jpeg",
  1007. jpeg: "image/jpeg",
  1008. jxl: "image/jxl",
  1009. png: "image/png",
  1010. apng: "image/apng",
  1011. gif: "image/gif",
  1012. avif: "image/avif",
  1013. webp: "image/webp",
  1014. bmp: "image/bmp",
  1015. mp4: "video/mp4",
  1016. webm: "video/webm",
  1017. m4v: "video/x-m4v",
  1018. ogg: "audio/ogg",
  1019. mp3: "audio/mpeg",
  1020. m4a: "audio/x-m4a",
  1021. wav: "audio/wav",
  1022. }[ext];
  1023. fullSrc = getFullMediaSrc(thumb, filemime);
  1024. isVideo = filemime && filemime.startsWith("video/");
  1025. isAudio = filemime && filemime.startsWith("audio/");
  1026. }
  1027. fullSrc = sanitizeUrl(fullSrc);
  1028. if (!fullSrc) return;
  1029. let volume = 0.5;
  1030. try {
  1031. if (typeof getSetting === "function") {
  1032. const v = await getSetting("hoverVideoVolume");
  1033. if (typeof v === "number" && !isNaN(v)) {
  1034. volume = Math.max(0, Math.min(1, v / 100));
  1035. }
  1036. }
  1037. } catch { }
  1038.  
  1039. if (isAudio) {
  1040. let container = thumb.closest("a.linkThumb, a.imgLink");
  1041. if (container && !container.style.position) {
  1042. container.style.position = "relative";
  1043. }
  1044. floatingMedia = document.createElement("audio");
  1045. floatingMedia.src = fullSrc;
  1046. floatingMedia.controls = false;
  1047. floatingMedia.style.display = "none";
  1048. floatingMedia.volume = volume;
  1049. document.body.appendChild(floatingMedia);
  1050. floatingMedia.play().catch(() => { });
  1051. const indicator = document.createElement("div");
  1052. indicator.classList.add("audio-preview-indicator");
  1053. indicator.textContent = AUDIO_INDICATOR_TEXT;
  1054. if (container) {
  1055. container.appendChild(indicator);
  1056. }
  1057. currentAudioIndicator = indicator;
  1058. const cleanup = () => cleanupFloatingMedia();
  1059. thumb.addEventListener("mouseleave", cleanup, { once: true });
  1060. if (container) container.addEventListener("click", cleanup, { once: true });
  1061. window.addEventListener("scroll", cleanup, { passive: true, once: true });
  1062. cleanupFns.push(() => thumb.removeEventListener("mouseleave", cleanup));
  1063. if (container) cleanupFns.push(() => container.removeEventListener("click", cleanup));
  1064. cleanupFns.push(() => window.removeEventListener("scroll", cleanup));
  1065. return;
  1066. }
  1067. floatingMedia = isVideo ? document.createElement("video") : document.createElement("img");
  1068. floatingMedia.src = fullSrc;
  1069. floatingMedia.id = "hover-preview-media";
  1070. floatingMedia.style.position = "fixed";
  1071. floatingMedia.style.zIndex = "9999";
  1072. floatingMedia.style.pointerEvents = "none";
  1073. floatingMedia.style.opacity = MEDIA_OPACITY_LOADING;
  1074. floatingMedia.style.left = "-9999px";
  1075. floatingMedia.style.top = "-9999px";
  1076. floatingMedia.style.maxWidth = MEDIA_MAX_WIDTH;
  1077. const availableHeight = window.innerHeight * (1 - MEDIA_BOTTOM_MARGIN / 100);
  1078. floatingMedia.style.maxHeight = `${availableHeight}px`;
  1079. if (isVideo) {
  1080. floatingMedia.autoplay = true;
  1081. floatingMedia.loop = true;
  1082. floatingMedia.muted = false;
  1083. floatingMedia.playsInline = true;
  1084. floatingMedia.volume = volume;
  1085. }
  1086. document.body.appendChild(floatingMedia);
  1087. function mouseMoveHandler(ev) {
  1088. lastMouseEvent = ev;
  1089. positionFloatingMedia(ev);
  1090. }
  1091. document.addEventListener("mousemove", mouseMoveHandler, { passive: true });
  1092. thumb.addEventListener("mouseleave", leaveHandler, { passive: true, once: true });
  1093. cleanupFns.push(() => document.removeEventListener("mousemove", mouseMoveHandler));
  1094. if (lastMouseEvent) {
  1095. positionFloatingMedia(lastMouseEvent);
  1096. }
  1097. if (isVideo) {
  1098. floatingMedia.onloadeddata = function () {
  1099. if (floatingMedia) {
  1100. floatingMedia.style.opacity = MEDIA_OPACITY_LOADED;
  1101. if (lastMouseEvent) positionFloatingMedia(lastMouseEvent);
  1102. }
  1103. };
  1104. } else {
  1105. floatingMedia.onload = function () {
  1106. if (floatingMedia) {
  1107. floatingMedia.style.opacity = MEDIA_OPACITY_LOADED;
  1108. if (lastMouseEvent) positionFloatingMedia(lastMouseEvent);
  1109. }
  1110. };
  1111. }
  1112. floatingMedia.onerror = cleanupFloatingMedia;
  1113. function leaveHandler() { cleanupFloatingMedia(); }
  1114. thumb.addEventListener("mouseleave", leaveHandler, { once: true });
  1115. window.addEventListener("scroll", leaveHandler, { passive: true, once: true });
  1116. cleanupFns.push(() => thumb.removeEventListener("mouseleave", leaveHandler));
  1117. cleanupFns.push(() => window.removeEventListener("scroll", leaveHandler));
  1118. }
  1119. function attachThumbListeners(root = document) {
  1120. root.querySelectorAll("a.linkThumb > img, a.imgLink > img").forEach(thumb => {
  1121. if (!thumb._fullImgHoverBound) {
  1122. thumb.addEventListener("mouseenter", onThumbEnter);
  1123. thumb._fullImgHoverBound = true;
  1124. }
  1125. });
  1126. if (
  1127. root.tagName === "IMG" &&
  1128. root.parentElement &&
  1129. (root.parentElement.matches("a.linkThumb") || root.parentElement.matches("a.imgLink")) &&
  1130. !root._fullImgHoverBound
  1131. ) {
  1132. root.addEventListener("mouseenter", onThumbEnter);
  1133. root._fullImgHoverBound = true;
  1134. }
  1135. }
  1136. attachThumbListeners();
  1137. if (divThreads) {
  1138. const observer = new MutationObserver((mutations) => {
  1139. const addedElements = [];
  1140. for (const mutation of mutations) {
  1141. for (const node of mutation.addedNodes) {
  1142. if (node.nodeType === 1) {
  1143. addedElements.push(node);
  1144. }
  1145. }
  1146. }
  1147. addedElements.forEach(node => attachThumbListeners(node));
  1148. });
  1149. observer.observe(divThreads, { childList: true, subtree: true });
  1150. }
  1151. }
  1152. function featureBlurSpoilers() {
  1153. function getExtensionForMimeType(mime) {
  1154. const map = {
  1155. "image/jpeg": ".jpg",
  1156. "image/jpg": ".jpg",
  1157. "image/jxl": ".jxl",
  1158. "image/png": ".png",
  1159. "image/apng": ".png",
  1160. "image/gif": ".gif",
  1161. "image/avif": ".avif",
  1162. "image/webp": ".webp",
  1163. "image/bmp": ".bmp",
  1164. };
  1165. return map[mime.toLowerCase()] || "";
  1166. }
  1167. function revealSpoilers() {
  1168. const spoilerLinks = document.querySelectorAll("a.imgLink");
  1169. spoilerLinks.forEach(async (link) => {
  1170. const img = link.querySelector("img");
  1171. if (!img) return;
  1172. if (
  1173. /\/\.media\/[^\/]+?\.[a-zA-Z0-9]+$/.test(img.src) &&
  1174. !/\/\.media\/t_[^\/]+?\.[a-zA-Z0-9]+$/.test(img.src)
  1175. ) {
  1176. return;
  1177. }
  1178. const isCustomSpoiler = img.src.includes("/custom.spoiler")
  1179. || img.src.includes("/*/custom.spoiler")
  1180. || img.src.includes("/spoiler.png");
  1181. const isNotThumbnail = !img.src.includes("/.media/t_");
  1182. const hasFilenameExtension = !isCustomSpoiler && /\.[a-zA-Z0-9]+$/.test(img.src);
  1183.  
  1184. if (isNotThumbnail || isCustomSpoiler) {
  1185. let href = link.getAttribute("href");
  1186. if (!href) return;
  1187. const match = href.match(/\/\.media\/([^\/]+)\.[a-zA-Z0-9]+$/);
  1188. if (!match) return;
  1189. const fileMime = link.getAttribute("data-filemime") || "";
  1190. const ext = getExtensionForMimeType(fileMime);
  1191. const fileWidthAttr = link.getAttribute("data-filewidth");
  1192. const fileHeightAttr = link.getAttribute("data-fileheight");
  1193. let transformedSrc;
  1194. if (
  1195. (fileWidthAttr && Number(fileWidthAttr) < 250) ||
  1196. (fileHeightAttr && Number(fileHeightAttr) < 250)
  1197. ) {
  1198. transformedSrc = `/.media/${match[1]}${ext}`;
  1199. } else if (!hasFilenameExtension && isCustomSpoiler) {
  1200. transformedSrc = `/.media/t_${match[1]}`;
  1201. } else {
  1202. return;
  1203. }
  1204. const initialWidth = img.offsetWidth;
  1205. const initialHeight = img.offsetHeight;
  1206. img.style.width = initialWidth + "px";
  1207. img.style.height = initialHeight + "px";
  1208. img.src = transformedSrc;
  1209. img.onload = function () {
  1210. img.style.width = img.naturalWidth + "px";
  1211. img.style.height = img.naturalHeight + "px";
  1212. };
  1213. if (await getSetting("blurSpoilers_removeSpoilers")) {
  1214. img.style.filter = "";
  1215. img.style.transition = "";
  1216. img.style.border = "1px dotted var(--border-color)";
  1217. img.onmouseover = null;
  1218. img.onmouseout = null;
  1219. return;
  1220. } else {
  1221. img.style.filter = "blur(5px)";
  1222. img.style.transition = "filter 0.3s ease";
  1223. img.addEventListener("mouseover", () => {
  1224. img.style.filter = "none";
  1225. });
  1226. img.addEventListener("mouseout", () => {
  1227. img.style.filter = "blur(5px)";
  1228. });
  1229. }
  1230. }
  1231. });
  1232. }
  1233. revealSpoilers();
  1234. const observer = new MutationObserver(revealSpoilers);
  1235. observer.observe(divThreads, { childList: true, subtree: true });
  1236. };
  1237. function autoHideHeaderOnScroll() {
  1238. const header = document.getElementById('dynamicHeaderThread');
  1239. if (!header) return;
  1240. const scrollThreshold = 50;
  1241. let lastScrollY = window.scrollY;
  1242. let scrollDirection = 'none';
  1243. let ticking = false;
  1244.  
  1245. function updateHeaderVisibility() {
  1246. const currentScrollY = window.scrollY;
  1247. scrollDirection = currentScrollY > lastScrollY ? 'down' : 'up';
  1248. lastScrollY = currentScrollY;
  1249. const isNearTop = currentScrollY < 100;
  1250. if (scrollDirection === 'up' || isNearTop) {
  1251. header.classList.remove('nav-hidden');
  1252. } else if (scrollDirection === 'down' && currentScrollY > scrollThreshold) {
  1253. header.classList.add('nav-hidden');
  1254. }
  1255.  
  1256. ticking = false;
  1257. }
  1258. const style = document.createElement('style');
  1259. style.textContent = `
  1260. #dynamicHeaderThread {
  1261. transition: transform 0.3s ease;
  1262. }
  1263. #dynamicHeaderThread.nav-hidden {
  1264. transform: translateY(-100%);
  1265. }
  1266. :root.bottom-header #dynamicHeaderThread.nav-hidden {
  1267. transform: translateY(100%);
  1268. }
  1269. `;
  1270. document.head.appendChild(style);
  1271. window.addEventListener('scroll', () => {
  1272. if (!ticking) {
  1273. window.requestAnimationFrame(updateHeaderVisibility);
  1274. ticking = true;
  1275. }
  1276. }, { passive: true });
  1277. updateHeaderVisibility();
  1278. }
  1279. const decodeHtmlEntitiesTwice = (() => {
  1280. const txt = document.createElement('textarea');
  1281. return function (html) {
  1282. txt.innerHTML = html;
  1283. const once = txt.value;
  1284. txt.innerHTML = once;
  1285. return txt.value;
  1286. };
  1287. })();
  1288. function highlightMentions() {
  1289. const watchedCells = document.querySelectorAll("#watchedMenu .watchedCell");
  1290. const watchButton = document.querySelector(".opHead .watchButton");
  1291. if (!watchedCells.length) return;
  1292. watchedCells.forEach((cell) => {
  1293. const notification = cell.querySelector(".watchedCellLabel span.watchedNotification");
  1294. if (!notification) return;
  1295.  
  1296. const labelLink = cell.querySelector(".watchedCellLabel a");
  1297. if (!labelLink) return;
  1298. if (!labelLink.dataset.board) {
  1299. const href = labelLink.getAttribute("href");
  1300. const match = href?.match(/^(?:https?:\/\/[^\/]+)?\/([^\/]+)\//);
  1301. if (match) {
  1302. labelLink.dataset.board = `/${match[1]}/ -`;
  1303. }
  1304. if (document.location.href.includes(href)) {
  1305. if (watchButton) {
  1306. watchButton.style.color = "var(--board-title-color)";
  1307. watchButton.title = "Watched";
  1308. }
  1309. }
  1310. const originalText = labelLink.textContent;
  1311. const decodedText = decodeHtmlEntitiesTwice(originalText);
  1312. if (labelLink.textContent !== decodedText) {
  1313. labelLink.textContent = decodedText;
  1314. }
  1315. }
  1316. const notificationText = notification.textContent.trim();
  1317.  
  1318. function styleMentionYou(labelLink, notification, totalReplies) {
  1319. labelLink.style.color = "var(--board-title-color)";
  1320. notification.style.color = "var(--board-title-color)";
  1321. notification.textContent = ` (${totalReplies}) (You)`;
  1322. notification.style.fontWeight = "bold";
  1323. }
  1324.  
  1325. function styleMentionNumber(notification, notificationText) {
  1326. notification.textContent = ` (${notificationText})`;
  1327. notification.style.color = "var(--link-color)";
  1328. notification.style.fontWeight = "bold";
  1329. }
  1330. if (notificationText.startsWith("(") === true) {
  1331. return;
  1332. }
  1333. if (notificationText.includes("(you)") === true) {
  1334. const parts = notificationText.split(", ");
  1335. const totalReplies = parts[0];
  1336. styleMentionYou(labelLink, notification, totalReplies);
  1337. }
  1338. else if (/^\d+$/.test(notificationText)) {
  1339. styleMentionNumber(notification, notificationText);
  1340. }
  1341. notification.dataset.processed = "true";
  1342. });
  1343. }
  1344. highlightMentions();
  1345. const watchedMenu = document.getElementById("watchedMenu");
  1346. if (watchedMenu) {
  1347. const observer = new MutationObserver(() => {
  1348. highlightMentions();
  1349. });
  1350. observer.observe(watchedMenu, { childList: true, subtree: true });
  1351. }
  1352. async function featureWatchThreadOnReply() {
  1353. const getWatchButton = () => document.querySelector(".watchButton");
  1354. function watchThreadIfNotWatched() {
  1355. const btn = getWatchButton();
  1356. if (btn && !btn.classList.contains("watched-active")) {
  1357. btn.click();
  1358. setTimeout(() => {
  1359. btn.classList.add("watched-active");
  1360. }, 100);
  1361. }
  1362. }
  1363. function updateWatchButtonClass() {
  1364. const btn = getWatchButton();
  1365. if (!btn) return;
  1366. if (btn.classList.contains("watched-active")) {
  1367. btn.classList.add("watched-active");
  1368. } else {
  1369. btn.classList.remove("watched-active");
  1370. }
  1371. }
  1372. const submitButton = document.getElementById("qrbutton");
  1373. if (submitButton) {
  1374. submitButton.removeEventListener("click", submitButton._watchThreadHandler || (() => { }));
  1375. submitButton._watchThreadHandler = async function () {
  1376. if (await getSetting("watchThreadOnReply")) {
  1377. setTimeout(watchThreadIfNotWatched, 500);
  1378. }
  1379. };
  1380. submitButton.addEventListener("click", submitButton._watchThreadHandler);
  1381. }
  1382. updateWatchButtonClass();
  1383. const btn = getWatchButton();
  1384. if (btn) {
  1385. btn.removeEventListener("click", btn._updateWatchHandler || (() => { }));
  1386. btn._updateWatchHandler = () => setTimeout(updateWatchButtonClass, 100);
  1387. btn.addEventListener("click", btn._updateWatchHandler);
  1388. }
  1389. }
  1390. async function featureAlwaysShowTW() {
  1391. if (!(await getSetting("alwaysShowTW"))) return;
  1392.  
  1393. function showThreadWatcher() {
  1394. const watchedMenu = document.getElementById("watchedMenu");
  1395. if (watchedMenu) {
  1396. watchedMenu.style.display = "flex";
  1397. }
  1398. }
  1399.  
  1400. showThreadWatcher();
  1401. }
  1402. function markAllThreadsAsRead() {
  1403. const handleDiv = document.querySelector('#watchedMenu > div.handle');
  1404. if (!handleDiv) return;
  1405. if (handleDiv.querySelector('.watchedCellDismissButton.markAllRead')) return;
  1406. const btn = document.createElement('a');
  1407. btn.className = 'watchedCellDismissButton glowOnHover coloredIcon markAllRead';
  1408. btn.title = 'Mark all threads as read';
  1409. btn.style.float = 'right';
  1410. btn.style.paddingTop = '3px';
  1411. function hasUnreadThreads() {
  1412. const watchedMenu = document.querySelector('#watchedMenu > div.floatingContainer');
  1413. if (!watchedMenu) return false;
  1414. return watchedMenu.querySelectorAll('td.watchedCellDismissButton.glowOnHover.coloredIcon[title="Mark as read"]').length > 0;
  1415. }
  1416. function updateButtonState() {
  1417. if (hasUnreadThreads()) {
  1418. btn.style.opacity = '1';
  1419. btn.style.pointerEvents = 'auto';
  1420. btn.title = 'Mark all threads as read';
  1421. } else {
  1422. btn.style.opacity = '0.5';
  1423. btn.style.pointerEvents = 'none';
  1424. btn.title = 'No unread threads';
  1425. }
  1426. }
  1427. function clickAllMarkAsReadButtons(watchedMenu) {
  1428. const markButtons = watchedMenu.querySelectorAll('td.watchedCellDismissButton.glowOnHover.coloredIcon[title="Mark as read"]');
  1429. markButtons.forEach(btn => {
  1430. try {
  1431. btn.click();
  1432. } catch (e) {
  1433. console.log("Error clicking button:", e);
  1434. }
  1435. });
  1436. return markButtons.length;
  1437. }
  1438. function markAllThreadsAsReadWithRetry(retriesLeft, callback) {
  1439. setTimeout(function () {
  1440. const watchedMenu = document.querySelector('#watchedMenu > div.floatingContainer');
  1441. if (!watchedMenu) {
  1442. if (callback) callback();
  1443. return;
  1444. }
  1445. const clickedCount = clickAllMarkAsReadButtons(watchedMenu);
  1446. if (clickedCount === 0) {
  1447. updateButtonState();
  1448. if (callback) callback();
  1449. return;
  1450. }
  1451. if (retriesLeft > 0) {
  1452. setTimeout(() => markAllThreadsAsReadWithRetry(retriesLeft - 1, callback), 200);
  1453. } else if (callback) {
  1454. callback();
  1455. }
  1456. }, 100);
  1457. }
  1458. function debounce(fn, delay) {
  1459. let timeout;
  1460. return function (...args) {
  1461. clearTimeout(timeout);
  1462. timeout = setTimeout(() => fn.apply(this, args), delay);
  1463. };
  1464. }
  1465. const watchedMenu = document.querySelector('#watchedMenu > div.floatingContainer');
  1466. let observer = null;
  1467. if (watchedMenu) {
  1468. const debouncedUpdate = debounce(updateButtonState, 100);
  1469. observer = new MutationObserver(debouncedUpdate);
  1470. observer.observe(watchedMenu, { childList: true, subtree: true });
  1471. const removalObserver = new MutationObserver(() => {
  1472. if (!document.body.contains(watchedMenu) || watchedMenu.style.display === "none") {
  1473. observer.disconnect();
  1474. removalObserver.disconnect();
  1475. }
  1476. });
  1477. removalObserver.observe(document.body, { childList: true, subtree: true });
  1478. }
  1479. updateButtonState();
  1480. handleDiv.appendChild(btn);
  1481. document.body.addEventListener('click', function (e) {
  1482. const closeBtn = e.target.closest('#watchedMenu .close-btn');
  1483. if (closeBtn) {
  1484. const watchedMenu = document.getElementById("watchedMenu");
  1485. if (watchedMenu) watchedMenu.style.display = "none";
  1486. return;
  1487. }
  1488. const markAllBtn = e.target.closest('.watchedCellDismissButton.markAllRead');
  1489. if (markAllBtn) {
  1490. e.preventDefault();
  1491. if (markAllBtn.style.pointerEvents === 'none' || markAllBtn.dataset.processing === 'true') return;
  1492. markAllBtn.dataset.processing = 'true';
  1493. markAllBtn.style.opacity = '0.5';
  1494. markAllThreadsAsReadWithRetry(3, function () {
  1495. markAllBtn.dataset.processing = 'false';
  1496. updateButtonState();
  1497. });
  1498. }
  1499. });
  1500. }
  1501. markAllThreadsAsRead();
  1502. function hashNavigation() {
  1503. if (!document.documentElement.classList.contains("is-thread")) return;
  1504. const processedLinks = new WeakSet();
  1505. function addHashLinks(container = document) {
  1506. const links = container.querySelectorAll('.panelBacklinks a, .altBacklinks a, .divMessage .quoteLink');
  1507. links.forEach(link => {
  1508. if (
  1509. processedLinks.has(link) ||
  1510. (link.nextSibling && link.nextSibling.classList && link.nextSibling.classList.contains('hash-link-container'))
  1511. ) return;
  1512. const hashSpan = document.createElement('span');
  1513. hashSpan.textContent = ' #';
  1514. hashSpan.className = 'hash-link';
  1515. hashSpan.style.cursor = 'pointer';
  1516. hashSpan.style.color = 'var(--navbar-text-color)';
  1517. hashSpan.title = 'Scroll to post';
  1518. const wrapper = document.createElement('span');
  1519. wrapper.className = 'hash-link-container';
  1520. wrapper.appendChild(hashSpan);
  1521.  
  1522. link.insertAdjacentElement('afterend', wrapper);
  1523. processedLinks.add(link);
  1524. });
  1525. }
  1526. function debounce(fn, delay) {
  1527. let timer;
  1528. return function (...args) {
  1529. clearTimeout(timer);
  1530. timer = setTimeout(() => fn.apply(this, args), delay);
  1531. };
  1532. }
  1533. addHashLinks();
  1534. if (window.tooltips) {
  1535. ['loadTooltip', 'addLoadedTooltip'].forEach(fn => {
  1536. if (typeof tooltips[fn] === 'function') {
  1537. const orig = tooltips[fn];
  1538. tooltips[fn] = function (...args) {
  1539. const result = orig.apply(this, args);
  1540. let container = args[0];
  1541. if (container && container.nodeType === Node.ELEMENT_NODE) {
  1542. addHashLinks(container);
  1543. }
  1544. return result;
  1545. };
  1546. }
  1547. });
  1548. ['addInlineClick', 'processQuote'].forEach(fn => {
  1549. if (typeof tooltips[fn] === 'function') {
  1550. const orig = tooltips[fn];
  1551. tooltips[fn] = function (quote, ...rest) {
  1552. if (
  1553. !quote.href ||
  1554. quote.classList.contains('hash-link') ||
  1555. quote.closest('.hash-link-container') ||
  1556. quote.href.includes('#q')
  1557. ) {
  1558. return;
  1559. }
  1560. return orig.apply(this, [quote, ...rest]);
  1561. };
  1562. }
  1563. });
  1564. }
  1565. const postsContainer = divPosts || document.body;
  1566. postsContainer.addEventListener('click', function (e) {
  1567. if (e.target.classList.contains('hash-link')) {
  1568. e.preventDefault();
  1569. const link = e.target.closest('.hash-link-container').previousElementSibling;
  1570. if (!link || !link.href) return;
  1571. const hashMatch = link.href.match(/#(\d+)$/);
  1572. if (!hashMatch) return;
  1573. const postId = hashMatch[1];
  1574. const safePostId = /^[0-9]+$/.test(postId) ? postId : null;
  1575. if (!safePostId) return;
  1576. const postElem = document.getElementById(safePostId);
  1577. if (postElem) {
  1578. window.location.hash = `#${safePostId}`;
  1579. if (postElem.classList.contains('opCell')) {
  1580. const offset = 25;
  1581. const rect = postElem.getBoundingClientRect();
  1582. const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
  1583. const targetY = rect.top + scrollTop - offset;
  1584. window.scrollTo({ top: targetY, behavior: "smooth" });
  1585. } else {
  1586. postElem.scrollIntoView({ behavior: "smooth", block: "center" });
  1587. }
  1588. }
  1589. }
  1590. }, true);
  1591.  
  1592. const debouncedAddHashLinks = debounce(addHashLinks, 25);
  1593.  
  1594. const observer = new MutationObserver(mutations => {
  1595. let shouldUpdate = false;
  1596. mutations.forEach(mutation => {
  1597. mutation.addedNodes.forEach(node => {
  1598. if (node.nodeType === Node.ELEMENT_NODE) {
  1599. shouldUpdate = true;
  1600. }
  1601. });
  1602. });
  1603. if (shouldUpdate) debouncedAddHashLinks();
  1604. });
  1605. observer.observe(postsContainer, { childList: true, subtree: true });
  1606. }
  1607. function featureScrollArrows() {
  1608. if (document.getElementById("scroll-arrow-up") || document.getElementById("scroll-arrow-down")) {
  1609. return;
  1610. }
  1611. const upBtn = document.createElement("button");
  1612. upBtn.id = "scroll-arrow-up";
  1613. upBtn.className = "scroll-arrow-btn";
  1614. upBtn.title = "Scroll to top";
  1615. upBtn.innerHTML = "▲";
  1616. upBtn.addEventListener("click", () => {
  1617. window.scrollTo({ top: 0, behavior: "smooth" });
  1618. });
  1619. const downBtn = document.createElement("button");
  1620. downBtn.id = "scroll-arrow-down";
  1621. downBtn.className = "scroll-arrow-btn";
  1622. downBtn.title = "Scroll to bottom";
  1623. downBtn.innerHTML = "▼";
  1624. downBtn.addEventListener("click", () => {
  1625. window.scrollTo({ top: document.body.scrollHeight, behavior: "smooth" });
  1626. });
  1627.  
  1628. document.body.appendChild(upBtn);
  1629. document.body.appendChild(downBtn);
  1630. }
  1631. function featureDeleteNameCheckbox() {
  1632. const nameExists = document.getElementById("qr-name-row");
  1633. if (nameExists && nameExists.classList.contains("hidden")) {
  1634. return;
  1635. }
  1636.  
  1637. const alwaysUseBypassCheckbox = document.getElementById("qralwaysUseBypassCheckBox");
  1638. if (!alwaysUseBypassCheckbox) {
  1639. return;
  1640. }
  1641.  
  1642. const checkbox = document.createElement("input");
  1643. checkbox.type = "checkbox";
  1644. checkbox.id = "saveNameCheckbox";
  1645. checkbox.classList.add("postingCheckbox");
  1646.  
  1647. const label = document.createElement("label");
  1648. label.htmlFor = "saveNameCheckbox";
  1649. label.textContent = "Delete Name";
  1650. label.title = "Delete Name on refresh";
  1651.  
  1652. alwaysUseBypassCheckbox.parentNode.insertBefore(checkbox, alwaysUseBypassCheckbox);
  1653. alwaysUseBypassCheckbox.parentNode.insertBefore(label, checkbox.nextSibling);
  1654. const savedCheckboxState = localStorage.getItem("8chanSS_deleteNameCheckbox") === "true";
  1655. checkbox.checked = savedCheckboxState;
  1656.  
  1657. const nameInput = document.getElementById("qrname");
  1658. if (nameInput) {
  1659. if (checkbox.checked) {
  1660. nameInput.value = "";
  1661. localStorage.removeItem("name");
  1662. }
  1663. checkbox.addEventListener("change", function () {
  1664. localStorage.setItem("8chanSS_deleteNameCheckbox", checkbox.checked);
  1665. });
  1666. }
  1667. }
  1668. async function featureHideAnnouncement() {
  1669. function getContentHash(str) {
  1670. let hash = 5381;
  1671. for (let i = 0; i < str.length; i++) {
  1672. hash = ((hash << 5) + hash) + str.charCodeAt(i);
  1673. }
  1674. return hash >>> 0;
  1675. }
  1676. async function processElement(selector, settingKey, hashKey) {
  1677. const el = document.querySelector(selector);
  1678. if (!el) return;
  1679.  
  1680. const content = el.textContent || "";
  1681. const sanitizedContent = content.replace(/[^\w\s.,!?-]/g, "");
  1682. const hash = getContentHash(sanitizedContent);
  1683. const shouldHide = await GM.getValue(`8chanSS_${settingKey}`, "false") === "true";
  1684. const storedHash = await GM.getValue(`8chanSS_${hashKey}`, null);
  1685. const root = document.documentElement;
  1686.  
  1687. if (shouldHide) {
  1688. if (storedHash !== null && String(storedHash) !== String(hash)) {
  1689. if (typeof window.setSetting === "function") {
  1690. await window.setSetting("hideAnnouncement", false);
  1691. }
  1692. await GM.setValue(`8chanSS_${settingKey}`, "false");
  1693. await GM.deleteValue(`8chanSS_${hashKey}`);
  1694. return;
  1695. }
  1696. root.classList.add("hide-announcement");
  1697. await GM.setValue(`8chanSS_${hashKey}`, hash);
  1698. } else {
  1699. root.classList.remove("hide-announcement");
  1700. await GM.deleteValue(`8chanSS_${hashKey}`);
  1701. }
  1702. }
  1703.  
  1704. await processElement("#dynamicAnnouncement", "hideAnnouncement", "announcementHash");
  1705. }
  1706. async function featureBeepOnYou() {
  1707. if (!divPosts) return;
  1708. let audioContext = null;
  1709. function createBeepSound() {
  1710. if (!audioContext) {
  1711. audioContext = new (window.AudioContext || window.webkitAudioContext)();
  1712. }
  1713. return function playBeep() {
  1714. try {
  1715. const oscillator = audioContext.createOscillator();
  1716. const gainNode = audioContext.createGain();
  1717.  
  1718. oscillator.type = 'sine';
  1719. oscillator.frequency.value = 550;
  1720. gainNode.gain.value = 0.1;
  1721.  
  1722. oscillator.connect(gainNode);
  1723. gainNode.connect(audioContext.destination);
  1724. oscillator.start();
  1725. setTimeout(() => {
  1726. oscillator.stop();
  1727. }, 100);
  1728. } catch (e) {
  1729. console.warn("Beep failed:", e);
  1730. }
  1731. };
  1732. }
  1733. window.originalTitle = document.title;
  1734. window.isNotifying = false;
  1735. let beepOnYouSetting = false;
  1736. let notifyOnYouSetting = false;
  1737. let customMsgSetting = "(!) ";
  1738. let previousFaviconState = null;
  1739. async function initSettings() {
  1740. beepOnYouSetting = await getSetting("beepOnYou");
  1741. notifyOnYouSetting = await getSetting("notifyOnYou");
  1742. const customMsg = await getSetting("notifyOnYou_customMessage");
  1743. if (customMsg) customMsgSetting = customMsg;
  1744. }
  1745. await initSettings();
  1746. const playBeep = createBeepSound();
  1747. let scrollHandlerActive = false;
  1748. async function notifyOnYou() {
  1749. if (!window.isNotifying) {
  1750. window.isNotifying = true;
  1751. document.title = customMsgSetting + " " + window.originalTitle;
  1752. if (await getSetting("customFavicon")) {
  1753. const { style, state } = faviconManager.getCurrentFaviconState();
  1754. if (state !== "notif") {
  1755. previousFaviconState = { style, state };
  1756. }
  1757. faviconManager.setFaviconStyle(style, "notif");
  1758. }
  1759. }
  1760. }
  1761. function setupNotificationScrollHandler() {
  1762. if (scrollHandlerActive) return;
  1763. scrollHandlerActive = true;
  1764. const BOTTOM_OFFSET = 45;
  1765. function checkScrollPosition() {
  1766. if (!window.isNotifying) return;
  1767. const scrollPosition = window.scrollY + window.innerHeight;
  1768. const documentHeight = document.documentElement.scrollHeight;
  1769. if (scrollPosition >= documentHeight - BOTTOM_OFFSET) {
  1770. document.title = window.originalTitle;
  1771. window.isNotifying = false;
  1772. const { state } = faviconManager.getCurrentFaviconState();
  1773. if (state === "notif" && previousFaviconState) {
  1774. faviconManager.setFaviconStyle(previousFaviconState.style, previousFaviconState.state);
  1775. previousFaviconState = null;
  1776. } else if (state === "notif") {
  1777. faviconManager.setFavicon("base");
  1778. }
  1779. window.removeEventListener('scroll', checkScrollPosition);
  1780. scrollHandlerActive = false;
  1781. }
  1782. }
  1783. window.addEventListener('scroll', checkScrollPosition);
  1784. }
  1785. window.addEventListener("focus", () => {
  1786. if (window.isNotifying) {
  1787. setupNotificationScrollHandler();
  1788. }
  1789. });
  1790. const observer = new MutationObserver((mutations) => {
  1791. for (const mutation of mutations) {
  1792. for (const node of mutation.addedNodes) {
  1793. if (
  1794. node.nodeType === 1 &&
  1795. typeof node.matches === "function" &&
  1796. (node.matches('.postCell') || node.matches('.opCell')) &&
  1797. node.querySelector("a.quoteLink.you") &&
  1798. !node.closest('.innerPost')
  1799. ) {
  1800. if (beepOnYouSetting) {
  1801. playBeep();
  1802. }
  1803. if (notifyOnYouSetting) {
  1804. notifyOnYou();
  1805. setupNotificationScrollHandler();
  1806. }
  1807. }
  1808. }
  1809. }
  1810. });
  1811.  
  1812. observer.observe(divPosts, { childList: true, subtree: false });
  1813. window.addEventListener("8chanSS_settingChanged", async (e) => {
  1814. if (e.detail && e.detail.key) {
  1815. const key = e.detail.key;
  1816. if (key === "beepOnYou") {
  1817. beepOnYouSetting = await getSetting("beepOnYou");
  1818. } else if (key === "notifyOnYou") {
  1819. notifyOnYouSetting = await getSetting("notifyOnYou");
  1820. } else if (key === "notifyOnYou_customMessage") {
  1821. const customMsg = await getSetting("notifyOnYou_customMessage");
  1822. if (customMsg) customMsgSetting = customMsg;
  1823. }
  1824. }
  1825. });
  1826. }
  1827. featureBeepOnYou();
  1828. function enhanceYouTubeLinks() {
  1829. const ytTitleCache = {};
  1830. function loadCache() {
  1831. try {
  1832. const data = localStorage.getItem('ytTitleCache');
  1833. if (data) Object.assign(ytTitleCache, JSON.parse(data));
  1834. } catch (e) { }
  1835. }
  1836. function saveCache() {
  1837. try {
  1838. localStorage.setItem('ytTitleCache', JSON.stringify(ytTitleCache));
  1839. } catch (e) { }
  1840. }
  1841. loadCache();
  1842. function getYouTubeId(url) {
  1843. try {
  1844. const u = new URL(url);
  1845. if (u.hostname.endsWith('youtube.com')) {
  1846. return u.searchParams.get('v');
  1847. }
  1848. if (u.hostname === 'youtu.be') {
  1849. return u.pathname.slice(1);
  1850. }
  1851. } catch (e) { }
  1852. return null;
  1853. }
  1854. function sanitizeYouTubeId(videoId) {
  1855. if (!videoId) return null;
  1856. const match = videoId.match(/([a-zA-Z0-9_-]{11})/);
  1857. return match ? match[1] : null;
  1858. }
  1859. async function fetchYouTubeTitle(videoId) {
  1860. const cleanId = sanitizeYouTubeId(videoId);
  1861. if (!cleanId) return null;
  1862. if (ytTitleCache[cleanId]) {
  1863. return Promise.resolve(ytTitleCache[cleanId]);
  1864. }
  1865. return fetch(`https://www.youtube.com/oembed?url=https://www.youtube.com/watch?v=${cleanId}&format=json`)
  1866. .then(r => r.ok ? r.json() : null)
  1867. .then(data => {
  1868. const title = data ? data.title : null;
  1869. if (title) {
  1870. ytTitleCache[cleanId] = title;
  1871. saveCache();
  1872. }
  1873. return title;
  1874. })
  1875. .catch(() => null);
  1876. }
  1877. function processLinks(root = document) {
  1878. root.querySelectorAll('a[href*="youtu"]').forEach(link => {
  1879. if (link.dataset.ytEnhanced) return;
  1880. const videoId = getYouTubeId(link.href);
  1881. if (!videoId) return;
  1882. link.dataset.ytEnhanced = "1";
  1883. fetchYouTubeTitle(videoId).then(title => {
  1884. if (title) {
  1885. link.innerHTML = `<img class="yt-icon" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAMCAYAAABr5z2BAAABIklEQVQoz53LvUrDUBjG8bOoOammSf1IoBSvoCB4JeIqOHgBLt6AIMRBBQelWurQ2kERnMRBsBUcIp5FJSBI5oQsJVkkUHh8W0o5nhaFHvjBgef/Mq+Q46RJBMkI/vE+aOus956tnEswIZe1LV0QyJ5sE2GzgZfVMtRNIdiDpccEssdlB1mW4bvTwdvWJtRdErM7U+8S/FJykCRJX5qm+KpVce8UMNLRLbulz4iSjTAMh6Iowsd5BeNadp3nUF0VlxAEwZBotXC0Usa4ll3meZdA1iguwvf9vpvDA2wvmKgYGtSud8suDB4TyGr2PF49D/vra9jRZ1BVdknMzgwuCGSnZEObwu6sBnVTCHZiaC7BhFx2PKdxUidiAH/4lLo9Mv0DELVs9qsOHXwAAAAASUVORK5CYII="><span>[Youtube]</span> ${title}`;
  1886. }
  1887. });
  1888. });
  1889. }
  1890. processLinks(document);
  1891. new MutationObserver(() => processLinks(divThreads)).observe(divThreads, { childList: true, subtree: true });
  1892. }
  1893. function featureLabelCreated12h() {
  1894. function convertLabelCreatedTimes(root = document) {
  1895. (root.querySelectorAll
  1896. ? root.querySelectorAll('.labelCreated')
  1897. : []).forEach(span => {
  1898. if (span.dataset.timeConverted === "1") return;
  1899.  
  1900. const text = span.textContent;
  1901. const match = text.match(/^(.+\))\s+(\d{2}):(\d{2}):(\d{2})$/);
  1902. if (!match) return;
  1903.  
  1904. const [_, datePart, hourStr, minStr, secStr] = match;
  1905. let hour = parseInt(hourStr, 10);
  1906. const min = minStr;
  1907. const sec = secStr;
  1908. const ampm = hour >= 12 ? 'PM' : 'AM';
  1909. let hour12 = hour % 12;
  1910. if (hour12 === 0) hour12 = 12;
  1911.  
  1912. const newText = `${datePart} ${hour12}:${min}:${sec} ${ampm}`;
  1913. span.textContent = newText;
  1914. span.dataset.timeConverted = "1";
  1915. });
  1916. }
  1917. convertLabelCreatedTimes();
  1918. if (divPosts) {
  1919. new MutationObserver(() => {
  1920. convertLabelCreatedTimes(divPosts);
  1921. }).observe(divPosts, { childList: true, subtree: true });
  1922. }
  1923. }
  1924. function truncateFilenames(filenameLength) {
  1925. function processLinks(root = document) {
  1926. root.querySelectorAll('a.originalNameLink').forEach(link => {
  1927. if (link.dataset.truncated === "1") return;
  1928. const fullFilename = link.getAttribute('download');
  1929. if (!fullFilename) return;
  1930. const lastDot = fullFilename.lastIndexOf('.');
  1931. if (lastDot === -1) return;
  1932. const name = fullFilename.slice(0, lastDot);
  1933. const ext = fullFilename.slice(lastDot);
  1934. let truncated = fullFilename;
  1935. if (name.length > filenameLength) {
  1936. truncated = name.slice(0, filenameLength) + '(...)' + ext;
  1937. }
  1938. link.textContent = truncated;
  1939. link.dataset.truncated = "1";
  1940. link.addEventListener('mouseenter', function () {
  1941. link.textContent = fullFilename;
  1942. });
  1943. link.addEventListener('mouseleave', function () {
  1944. link.textContent = truncated;
  1945. });
  1946. link.title = fullFilename;
  1947. });
  1948. }
  1949. processLinks(document);
  1950. if (divThreads) {
  1951. new MutationObserver(() => {
  1952. processLinks(divThreads);
  1953. }).observe(divThreads, { childList: true, subtree: true });
  1954. }
  1955. }
  1956. function threadInfoHeader(retries = 10, delay = 200) {
  1957. const navHeader = document.querySelector('.navHeader');
  1958. const navOptionsSpan = document.getElementById('navOptionsSpan');
  1959. const postCountEl = document.getElementById('postCount');
  1960. const userCountEl = document.getElementById('userCountLabel');
  1961. const fileCountEl = document.getElementById('fileCount');
  1962. function retryIfElementsMissing(checkFn, callback, retries, delay) {
  1963. if (!checkFn()) {
  1964. if (retries > 0) {
  1965. setTimeout(() => retryIfElementsMissing(checkFn, callback, retries - 1, delay), delay);
  1966. }
  1967. return true;
  1968. }
  1969. return false;
  1970. }
  1971.  
  1972. if (retryIfElementsMissing(
  1973. () => navHeader && navOptionsSpan && postCountEl && userCountEl && fileCountEl,
  1974. () => threadInfoHeader(retries - 1, delay),
  1975. retries,
  1976. delay
  1977. )) return;
  1978. const postCount = postCountEl.textContent || '0';
  1979. const userCount = userCountEl.textContent || '0';
  1980. const fileCount = fileCountEl.textContent || '0';
  1981. let statsDisplay = navHeader.querySelector('.thread-stats-display');
  1982. if (!statsDisplay) {
  1983. statsDisplay = document.createElement('span');
  1984. statsDisplay.className = 'thread-stats-display';
  1985. statsDisplay.style.marginRight = '1px';
  1986. }
  1987.  
  1988. statsDisplay.innerHTML = `
  1989. [
  1990. <span class="statLabel">Posts: </span><span class="statNumb">${postCount}</span> |
  1991. <span class="statLabel">Users: </span><span class="statNumb">${userCount}</span> |
  1992. <span class="statLabel">Files: </span><span class="statNumb">${fileCount}</span>
  1993. ]
  1994. `;
  1995. if (statsDisplay.parentNode && statsDisplay.parentNode !== navOptionsSpan) {
  1996. statsDisplay.parentNode.removeChild(statsDisplay);
  1997. }
  1998. if (navOptionsSpan.firstChild !== statsDisplay) {
  1999. navOptionsSpan.insertBefore(statsDisplay, navOptionsSpan.firstChild);
  2000. }
  2001. if (!threadInfoHeader._observerInitialized) {
  2002. const statIds = ['postCount', 'userCountLabel', 'fileCount'];
  2003. function debounce(fn, wait) {
  2004. let timeout;
  2005. return function (...args) {
  2006. clearTimeout(timeout);
  2007. timeout = setTimeout(() => fn.apply(this, args), wait);
  2008. };
  2009. }
  2010.  
  2011. if (!threadInfoHeader._debouncedUpdate) {
  2012. threadInfoHeader._debouncedUpdate = debounce(() => threadInfoHeader(0, delay), 100);
  2013. }
  2014. statIds.forEach(id => {
  2015. const el = document.getElementById(id);
  2016. if (el) {
  2017. new MutationObserver(threadInfoHeader._debouncedUpdate).observe(el, { childList: true, subtree: false, characterData: true });
  2018. }
  2019. });
  2020. threadInfoHeader._observerInitialized = true;
  2021. }
  2022. }
  2023. function mediaViewerPositioning() {
  2024. localStorage.setItem("mediaViewer", "true");
  2025. async function updateMediaViewerClass() {
  2026. const mediaViewer = document.getElementById('media-viewer');
  2027. if (!mediaViewer) return;
  2028.  
  2029. const isEnabled = await getSetting("enableMediaViewer");
  2030. if (!isEnabled) {
  2031. mediaViewer.classList.remove('topright', 'topleft');
  2032. return;
  2033. }
  2034.  
  2035. const viewerStyle = await getSetting("enableMediaViewer_viewerStyle");
  2036. mediaViewer.classList.remove('topright', 'topleft');
  2037. if (viewerStyle === 'topright' || viewerStyle === 'topleft') {
  2038. mediaViewer.classList.add(viewerStyle);
  2039. } else {
  2040. }
  2041. }
  2042. function setupIfMediaViewerExists() {
  2043. const mediaViewer = document.getElementById('media-viewer');
  2044. if (mediaViewer) {
  2045. updateMediaViewerClass();
  2046. return true;
  2047. }
  2048. return false;
  2049. }
  2050. if (setupIfMediaViewerExists()) {
  2051. } else {
  2052. const observer = new MutationObserver((mutations) => {
  2053. for (const mutation of mutations) {
  2054. if (mutation.addedNodes.length) {
  2055. for (const node of mutation.addedNodes) {
  2056. if (node.id === 'media-viewer' ||
  2057. (node.nodeType === 1 && node.querySelector('#media-viewer'))) {
  2058. updateMediaViewerClass();
  2059. observer.disconnect();
  2060. return;
  2061. }
  2062. }
  2063. }
  2064. }
  2065. });
  2066. observer.observe(document.body, { childList: true, subtree: false });
  2067. }
  2068. }
  2069. async function featureHighlightNewIds() {
  2070. const hlStyle = await getSetting("highlightNewIds_idHlStyle");
  2071. if (!divPosts) return;
  2072. if (!document.querySelector('.spanId')) return;
  2073. const styleClassMap = {
  2074. moetext: "moeText",
  2075. glow: "id-glow",
  2076. dotted: "id-dotted"
  2077. };
  2078. const styleClass = styleClassMap[hlStyle] || "moeText";
  2079. function highlightIds(root = divPosts) {
  2080. const idFrequency = {};
  2081. const labelSpans = root.querySelectorAll('.labelId');
  2082. labelSpans.forEach(span => {
  2083. const id = span.textContent.trim();
  2084. idFrequency[id] = (idFrequency[id] || 0) + 1;
  2085. });
  2086. const seen = {};
  2087. labelSpans.forEach(span => {
  2088. const id = span.textContent.trim();
  2089. span.classList.remove('moetext', 'id-glow', 'id-dotted');
  2090. if (!seen[id]) {
  2091. seen[id] = true;
  2092. span.classList.add(styleClass);
  2093. span.title = idFrequency[id] === 1
  2094. ? "This ID appears only once."
  2095. : "This was the first occurrence of this ID.";
  2096. } else {
  2097. span.title = "";
  2098. }
  2099. });
  2100. }
  2101. highlightIds();
  2102. const observer = new MutationObserver(mutations => {
  2103. let needsUpdate = false;
  2104. for (const mutation of mutations) {
  2105. for (const node of mutation.addedNodes) {
  2106. if (node.nodeType === 1 && node.querySelector && node.querySelector('.labelId')) {
  2107. needsUpdate = true;
  2108. break;
  2109. }
  2110. }
  2111. if (needsUpdate) break;
  2112. }
  2113. if (needsUpdate) {
  2114. highlightIds();
  2115. }
  2116. });
  2117. observer.observe(divPosts, { childList: true, subtree: true });
  2118. }
  2119. async function featureQuoteThreading() {
  2120. const isEnabled = typeof getSetting === "function"
  2121. ? await getSetting("quoteThreading")
  2122. : true;
  2123.  
  2124. if (!isEnabled) {
  2125. document.querySelector('.quoteThreadingRefresh')?.remove();
  2126. return;
  2127. }
  2128. function processPosts(posts) {
  2129. posts.forEach(post => {
  2130. if (post.closest('.threadedReplies')) {
  2131. return;
  2132. }
  2133.  
  2134. const backlinks = post.querySelectorAll('.panelBacklinks .backLink.postLink');
  2135.  
  2136. backlinks.forEach(backlink => {
  2137. const targetUri = backlink.getAttribute('data-target-uri');
  2138. if (!targetUri) return;
  2139.  
  2140. const targetPostId = targetUri.split('#')[1];
  2141. const targetPost = document.getElementById(targetPostId);
  2142.  
  2143. if (targetPost) {
  2144. if (targetPost.closest('.threadedReplies')) {
  2145. return;
  2146. }
  2147.  
  2148. let repliesContainer = post.nextElementSibling;
  2149. if (!repliesContainer?.classList.contains('threadedReplies')) {
  2150. repliesContainer = document.createElement('div');
  2151. repliesContainer.className = 'threadedReplies';
  2152. post.parentNode.insertBefore(repliesContainer, post.nextSibling);
  2153. }
  2154. if (!repliesContainer.contains(targetPost)) {
  2155. repliesContainer.appendChild(targetPost);
  2156. }
  2157. }
  2158. });
  2159. });
  2160. }
  2161. function threadAllPosts() {
  2162. processPosts(document.querySelectorAll('.divPosts .postCell'));
  2163. }
  2164.  
  2165. function threadNewPosts() {
  2166. const allPosts = document.querySelectorAll('.divPosts .postCell');
  2167. processPosts(Array.from(allPosts).slice(-5));
  2168. }
  2169. function setupPostObserver() {
  2170. const observer = new MutationObserver(mutations => {
  2171. mutations.forEach(mutation => {
  2172. if (mutation.addedNodes.length) {
  2173. setTimeout(threadNewPosts, 50);
  2174. }
  2175. });
  2176. });
  2177.  
  2178. if (typeof divPosts !== 'undefined') {
  2179. observer.observe(divPosts, {
  2180. childList: true,
  2181. subtree: false
  2182. });
  2183. }
  2184. }
  2185. function addRefreshButton() {
  2186. const replyButton = document.querySelector('.threadBottom .innerUtility #replyButton');
  2187. if (!replyButton || replyButton.nextElementSibling?.classList.contains('quoteThreadingBtn')) return;
  2188.  
  2189. const refreshBtn = document.createElement('a');
  2190. refreshBtn.href = "#";
  2191. refreshBtn.className = "quoteThreadingBtn";
  2192. refreshBtn.title = "Refresh quote threading";
  2193. refreshBtn.textContent = "ReThread";
  2194.  
  2195. replyButton.after(' ', refreshBtn);
  2196.  
  2197. refreshBtn.addEventListener('click', e => {
  2198. e.preventDefault();
  2199. threadAllPosts();
  2200. });
  2201. }
  2202. threadAllPosts();
  2203. addRefreshButton();
  2204. setupPostObserver();
  2205. }
  2206. async function createSettingsMenu() {
  2207. let menu = document.getElementById("8chanSS-menu");
  2208. if (menu) return menu;
  2209. menu = document.createElement("div");
  2210. menu.id = "8chanSS-menu";
  2211. menu.style.position = "fixed";
  2212. menu.style.top = "3rem";
  2213. menu.style.left = "20rem";
  2214. menu.style.zIndex = "99999";
  2215. menu.style.background = "rgb(from var(--menu-color) r g b / 1)";
  2216. menu.style.color = "var(--text-color)";
  2217. menu.style.borderColor = "1px solid var(--border-color)";
  2218. menu.style.padding = "0";
  2219. menu.style.boxShadow = "0 4px 16px rgba(0,0,0,0.25)";
  2220. menu.style.display = "none";
  2221. menu.style.minWidth = "220px";
  2222. menu.style.width = "100%";
  2223. menu.style.maxWidth = "470px";
  2224. menu.style.fontFamily = "sans-serif";
  2225. menu.style.userSelect = "none";
  2226. let isDragging = false,
  2227. dragOffsetX = 0,
  2228. dragOffsetY = 0;
  2229. const header = document.createElement("div");
  2230. header.style.display = "flex";
  2231. header.style.justifyContent = "space-between";
  2232. header.style.alignItems = "center";
  2233. header.style.marginBottom = "0";
  2234. header.style.cursor = "move";
  2235. header.style.color = "var(--subject-color)";
  2236. header.style.background = "rgb(from var(--contrast-color) r g b / 1)";
  2237. header.style.padding = "1px 18px 1px";
  2238. header.addEventListener("mousedown", function (e) {
  2239. isDragging = true;
  2240. const rect = menu.getBoundingClientRect();
  2241. dragOffsetX = e.clientX - rect.left;
  2242. dragOffsetY = e.clientY - rect.top;
  2243. document.body.style.userSelect = "none";
  2244. });
  2245. document.addEventListener("mousemove", function (e) {
  2246. if (!isDragging) return;
  2247. let newLeft = e.clientX - dragOffsetX;
  2248. let newTop = e.clientY - dragOffsetY;
  2249. const menuRect = menu.getBoundingClientRect();
  2250. const menuWidth = menuRect.width;
  2251. const menuHeight = menuRect.height;
  2252. const viewportWidth = window.innerWidth;
  2253. const viewportHeight = window.innerHeight;
  2254. newLeft = Math.max(0, Math.min(newLeft, viewportWidth - menuWidth));
  2255. newTop = Math.max(0, Math.min(newTop, viewportHeight - menuHeight));
  2256. menu.style.left = newLeft + "px";
  2257. menu.style.top = newTop + "px";
  2258. menu.style.right = "auto";
  2259. });
  2260. document.addEventListener("mouseup", function () {
  2261. isDragging = false;
  2262. document.body.style.userSelect = "";
  2263. });
  2264. const title = document.createElement("span");
  2265. title.textContent = "8chanSS Settings";
  2266. title.style.fontWeight = "bold";
  2267. header.appendChild(title);
  2268.  
  2269. const closeBtn = document.createElement("button");
  2270. closeBtn.textContent = "✕";
  2271. closeBtn.style.background = "none";
  2272. closeBtn.style.setProperty("background", "none", "important");
  2273. closeBtn.style.border = "none";
  2274. closeBtn.style.color = "var(--subject-color)";
  2275. closeBtn.style.fontSize = "18px";
  2276. closeBtn.style.cursor = "pointer";
  2277. closeBtn.style.marginLeft = "10px";
  2278. closeBtn.addEventListener("click", () => {
  2279. menu.style.display = "none";
  2280. });
  2281. header.appendChild(closeBtn);
  2282.  
  2283. menu.appendChild(header);
  2284. const closeOnOutsideClick = (e) => {
  2285. if (menu.style.display !== "none" && !menu.contains(e.target)) {
  2286. const menuToggle = document.getElementById("8chanSS-icon");
  2287. if (menuToggle && !menuToggle.contains(e.target)) {
  2288. menu.style.display = "none";
  2289. }
  2290. }
  2291. };
  2292. Object.defineProperty(menu.style, 'display', {
  2293. set: function (value) {
  2294. const oldValue = this.getPropertyValue('display');
  2295. this.setProperty('display', value);
  2296. if (oldValue === 'none' && value !== 'none') {
  2297. setTimeout(() => {
  2298. document.addEventListener('click', closeOnOutsideClick);
  2299. }, 10);
  2300. }
  2301. else if (oldValue !== 'none' && value === 'none') {
  2302. document.removeEventListener('click', closeOnOutsideClick);
  2303. }
  2304. },
  2305. get: function () {
  2306. return this.getPropertyValue('display');
  2307. }
  2308. });
  2309. const tabNav = document.createElement("div");
  2310. tabNav.style.display = "flex";
  2311. tabNav.style.borderBottom = "1px solid #444";
  2312. tabNav.style.background = "rgb(from var(--menu-color) r g b / 1)";
  2313. const tabContent = document.createElement("div");
  2314. tabContent.style.padding = "15px 16px";
  2315. tabContent.style.maxHeight = "65vh";
  2316. tabContent.style.overflowY = "auto";
  2317. tabContent.style.scrollbarWidth = "thin";
  2318. tabContent.style.fontSize = "smaller";
  2319. const tempSettings = {};
  2320. await Promise.all(
  2321. Object.keys(flatSettings).map(async (key) => {
  2322. tempSettings[key] = await getSetting(key);
  2323. })
  2324. );
  2325. const tabs = {
  2326. site: {
  2327. label: "Site",
  2328. content: createTabContent("site", tempSettings),
  2329. },
  2330. threads: {
  2331. label: "Threads",
  2332. content: createTabContent("threads", tempSettings),
  2333. },
  2334. catalog: {
  2335. label: "Catalog",
  2336. content: createTabContent("catalog", tempSettings),
  2337. },
  2338. styling: {
  2339. label: "Style",
  2340. content: createTabContent("styling", tempSettings),
  2341. },
  2342. miscel: {
  2343. label: "Misc.",
  2344. content: createTabContent("miscel", tempSettings),
  2345. },
  2346. shortcuts: {
  2347. label: "⌨️",
  2348. content: createShortcutsTab(),
  2349. },
  2350. };
  2351. Object.keys(tabs).forEach((tabId, index, arr) => {
  2352. const tab = tabs[tabId];
  2353. const tabButton = document.createElement("button");
  2354. tabButton.textContent = tab.label;
  2355. tabButton.dataset.tab = tabId;
  2356. tabButton.style.background = index === 0 ? "var(--contrast-color)" : "transparent";
  2357. tabButton.style.border = "none";
  2358. tabButton.style.borderRight = "1px solid #444";
  2359. tabButton.style.setProperty("border-left-radius", "0", "important");
  2360. tabButton.style.color = "var(--text-color)";
  2361. tabButton.style.padding = "8px 15px";
  2362. tabButton.style.margin = "5px 0 0 0";
  2363. tabButton.style.setProperty("border-top-right-radius", "0", "important");
  2364. tabButton.style.setProperty("border-bottom-right-radius", "0", "important");
  2365. tabButton.style.cursor = "pointer";
  2366. tabButton.style.flex = "1";
  2367. tabButton.style.fontSize = "14px";
  2368. tabButton.style.transition = "background 0.2s";
  2369. if (index === 0) {
  2370. tabButton.style.setProperty("border-top-left-radius", "8px", "important");
  2371. tabButton.style.setProperty("border-top-right-radius", "0", "important");
  2372. tabButton.style.setProperty("border-bottom-left-radius", "0", "important");
  2373. tabButton.style.setProperty("border-bottom-right-radius", "0", "important");
  2374. tabButton.style.margin = "5px 0 0 5px";
  2375. }
  2376. if (index === arr.length - 1) {
  2377. tabButton.style.setProperty("border-top-right-radius", "8px", "important");
  2378. tabButton.style.setProperty("border-top-left-radius", "0", "important");
  2379. tabButton.style.setProperty("border-bottom-left-radius", "0", "important");
  2380. tabButton.style.setProperty("border-bottom-right-radius", "0", "important");
  2381. tabButton.style.margin = "5px 5px 0 0";
  2382. tabButton.style.borderRight = "none";
  2383. }
  2384.  
  2385. tabButton.addEventListener("click", () => {
  2386. Object.values(tabs).forEach((t) => {
  2387. t.content.style.display = "none";
  2388. });
  2389. tab.content.style.display = "block";
  2390. tabNav.querySelectorAll("button").forEach((btn) => {
  2391. btn.style.background = "transparent";
  2392. });
  2393. tabButton.style.background = "var(--contrast-color)";
  2394. });
  2395.  
  2396. tabNav.appendChild(tabButton);
  2397. });
  2398.  
  2399. menu.appendChild(tabNav);
  2400. Object.values(tabs).forEach((tab, index) => {
  2401. tab.content.style.display = index === 0 ? "block" : "none";
  2402. tabContent.appendChild(tab.content);
  2403. });
  2404.  
  2405. menu.appendChild(tabContent);
  2406. const buttonContainer = document.createElement("div");
  2407. buttonContainer.style.display = "flex";
  2408. buttonContainer.style.gap = "10px";
  2409. buttonContainer.style.padding = "0 18px 15px";
  2410. const saveBtn = document.createElement("button");
  2411. saveBtn.textContent = "Save";
  2412. saveBtn.style.setProperty("background", "#4caf50", "important");
  2413. saveBtn.style.setProperty("color", "#fff", "important");
  2414. saveBtn.style.border = "none";
  2415. saveBtn.style.borderRadius = "4px";
  2416. saveBtn.style.padding = "8px 18px";
  2417. saveBtn.style.fontSize = "15px";
  2418. saveBtn.style.cursor = "pointer";
  2419. saveBtn.style.flex = "1";
  2420. saveBtn.addEventListener("click", async function () {
  2421. for (const key of Object.keys(tempSettings)) {
  2422. await setSetting(key, tempSettings[key]);
  2423. }
  2424. saveBtn.textContent = "Saved!";
  2425. setTimeout(() => {
  2426. saveBtn.textContent = "Save";
  2427. }, 900);
  2428. setTimeout(() => {
  2429. window.location.reload();
  2430. }, 400);
  2431. });
  2432. buttonContainer.appendChild(saveBtn);
  2433. const resetBtn = document.createElement("button");
  2434. resetBtn.textContent = "Reset";
  2435. resetBtn.style.setProperty("background", "#dd3333", "important");
  2436. resetBtn.style.setProperty("color", "#fff", "important");
  2437. resetBtn.style.border = "none";
  2438. resetBtn.style.borderRadius = "4px";
  2439. resetBtn.style.padding = "8px 18px";
  2440. resetBtn.style.fontSize = "15px";
  2441. resetBtn.style.cursor = "pointer";
  2442. resetBtn.style.flex = "1";
  2443. resetBtn.addEventListener("click", async function () {
  2444. if (confirm("Reset all 8chanSS settings to defaults?")) {
  2445. const keys = await GM.listValues();
  2446. for (const key of keys) {
  2447. if (key.startsWith("8chanSS_")) {
  2448. await GM.deleteValue(key);
  2449. }
  2450. }
  2451. resetBtn.textContent = "Reset!";
  2452. setTimeout(() => {
  2453. resetBtn.textContent = "Reset";
  2454. }, 900);
  2455. setTimeout(() => {
  2456. window.location.reload();
  2457. }, 400);
  2458. }
  2459. });
  2460. buttonContainer.appendChild(resetBtn);
  2461.  
  2462. menu.appendChild(buttonContainer);
  2463. const info = document.createElement("div");
  2464. info.style.fontSize = "11px";
  2465. info.style.padding = "0 18px 12px";
  2466. info.style.opacity = "0.7";
  2467. info.style.textAlign = "center";
  2468. info.innerHTML = 'Press Save to apply changes. Page will reload. - <a href="https://github.com/otacoo/8chanSS/blob/main/CHANGELOG.md" target="_blank" title="Check the changelog." style="color: var(--link-color); text-decoration: underline dashed;">Ver. 1.47.0</a>';
  2469. menu.appendChild(info);
  2470.  
  2471. document.body.appendChild(menu);
  2472. return menu;
  2473. }
  2474. function createTabContent(category, tempSettings) {
  2475. const container = document.createElement("div");
  2476. const categorySettings = scriptSettings[category];
  2477.  
  2478. Object.keys(categorySettings).forEach((key) => {
  2479. const setting = categorySettings[key];
  2480. if (setting.type === "separator") {
  2481. const hr = document.createElement("hr");
  2482. hr.style.border = "none";
  2483. hr.style.borderTop = "1px solid #444";
  2484. hr.style.margin = "12px 0";
  2485. container.appendChild(hr);
  2486. return;
  2487. }
  2488. if (setting.type === "title") {
  2489. const title = document.createElement("div");
  2490. title.textContent = setting.label;
  2491. title.style.fontWeight = "bold";
  2492. title.style.color = "var(--subject-title)";
  2493. title.style.fontSize = "1rem";
  2494. title.style.margin = "10px 0 6px 0";
  2495. title.style.opacity = "0.9";
  2496. container.appendChild(title);
  2497. return;
  2498. }
  2499. const parentRow = document.createElement("div");
  2500. parentRow.style.display = "flex";
  2501. parentRow.style.alignItems = "center";
  2502. parentRow.style.marginBottom = "0px";
  2503. if (key === "hoverVideoVolume" && setting.type === "number") {
  2504. const label = document.createElement("label");
  2505. label.htmlFor = "setting_" + key;
  2506. label.textContent = setting.label + ": ";
  2507. label.style.flex = "1";
  2508.  
  2509. const sliderContainer = document.createElement("div");
  2510. sliderContainer.style.display = "flex";
  2511. sliderContainer.style.alignItems = "center";
  2512. sliderContainer.style.flex = "1";
  2513.  
  2514. const slider = document.createElement("input");
  2515. slider.type = "range";
  2516. slider.id = "setting_" + key;
  2517. slider.min = setting.min;
  2518. slider.max = setting.max;
  2519. slider.value = Number(tempSettings[key]).toString();
  2520. slider.style.flex = "unset";
  2521. slider.style.width = "100px";
  2522. slider.style.marginRight = "10px";
  2523.  
  2524. const valueLabel = document.createElement("span");
  2525. valueLabel.textContent = slider.value + "%";
  2526. valueLabel.style.minWidth = "40px";
  2527. valueLabel.style.textAlign = "right";
  2528.  
  2529. slider.addEventListener("input", function () {
  2530. let val = Number(slider.value);
  2531. if (isNaN(val)) val = setting.default;
  2532. val = Math.max(setting.min, Math.min(setting.max, val));
  2533. slider.value = val.toString();
  2534. tempSettings[key] = val;
  2535. valueLabel.textContent = val + "%";
  2536. });
  2537.  
  2538. sliderContainer.appendChild(slider);
  2539. sliderContainer.appendChild(valueLabel);
  2540.  
  2541. parentRow.appendChild(label);
  2542. parentRow.appendChild(sliderContainer);
  2543. const wrapper = document.createElement("div");
  2544. wrapper.style.marginBottom = "10px";
  2545. wrapper.appendChild(parentRow);
  2546. container.appendChild(wrapper);
  2547. return;
  2548. }
  2549. const checkbox = document.createElement("input");
  2550. checkbox.type = "checkbox";
  2551. checkbox.id = "setting_" + key;
  2552. checkbox.checked =
  2553. tempSettings[key] === true || tempSettings[key] === "true";
  2554. checkbox.style.marginRight = "8px";
  2555. const label = document.createElement("label");
  2556. label.htmlFor = checkbox.id;
  2557. label.textContent = setting.label;
  2558. label.style.flex = "1";
  2559. let chevron = null;
  2560. let subOptionsContainer = null;
  2561. if (setting?.subOptions) {
  2562. chevron = document.createElement("span");
  2563. chevron.className = "ss-chevron";
  2564. chevron.innerHTML = "&#9654;";
  2565. chevron.style.display = "inline-block";
  2566. chevron.style.transition = "transform 0.2s";
  2567. chevron.style.marginLeft = "6px";
  2568. chevron.style.fontSize = "12px";
  2569. chevron.style.userSelect = "none";
  2570. chevron.style.transform = checkbox.checked
  2571. ? "rotate(90deg)"
  2572. : "rotate(0deg)";
  2573. }
  2574. checkbox.addEventListener("change", function () {
  2575. tempSettings[key] = checkbox.checked;
  2576. if (!setting?.subOptions) return;
  2577. if (!subOptionsContainer) return;
  2578.  
  2579. subOptionsContainer.style.display = checkbox.checked
  2580. ? "block"
  2581. : "none";
  2582.  
  2583. if (!chevron) return;
  2584. chevron.style.transform = checkbox.checked
  2585. ? "rotate(90deg)"
  2586. : "rotate(0deg)";
  2587. });
  2588.  
  2589. parentRow.appendChild(checkbox);
  2590. parentRow.appendChild(label);
  2591. if (chevron) parentRow.appendChild(chevron);
  2592. const wrapper = document.createElement("div");
  2593. wrapper.style.marginBottom = "10px";
  2594.  
  2595. wrapper.appendChild(parentRow);
  2596. if (setting?.subOptions) {
  2597. subOptionsContainer = document.createElement("div");
  2598. subOptionsContainer.style.marginLeft = "25px";
  2599. subOptionsContainer.style.marginTop = "5px";
  2600. subOptionsContainer.style.display = checkbox.checked ? "block" : "none";
  2601.  
  2602. Object.keys(setting.subOptions).forEach((subKey) => {
  2603. const subSetting = setting.subOptions[subKey];
  2604. const fullKey = `${key}_${subKey}`;
  2605.  
  2606. const subWrapper = document.createElement("div");
  2607. subWrapper.style.marginBottom = "5px";
  2608.  
  2609. if (subSetting.type === "text") {
  2610. const subLabel = document.createElement("label");
  2611. subLabel.htmlFor = "setting_" + fullKey;
  2612. subLabel.textContent = subSetting.label + ": ";
  2613.  
  2614. const subInput = document.createElement("input");
  2615. subInput.type = "text";
  2616. subInput.id = "setting_" + fullKey;
  2617. subInput.value = tempSettings[fullKey] || "";
  2618. subInput.maxLength = subSetting.maxLength;
  2619. subInput.style.width = "60px";
  2620. subInput.style.marginLeft = "2px";
  2621. subInput.placeholder = "(!) ";
  2622. subInput.addEventListener("input", function () {
  2623. let val = subInput.value.replace(/[<>"']/g, "");
  2624. if (val.length > subInput.maxLength) {
  2625. val = val.slice(0, subInput.maxLength);
  2626. }
  2627. subInput.value = val;
  2628. tempSettings[fullKey] = val;
  2629. });
  2630.  
  2631. subWrapper.appendChild(subLabel);
  2632. subWrapper.appendChild(subInput);
  2633. } else if (subSetting.type === "textarea") {
  2634. const subLabel = document.createElement("label");
  2635. subLabel.htmlFor = "setting_" + fullKey;
  2636. subLabel.textContent = subSetting.label + ": ";
  2637.  
  2638. const subTextarea = document.createElement("textarea");
  2639. subTextarea.id = "setting_" + fullKey;
  2640. subTextarea.value = tempSettings[fullKey] || "";
  2641. subTextarea.rows = subSetting.rows || 4;
  2642. subTextarea.style.width = "90%";
  2643. subTextarea.style.margin = "5px 0 0";
  2644. subTextarea.placeholder = subSetting.placeholder || "";
  2645.  
  2646. subTextarea.addEventListener("input", function () {
  2647. tempSettings[fullKey] = subTextarea.value;
  2648. });
  2649.  
  2650. subWrapper.appendChild(subLabel);
  2651. subWrapper.appendChild(document.createElement("br"));
  2652. subWrapper.appendChild(subTextarea);
  2653. } else if (subSetting.type === "number") {
  2654. const subLabel = document.createElement("label");
  2655. subLabel.htmlFor = "setting_" + fullKey;
  2656. subLabel.textContent = subSetting.label + ": ";
  2657.  
  2658. const subInput = document.createElement("input");
  2659. subInput.type = "number";
  2660. subInput.id = "setting_" + fullKey;
  2661. subInput.value = tempSettings[fullKey] || subSetting.default;
  2662. if (subSetting.min !== undefined) subInput.min = subSetting.min;
  2663. if (subSetting.max !== undefined) subInput.max = subSetting.max;
  2664. subInput.style.width = "60px";
  2665. subInput.style.marginLeft = "2px";
  2666.  
  2667. subInput.addEventListener("input", function () {
  2668. let val = Number(subInput.value);
  2669. if (isNaN(val)) val = subSetting.default;
  2670. if (subSetting.min !== undefined) val = Math.max(subSetting.min, val);
  2671. if (subSetting.max !== undefined) val = Math.min(subSetting.max, val);
  2672. subInput.value = val;
  2673. tempSettings[fullKey] = val;
  2674. });
  2675.  
  2676. subWrapper.appendChild(subLabel);
  2677. subWrapper.appendChild(subInput);
  2678. } else if (subSetting.type === "select") {
  2679. const subSelect = document.createElement("select");
  2680. subSelect.id = "setting_" + fullKey;
  2681. subSelect.style.marginLeft = "5px";
  2682. subSelect.style.width = "120px";
  2683. if (Array.isArray(subSetting.options)) {
  2684. subSetting.options.forEach(option => {
  2685. const optionEl = document.createElement("option");
  2686. optionEl.value = option.value;
  2687. optionEl.textContent = option.label;
  2688. if (tempSettings[fullKey] === option.value) {
  2689. optionEl.selected = true;
  2690. }
  2691. subSelect.appendChild(optionEl);
  2692. });
  2693. }
  2694. if (!subSelect.value && subSetting.default) {
  2695. subSelect.value = subSetting.default;
  2696. tempSettings[fullKey] = subSetting.default;
  2697. }
  2698. subSelect.addEventListener("change", function () {
  2699. tempSettings[fullKey] = subSelect.value;
  2700. if (key === "customFavicon" && tempSettings["customFavicon"]) {
  2701. faviconManager.setFaviconStyle(subSelect.value, "base");
  2702. }
  2703. if (key === "faviconStyle" && tempSettings["customFavicon"]) {
  2704. faviconManager.setFaviconStyle(subSelect.value, "base");
  2705. }
  2706. });
  2707.  
  2708. const subLabel = document.createElement("label");
  2709. subLabel.htmlFor = "setting_" + fullKey;
  2710. subLabel.textContent = subSetting.label || fullKey;
  2711. subLabel.style.marginLeft = "10px";
  2712.  
  2713. subWrapper.appendChild(subLabel);
  2714. subWrapper.appendChild(subSelect);
  2715. } else {
  2716. const subCheckbox = document.createElement("input");
  2717. subCheckbox.type = "checkbox";
  2718. subCheckbox.id = "setting_" + fullKey;
  2719. subCheckbox.checked = tempSettings[fullKey];
  2720. subCheckbox.style.marginRight = "8px";
  2721.  
  2722. subCheckbox.addEventListener("change", function () {
  2723. tempSettings[fullKey] = subCheckbox.checked;
  2724. });
  2725.  
  2726. const subLabel = document.createElement("label");
  2727. subLabel.htmlFor = subCheckbox.id;
  2728. subLabel.textContent = subSetting.label;
  2729.  
  2730. subWrapper.appendChild(subCheckbox);
  2731. subWrapper.appendChild(subLabel);
  2732. }
  2733. subOptionsContainer.appendChild(subWrapper);
  2734. });
  2735.  
  2736. wrapper.appendChild(subOptionsContainer);
  2737. }
  2738.  
  2739. container.appendChild(wrapper);
  2740. });
  2741.  
  2742. return container;
  2743. }
  2744. const themeSelector = document.getElementById("themesBefore");
  2745. let link = null;
  2746. let bracketSpan = null;
  2747. if (themeSelector) {
  2748. bracketSpan = document.createElement("span");
  2749. bracketSpan.textContent = "] [ ";
  2750. link = document.createElement("a");
  2751. link.id = "8chanSS-icon";
  2752. link.href = "#";
  2753. link.textContent = "8chanSS";
  2754. link.style.fontWeight = "bold";
  2755.  
  2756. themeSelector.parentNode.insertBefore(
  2757. bracketSpan,
  2758. themeSelector.nextSibling
  2759. );
  2760. themeSelector.parentNode.insertBefore(link, bracketSpan.nextSibling);
  2761. }
  2762. function createShortcutsTab() {
  2763. const container = document.createElement("div");
  2764. const title = document.createElement("h3");
  2765. title.textContent = "Keyboard Shortcuts";
  2766. title.style.margin = "0 0 15px 0";
  2767. title.style.fontSize = "16px";
  2768. container.appendChild(title);
  2769. const table = document.createElement("table");
  2770. table.style.width = "100%";
  2771. table.style.borderCollapse = "collapse";
  2772. const tableStyles = {
  2773. th: {
  2774. textAlign: "left",
  2775. padding: "8px 5px",
  2776. borderBottom: "1px solid #444",
  2777. fontSize: "14px",
  2778. fontWeight: "bold",
  2779. },
  2780. td: {
  2781. padding: "8px 5px",
  2782. borderBottom: "1px solid #333",
  2783. fontSize: "13px",
  2784. },
  2785. kbd: {
  2786. background: "#f7f7f7",
  2787. color: "#000",
  2788. border: "1px solid #555",
  2789. borderRadius: "3px",
  2790. padding: "2px 5px",
  2791. fontSize: "12px",
  2792. fontFamily: "monospace",
  2793. },
  2794. };
  2795. const headerRow = document.createElement("tr");
  2796. const shortcutHeader = document.createElement("th");
  2797. shortcutHeader.textContent = "Shortcut";
  2798. Object.assign(shortcutHeader.style, tableStyles.th);
  2799. headerRow.appendChild(shortcutHeader);
  2800.  
  2801. const actionHeader = document.createElement("th");
  2802. actionHeader.textContent = "Action";
  2803. Object.assign(actionHeader.style, tableStyles.th);
  2804. headerRow.appendChild(actionHeader);
  2805.  
  2806. table.appendChild(headerRow);
  2807. const shortcuts = [
  2808. { keys: ["Ctrl", "F1"], action: "Open 8chanSS settings" },
  2809. { keys: ["Tab"], action: "Target Quick Reply text area" },
  2810. { keys: ["R"], action: "Refresh Thread (5 sec. cooldown)" },
  2811. { keys: ["Ctrl", "Q"], action: "Toggle Quick Reply" },
  2812. { keys: ["Ctrl", "Enter"], action: "Submit post" },
  2813. { keys: ["Escape"], action: "Clear QR textarea and hide all dialogs" },
  2814. { keys: ["ALT", "W"], action: "Watch Thread" },
  2815. { keys: ["SHIFT", "T"], action: "Toggle Quote Threading" },
  2816. { keys: ["SHIFT", "M1"], action: "Hide Thread in Catalog" },
  2817. { keys: ["CTRL", "UP/DOWN"], action: "Scroll between Your Replies" },
  2818. { keys: ["CTRL", "SHIFT", "UP/DOWN"], action: "Scroll between Replies to You" },
  2819. { keys: ["Ctrl", "B"], action: "Bold text" },
  2820. { keys: ["Ctrl", "I"], action: "Italic text" },
  2821. { keys: ["Ctrl", "U"], action: "Underline text" },
  2822. { keys: ["Ctrl", "S"], action: "Spoiler text" },
  2823. { keys: ["Ctrl", "D"], action: "Srz Bizniz text" },
  2824. { keys: ["Ctrl", "M"], action: "Moe text" },
  2825. { keys: ["Alt", "C"], action: "Code block" },
  2826. ];
  2827. shortcuts.forEach((shortcut) => {
  2828. const row = document.createElement("tr");
  2829. const shortcutCell = document.createElement("td");
  2830. Object.assign(shortcutCell.style, tableStyles.td);
  2831. shortcut.keys.forEach((key, index) => {
  2832. const kbd = document.createElement("kbd");
  2833. kbd.textContent = key;
  2834. Object.assign(kbd.style, tableStyles.kbd);
  2835. shortcutCell.appendChild(kbd);
  2836. if (index < shortcut.keys.length - 1) {
  2837. const plus = document.createTextNode(" + ");
  2838. shortcutCell.appendChild(plus);
  2839. }
  2840. });
  2841.  
  2842. row.appendChild(shortcutCell);
  2843. const actionCell = document.createElement("td");
  2844. actionCell.textContent = shortcut.action;
  2845. Object.assign(actionCell.style, tableStyles.td);
  2846. row.appendChild(actionCell);
  2847.  
  2848. table.appendChild(row);
  2849. });
  2850.  
  2851. container.appendChild(table);
  2852. const note = document.createElement("p");
  2853. note.textContent =
  2854. "Text formatting shortcuts work when text is selected or when inserting at cursor position.";
  2855. note.style.fontSize = "12px";
  2856. note.style.marginTop = "15px";
  2857. note.style.opacity = "0.7";
  2858. note.style.fontStyle = "italic";
  2859. container.appendChild(note);
  2860.  
  2861. return container;
  2862. }
  2863. if (link) {
  2864. let menu = await createSettingsMenu();
  2865. link.style.cursor = "pointer";
  2866. link.title = "Open 8chanSS settings";
  2867. link.addEventListener("click", async function (e) {
  2868. e.preventDefault();
  2869. let menu = await createSettingsMenu();
  2870. menu.style.display = menu.style.display === "none" ? "block" : "none";
  2871. });
  2872. }
  2873. async function shortcutsGloballyEnabled() {
  2874. return await getSetting("enableShortcuts");
  2875. }
  2876. const bbCodeCombinations = new Map([
  2877. ["s", ["[spoiler]", "[/spoiler]"]],
  2878. ["b", ["'''", "'''"]],
  2879. ["u", ["__", "__"]],
  2880. ["i", ["''", "''"]],
  2881. ["d", ["==", "=="]],
  2882. ["m", ["[moe]", "[/moe]"]],
  2883. ["c", ["[code]", "[/code]"]],
  2884. ]);
  2885. function applyBBCode(textBox, key) {
  2886. const [openTag, closeTag] = bbCodeCombinations.get(key);
  2887. const { selectionStart, selectionEnd, value } = textBox;
  2888.  
  2889. if (selectionStart === selectionEnd) {
  2890. const before = value.slice(0, selectionStart);
  2891. const after = value.slice(selectionEnd);
  2892. const newCursor = selectionStart + openTag.length;
  2893. textBox.value = before + openTag + closeTag + after;
  2894. textBox.selectionStart = textBox.selectionEnd = newCursor;
  2895. } else {
  2896. const before = value.slice(0, selectionStart);
  2897. const selected = value.slice(selectionStart, selectionEnd);
  2898. const after = value.slice(selectionEnd);
  2899. textBox.value = before + openTag + selected + closeTag + after;
  2900. textBox.selectionStart = selectionStart + openTag.length;
  2901. textBox.selectionEnd = selectionEnd + openTag.length;
  2902. }
  2903. }
  2904. let lastHighlighted = null;
  2905. let lastType = null;
  2906. let lastRefreshTime = 0;
  2907.  
  2908. function getEligiblePostCells(isOwnReply) {
  2909. const selector = isOwnReply
  2910. ? '.postCell:has(a.youName), .opCell:has(a.youName)'
  2911. : '.postCell:has(a.quoteLink.you), .opCell:has(a.quoteLink.you)';
  2912. return Array.from(document.querySelectorAll(selector));
  2913. }
  2914.  
  2915. function scrollToReply(isOwnReply = true, getNextReply = true) {
  2916. const postCells = getEligiblePostCells(isOwnReply);
  2917. if (!postCells.length) return;
  2918. let currentIndex = -1;
  2919. const expectedType = isOwnReply ? "own" : "reply";
  2920. if (
  2921. lastType === expectedType &&
  2922. lastHighlighted
  2923. ) {
  2924. const container = lastHighlighted.closest('.postCell, .opCell');
  2925. currentIndex = postCells.indexOf(container);
  2926. }
  2927. if (currentIndex === -1) {
  2928. const viewportMiddle = window.innerHeight / 2;
  2929. currentIndex = postCells.findIndex(cell => {
  2930. const rect = cell.getBoundingClientRect();
  2931. return rect.top + rect.height / 2 > viewportMiddle;
  2932. });
  2933. if (currentIndex === -1) {
  2934. currentIndex = getNextReply ? -1 : postCells.length;
  2935. }
  2936. }
  2937. const targetIndex = getNextReply ? currentIndex + 1 : currentIndex - 1;
  2938. if (targetIndex < 0 || targetIndex >= postCells.length) return;
  2939.  
  2940. const postContainer = postCells[targetIndex];
  2941. if (postContainer) {
  2942. postContainer.scrollIntoView({ behavior: "smooth", block: "center" });
  2943. if (lastHighlighted) {
  2944. lastHighlighted.classList.remove('target-highlight');
  2945. }
  2946. let anchorId = null;
  2947. let anchorElem = postContainer.querySelector('[id^="p"]');
  2948. if (anchorElem && anchorElem.id) {
  2949. anchorId = anchorElem.id;
  2950. } else if (postContainer.id) {
  2951. anchorId = postContainer.id;
  2952. }
  2953. if (anchorId && location.hash !== '#' + anchorId) {
  2954. history.replaceState(null, '', '#' + anchorId);
  2955. }
  2956. const innerPost = postContainer.querySelector('.innerPost');
  2957. if (innerPost) {
  2958. innerPost.classList.add('target-highlight');
  2959. lastHighlighted = innerPost;
  2960. } else {
  2961. lastHighlighted = null;
  2962. }
  2963. lastType = isOwnReply ? "own" : "reply";
  2964. }
  2965. }
  2966. window.addEventListener('hashchange', () => {
  2967. if (lastHighlighted) {
  2968. lastHighlighted.classList.remove('target-highlight');
  2969. lastHighlighted = null;
  2970. }
  2971. const hash = location.hash.replace('#', '');
  2972. if (hash) {
  2973. const postElem = document.getElementById(hash);
  2974. if (postElem) {
  2975. const innerPost = postElem.querySelector('.innerPost');
  2976. if (innerPost) {
  2977. innerPost.classList.add('target-highlight');
  2978. lastHighlighted = innerPost;
  2979. }
  2980. }
  2981. }
  2982. });
  2983. document.addEventListener("keydown", async function (event) {
  2984. if (!(await shortcutsGloballyEnabled())) return;
  2985. const active = document.activeElement;
  2986. if (
  2987. active &&
  2988. event.key !== "Tab" &&
  2989. (active.tagName === "INPUT" ||
  2990. active.tagName === "TEXTAREA" ||
  2991. active.isContentEditable)
  2992. ) {
  2993. return;
  2994. }
  2995. if (event.ctrlKey && event.key === "F1") {
  2996. event.preventDefault();
  2997. let menu = document.getElementById("8chanSS-menu") || (await createSettingsMenu());
  2998. menu.style.display = menu.style.display === "none" || menu.style.display === "" ? "block" : "none";
  2999. return;
  3000. }
  3001. if (event.ctrlKey && (event.key === "q" || event.key === "Q")) {
  3002. event.preventDefault();
  3003. const hiddenDiv = document.getElementById("quick-reply");
  3004. if (!hiddenDiv) return;
  3005. const isHidden = hiddenDiv.style.display === "none" || hiddenDiv.style.display === "";
  3006. hiddenDiv.style.display = isHidden ? "block" : "none";
  3007. if (isHidden) {
  3008. setTimeout(() => {
  3009. const textarea = document.getElementById("qrbody");
  3010. if (textarea) textarea.focus();
  3011. }, 50);
  3012. }
  3013. return;
  3014. }
  3015. if (event.key === "Tab") {
  3016. const qrbody = document.getElementById("qrbody");
  3017. const captcha = document.getElementById("QRfieldCaptcha");
  3018.  
  3019. if (qrbody) {
  3020. if (document.activeElement === qrbody && captcha) {
  3021. event.preventDefault();
  3022. captcha.focus();
  3023. } else if (document.activeElement === captcha) {
  3024. event.preventDefault();
  3025. qrbody.focus();
  3026. } else if (document.activeElement !== qrbody) {
  3027. event.preventDefault();
  3028. qrbody.focus();
  3029. }
  3030. }
  3031. return;
  3032. }
  3033. if (event.key === "r" || event.key === "R") {
  3034. const isThread = document.documentElement.classList.contains("is-thread");
  3035. const isCatalog = document.documentElement.classList.contains("is-catalog");
  3036. const threadRefreshBtn = document.getElementById("refreshButton");
  3037. const catalogRefreshBtn = document.getElementById("catalogRefreshButton");
  3038. const now = Date.now();
  3039.  
  3040. if (
  3041. (isThread && threadRefreshBtn) ||
  3042. (isCatalog && catalogRefreshBtn)
  3043. ) {
  3044. if (now - lastRefreshTime >= 5000) {
  3045. event.preventDefault();
  3046. if (isThread && threadRefreshBtn) {
  3047. threadRefreshBtn.click();
  3048. } else if (isCatalog && catalogRefreshBtn) {
  3049. catalogRefreshBtn.click();
  3050. }
  3051. lastRefreshTime = now;
  3052. } else {
  3053. event.preventDefault();
  3054. }
  3055. return;
  3056. }
  3057. }
  3058. if (event.shiftKey && !event.ctrlKey && !event.altKey && (event.key === "t" || event.key === "T")) {
  3059. event.preventDefault();
  3060.  
  3061. const current = await getSetting("quoteThreading");
  3062. await setSetting("quoteThreading", !current);
  3063. try {
  3064. const msg = `Quote threading ${!current ? "enabled" : "disabled"}`;
  3065. if (window.showToast) {
  3066. window.showToast(msg);
  3067. } else {
  3068. const icon = document.getElementById("8chanSS-icon");
  3069. if (icon) {
  3070. let toast = document.createElement("span");
  3071. toast.textContent = msg;
  3072. toast.style.position = "absolute";
  3073. toast.style.background = "#222";
  3074. toast.style.color = "#fff";
  3075. toast.style.padding = "2px 8px";
  3076. toast.style.borderRadius = "4px";
  3077. toast.style.fontSize = "13px";
  3078. toast.style.zIndex = 99999;
  3079. toast.style.left = (icon.offsetLeft - 50) + "px";
  3080. toast.style.top = "27px";
  3081. toast.style.transition = "opacity 0.3s";
  3082. icon.parentNode.appendChild(toast);
  3083. setTimeout(() => { toast.style.opacity = "0"; }, 900);
  3084. setTimeout(() => { toast.remove(); }, 1200);
  3085. }
  3086. }
  3087. } catch { }
  3088. setTimeout(() => window.location.reload(), 1400);
  3089. return;
  3090. }
  3091. if (event.key === "Escape") {
  3092. const textarea = document.getElementById("qrbody");
  3093. if (textarea) textarea.value = "";
  3094. const quickReply = document.getElementById("quick-reply");
  3095. if (quickReply) quickReply.style.display = "none";
  3096. const threadWatcher = document.getElementById("watchedMenu");
  3097. if (threadWatcher) threadWatcher.style.display = "none";
  3098. return;
  3099. }
  3100. if (event.ctrlKey && (event.key === 'ArrowDown' || event.key === 'ArrowUp')) {
  3101. event.preventDefault();
  3102. const isOwnReply = !event.shiftKey;
  3103. const isNext = event.key === 'ArrowDown';
  3104. scrollToReply(isOwnReply, isNext);
  3105. return;
  3106. }
  3107. if (
  3108. event.altKey &&
  3109. (event.key === "w" || event.key === "W")
  3110. ) {
  3111. event.preventDefault();
  3112. const btn = document.querySelector(".watchButton");
  3113. if (btn && !btn.classList.contains("watched-active")) {
  3114. btn.click();
  3115. setTimeout(() => {
  3116. btn.classList.add("watched-active");
  3117. }, 100);
  3118. }
  3119. return;
  3120. }
  3121. });
  3122. const replyTextarea = document.getElementById("qrbody");
  3123. if (replyTextarea) {
  3124. replyTextarea.addEventListener("keydown", async function (event) {
  3125. if (event.ctrlKey && event.key === "Enter") {
  3126. event.preventDefault();
  3127. const submitButton = document.getElementById("qrbutton");
  3128. if (submitButton) {
  3129. submitButton.click();
  3130. if (await getSetting("watchThreadOnReply")) {
  3131. setTimeout(() => {
  3132. const btn = document.querySelector(".watchButton");
  3133. if (btn && !btn.classList.contains("watched-active")) {
  3134. btn.click();
  3135. setTimeout(() => {
  3136. btn.classList.add("watched-active");
  3137. }, 100);
  3138. }
  3139. }, 500);
  3140. }
  3141. }
  3142. }
  3143. });
  3144. replyTextarea.addEventListener("keydown", function (event) {
  3145. const key = event.key.toLowerCase();
  3146. if (key === "c" && event.altKey && !event.ctrlKey && bbCodeCombinations.has(key)) {
  3147. event.preventDefault();
  3148. applyBBCode(event.target, key);
  3149. return;
  3150. }
  3151. if (event.ctrlKey && !event.altKey && bbCodeCombinations.has(key) && key !== "c") {
  3152. event.preventDefault();
  3153. applyBBCode(event.target, key);
  3154. return;
  3155. }
  3156. });
  3157. }
  3158. function featureCatalogHiding() {
  3159. const STORAGE_KEY = "8chanSS_hiddenCatalogThreads";
  3160. let showHiddenMode = false;
  3161. function getBoardAndThreadNumFromCell(cell) {
  3162. const link = cell.querySelector("a.linkThumb[href*='/res/']");
  3163. if (!link) return { board: null, threadNum: null };
  3164. const match = link.getAttribute("href").match(/^\/([^/]+)\/res\/(\d+)\.html/);
  3165. if (!match) return { board: null, threadNum: null };
  3166. return { board: match[1], threadNum: match[2] };
  3167. }
  3168. async function loadHiddenThreadsObj() {
  3169. const raw = await GM.getValue(STORAGE_KEY, "{}");
  3170. try {
  3171. const obj = JSON.parse(raw);
  3172. return typeof obj === "object" && obj !== null ? obj : {};
  3173. } catch {
  3174. return {};
  3175. }
  3176. }
  3177. async function saveHiddenThreadsObj(obj) {
  3178. await GM.setValue(STORAGE_KEY, JSON.stringify(obj));
  3179. }
  3180. async function applyHiddenThreads() {
  3181. const hiddenThreadsObjRaw = await GM.getValue(STORAGE_KEY, "{}");
  3182. let hiddenThreadsObj;
  3183. try {
  3184. hiddenThreadsObj = JSON.parse(hiddenThreadsObjRaw);
  3185. if (typeof hiddenThreadsObj !== "object" || hiddenThreadsObj === null) hiddenThreadsObj = {};
  3186. } catch {
  3187. hiddenThreadsObj = {};
  3188. }
  3189. document.querySelectorAll(".catalogCell").forEach(cell => {
  3190. const { board, threadNum } = getBoardAndThreadNumFromCell(cell);
  3191. if (!board || !threadNum) return;
  3192. const hiddenThreads = hiddenThreadsObj[board] || [];
  3193.  
  3194. if (typeof showHiddenMode !== "undefined" && showHiddenMode) {
  3195. if (hiddenThreads.includes(threadNum)) {
  3196. cell.style.display = "";
  3197. cell.classList.add("ss-unhide-thread");
  3198. cell.classList.remove("ss-hidden-thread");
  3199. } else {
  3200. cell.style.display = "none";
  3201. cell.classList.remove("ss-unhide-thread", "ss-hidden-thread");
  3202. }
  3203. } else {
  3204. if (hiddenThreads.includes(threadNum)) {
  3205. cell.style.display = "none";
  3206. cell.classList.add("ss-hidden-thread");
  3207. cell.classList.remove("ss-unhide-thread");
  3208. } else {
  3209. cell.style.display = "";
  3210. cell.classList.remove("ss-hidden-thread", "ss-unhide-thread");
  3211. }
  3212. }
  3213. });
  3214. }
  3215. async function onCatalogCellClick(e) {
  3216. const cell = e.target.closest(".catalogCell");
  3217. if (!cell) return;
  3218. if (e.shiftKey && e.button === 0) {
  3219. const { board, threadNum } = getBoardAndThreadNumFromCell(cell);
  3220. if (!board || !threadNum) return;
  3221.  
  3222. let hiddenThreadsObj = await loadHiddenThreadsObj();
  3223. if (!hiddenThreadsObj[board]) hiddenThreadsObj[board] = [];
  3224. let hiddenThreads = hiddenThreadsObj[board];
  3225.  
  3226. if (showHiddenMode) {
  3227. hiddenThreads = hiddenThreads.filter(num => num !== threadNum);
  3228. hiddenThreadsObj[board] = hiddenThreads;
  3229. await saveHiddenThreadsObj(hiddenThreadsObj);
  3230. await applyHiddenThreads();
  3231. } else {
  3232. if (!hiddenThreads.includes(threadNum)) {
  3233. hiddenThreads.push(threadNum);
  3234. hiddenThreadsObj[board] = hiddenThreads;
  3235. }
  3236. await saveHiddenThreadsObj(hiddenThreadsObj);
  3237. cell.style.display = "none";
  3238. cell.classList.add("ss-hidden-thread");
  3239. }
  3240. e.preventDefault();
  3241. e.stopPropagation();
  3242. }
  3243. }
  3244. async function showAllHiddenThreads() {
  3245. showHiddenMode = true;
  3246. await applyHiddenThreads();
  3247. const btn = document.getElementById("ss-show-hidden-btn");
  3248. if (btn) btn.textContent = "Hide Hidden";
  3249. }
  3250. async function hideAllHiddenThreads() {
  3251. showHiddenMode = false;
  3252. await applyHiddenThreads();
  3253. const btn = document.getElementById("ss-show-hidden-btn");
  3254. if (btn) btn.textContent = "Show Hidden";
  3255. }
  3256. async function toggleShowHiddenThreads() {
  3257. if (showHiddenMode) {
  3258. await hideAllHiddenThreads();
  3259. } else {
  3260. await showAllHiddenThreads();
  3261. }
  3262. }
  3263. function addShowHiddenButton() {
  3264. if (document.getElementById("ss-show-hidden-btn")) return;
  3265. const refreshBtn = document.querySelector("#catalogRefreshButton");
  3266. if (!refreshBtn) return;
  3267. const btn = document.createElement("button");
  3268. btn.id = "ss-show-hidden-btn";
  3269. btn.className = "catalogLabel";
  3270. btn.type = "button";
  3271. btn.textContent = "Show Hidden";
  3272. btn.style.marginRight = "8px";
  3273. btn.addEventListener("click", toggleShowHiddenThreads);
  3274. refreshBtn.parentNode.insertBefore(btn, refreshBtn);
  3275. }
  3276. function hideThreadsOnRefresh() {
  3277. if (!/\/catalog\.html$/.test(window.location.pathname)) return;
  3278. onReady(addShowHiddenButton);
  3279. onReady(applyHiddenThreads);
  3280. const catalogContainer = document.querySelector(".catalogWrapper, .catalogDiv");
  3281. if (catalogContainer) {
  3282. catalogContainer.addEventListener("click", onCatalogCellClick, true);
  3283. const observer = new MutationObserver(applyHiddenThreads);
  3284. observer.observe(catalogContainer, { childList: true, subtree: false });
  3285. }
  3286. }
  3287. hideThreadsOnRefresh();
  3288. }
  3289. function noCaptchaHistory() {
  3290. const captchaInput = document.getElementById("QRfieldCaptcha");
  3291. if (captchaInput) {
  3292. captchaInput.autocomplete = "off";
  3293. }
  3294. }
  3295. noCaptchaHistory();
  3296. function preventFooterScrollIntoView() {
  3297. const footer = document.getElementById('footer');
  3298. if (footer && !footer._scrollBlocked) {
  3299. footer._scrollBlocked = true;
  3300. footer.scrollIntoView = function () {
  3301. return;
  3302. };
  3303. }
  3304. }
  3305. function moveFileUploadsBelowOp() {
  3306. if (opHeadTitle && innerOP) {
  3307. innerOP.insertBefore(opHeadTitle, innerOP.firstChild);
  3308. }
  3309. }
  3310. moveFileUploadsBelowOp();
  3311. document.addEventListener('click', function (e) {
  3312. const a = e.target.closest('.panelBacklinks > a');
  3313. if (a) {
  3314. setTimeout(() => {
  3315. a.classList.toggle('reply-inlined');
  3316. }, 0);
  3317. return;
  3318. }
  3319. const b = e.target.closest('a.quoteLink');
  3320. if (b) {
  3321. setTimeout(() => {
  3322. b.classList.toggle('quote-inlined');
  3323. }, 0);
  3324. }
  3325. });
  3326. function enableIdFiltering() {
  3327. const postCellSelector = ".postCell";
  3328. const labelIdSelector = ".labelId";
  3329. const hiddenClassName = "is-hidden-by-filter";
  3330. let activeFilterColor = null;
  3331. function applyFilter(targetRgbColor) {
  3332. activeFilterColor = targetRgbColor;
  3333. document.querySelectorAll(postCellSelector).forEach(cell => {
  3334. const label = cell.querySelector(labelIdSelector);
  3335. const matches = label && window.getComputedStyle(label).backgroundColor === targetRgbColor;
  3336. cell.classList.toggle(hiddenClassName, !!targetRgbColor && !matches);
  3337. });
  3338. }
  3339. function handleClick(event) {
  3340. const clickedLabel = event.target.closest(labelIdSelector);
  3341. if (clickedLabel && clickedLabel.closest(postCellSelector) && !clickedLabel.closest(".de-pview")) {
  3342. event.preventDefault();
  3343. event.stopPropagation();
  3344.  
  3345. const clickedColor = window.getComputedStyle(clickedLabel).backgroundColor;
  3346. const rect = clickedLabel.getBoundingClientRect();
  3347. const cursorOffsetY = event.clientY - rect.top;
  3348.  
  3349. if (activeFilterColor === clickedColor) {
  3350. applyFilter(null);
  3351. } else {
  3352. applyFilter(clickedColor);
  3353. }
  3354. clickedLabel.scrollIntoView({ behavior: "instant", block: "center" });
  3355. window.scrollBy(0, cursorOffsetY - rect.height / 2);
  3356. }
  3357. }
  3358. document.body.addEventListener("click", handleClick);
  3359. }
  3360. });