cyberdevil.invidious.keyboard.user.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391
  1. // ==UserScript==
  2. // @name Invidious Keyboard Browse
  3. // @version 1
  4. // @grant none
  5. // @include https://yewtu.be/channel/*
  6. // @include https://yewtu.be/watch*
  7. // @include https://yewtu.be/search*
  8. // @include https://yewtu.be/feed/*
  9. // @include https://vid.puffyan.us/channel/*
  10. // @include https://vid.puffyan.us/watch*
  11. // @include https://vid.puffyan.us/search*
  12. // @include https://vid.puffyan.us/feed/*
  13. // @include https://inv.riverside.rocks/channel/*
  14. // @include https://inv.riverside.rocks/watch*
  15. // @include https://inv.riverside.rocks/search*
  16. // @include https://inv.riverside.rocks/feed/*
  17. // @include https://invidious.kavin.rocks/channel/*
  18. // @include https://invidious.kavin.rocks/watch*
  19. // @include https://invidious.kavin.rocks/search*
  20. // @include https://invidious.kavin.rocks/feed/*
  21. // @include https://invidio.xamh.de/channel/*
  22. // @include https://invidio.xamh.de/watch*
  23. // @include https://invidio.xamh.de/search*
  24. // @include https://invidio.xamh.de/feed/*
  25. // @include https://y.com.sb/channel/*
  26. // @include https://y.com.sb/watch*
  27. // @include https://y.com.sb/search*
  28. // @include https://y.com.sb/feed/*
  29. // @include https://invidious.nerdvpn.de/channel/*
  30. // @include https://invidious.nerdvpn.de/watch*
  31. // @include https://invidious.nerdvpn.de/search*
  32. // @include https://invidious.nerdvpn.de/feed/*
  33. // @include https://yt.artemislena.eu/channel/*
  34. // @include https://yt.artemislena.eu/watch*
  35. // @include https://yt.artemislena.eu/search*
  36. // @include https://yt.artemislena.eu/feed/*
  37. // @include https://invidious.tiekoetter.com/channel/*
  38. // @include https://invidious.tiekoetter.com/watch*
  39. // @include https://invidious.tiekoetter.com/search*
  40. // @include https://invidious.tiekoetter.com/feed/*
  41. // @include https://invidious.flokinet.to/channel/*
  42. // @include https://invidious.flokinet.to/watch*
  43. // @include https://invidious.flokinet.to/search*
  44. // @include https://invidious.flokinet.to/feed/*
  45. // @include https://inv.bp.projectsegfau.lt/channel/*
  46. // @include https://inv.bp.projectsegfau.lt/watch*
  47. // @include https://inv.bp.projectsegfau.lt/search*
  48. // @include https://inv.bp.projectsegfau.lt/feed/*
  49. // @include https://inv.vern.cc/channel/*
  50. // @include https://inv.vern.cc/watch*
  51. // @include https://inv.vern.cc/search*
  52. // @include https://inv.vern.cc/feed/*
  53. // @include https://inv.odyssey346.dev/channel/*
  54. // @include https://inv.odyssey346.dev/watch*
  55. // @include https://inv.odyssey346.dev/search*
  56. // @include https://inv.odyssey346.dev/feed/*
  57. // @include https://invidious.snopyta.org/channel/*
  58. // @include https://invidious.snopyta.org/watch*
  59. // @include https://invidious.snopyta.org/search*
  60. // @include https://invidious.snopyta.org/feed/*
  61. // @include https://invidious.baczek.me/channel/*
  62. // @include https://invidious.baczek.me/watch*
  63. // @include https://invidious.baczek.me/search*
  64. // @include https://invidious.baczek.me/feed/*
  65. // @include https://invidious.sethforprivacy.com/channel/*
  66. // @include https://invidious.sethforprivacy.com/watch*
  67. // @include https://invidious.sethforprivacy.com/search*
  68. // @include https://invidious.sethforprivacy.com/feed/*
  69. // @include https://yt.funami.tech/channel/*
  70. // @include https://yt.funami.tech/watch*
  71. // @include https://yt.funami.tech/search*
  72. // @include https://yt.funami.tech/feed/*
  73. // @include https://invidious.drivet.xyz/channel/*
  74. // @include https://invidious.drivet.xyz/watch*
  75. // @include https://invidious.drivet.xyz/search*
  76. // @include https://invidious.drivet.xyz/feed/*
  77. // @include https://vid.priv.au/channel/*
  78. // @include https://vid.priv.au/watch*
  79. // @include https://vid.priv.au/search*
  80. // @include https://vid.priv.au/feed/*
  81. // @include https://invidious.silur.me/channel/*
  82. // @include https://invidious.silur.me/watch*
  83. // @include https://invidious.silur.me/search*
  84. // @include https://invidious.silur.me/feed/*
  85. // @include https://invidious.epicsite.xyz/channel/*
  86. // @include https://invidious.epicsite.xyz/watch*
  87. // @include https://invidious.epicsite.xyz/search*
  88. // @include https://invidious.epicsite.xyz/feed/*
  89. // @include https://invidious.slipfox.xyz/channel/*
  90. // @include https://invidious.slipfox.xyz/watch*
  91. // @include https://invidious.slipfox.xyz/search*
  92. // @include https://invidious.slipfox.xyz/feed/*
  93. // @include https://iv.ggtyler.dev/channel/*
  94. // @include https://iv.ggtyler.dev/watch*
  95. // @include https://iv.ggtyler.dev/search*
  96. // @include https://iv.ggtyler.dev/feed/*
  97. // @include https://invidious.dhusch.de/channel/*
  98. // @include https://invidious.dhusch.de/watch*
  99. // @include https://invidious.dhusch.de/search*
  100. // @include https://invidious.dhusch.de/feed/*
  101. // @include https://invidious.weblibre.org/channel/*
  102. // @include https://invidious.weblibre.org/watch*
  103. // @include https://invidious.weblibre.org/search*
  104. // @include https://invidious.weblibre.org/feed/*
  105. // @include https://invidious.esmailelbob.xyz/channel/*
  106. // @include https://invidious.esmailelbob.xyz/watch*
  107. // @include https://invidious.esmailelbob.xyz/search*
  108. // @include https://invidious.esmailelbob.xyz/feed/*
  109. // @include https://iv.melmac.space/channel/*
  110. // @include https://iv.melmac.space/watch*
  111. // @include https://iv.melmac.space/search*
  112. // @include https://iv.melmac.space/feed/*
  113. // @include https://invidious.privacydev.net/channel/*
  114. // @include https://invidious.privacydev.net/watch*
  115. // @include https://invidious.privacydev.net/search*
  116. // @include https://invidious.privacydev.net/feed/*
  117. // @include https://invidious.lidarshield.cloud/channel/*
  118. // @include https://invidious.lidarshield.cloud/watch*
  119. // @include https://invidious.lidarshield.cloud/search*
  120. // @include https://invidious.lidarshield.cloud/feed/*
  121. // @include https://invidious.namazso.eu/channel/*
  122. // @include https://invidious.namazso.eu/watch*
  123. // @include https://invidious.namazso.eu/search*
  124. // @include https://invidious.namazso.eu/feed/*
  125. // ==/UserScript==
  126. /* Browse video's on Invidious with the keyboard
  127. *
  128. * Shortcuts:
  129. * SHIFT + arrow-up Go up a video
  130. * SHIFT + arrow-down Go down a video
  131. * SHIFT + arrow-left Go one video to the left
  132. * SHIFT + arrow-right Go one video to the right
  133. * ENTER Follow link of selected video
  134. * ESCAPE De-select selected video
  135. *
  136. * When no video is selected yet it will select the first visible video, when
  137. * no video is visible it will select the first video. (On usage of
  138. * SHIFT + arrow)*/
  139. const KEY_UP = 38;
  140. const KEY_RIGHT = 39;
  141. const KEY_DOWN = 40;
  142. const KEY_LEFT = 37;
  143. const KEY_ENTER = 13;
  144. const KEY_ESC = 27;
  145. const UP = 1;
  146. const RIGHT = 2;
  147. const DOWN = 3;
  148. const LEFT = 4;
  149. var SELECTED_ELEM = null;
  150. function itemIsVisible(item) {
  151. if ((item.offsetTop - window.pageYOffset) < 0 || (item.offsetTop + item.clientHeight) > (window.innerHeight + window.pageYOffset)) {
  152. return false;
  153. }
  154. return true;
  155. }
  156. function* getWantedElements() {
  157. const thumbnailElems = document.getElementsByClassName("thumbnail");
  158. for (const item of thumbnailElems) {
  159. if (item.parentNode.tagName !== "A") {
  160. continue;
  161. }
  162. yield item;
  163. }
  164. return [];
  165. }
  166. function* getVisibleElements() {
  167. for (const item of getWantedElements()) {
  168. if (itemIsVisible(item) === false) {
  169. continue;
  170. }
  171. yield item;
  172. }
  173. }
  174. function* getRowElements() {
  175. curOffsetTop = SELECTED_ELEM.offsetTop;
  176. for (const item of getVisibleElements()) {
  177. if (item.offsetTop !== curOffsetTop) {
  178. continue;
  179. }
  180. yield item;
  181. }
  182. }
  183. function* getColumnElements() {
  184. currOffsetLeft = SELECTED_ELEM.offsetLeft;
  185. for (const item of getWantedElements()) {
  186. if (item.offsetLeft !== currOffsetLeft) {
  187. continue;
  188. }
  189. yield item;
  190. }
  191. }
  192. function getDownElement() {
  193. currOffsetTop = SELECTED_ELEM.offsetTop;
  194. for (const item of getColumnElements()) {
  195. if (item.offsetTop > currOffsetTop) {
  196. return item;
  197. }
  198. }
  199. return null;
  200. }
  201. function getUpElement() {
  202. prevItem = null;
  203. for (const item of getColumnElements()) {
  204. if (item === SELECTED_ELEM) {
  205. return prevItem;
  206. }
  207. prevItem = item;
  208. }
  209. return null;
  210. }
  211. function getRightElement() {
  212. curOffsetLeft = SELECTED_ELEM.offsetLeft;
  213. for (const item of getRowElements()) {
  214. if (item.offsetLeft > curOffsetLeft) {
  215. return item;
  216. }
  217. }
  218. return null;
  219. }
  220. function getLeftElement() {
  221. prevItem = null;
  222. for (const item of getRowElements()) {
  223. if (item === SELECTED_ELEM) {
  224. return prevItem;
  225. }
  226. prevItem = item;
  227. }
  228. return null;
  229. }
  230. function setSelected(item) {
  231. item.focus();
  232. item.style.border = "2px solid red";
  233. if (itemIsVisible(item) == false) {
  234. item.scrollIntoView();
  235. }
  236. SELECTED_ELEM = item;
  237. }
  238. function clearSelection() {
  239. SELECTED_ELEM.style.border = "none";
  240. SELECTED_ELEM = null;
  241. }
  242. function mayProcess() {
  243. let activeElem = document.activeElement;
  244. if (activeElem !== document.body) {
  245. if (activeElem.tagName === "INPUT") {
  246. return false;
  247. }
  248. if (activeElem.tagName === "VIDEO") {
  249. return false;
  250. }
  251. }
  252. return true;
  253. }
  254. window.onkeydown = function keydown(event) {
  255. let keyAction = null;
  256. if (mayProcess() === false) {
  257. return;
  258. }
  259. // remove potential selection
  260. document.getSelection().removeAllRanges();
  261. if (event.keyCode === KEY_ESC) { // escape
  262. if (SELECTED_ELEM !== null) {
  263. clearSelection();
  264. }
  265. return;
  266. }
  267. else if (event.keyCode === KEY_ENTER) { // enter
  268. if (SELECTED_ELEM === null) {
  269. return;
  270. }
  271. const item = SELECTED_ELEM;
  272. clearSelection();
  273. // Make sure document.body is the active element, else it may follow the
  274. // active element.
  275. if (document.activeElement !== document.body) {
  276. document.activeElement.blur();
  277. }
  278. item.parentNode.click();
  279. return;
  280. }
  281. // SHIFT is our modifier key
  282. if (event.shiftKey === false) {
  283. return;
  284. }
  285. if (event.keyCode === KEY_LEFT) { // arrow-left
  286. keyAction = LEFT;
  287. }
  288. else if (event.keyCode === KEY_UP) { // arrow-up
  289. keyAction = UP;
  290. }
  291. else if (event.keyCode === KEY_RIGHT) { // arrow-right
  292. keyAction = RIGHT;
  293. }
  294. else if (event.keyCode === KEY_DOWN) { // arrow-down
  295. keyAction = DOWN;
  296. }
  297. else {
  298. return;
  299. }
  300. if (SELECTED_ELEM !== null) {
  301. if (itemIsVisible(SELECTED_ELEM) === false) {
  302. // clear previous selected
  303. clearSelection();
  304. }
  305. }
  306. if (SELECTED_ELEM === null) {
  307. // None selected yet, no matter the action, just set the first
  308. for (const item of getVisibleElements()) {
  309. setSelected(item);
  310. return;
  311. }
  312. // No items visible, select first item
  313. for (const item of getWantedElements()) {
  314. setSelected(item);
  315. break;
  316. }
  317. return;
  318. }
  319. if (keyAction === RIGHT) {
  320. let rightItem = getRightElement();
  321. if (rightItem !== null) {
  322. clearSelection();
  323. setSelected(rightItem);
  324. }
  325. }
  326. else if (keyAction === LEFT) {
  327. let item = getLeftElement();
  328. if (item !== null) {
  329. clearSelection();
  330. setSelected(item);
  331. }
  332. }
  333. else if (keyAction === DOWN) {
  334. let item = getDownElement();
  335. if (item !== null) {
  336. clearSelection();
  337. setSelected(item);
  338. }
  339. }
  340. else if (keyAction === UP) {
  341. let item = getUpElement();
  342. if (item !== null) {
  343. clearSelection();
  344. setSelected(item);
  345. }
  346. }
  347. }