osu! Beatmap Downloaded Indicator

Indicate if an osu! beatmap in beatmap listing has been installed in the local game. An additional local daemon is required.

当前为 2020-07-15 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name osu! Beatmap Downloaded Indicator
  3. // @namespace https://github.com/karin0/osu-bdi
  4. // @version 0.2
  5. // @description Indicate if an osu! beatmap in beatmap listing has been installed in the local game. An additional local daemon is required.
  6. // @author karin0
  7. // @icon https://osu.ppy.sh/favicon.ico
  8. // @include http*://osu.ppy.sh/beatmapsets*
  9. // @grant none
  10. // ==/UserScript==
  11.  
  12. (function () {
  13. const port_default = '35677', obdi_page = 'https://github.com/karin0/osu-bdi';
  14.  
  15. const css = document.createElement('style');
  16. css.type = 'text/css';
  17. css.innerText = `
  18. .di-done {
  19. filter: brightness(80%) contrast(80%) opacity(20%);
  20. }
  21. .di-input {
  22. width: 6em;
  23. height: 2.2em;
  24. margin: auto;
  25. padding: 10px;
  26. background-color: hsl(var(--hsl-b2));
  27. border: 1px solid hsl(var(--hsl-b4));
  28. -moz-appearance: textfield;
  29. }
  30. .di-input::-webkit-inner-spin-button {
  31. -webkit-appearance: none;
  32. margin: 0;
  33. }
  34. .di-status {
  35. align-items: center;
  36. display: flex;
  37. color: #fff;
  38. margin: auto 0.6em;
  39. }
  40. `;
  41.  
  42. const map = new Map();
  43. const set = new Set();
  44.  
  45. function get_id(e) {
  46. if (!e.dataset.diid) {
  47. const a = e.getElementsByTagName('a')[0];
  48. if (!a)
  49. return undefined;
  50. const href = a.getAttribute('href');
  51. if (!href)
  52. return undefined;
  53. const id = Number(href.substring(href.lastIndexOf('/') + 1));
  54. if (!id)
  55. return undefined;
  56. map[e.dataset.diid = id] = e;
  57. return id;
  58. }
  59. return Number(e.dataset.diid);
  60. }
  61.  
  62. function set_downloaded(e) {
  63. if (!e)
  64. return;
  65. e.classList.add('di-done')
  66. const i = e.querySelector('i.fa-download');
  67. if (i) {
  68. i.classList.remove('fa-download');
  69. i.classList.add('fa-check-circle');
  70. }
  71. }
  72.  
  73. function set_undownloaded(e) {
  74. if (!e)
  75. return;
  76. e.classList.remove('di-done')
  77. const i = e.querySelector('i.fa-check-circle');
  78. if (i) {
  79. i.classList.remove('fa-check-circle');
  80. i.classList.add('fa-download');
  81. }
  82. }
  83.  
  84. function add(id) {
  85. if (!set.has(id)) {
  86. set.add(id);
  87. set_downloaded(map[id]);
  88. }
  89. }
  90.  
  91. function remove(id) {
  92. if (set.has(id)) {
  93. set.remove(id);
  94. set_undownloaded(map[id]);
  95. }
  96. }
  97.  
  98. const port_input = document.createElement('input');
  99. port_input.type = 'number';
  100. port_input.min = 1;
  101. port_input.max = 65535;
  102. port_input.classList.add('di-input');
  103. port_input.placeholder = 'obdi Port'
  104.  
  105. const stored_port = localStorage.getItem('di_port');
  106. port_input.value = stored_port ? stored_port : port_default.toString();
  107.  
  108. const status = document.createElement('a');
  109. status.classList.add('di-status');
  110. status.href = obdi_page;
  111. status.target = '_blank';
  112.  
  113. function on_message(e) {
  114. let mode;
  115. console.log('received', e.data);
  116. for (const s of e.data.split(' ')) {
  117. const id = Number(s);
  118. if (id)
  119. (mode ? add : remove)(id);
  120. else if (s == '+')
  121. mode = 1;
  122. else if (s == '-')
  123. mode = 0;
  124. else {
  125. for (const id of set)
  126. set_undownloaded(map[id]);
  127. set.clear();
  128. }
  129. }
  130. }
  131.  
  132. function on_open() {
  133. status.innerText = 'obdi Connected';
  134. }
  135.  
  136. let socket = null, tryer = 0, tryer_cnt = 0;
  137.  
  138. function disconnect() {
  139. if (socket) {
  140. socket.onmessage = socket.onopen = socket.onclose = null;
  141. socket.close()
  142. }
  143. }
  144.  
  145. function retry(id) {
  146. if (tryer != id)
  147. return;
  148. console.log(id, 'retrying', socket, socket ? socket.readyState : 'qwq');
  149. const state = socket ? socket.readyState : null;
  150. if (state == WebSocket.OPEN)
  151. return tryer = 0;
  152. if (state != WebSocket.CONNECTING) {
  153. disconnect();
  154. socket = new WebSocket('ws://127.0.0.1:' + port_input.value);
  155. socket.onmessage = on_message;
  156. socket.onopen = on_open;
  157. socket.onclose = connect;
  158. }
  159. setTimeout(() => retry(id), 1000);
  160. }
  161.  
  162. function connect() {
  163. if (tryer)
  164. return;
  165. status.innerText = 'obdi Disconnected';
  166. tryer = ++tryer_cnt;
  167. console.log('connects', tryer);
  168. retry(tryer);
  169. }
  170.  
  171. port_input.onchange = function () {
  172. console.log('change to', port_input.value, tryer);
  173. localStorage.setItem('di_port', port_input.value);
  174. if (tryer)
  175. tryer = 0;
  176. disconnect();
  177. const port = Number(port_input.value);
  178. if (0 < port && port < 65536)
  179. connect();
  180. else
  181. status.innerText = 'obdi Disconnected';
  182. };
  183.  
  184. const observer = new MutationObserver(function (muts) {
  185. for (const mut of muts)
  186. for (const node of mut.addedNodes)
  187. for (const e of node.querySelectorAll('div.beatmapsets__item')) {
  188. const id = get_id(e);
  189. if (id && set.has(id))
  190. set_downloaded(e);
  191. }
  192. });
  193.  
  194. window.addEventListener('load', function () {
  195. if (document.body.dataset.obdi)
  196. return;
  197. document.body.dataset.obdi = 1;
  198.  
  199. document.head.appendChild(css);
  200.  
  201. const topbar = document.querySelector('div.nav2__colgroup');
  202. topbar.appendChild(port_input);
  203. topbar.appendChild(status);
  204.  
  205. observer.observe(document.querySelector('div.osu-layout__row'), {
  206. childList: true, subtree: true
  207. });
  208. for (const e of document.querySelectorAll('div.beatmapsets__item'))
  209. get_id(e);
  210.  
  211. connect();
  212. });
  213. })();