番组计划(bangumi)目录页多标签筛选

filter space separated tags in comment box on bangumi index page

  1. // ==UserScript==
  2. // @name 番组计划(bangumi)目录页多标签筛选
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.1.3.3
  5. // @description filter space separated tags in comment box on bangumi index page
  6. // @author oscardoudou
  7. // @include /^https?://(bangumi|bgm).tv/index.*$/
  8. // @icon https://bangumi.tv/img/favicon.ico
  9. // @require https://code.jquery.com/ui/1.12.1/jquery-ui.js
  10. // @grant unsafeWindow
  11. // @grant GM_addStyle
  12. // @grant GM_getResourceText
  13. // @grant GM.setValue
  14. // @grant GM.getValue
  15. // @grant GM_setValue
  16. // @grant GM_getValue
  17.  
  18. // ==/UserScript==
  19. GM_addStyle(".tag_filter { display: inline-block; margin: 0 5px 5px 0; padding: 2px 5px; font-size: 12px; color: #dcdcdc; border-radius: 5px; line-height: 150%; background: #6e6e6e; cursor: pointer;}");
  20. GM_addStyle(".tag_filter { font-family: 'SF Pro SC','SF Pro Display','PingFang SC','Lucida Grande','Helvetica Neue',Helvetica,Arial,Verdana,sans-serif,Hiragino Sans GB;}");
  21. GM_addStyle(".tag { margin: 1px; cursor: pointer}");
  22. GM_addStyle(".searchContainer { height: 25px; min-width: 100%}")
  23. GM_addStyle(".searchLabel { height: 25px; min-width: 100%}")
  24. GM_addStyle(".searchInput { position: relative; width: 80%; top: 0; left: 0; margin: 0; height: 25px !important; border-radius: 4px; background-color: white !important; outline: 0px solid white !important; border: 0p}")
  25. GM_addStyle("#browserTools { height: 55px;}")
  26. GM_addStyle(".grey {font-size: 10px; color: #999;}")
  27.  
  28. //global var
  29. //var $ = unsafeWindow.jQuery;
  30. let GM4 = (typeof GM.getValue === 'undefined') ? false : true;
  31. if (window.itemList == undefined) {
  32. window.itemList = $('#browserItemList')[0].children
  33. window.comments = $('#browserItemList #comment_box .text')
  34. window.infotips = $('#browserItemList .info.tip')
  35. window.filterBar = $('#browserTools')[0]
  36. window.browserTypeSelector = $('#browserTools')[0].children[0]
  37. window.map = new Map()
  38. window.dict0 = new Map()
  39. window.dict1 = new Map()
  40. }
  41. //get rid of eslint syntax complaint
  42. var itemList = window.itemList
  43. var comments = window.comments
  44. var infotips = window.infotips
  45. var filterBar = window.filterBar
  46. var browserTypeSelector = window.browserTypeSelector
  47. var map = window.map
  48. //store comment tag count
  49. var dict0 = window.dict0
  50. //store infotip tag count, we need 2 map bc each would generate a panel
  51. var dict1 = window.dict1
  52. //min occurrence to be present in tag summary on the side
  53. const minOccurence = 3
  54. //max length of a tag from infotip, avoid some crazy long metadata tag
  55. //this makes sure xxxx年xx月xx日 won't be filtered out, as we keep full date as tag
  56. const maxInfoTipTagLength = 12
  57. //global var that store previous used filter/tags
  58. let indexUrl = window.location.href
  59. let indexId = indexUrl.substring(indexUrl.lastIndexOf("/") + 1, indexUrl.length);
  60. let persistKey = `bgm_index_${indexId}_tags`
  61. //let tagList = getStorage(persistKey, [])
  62. var tagList = []
  63.  
  64.  
  65. //create hash from string
  66. Object.defineProperty(String.prototype, 'hashCode', {
  67. value: function() {
  68. var hash = 0, i, chr;
  69. for (i = 0; i < this.length; i++) {
  70. chr = this.charCodeAt(i);
  71. hash = ((hash << 5) - hash) + chr;
  72. hash |= 0; // Convert to 32bit integer
  73. }
  74. return hash;
  75. }
  76. });
  77.  
  78. (function() {
  79. getStorage(persistKey, []).then(x => main())
  80. })();
  81.  
  82. function main(){
  83. for ( let i = 0 ; i < infotips.length ; i++){
  84. process(comments[i], true)
  85. process(infotips[i], false)
  86. }
  87. addSearchBar()
  88. addSideSummary("标签汇总",true)
  89. addSideSummary("时间/制作标签汇总",false)
  90. //reapply previous used filter/tags
  91. console.log("tagList:"+ tagList)
  92. tagList.forEach(tag => filterTag(tag))
  93. }
  94.  
  95. //to use GM.getValue or GM.setValue you have to wrap it in async await or use the value in callback then
  96. async function getStorage(key, defaultValue){
  97. if(!GM4){
  98. console.log('not GM4')
  99. tagList = GM_getValue(key, defaultValue)
  100. }else{
  101. // GM.getValue(key, defaultValue).then(x => {tagList = x; console.log("tagList: "+tagList); main()});
  102. tagList = await GM.getValue(key, defaultValue)
  103. }
  104. }
  105.  
  106. //to use GM.getValue or GM.setValue you have to wrap it in async await or use the value in callback then
  107. async function setStorage(key, value){
  108. if(!GM4){
  109. console.log('not GM4')
  110. GM_setValue(key, value)
  111. }else{
  112. await GM.setValue(key, value)
  113. }
  114. }
  115.  
  116.  
  117. //use for both comment and infotip
  118. function process(element, isComment){
  119. if(typeof element == 'undefined') return;
  120. let tags
  121. let tagIds = []
  122. var dict
  123. if(isComment){
  124. tags = element.innerHTML.split(" ").slice(1);
  125. dict = dict0
  126. }else{
  127. //split infotip into different section, use innerText to avoid triming innerHTML
  128. let infotip = element.innerText.split(" / ");
  129. //extract all(global) number from the 1st section(date info) of infotip, map first 2 as YYYY and month, add default value to deal with unavailable date metadata
  130. const [year, month, day] = (infotip[0].match(/\d+/g) || [0,0]).map(Number)
  131. //concat date info and rest info into infoTag array, convert number to string in this step, so hashCode() won't yell
  132. if(year != 0 && month != 0){
  133. tags = infotip.concat([year+"年",month+"月"])}
  134. else{
  135. tags = infotip
  136. }
  137. //stop some crazy long infotip become a tag
  138. tags = tags.filter(tag => tag.length <= maxInfoTipTagLength)
  139. dict = dict1
  140. }
  141. element.innerHTML=''
  142. tags.forEach((tag) => {
  143. let anchor = document.createElement('a');
  144. //keep one trailing space, so when edit again in comment box, it will still be space separated
  145. anchor.innerHTML = `${tag} `
  146. anchor.className = 'tag'
  147. let tagId = tag.hashCode()
  148. anchor.setAttribute('tagId', tagId)
  149. //set onClick function of anchor is not viable, due to function is defined in userscript scope, which is outside target page scope. That's why when it evaluate the value of onClick attribute, it yells func not defined
  150. anchor.addEventListener('click',function(){ filterTag(tag)}, false)
  151. //store map as string -> id
  152. if(!map.has(`${tag}`)){
  153. map.set(`${tag}`, tagId)
  154. }
  155. if(!dict.has(`${tag}`)){
  156. dict.set(`${tag}`,1)
  157. }else{
  158. dict.set(`${tag}`, dict.get(`${tag}`)+1)
  159. }
  160. element.appendChild(anchor)
  161. tagIds.push(tagId)
  162. })
  163. $(element).data('tagIds',tagIds)
  164. }
  165.  
  166. function addSearchBar(){
  167. //create search bar
  168. let searchbar = document.createElement('div')
  169. searchbar.className = 'ui-widget searchContainer'
  170. filterBar.insertBefore(searchbar, browserTypeSelector)
  171. let activeFilter = document.createElement('div')
  172. activeFilter.style = 'display:flex'
  173. activeFilter.id = 'active_filter'
  174. searchbar.appendChild(activeFilter)
  175. let label = document.createElement('label')
  176. label.className = 'searchLabel'
  177. label.setAttribute('for','tags')
  178. label.innerHTML = '标签: '
  179. let input = document.createElement('input')
  180. input.id = 'tags'
  181. input.className = 'searchInput'
  182. searchbar.appendChild(label)
  183. searchbar.appendChild(input)
  184. //auto complete is a funciton in jquery-ui, need @require
  185. $('#tags').autocomplete ( {source: Array.from(map.keys()) ,
  186. minLength: 1});
  187. //$('#tags').attr('autocomplete','on');
  188. $("#tags").autocomplete({
  189. select: function( event, ui ) { filterTag(ui.item.value); $(this).val(''); return false;}
  190. })
  191. }
  192.  
  193. function addSideSummary(groupName, isComment){
  194. //add tags that occurence >= minOccurence(3) to tag summary on the side
  195. let toBeInserted = $('#columnSubjectBrowserB')[0]
  196. let panel = document.createElement('div')
  197. panel.className = 'SidePanel png_bg'
  198. toBeInserted.append(panel)
  199. let panelTitle = document.createElement('h2')
  200. panelTitle.innerText = groupName
  201. panel.append(panelTitle)
  202. var tempDict = isComment ? dict0 : dict1 ;
  203. var dict = new Map([...tempDict].filter(([k,v]) => v >= minOccurence && k !== "" ).sort((a, b) => (a[1] < b[1] && 1) || (a[1] === b[1] ? 0 : -1)))
  204. dict.forEach(function(value, key, map){
  205. //map here is map variable, thus the dict being iterated now, not the global var map
  206. //console.log(`${key}:${map.get(key)}`)
  207. let tag = document.createElement('a')
  208. let count = document.createElement('small')
  209. tag.innerHTML = key
  210. tag.className = 'tag l'
  211. //Ugh, horrible naming
  212. tag.setAttribute('tagId', window.map.get(key))
  213. tag.addEventListener('click',function(){ filterTag(key)}, false)
  214. count.innerHTML = `(${value})`
  215. count.className = "grey"
  216. this.appendChild(tag)
  217. this.appendChild(count)
  218. // &nbsp;?
  219. this.append(' ')
  220. }, panel)
  221. }
  222.  
  223. function filterTag(tag){
  224. var tagId = window.map.get(tag)
  225. //if tag is one of the active filters, then ignore it
  226. if(getActiveFilterIds().indexOf(tagId) != -1){
  227. return;
  228. }
  229. let filterButton = document.createElement('a')
  230. filterButton.innerHTML = tag
  231. filterButton.className = 'tag_filter'
  232. filterButton.id = tagId
  233. filterButton.addEventListener('click',function(){removeFilter(this)}, false)
  234. $('#active_filter')[0].appendChild(filterButton)
  235. for( let i = 0 ; i < itemList.length ; i++){
  236. var tagAnchorList;
  237. var hide = true;
  238. if(typeof $('#browserItemList #comment_box .text')[0] != 'undefined'){
  239. tagAnchorList = comments[i].children
  240. //comment
  241. for( let j = 0 ; j < tagAnchorList.length; j++){
  242. if(tagAnchorList[j].getAttribute('tagId') == tagId){
  243. hide = false
  244. }
  245. }
  246. }
  247. //infotip
  248. tagAnchorList = infotips[i].children
  249. for( let j = 0; j < tagAnchorList.length; j++){
  250. if(tagAnchorList[j].getAttribute('tagId') == tagId){
  251. hide = false
  252. }
  253. }
  254. if(hide){
  255. itemList[i].style.display = 'none'
  256. }
  257. }
  258.  
  259. //add tag to persistent storage
  260. if(tagList.indexOf(tag) == -1) {
  261. tagList.push(tag)
  262. }
  263. setStorage(persistKey, tagList)
  264. }
  265.  
  266. function removeFilter(tag){
  267. tag.parentNode.removeChild(tag);
  268. //hide all items first
  269. $('.item.clearit').css( "display", "none" )
  270. let activeFitlerIds = getActiveFilterIds()
  271. for(let i = 0; i < itemList.length; i++){
  272. if(itemQualified(activeFitlerIds, comments[i], infotips[i])){
  273. itemList[i].style.display = 'block'
  274. }
  275. }
  276.  
  277. //remove tag from persistent storage
  278. const index = tagList.indexOf(tag.innerHTML)
  279. if(index > -1){
  280. tagList.splice(index, 1)
  281. }
  282. setStorage(persistKey, tagList)
  283. }
  284.  
  285. function getActiveFilterIds(){
  286. return $('.tag_filter').map(function(){
  287. //return int instead of string
  288. return parseInt(this.id)
  289. })
  290. //return array instead of jquery object
  291. .get()
  292. }
  293.  
  294. function itemQualified(activeFitlerIds, comment, infotip){
  295. for(let i = 0 ; i < activeFitlerIds.length; i++){
  296. //if any of actived filter not present in either comment or infotip's tagIds, then item is not qualified
  297. if( (typeof comment == 'undefined' || (typeof comment != 'undefined' && $(comment).data('tagIds').indexOf(activeFitlerIds[i]) == -1)) && $(infotip).data('tagIds').indexOf(activeFitlerIds[i]) == -1){
  298. return false;
  299. }
  300. }
  301. return true;
  302. }
  303.  
  304.  

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址