Github News Feed Filter

Add filters for Github homepage news feed items

目前为 2015-08-22 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name Github News Feed Filter
  3. // @namespace https://github.com/jerone/UserScripts
  4. // @description Add filters for Github homepage news feed items
  5. // @author jerone
  6. // @copyright 2014+, jerone (http://jeroenvanwarmerdam.nl)
  7. // @license GNU GPLv3
  8. // @homepage https://github.com/jerone/UserScripts/tree/master/Github_News_Feed_Filter
  9. // @homepageURL https://github.com/jerone/UserScripts/tree/master/Github_News_Feed_Filter
  10. // @include https://github.com/
  11. // @include https://github.com/?*
  12. // @include https://github.com/orgs/*/dashboard
  13. // @include https://github.com/orgs/*/dashboard?*
  14. // @include https://github.com/*tab=activity*
  15. // @version 6.1
  16. // @grant none
  17. // ==/UserScript==
  18. /* global Event */
  19.  
  20. (function() {
  21.  
  22. var FILTERS = [
  23. { id: "*", text: "All news feed", icon: "octicon-radio-tower", classNames: ["*"] },
  24. {
  25. id: "issues", text: "Issues", icon: "octicon-issue-opened", classNames: ["issues_opened", "issues_closed", "issues_reopened", "issues_comment"], subFilters: [
  26. { id: "issues opened", text: "Opened", icon: "octicon-issue-opened", classNames: ["issues_opened"] },
  27. { id: "issues closed", text: "Closed", icon: "octicon-issue-closed", classNames: ["issues_closed"] },
  28. { id: "issues reopened", text: "Reopened", icon: "octicon-issue-reopened", classNames: ["issues_reopened"] },
  29. { id: "issues comments", text: "Comments", icon: "octicon-comment-discussion", classNames: ["issues_comment"] }
  30. ]
  31. },
  32. {
  33. id: "commits", text: "Commits", icon: "octicon-git-commit", classNames: ["push", "commit_comment"], subFilters: [
  34. { id: "commits pushed", text: "Pushed", icon: "octicon-git-commit", classNames: ["push"] },
  35. { id: "commits comments", text: "Comments", icon: "octicon-comment-discussion", classNames: ["commit_comment"] }
  36. ]
  37. },
  38. {
  39. id: "pr", text: "Pull Requests", icon: "octicon-git-pull-request", classNames: ["pull_request_opened", "pull_request_closed", "pull_request_merged", "pull_request_comment"], subFilters: [
  40. { id: "pr opened", text: "Opened", icon: "octicon-git-pull-request", classNames: ["pull_request_opened"] },
  41. { id: "pr closed", text: "Closed", icon: "octicon-git-pull-request-abandoned", classNames: ["pull_request_closed"] },
  42. { id: "pr merged", text: "Merged", icon: "octicon-git-merge", classNames: ["pull_request_merged"] },
  43. { id: "pr comments", text: "Comments", icon: "octicon-comment-discussion", classNames: ["pull_request_comment"] }
  44. ]
  45. },
  46. {
  47. id: "repo", text: "Repo", icon: "octicon-repo", classNames: ["create", "public", "fork", "branch_create", "branch_delete", "tag_add", "tag_remove", "release", "delete"], subFilters: [
  48. { id: "repo created", text: "Created", icon: "octicon-repo-create", classNames: ["create"] },
  49. { id: "repo public", text: "Public", icon: "octicon-repo-push", classNames: ["public"] },
  50. { id: "repo forked", text: "Forked", icon: "octicon-repo-forked", classNames: ["fork"] },
  51. { id: "repo deleted", text: "Deleted", icon: "octicon-repo-delete", classNames: ["delete"] },
  52. { id: "repo released", text: "Release", icon: "octicon-repo-pull", classNames: ["release"] },
  53. {
  54. id: "repo branched", text: "Branch", icon: "octicon-git-branch", classNames: ["branch_create", "branch_delete"], subFilters: [
  55. { id: "repo branch created", text: "Created", icon: "octicon-git-branch-create", classNames: ["branch_create"] },
  56. { id: "repo branch deleted", text: "Deleted", icon: "octicon-git-branch-delete", classNames: ["branch_delete"] }
  57. ]
  58. },
  59. {
  60. id: "repo tagged", text: "Tag", icon: "octicon-tag", classNames: ["tag_add", "tag_remove"], subFilters: [
  61. { id: "repo tag added", text: "Added", icon: "octicon-tag-add", classNames: ["tag_add"] },
  62. { id: "repo tag removed", text: "Removed", icon: "octicon-tag-remove", classNames: ["tag_remove"] }
  63. ]
  64. }
  65. ]
  66. },
  67. {
  68. id: "user", text: "User", icon: "octicon-person", classNames: ["watch_started", "member_add", "team_add"], subFilters: [
  69. { id: "user starred", text: "Starred", icon: "octicon-star", classNames: ["watch_started"] },
  70. { id: "user added", text: "Member added", icon: "octicon-person-add", classNames: ["member_add", "team_add"] }
  71. ]
  72. },
  73. {
  74. id: "wiki", text: "Wiki", icon: "octicon-book", classNames: ["wiki_created", "wiki_edited"], subFilters: [
  75. { id: "wiki created", text: "Created", icon: "octicon-plus", classNames: ["wiki_created"] },
  76. { id: "wiki edited", text: "Edited", icon: "octicon-book", classNames: ["wiki_edited"] }
  77. ]
  78. },
  79. {
  80. id: "gist", text: "Gist", icon: "octicon-gist", classNames: ["gist_created", "gist_updated"], subFilters: [
  81. { id: "gist created", text: "Created", icon: "octicon-gist-new", classNames: ["gist_created"] },
  82. { id: "gist updated", text: "Updated", icon: "octicon-gist", classNames: ["gist_updated"] }
  83. ]
  84. }
  85. // Possible other classes: follow
  86. ];
  87.  
  88. var datasetId = "githubNewsFeedFilterId";
  89.  
  90. function proxy(fn) {
  91. return function() {
  92. var that = this;
  93. return function(e) {
  94. var args = that.slice(0); // clone;
  95. args.unshift(e); // prepend event;
  96. fn.apply(this, args);
  97. };
  98. }.call([].slice.call(arguments, 1));
  99. }
  100.  
  101. function addStyle(css) {
  102. var node = document.createElement("style");
  103. node.type = "text/css";
  104. node.appendChild(document.createTextNode(css));
  105. document.head.appendChild(node);
  106. }
  107.  
  108. addStyle("\
  109. .GitHubNewsFeedFilter .count { margin-right: 15px; }\
  110. \
  111. .GitHubNewsFeedFilter .filter-list .filter-list .mini-repo-list-item { padding-left: 40px; border-top: 1px dashed #E5E5E5; }\
  112. .GitHubNewsFeedFilter .filter-list .filter-list .filter-list .mini-repo-list-item { padding-left: 50px; }\
  113. \
  114. .GitHubNewsFeedFilter .filter-list-item > ul { display: none; }\
  115. .GitHubNewsFeedFilter .filter-list-item.open > ul { display: block; }\
  116. \
  117. .GitHubNewsFeedFilter .private { font-weight: bold; }\
  118. \
  119. .GitHubNewsFeedFilter .stars .octicon { position: absolute; right: -4px; }\
  120. .GitHubNewsFeedFilter .filter-list-item.open > a > .stars > .octicon:before { content: '\\f05b'; }\
  121. \
  122. .no-alerts { font-style: italic; }\
  123. ")
  124.  
  125. function addFilterMenu(filters, parent, container, sidebar, main) {
  126. var ul = document.createElement("ul");
  127. ul.classList.add("filter-list");
  128. if (main) {
  129. ul.classList.add("boxed-group-inner", "mini-repo-list");
  130. }
  131. parent.appendChild(ul);
  132.  
  133. filters.forEach(function(subFilter) {
  134. var li = addFilterMenuItem(subFilter, ul, container, sidebar);
  135.  
  136. if (subFilter.subFilters) {
  137. addFilterMenu(subFilter.subFilters, li, container, sidebar, false);
  138. }
  139. });
  140. }
  141.  
  142. function addFilterMenuItem(filter, parent, container, sidebar) {
  143. var a = document.createElement("a");
  144. a.classList.add("mini-repo-list-item", "css-truncate");
  145. a.setAttribute("href", "/");
  146. a.setAttribute("title", filter.classNames.join(" & "));
  147. a.dataset[datasetId] = filter.id;
  148.  
  149. // Filter icon;
  150. var i = document.createElement("span");
  151. i.classList.add("repo-icon", "octicon", filter.icon);
  152. a.appendChild(i);
  153.  
  154. // Filter count & sub list arrow;
  155. var s = document.createElement("span");
  156. s.classList.add("stars");
  157. var c = document.createElement("span");
  158. c.classList.add("count");
  159. c.appendChild(document.createTextNode("0"));
  160. s.appendChild(c);
  161. if (filter.subFilters) {
  162. s.appendChild(document.createTextNode(" "));
  163. var o = document.createElement("span");
  164. o.classList.add("octicon", "octicon-triangle-left");
  165. s.appendChild(o);
  166. }
  167. a.appendChild(s);
  168.  
  169. // Filter text;
  170. a.appendChild(document.createTextNode(filter.text));
  171.  
  172. a.addEventListener("click", proxy(function(e, classNames) {
  173. e.preventDefault();
  174.  
  175. // Show/hide message about no alerts;
  176. var any = false,
  177. all = classNames[0] === "*",
  178. some = function(alert) { return classNames.some(function(cl) { return alert.classList.contains(cl); }); };
  179. Array.forEach(container.querySelectorAll(".alert"), function(alert) {
  180. alert.style.display = (all || some(alert)) && (any = true) ? "block" : "none";
  181. });
  182. var none = container.querySelector(".no-alerts");
  183. if (any && none) {
  184. none.parentNode.removeChild(none);
  185. } else if (!any && !none) {
  186. none = document.createElement("div");
  187. none.classList.add("no-alerts", "protip");
  188. none.appendChild(document.createTextNode("No feed items for this filter. Please select another filter."));
  189. container.insertBefore(none, container.firstElementChild.nextElementSibling);
  190. }
  191.  
  192. // Open/close sub list;
  193. Array.forEach(sidebar.querySelectorAll(".GitHubNewsFeedFilter .open"), function(item) { item.classList.remove("open"); });
  194. showParentMenu(this);
  195. this.parentNode.classList.add("open");
  196.  
  197. // Give it a colored background;
  198. Array.forEach(sidebar.querySelectorAll(".GitHubNewsFeedFilter .private"), function(m) { m.classList.remove("private"); });
  199. this.parentNode.classList.add("private");
  200.  
  201. // Push filter to url;
  202. if (this.dataset[datasetId] !== "*") {
  203. var urlSearch = "filter=" + encodeURIComponent(this.dataset[datasetId]);
  204. history.pushState(null, null, location.search && /filter=[^&]*/g.test(location.search)
  205. ? location.href.replace(/filter=[^&]*/g, urlSearch)
  206. : location.href + (location.search ? "&" : "?") + urlSearch);
  207. } else {
  208. history.pushState(null, null, location.href.replace(/(filter=[^&]*&|\?filter=[^&]*$|&filter=[^&]*)/g, "")); // http://regexr.com/398lv
  209. }
  210. }, filter.classNames));
  211.  
  212. var li = document.createElement("li");
  213. li.classList.add("filter-list-item");
  214. li.appendChild(a);
  215. li.filterClassNames = filter.classNames;
  216.  
  217. parent.appendChild(li);
  218.  
  219. return li;
  220. }
  221.  
  222. // Traverse back up the tree to open sub lists;
  223. function showParentMenu(menuItem) {
  224. var parentMenuItem = menuItem.parentNode;
  225. if (parentMenuItem.classList.contains("filter-list-item")) {
  226. parentMenuItem.classList.add("open");
  227. showParentMenu(parentMenuItem.parentNode);
  228. }
  229. }
  230.  
  231. function pageUpdate(container, sidebar, wrapper) {
  232. // Fix filter identification;
  233. Array.forEach(container.querySelectorAll(".alert"), function(alert) {
  234. if (alert.getElementsByClassName("octicon-git-branch").length > 0 && !alert.classList.contains("fork")) {
  235. alert.classList.remove("create");
  236. alert.classList.add("branch_create");
  237. } else if (alert.getElementsByClassName("octicon-git-branch-delete").length > 0) {
  238. alert.classList.remove("delete");
  239. alert.classList.add("branch_delete");
  240. } else if (alert.getElementsByClassName("octicon-tag").length > 0 && !alert.classList.contains("release")) {
  241. alert.classList.remove("create");
  242. alert.classList.add("tag_add");
  243. } else if (alert.getElementsByClassName("octicon-tag-remove").length > 0) {
  244. alert.classList.remove("delete");
  245. alert.classList.add("tag_remove");
  246. } else if (alert.getElementsByClassName("octicon-git-pull-request").length > 0) {
  247. if (alert.classList.contains("issues_opened")) {
  248. alert.classList.remove("issues_opened");
  249. alert.classList.add("pull_request_opened");
  250. } else if (alert.classList.contains("issues_closed")) {
  251. alert.classList.remove("issues_closed");
  252. if (!!~alert.querySelector('.title').textContent.indexOf('merged pull request')) {
  253. alert.classList.add("pull_request_merged");
  254. } else {
  255. alert.classList.add("pull_request_closed");
  256. }
  257. }
  258. } else if (alert.classList.contains("issues_comment") && alert.querySelectorAll(".title a")[1].href.split("/")[5] === "pull") {
  259. alert.classList.remove("issues_comment");
  260. alert.classList.add("pull_request_comment");
  261. } else if (alert.classList.contains("gollum")) {
  262. alert.classList.remove("gollum");
  263. if (!!~alert.querySelector('.title').textContent.indexOf(" created the ")) {
  264. alert.classList.add("wiki_created");
  265. } else if (!!~alert.querySelector('.title').textContent.indexOf(" edited the ")) {
  266. alert.classList.add("wiki_edited");
  267. }
  268. } else if (alert.classList.contains("gist")) {
  269. alert.classList.remove("gist");
  270. alert.classList.add("gist_" + alert.querySelector(".title span").textContent);
  271. }
  272. });
  273.  
  274. // Update filter counts;
  275. Array.forEach(wrapper.querySelectorAll("li"), function(li) {
  276. var c = li.querySelector(".count");
  277. if (li.filterClassNames[0] === "*") {
  278. c.textContent = container.querySelectorAll(".alert").length;
  279. } else {
  280. c.textContent = "0";
  281. Array.forEach(container.querySelectorAll(".alert"), function(alert) {
  282. if (li.filterClassNames.some(function(cl) { return alert.classList.contains(cl); })) {
  283. c.textContent = parseInt(c.textContent, 10) + 1;
  284. }
  285. });
  286. }
  287. });
  288.  
  289. // Apply filter from url;
  290. var filter = /filter=[^&]*/g.test(location.search)
  291. ? decodeURIComponent(/filter=([^&]*)/g.exec(location.search)[1])
  292. : "*";
  293. wrapper.querySelector('.GitHubNewsFeedFilter [data-github-news-feed-filter-id="' + filter + '"]').dispatchEvent(new Event("click"));
  294. }
  295.  
  296. function addFilters() {
  297. var container = document.querySelector(".news");
  298. if (!container) { return; }
  299.  
  300. var sidebar = document.querySelector(".dashboard-sidebar") || document.querySelector(".column.one-fourth.vcard");
  301.  
  302. var rule = document.createElement("div");
  303. rule.classList.add("rule");
  304. sidebar.insertBefore(rule, sidebar.firstChild);
  305.  
  306. var wrapper = document.createElement("div");
  307. wrapper.classList.add("GitHubNewsFeedFilter", "boxed-group", "flush");
  308. sidebar.insertBefore(wrapper, sidebar.firstChild);
  309.  
  310. var header = document.createElement("h3");
  311. header.appendChild(document.createTextNode("News feed filter"));
  312. wrapper.appendChild(header);
  313.  
  314. addFilterMenu(FILTERS, wrapper, container, sidebar, true);
  315.  
  316. pageUpdate(container, sidebar, wrapper);
  317.  
  318. // Update on clicking "More"-button;
  319. new MutationObserver(function() {
  320. pageUpdate(container, sidebar, wrapper);
  321. }).observe(container, { childList: true });
  322. }
  323.  
  324. // init;
  325. addFilters();
  326.  
  327. })();