您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
为github的star增加标签管理功能
- // ==UserScript==
- // github-stars-tagger
- // 需要在页面https://github.com/stars中管理标签, 所以在导航栏上增加了 /stars 的链接
- // 代码来自: https://github.com/artisologic/github-stars-tagger
- // 存储改为了 localStorage
- //
- // @name github-stars-tagger
- // @namespace http://tampermonkey.net/
- // @version 0.1
- // @description 为github的star增加标签管理功能
- // @author You
- // @match https://github.com/*
- // @grant none
- // ==/UserScript==
- // libs/EventEmitter.js
- ((window) => {
- 'use strict';
- /**
- * @class EventEmitter
- */
- class EventEmitter {
- constructor() {
- this._listeners = [];
- }
- on(eventName, callback) {
- this._listeners.push({
- name: eventName,
- callback: callback
- });
- return this;
- }
- off(eventName, callback) {
- this._listeners.forEach((listener, index) => {
- if (listener.name === eventName && listener.callback === callback) {
- this._listeners.splice(index, 1);
- }
- });
- return this;
- }
- emit(eventName, data) {
- this._listeners
- .filter(listener => listener.name === eventName)
- .forEach(listener => listener.callback(data, this, eventName));
- return this;
- }
- }
- window.GSM = window.GSM || {};
- GSM.EventEmitter = EventEmitter;
- })(window);
- // libs/Model.js
- ((window) => {
- 'use strict';
- /**
- * @class Model
- */
- class Model extends GSM.EventEmitter {
- constructor(data) {
- super();
- this.data = data;
- }
- }
- window.GSM = window.GSM || {};
- GSM.Model = Model;
- })(window);
- // libs/TagsStore.js
- ((window) => {
- 'use strict';
- const KEY = 'github-stars-tagger';
- /**
- * @class TagsStore
- */
- class TagsStore {
- constructor() {
- }
- get(key) {
- // console.log('get', key);
- const promise = new Promise((resolve, reject) => {
- var items = localStorage.getItem(KEY);
- var data = {};
- if (items) {
- data = JSON.parse(items);
- }
- if (key === undefined) {
- resolve(data);
- } else if (typeof data[key] !== 'undefined') {
- resolve(data[key]);
- } else {
- reject('TagsStore.get('+key+')获取失败');
- }
- });
- promise.catch(error => {
- GSM.utils.track('Sync', 'get', 'error', error);
- });
- return promise;
- }
- set(key, value) {
- // console.log('set', key, value);
- var items = localStorage.getItem(KEY);
- var data = {};
- if (items) {
- data = JSON.parse(items);
- }
- data[key] = value;
- const promise = new Promise((resolve, reject) => {
- if (localStorage.setItem(KEY, JSON.stringify(data))) {
- resolve();
- } else {
- reject();
- }
- });
- promise.catch(error => {
- GSM.utils.track('Sync', 'set', 'error', error);
- });
- return promise;
- }
- remove(key) {
- // console.log('remove', key);
- const promise = new Promise((resolve, reject) => {
- if (localStorage.setItem(key, null)) {
- resolve();
- } else {
- reject('remove error');
- }
- });
- promise.catch(error => {
- GSM.utils.track('Sync', 'remove', 'error', error);
- });
- return promise;
- }
- clear() {
- // console.log('clear');
- const promise = new Promise((resolve, reject) => {
- });
- promise.catch(error => {
- GSM.utils.track('Sync', 'clear', 'error', error);
- });
- return promise;
- }
- }
- window.GSM = window.GSM || {};
- GSM.TagsStore = TagsStore;
- })(window);
- // libs/utils.js
- ((window) => {
- 'use strict';
- const utils = {
- insertAfter(newNode, referenceNode) {
- referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling);
- },
- unique(array) {
- const hash = {};
- const res = [];
- for (let i = 0; i < array.length; i++) {
- const item = array[i];
- if (!hash[item]) {
- hash[item] = true;
- res.push(item);
- }
- }
- return res;
- },
- message(command, data) {
- // chrome.runtime.sendMessage({ command, data });
- },
- track(category, action, label, value) {
- utils.message('trackEvent', { category, action, label, value });
- }
- };
- window.GSM = window.GSM || {};
- GSM.utils = utils;
- })(window);
- // libs/View.js
- ((window) => {
- 'use strict';
- /**
- * @class View
- */
- class View extends GSM.EventEmitter {
- constructor() {
- super();
- this.refs = {
- root: this.createRootElement()
- };
- this.handlers = {};
- }
- static getRootClass() {
- // override this method
- return '';
- }
- createRootElement() {
- const rootElem = document.createElement('div');
- rootElem.classList.add(this.constructor.getRootClass());
- return rootElem;
- }
- render() {
- // override this method
- }
- getElement(selector) {
- if (typeof selector === 'undefined') {
- return this.refs.root;
- } else {
- return this.refs.root.querySelector(selector);
- }
- }
- injectInto(parentElem) {
- parentElem.appendChild(this.getElement());
- }
- injectAfter(siblingElem) {
- GSM.utils.insertAfter(this.getElement(), siblingElem);
- }
- }
- window.GSM = window.GSM || {};
- GSM.View = View;
- })(window);
- // models/Tags.js
- ((window) => {
- 'use strict';
- /**
- * @class Tags
- */
- class Tags extends GSM.Model {
- constructor(data) {
- super(data);
- }
- getTagsForRepo(repoId) {
- return this.data[repoId] || [];
- }
- setTagsForRepo(repoId, unserializedTags) {
- const serializedTags = unserializedTags.split(',')
- .map(tag => tag.trim())
- .filter(tag => tag !== '');
- const hasNoTags = serializedTags.length === 0;
- const repoChangeEventName = 'change:' + repoId;
- if (hasNoTags) {
- delete this.data[repoId];
- const changeData = { key: repoId, deleted: true };
- this.emit('change', changeData);
- this.emit(repoChangeEventName, changeData);
- } else {
- const newTags = GSM.utils.unique(serializedTags);
- const changeData = { key: repoId, value: newTags };
- this.data[repoId] = newTags;
- this.emit('change', changeData);
- this.emit(repoChangeEventName, changeData);
- }
- }
- getDeserializedTagsForRepo(repoId) {
- return this.getTagsForRepo(repoId).join(', ');
- }
- byTag() {
- const pivotedData = {};
- for (const repoId in this.data) {
- const tags = this.getTagsForRepo(repoId);
- tags.forEach(tag => {
- if (!(tag in pivotedData)) { pivotedData[tag] = []; }
- pivotedData[tag].push(repoId);
- });
- }
- return pivotedData;
- }
- byTagSortedByUse() {
- const modelByTag = this.byTag();
- return Object.keys(modelByTag)
- .map(tag => createTagObject(tag))
- .sort(byMostUsed);
- function createTagObject(tag) {
- return {
- name: tag,
- repos: modelByTag[tag]
- };
- }
- function byMostUsed(tagObject1, tagObject2) {
- const diff = tagObject2.repos.length - tagObject1.repos.length;
- // default to alphanumerical sort
- if (diff === 0) { return tagObject2.name < tagObject1.name ? 1 : -1; }
- return diff;
- }
- }
- }
- window.GSM = window.GSM || {};
- GSM.Tags = Tags;
- })(window);
- // views/TagLineView.js
- ((window) => {
- 'use strict';
- /**
- * @class TagLineView
- */
- class TagLineView extends GSM.View {
- constructor(model, repoId) {
- super();
- this.model = model;
- this.repoId = repoId;
- }
- static getRootClass() {
- return 'GsmTagLine';
- }
- createRootElement() {
- const rootElem = document.createElement('p');
- rootElem.classList.add(TagLineView.getRootClass(), 'f6', 'text-gray', 'mt-2');
- return rootElem;
- }
- render() {
- if (this.rendered) {
- this.removeEvents();
- }
- const tags = this.model.getDeserializedTagsForRepo(this.repoId);
- const noTagsModifierClass = 'GsmTagLine--noTags';
- this.getElement().classList.toggle(noTagsModifierClass, !tags);
- this.getElement().innerHTML = `
- <svg class="octicon octicon-tag GsmTagLine-icon" viewBox="0 0 14 16" version="1.1" width="14" height="16" aria-hidden="true">
- <path fill-rule="evenodd" d="M7.73 1.73C7.26 1.26 6.62 1 5.96 1H3.5C2.13 1 1 2.13 1 3.5v2.47c0 .66.27 1.3.73 1.77l6.06 6.06c.39.39 1.02.39 1.41 0l4.59-4.59a.996.996 0 0 0 0-1.41L7.73 1.73zM2.38 7.09c-.31-.3-.47-.7-.47-1.13V3.5c0-.88.72-1.59 1.59-1.59h2.47c.42 0 .83.16 1.13.47l6.14 6.13-4.73 4.73-6.13-6.15zM3.01 3h2v2H3V3h.01z"></path>
- </svg>
- <span class="GsmTagLine-tags">${ tags }</span>
- <span class="GsmTagLine-separator"> — </span>
- <button class="GsmTagLine-editButton" type="button" title="Click to edit">Edit</button>
- <input class="GsmTagLine-tagsInput form-control input-sm" type="text" value="${ tags }" placeholder="Enter comma-separated tags" spellcheck="false" autocomplete="off" />
- `;
- this.refs.editButton = this.getElement('.GsmTagLine-editButton');
- this.refs.tagsInput = this.getElement('.GsmTagLine-tagsInput');
- this.addEvents();
- this.rendered = true;
- }
- addEvents() {
- this.handlers = {
- modelChange: (changeData, target, eventName) => this.onModelChanged(changeData, target, eventName),
- editButtonClick: event => this.onEditButtonClicked(event),
- tagsInputKeydown: event => this.onTagsInputKeydowned(event),
- tagsInputBlur: event => this.onTagsInputBlurred(event)
- };
- this.model.on('change:' + this.repoId, this.handlers.modelChange);
- this.refs.editButton.addEventListener('click', this.handlers.editButtonClick);
- this.refs.tagsInput.addEventListener('keydown', this.handlers.tagsInputKeydown);
- this.refs.tagsInput.addEventListener('blur', this.handlers.tagsInputBlur);
- }
- removeEvents() {
- this.model.off('change:' + this.repoId, this.handlers.modelChange);
- this.refs.editButton.removeEventListener('click', this.handlers.editButtonClick);
- this.refs.tagsInput.removeEventListener('keydown', this.handlers.tagsInputKeydown);
- this.refs.tagsInput.removeEventListener('blur', this.handlers.tagsInputBlur);
- this.handlers = {};
- }
- onModelChanged() {
- this.render();
- }
- onEditButtonClicked() {
- this.enterEditMode();
- GSM.utils.track('TagLine', 'edit');
- }
- onTagsInputKeydowned(event) {
- const ENTER = 13;
- const ESCAPE = 27;
- if (event.keyCode === ESCAPE) {
- this.exitEditMode();
- GSM.utils.track('TagLine', 'escape');
- } else if (event.keyCode === ENTER) {
- const newTags = event.currentTarget.value;
- this.exitEditMode(newTags);
- GSM.utils.track('TagLine', 'save');
- }
- }
- onTagsInputBlurred() {
- this.exitEditMode();
- GSM.utils.track('TagLine', 'blur');
- }
- enterEditMode() {
- this.getElement().classList.add('-is-editing');
- // help entering next tag
- if (this.refs.tagsInput.value !== '') { this.refs.tagsInput.value += ', '; }
- // focus at the end of input
- this.refs.tagsInput.focus();
- const length = this.refs.tagsInput.value.length;
- this.refs.tagsInput.setSelectionRange(length, length);
- }
- exitEditMode(newTags) {
- if (typeof newTags === 'undefined') {
- this.render();
- } else {
- this.model.setTagsForRepo(this.repoId, newTags);
- }
- this.getElement().classList.remove('-is-editing');
- }
- }
- window.GSM = window.GSM || {};
- GSM.TagLineView = TagLineView;
- })(window);
- // views/TagSidebarView.js
- ((window) => {
- 'use strict';
- /**
- * @class TagSidebarView
- */
- class TagSidebarView extends GSM.View {
- constructor(model) {
- super();
- this.model = model;
- }
- static getRootClass() {
- return 'GsmTagSidebar';
- }
- render() {
- if (this.rendered) {
- this.removeEvents();
- }
- const sortedTags = this.model.byTagSortedByUse();
- const tagsCount = sortedTags.length;
- const tagsCountIndicator = tagsCount ? `<span class="count">${ tagsCount }</span>` : '';
- this.getElement().innerHTML = `
- <h3 class="h4 mb-2">
- Filter by tags
- ${ tagsCountIndicator }
- </h3>
- <ul class="filter-list small GsmTagSidebar-tagList">
- ${ this.renderTags(sortedTags) }
- </ul>
- <hr />
- `;
- this.addEvents();
- this.rendered = true;
- }
- renderTags(sortedTags) {
- if (sortedTags.length === 0) {
- return `<span class="filter-item GsmTagSidebar-noTagsMessage">No tags</span>`;
- }
- return sortedTags.map(tagModel => this.renderTag(tagModel)).join('');
- }
- renderTag(tagModel) {
- return `
- <li>
- <label class="GsmTagSidebar-label">
- <span class="filter-item">
- ${ tagModel.name }
- <span class="count">${ tagModel.repos.length }</span>
- </span>
- <input class="GsmTagSidebar-checkbox" type="checkbox" />
- <ul class="GsmRepoList">
- ${ this.renderTagRepos(tagModel) }
- </ul>
- </label>
- </li>
- `;
- }
- renderTagRepos(tagModel) {
- return tagModel.repos.map(tagModel => this.renderTagRepo(tagModel)).join('');
- }
- renderTagRepo(repoId) {
- return `
- <li class="GsmRepoList-item css-truncate">
- <a class="css-truncate-target" href="/${ repoId }">${ repoId }</a>
- </li>
- `;
- }
- addEvents() {
- this.handlers = {
- modelChange: (changeData, target, eventName) => this.onModelChanged(changeData, target, eventName),
- click: (event) => this.onClicked(event)
- };
- this.model.on('change', this.handlers.modelChange);
- this.getElement().addEventListener('click', this.handlers.click, false);
- }
- removeEvents() {
- this.model.off('change', this.handlers.modelChange);
- this.getElement().removeEventListener('click', this.handlers.click, false);
- this.handlers = {};
- }
- onModelChanged() {
- this.render();
- }
- onClicked(event) {
- if (event.target && event.target.classList.contains('filter-item')) {
- GSM.utils.track('TagSidebar', 'click', 'tag');
- }
- }
- }
- window.GSM = window.GSM || {};
- GSM.TagSidebarView = TagSidebarView;
- })(window);
- githubStarsTaggerInit();
- // main.js
- function githubStarsTaggerInit() {
- 'use strict';
- addStarPageBtn();
- const tagsStore = new GSM.TagsStore();
- if (isStarPage(location.href)) {
- addStyle();
- tagsStore.get()
- .then(createModel)
- .then(initViews)
- .then(initSync);
- }
- function addStarPageBtn() {
- var navs = document.querySelector('ul.flex-items-center.text-bold');
- if (navs) {
- navs.innerHTML += '<li><a href="/stars" class="js-selected-navigation-item HeaderNavlink px-2">Stars</a></li>';
- }
- }
- function isStarPage(path) {
- return path === '/stars' || path === '/stars/' || Boolean(path.match(/\/stars/));
- }
- function createModel(data) {
- return new GSM.Tags(data);
- }
- function initViews(tagsModel) {
- initTagLines(tagsModel);
- initTagSidebar(tagsModel);
- return tagsModel;
- function initTagLines(model) {
- const repoItemSelector = '.repo-list > li';
- // on page load
- addTagLines();
- // when sorting, filtering, paginating was used
- addAjaxPageRefreshEventListener(onAjaxPageRefreshed);
- function onAjaxPageRefreshed(newPath) {
- removeTagLines();
- const shouldAddTagLines = isCurrentPathSupported(newPath);
- if (shouldAddTagLines) {
- addTagLines();
- }
- }
- function addTagLines() {
- const starredRepoElems = document.querySelectorAll(repoItemSelector);
- Array.from(starredRepoElems).forEach(starredRepoElem => addTagLine(starredRepoElem));
- function addTagLine(starredRepoElem) {
- const repoId = starredRepoElem.querySelector('h3 a').getAttribute('href').substring(1);
- const view = new GSM.TagLineView(model, repoId);
- view.render();
- view.injectInto(starredRepoElem);
- }
- }
- function removeTagLines() {
- const starredRepoElems = document.querySelectorAll(repoItemSelector);
- Array.from(starredRepoElems).forEach(starredRepoElem => removeTagLine(starredRepoElem));
- function removeTagLine(starredRepoElem) {
- const oldTagLineElem = starredRepoElem.querySelector('.' + GSM.TagLineView.getRootClass());
- if (oldTagLineElem) { oldTagLineElem.remove(); }
- }
- }
- function isCurrentPathSupported(path) {
- return path === '/stars' || path === '/stars/' || Boolean(path.match(/\/stars\/?\?.+/));
- }
- }
- function initTagSidebar(model) {
- const ajaxContentElem = document.querySelector('.explore-pjax-container');
- // on page load
- addSidebar();
- // when sorting, filtering, paginating was used
- addAjaxPageRefreshEventListener(onAjaxPageRefreshed);
- function onAjaxPageRefreshed(newPath) {
- removeSidebar();
- const shouldAddSidebar = isCurrentPathSupported(newPath);
- if (shouldAddSidebar) {
- addSidebar();
- }
- }
- function addSidebar() {
- const firstSidebarSeparatorElem = ajaxContentElem.querySelector('.col-md-3.float-md-left.mt-3 hr:first-of-type');
- const view = new GSM.TagSidebarView(model);
- view.render();
- view.injectAfter(firstSidebarSeparatorElem);
- }
- function removeSidebar() {
- const oldTagSidebarElem = ajaxContentElem.querySelector('.' + GSM.TagSidebarView.getRootClass());
- if (oldTagSidebarElem) { oldTagSidebarElem.remove(); }
- }
- }
- function addAjaxPageRefreshEventListener(callback) {
- const ajaxContentElem = document.querySelector('.explore-pjax-container');
- const observer = new MutationObserver(mutations => {
- mutations.forEach(mutation => {
- if (mutation.addedNodes.length > 0) {
- callback(document.location.pathname);
- }
- });
- });
- const config = { childList: true };
- observer.observe(ajaxContentElem, config);
- }
- }
- function initSync(tagsModel) {
- tagsModel.on('change', onModelChanged);
- function onModelChanged(changeData) {
- if (changeData.deleted) {
- tagsStore.remove(changeData.key);
- } else {
- tagsStore.set(changeData.key, changeData.value);
- }
- }
- }
- function addStyle() {
- var cssText = '<style>.GsmTagLine{position:relative}.GsmTagLine-icon{color:currentColor}.GsmTagLine--noTags .GsmTagLine-icon{opacity:.35}.GsmTagLine.-is-editing .GsmTagLine-icon{position:absolute;top:7px;left:7px}.GsmTagLine.-is-editing .GsmTagLine-tags{display:none}.GsmTagLine--noTags .GsmTagLine-tags{opacity:.35}.GsmTagLine-separator{opacity:.35}.GsmTagLine.-is-editing .GsmTagLine-separator{display:none}.GsmTagLine-editButton{padding:.5em;position:relative;margin:-0.5em;background-color:transparent;border:0;opacity:.35;outline:0;-webkit-appearance:none;-moz-appearance:none;appearance:none}.GsmTagLine-editButton:hover,.GsmTagLine-editButton:focus{opacity:1}.GsmTagLine.-is-editing .GsmTagLine-editButton{display:none}.GsmTagLine-tagsInput{display:none;width:400px;padding-left:25px !important}.GsmTagLine.-is-editing .GsmTagLine-tagsInput{display:inline-block} .GsmTagSidebar h3 .count{float:right;margin-right:10px}.GsmTagSidebar-noTagsMessage{margin-bottom:15px !important}.GsmTagSidebar-noTagsMessage:hover{background-color:transparent !important;cursor:auto}.GsmTagSidebar-tagList{max-height:19.2em;overflow:auto}.GsmTagSidebar-tagList>li:last-child{margin-bottom:15px}.GsmTagSidebar-tagList+hr{margin-top:0}.GsmTagSidebar-label{font-size:inherit;font-weight:inherit}.GsmTagSidebar-checkbox{display:none}.GsmRepoList{display:none;list-style:none}.GsmRepoList-item{display:block;padding:4px 10px;margin:0 0 2px;font-size:12px}.GsmRepoList-item a{display:block;max-width:210px !important}.GsmTagSidebar-checkbox:checked+.GsmRepoList{display:block}</style>';
- document.body.innerHTML += cssText;
- }
- }