MangaUpdates chapter links

Allows to add direct links to chapters in MangaUpdates release lists

2019-11-24 يوللانغان نەشرى. ئەڭ يېڭى نەشرىنى كۆرۈش.

  1. // ==UserScript==
  2. // @name MangaUpdates chapter links
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.2
  5. // @description Allows to add direct links to chapters in MangaUpdates release lists
  6. // @author You
  7. // @match https://www.mangaupdates.com/*
  8. // @require https://cdnjs.cloudflare.com/ajax/libs/mithril/2.0.4/mithril.min.js
  9. // @require https://cdn.jsdelivr.net/npm/coffeescript@2.4.1/lib/coffeescript-browser-compiler-legacy/coffeescript.js
  10. // @grant GM_addStyle
  11. // @grant GM_getValue
  12. // @grant GM_setValue
  13. // @grant GM_listValues
  14. // ==/UserScript==
  15.  
  16. var inline_src = String.raw`
  17.  
  18. GM_addStyle "html {background: black} body {background: initial}
  19. .center-side-bar {background: rgba(0, 0, 0, 0.1)}
  20. button.inbox.-edit {height: 15px; margin-right: 3px; cursor: pointer}
  21. button.inbox[disabled] {opacity: .5; cursor: not-allowed}
  22. button.-edit.-none {background: darkgrey} a.-chapter:not([href]) {color: darkgrey}
  23. .-overlay {position: fixed; top: 0; left: 0; height: 100%; width: 100%; z-index: 10000; pointer-events: none}
  24. .-dialog {pointer-events: all; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
  25. background: rgba(0, 0, 0, .8); color: white; padding: 2em}
  26. .-row {margin: 20px; display: block} .-buttons {display: flex; justify-content: space-between}
  27. .-dialog input {width: 500px} .-menu button.inbox {cursor: pointer; line-height: 1ex}"
  28.  
  29. $merge = Object.assign
  30. merge = (os) -> $merge {}, ...os
  31. fromPairs = (xs) -> merge xs.map ([k, v]) -> k and [k]: v
  32. qstr = (s) -> if not s.includes('?') then "" else s[1 + s.indexOf '?'..]
  33. query = (s) -> fromPairs (z.split('=').map(decodeURIComponent) for z in qstr(s).split('&') when z.includes '=')
  34. chunks = (n, l) -> (l[i ... i+n] for i in [0...l.length] by n)
  35. slug = (s) -> "#{s}".replace(/[^a-zA-Z]+/g, ' ').trim().replace(/ /g, '-')
  36. select = (o, ks...) -> o and merge ks.map((k) -> o[k] and {[k]: o[k]})
  37. fmt = (s, o) -> Object.keys(o).reduce ((s, k) -> s.replace '#{'+k+'}', o[k]), s
  38.  
  39. $e = (tag, options...) -> $merge document.createElement(tag), options...
  40. $get = (xpath, e=document) -> document.evaluate(xpath, e, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue
  41. $find = (selector, e=document) -> e.querySelector selector
  42. $find_ = (selector, e=document) -> Array.from e.querySelectorAll selector
  43. fullWidth = (e) -> e.classList.contains 'col-12'
  44.  
  45. URI = window.location.pathname
  46. QUERY = query window.location.search
  47.  
  48. releaseTable = switch
  49. when URI is "/releases.html" then $get("div[2]/div", main_content)
  50. when URI is "/groups.html" and 'id' of QUERY then $get("div/div[3]/div[2]/div/div", main_content)
  51.  
  52. if releaseTable
  53. console.log releaseTable
  54. releaseTable.style.position = 'relative'
  55. overlay = $e('div', className: '-overlay')
  56. document.body.appendChild overlay
  57.  
  58. state = editing: null
  59.  
  60. startEditing = (name, args...) -> -> $merge(state, editing: name, args...); m.redraw()
  61. closeDialog = -> $merge(state, {editing: null})
  62. editName = -> switch state.editing
  63. when 'groupUrl' then "Group URL (#{state.groupName})"
  64. when 'titleUri' then "Title ID (#{state.titleName})"
  65. saveChanges = ->
  66. cfg = GM_getValue(state.groupId, {})
  67. switch state.editing
  68. when 'groupUrl' then cfg.url = state.value
  69. when 'titleUri' then cfg.titles = $merge cfg.titles or {}, {[state.titleId]: state.value or undefined}
  70. GM_setValue state.groupId, cfg
  71. closeDialog()
  72. setTimeout recalc
  73.  
  74. chapterNumber = (s) -> s.match(/\d+/)?[0]
  75. $ensureButton = (e) ->
  76. btn = $find 'button', e
  77. unless btn
  78. btn = $e('button', className: "inbox -edit", title: "Edit", innerText: '*')
  79. e.insertBefore btn, e.firstChild
  80. btn
  81. $ensureHref = (e) ->
  82. href = $find 'a', e
  83. unless href
  84. href = $e('a', className: '-chapter', target: '_blank')
  85. href.appendChild e.firstChild while e.firstChild
  86. e.appendChild href
  87. href
  88.  
  89. recalc = ->
  90. _configs = {}
  91. config = (id) -> _configs[id] or (_configs[id] = GM_getValue(id, {}))
  92. items = $find_ ":scope > *", releaseTable
  93. separator = items.findIndex fullWidth
  94. columns = items[...separator].map (e) -> e.innerText.trim()
  95. _items = items[separator+1..]
  96. pager = _items.findIndex fullWidth
  97. cells = _items[...pager]
  98. rows = chunks columns.length, cells
  99. title = columns.indexOf "Title"
  100. chapter = columns.indexOf "Chp"
  101. group = columns.indexOf "Groups"
  102. for row in rows
  103. [titleName, groupName] = [title, group].map (k) -> row[k].innerText.replace(/^\*/, '')
  104. [titleId, groupId] = [title, group].map (k) -> query($find('a', row[k])?.href or "").id
  105. chapterId = chapterNumber row[chapter].innerText.trim()
  106. cfg = config [groupId]
  107. titleUri = cfg.titles?[titleId]
  108. [titleBtn, groupBtn] = [row[title], row[group]].map $ensureButton
  109. groupBtn.classList[if cfg.url then 'remove' else 'add']('-none')
  110. groupBtn.onclick = startEditing 'groupUrl', {groupId, groupName}, value: cfg.url or ""
  111. titleBtn.classList[if titleUri then 'remove' else 'add']('-none')
  112. titleBtn.onclick = startEditing 'titleUri', {groupId, groupName, titleId, titleName}, value: titleUri or ""
  113. titleBtn.disabled = not titleId
  114. chapterLink = $ensureHref row[chapter]
  115. if cfg.url and titleUri
  116. chapterLink.href = fmt(cfg.url, chapter: chapterId, title: titleUri)
  117. chapterLink.classList.add '-none'
  118. else
  119. chapterLink.removeAttribute 'href'
  120. chapterLink.classList.remove '-none'
  121.  
  122. recalc()
  123.  
  124. load = do (input = $e('input', type: 'file', accept: 'application/json')) -> -> new Promise (resolve) ->
  125. input.onchange = -> do (file = input.files[0], reader = new FileReader) -> if file
  126. reader.onload = -> resolve JSON.parse @result
  127. reader.readAsText file
  128. input.click()
  129. save = (name, data) ->
  130. $e('a', download: name, href: "data:application/json;base64,#{btoa JSON.stringify(data, null, 2)}").click()
  131. exportData = (type=state.editing) -> switch type
  132. when 'groupUrl' then save "#{slug state.groupName}.json", [state.groupId]: GM_getValue(state.groupId, {})
  133. when 'titleUri' then save "#{slug state.groupName}_#{slug state.titleName}.json",
  134. do (o = GM_getValue(state.groupId, {})) -> [state.groupId]: {o..., titles: select(o.titles, state.titleId)}
  135. else save "MangaUpdates_#{new Date().toJSON()}.json", merge GM_listValues().map (k) -> {[k]: GM_getValue(k, {})}
  136. importData = -> load().then (data) ->
  137. bad = ({groupId, url} for groupId, {url} of data).find ({groupId, url}) -> url isnt GM_getValue(groupId, {url}).url
  138. if not bad or confirm "Non-matching group URL was found (##{bad.groupId}).\nGroups with a non-matching URL will be replaced."
  139. for groupId, {url, titles} of data
  140. oldValue = GM_getValue(groupId, {url})
  141. GM_setValue groupId, {url, titles: (if url isnt oldValue.url then titles else merge [oldValue.titles, titles])}
  142. recalc()
  143.  
  144. importExport = $e 'div', className: '-menu', style: "position: absolute; top: 0; right: 0"
  145. m.render importExport, [m 'button.inbox', {title: "Export", onclick: -> exportData 'all'}, '↓'
  146. m 'button.inbox', {title: "Import", onclick: importData}, '↑']
  147. releaseTable.appendChild importExport
  148.  
  149. m.mount overlay, view: -> state.editing and m '.-dialog', [
  150. m 'label.-row', editName(state.editing),
  151. m 'input.inbox.-row', value: state.value, oninput: -> state.value = @value
  152. m '.-buttons.-row',
  153. m 'button.inbox', {onclick: closeDialog}, "Cancel"
  154. m 'button.inbox', {onclick: ->exportData()}, "Export"
  155. m 'button.inbox', {onclick: saveChanges}, "Ok"
  156. ]
  157.  
  158. `;
  159. eval( CoffeeScript.compile(inline_src) );