Last active
March 4, 2024 03:48
-
-
Save HelloWorld017/08ec3c9cc6541256737529c0ec2389b2 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name Github Action Pinning | |
// @namespace https://gist.github.com/HelloWorld017/08ec3c9cc6541256737529c0ec2389b2 | |
// @namespace http://tampermonkey.net/ | |
// @version 0.3 | |
// @description Pin your favorite actions workflow in GitHub | |
// @author nenw | |
// @match https://github.com/* | |
// @icon https://www.google.com/s2/favicons?sz=64&domain=github.com | |
// @grant none | |
// ==/UserScript== | |
/* Very Simple UI Library */ | |
const getReplaceState = (elem) => { | |
const elements = (elem instanceof DocumentFragment) ? [...elem.children] : [elem]; | |
return { elements }; | |
}; | |
const replaceWith = (replaceState, withElem) => { | |
const nextReplaceState = getReplaceState(withElem); | |
if (replaceState.elements.length === 1) { | |
replaceState.elements[0].parentNode.replaceChild(withElem, replaceState.elements[0]); | |
return nextReplaceState; | |
} | |
replaceState.elements[0].parentNode.insertBefore(withElem, replaceState.elements[0]); | |
replaceState.elements.forEach(elem => elem.parentNode.removeChild(elem)); | |
return nextReplaceState; | |
}; | |
const createElement = (tag, props, ns = null) => { | |
if (typeof tag === 'function') { | |
return tag(props); | |
} | |
const elem = ns === null ? document.createElement(tag) : document.createElementNS(ns, tag); | |
Object.keys(props).forEach(propKey => { | |
if (propKey.startsWith('on:')) { | |
elem.addEventListener(propKey.slice(3), props[propKey]); | |
return; | |
} | |
if (propKey === 'children') { | |
elem.appendChild(props[propKey]); | |
return; | |
} | |
elem.setAttribute(propKey, String(props[propKey])); | |
}); | |
return elem; | |
}; | |
const styled = (tag, style) => (props) => { | |
const elem = createElement(tag, props); | |
Object.keys(style).forEach(styleKey => { | |
elem.style[styleKey] = style[styleKey]; | |
}); | |
return elem; | |
}; | |
const text = (contents) => () => { | |
const node = document.createTextNode(contents); | |
return node; | |
}; | |
const svg = (tag) => (props) => | |
createElement(tag, props, 'http://www.w3.org/2000/svg'); | |
const jsx = (elems) => { | |
const jsxInternal = (elems) => { | |
if (elems.length <= 0) { | |
return []; | |
} | |
const [tag] = elems; | |
elems = elems.slice(1); | |
const props = {}; | |
const [passedProps] = elems; | |
if (typeof passedProps === 'object' && !Array.isArray(passedProps)) { | |
elems = elems.slice(1); | |
Object.assign(props, passedProps); | |
} | |
const [children] = elems; | |
if (Array.isArray(children)) { | |
elems = elems.slice(1); | |
const childElems = jsx(children); | |
props.children = childElems; | |
} | |
const self = createElement(tag, props); | |
return [ | |
self, | |
...jsxInternal(elems) | |
]; | |
}; | |
const output = jsxInternal(elems); | |
if (output.length === 0) { | |
return document.createComment('#'); | |
} | |
if (output.length === 1) { | |
return output[0]; | |
} | |
const fragment = document.createDocumentFragment(); | |
output.forEach(elem => { | |
fragment.appendChild(elem); | |
}); | |
return fragment; | |
}; | |
/* Pinning */ | |
const actionPinning = () => { | |
if (document.querySelector('[data-is="__action_pinning__"]')) { | |
return; | |
} | |
const parent = document.querySelector('[aria-label="Actions Workflows"] :where(.ActionListWrap, .ActionList)'); | |
if (!parent) { | |
return; | |
} | |
/* | |
* Action List | |
*/ | |
const ActionListItem = ({ name, href }) => jsx([ | |
'li', { class: `ActionList-item${window.location.pathname === href ? ' ActionList-item--navActive' : ''}`, 'data-key': href }, [ | |
'a', { href, class: 'ActionList-content ActionList-content--visual16' }, [ | |
'span', { class: 'ActionList-item-label ActionList-item-label--truncate' }, [ | |
text(name) | |
] | |
] | |
] | |
]); | |
const [, orgName, repoName] = window.location.href.match(/^https:\/\/github.com\/([^/]+)\/([^/]+)\/actions/) ?? ['', '', '']; | |
const pinKey = `${orgName}_${repoName}`; | |
const storageKey = `__action_pinning__${pinKey}`; | |
const pinnedActions = new Map(JSON.parse(window.localStorage.getItem(storageKey) ?? '[]')); | |
const PinnedActionsList = ({ pinnedActions }) => jsx([ | |
'li', { class: 'ActionList-sectionDivider' }, [ | |
'nav-list-group', [ | |
'div', [ | |
'div', { class: 'ActionList-sectionDivider' }, [ | |
'h3', { class: 'ActionList-sectionDivider-title' }, [ | |
text('Pinned') | |
] | |
], | |
'ul', { role: 'list', class: 'ActionListWrap' }, [ | |
...Array.from(pinnedActions.entries()).flatMap(([href, name]) => [ | |
ActionListItem, { href, name } | |
]) | |
] | |
] | |
] | |
], | |
'li', { role: 'presentation', 'aria-hidden': 'true', class: 'ActionList-sectionDivider', 'data-is': '__action_pinning__' }, | |
]); | |
const pinnedActionsElem = PinnedActionsList({ pinnedActions }); | |
let pinnedActionsElemState = getReplaceState(pinnedActionsElem); | |
parent.prepend(pinnedActionsElem); | |
const updateActions = (update) => { | |
update(); | |
window.localStorage.setItem(storageKey, JSON.stringify(Array.from(pinnedActions.entries()))); | |
// No reconciliation, just replace | |
pinnedActionsElemState = replaceWith(pinnedActionsElemState, PinnedActionsList({ pinnedActions })); | |
}; | |
const pinAction = (href, name) => | |
updateActions(() => { | |
pinnedActions.set(href, name); | |
}); | |
const unpinAction = (href) => | |
updateActions(() => { | |
pinnedActions.delete(href); | |
}); | |
/* | |
* Pin | |
*/ | |
const pageHeader = document.querySelector('.PageHeader'); | |
if (!pageHeader) { | |
return; | |
} | |
const href = location.pathname; | |
const name = pageHeader.querySelector('.PageHeader-title span').innerText; | |
const IconPin = () => jsx([ | |
svg('svg'), { viewBox: '0 0 16 16', width: 16, height: 16, class: 'octicon octicon-pin Button-visual' }, [ | |
svg('path'), { | |
d: 'm11.294.984 3.722 3.722a1.75 1.75 0 0 1-.504 2.826l-1.327.613a3.089 3.089 0 0 0-1.707 2.084l-.584 2.454c-.317 1.332-1.972 1.8-2.94.832L5.75 11.311 1.78 15.28' + | |
'a.749.749 0 1 1-1.06-1.06l3.969-3.97-2.204-2.204c-.968-.968-.5-2.623.832-2.94l2.454-.584a3.08 3.08 0 0 0 2.084-1.707l.613-1.327a1.75 1.75 0 0 1 2.826-.504Z' + | |
'M6.283 9.723l2.732 2.731a.25.25 0 0 0 .42-.119l.584-2.454a4.586 4.586 0 0 1 2.537-3.098l1.328-.613a.25.25 0 0 0 .072-.404l-3.722-3.722a.25.25 0 0 0-.404.072' + | |
'l-.613 1.328a4.584 4.584 0 0 1-3.098 2.537l-2.454.584a.25.25 0 0 0-.119.42l2.731 2.732Z' | |
} | |
] | |
]); | |
const IconUnpin = () => jsx([ | |
svg('svg'), { viewBox: '0 0 16 16', width: 16, height: 16, class: 'octicon octicon-pin-slash Button-visual' }, [ | |
svg('path'), { | |
d: 'm1.655.595 13.75 13.75q.22.219.22.53 0 .311-.22.53-.219.22-.53.22-.311 0-.53-.22L.595 1.655q-.22-.219-.22-.53 0-.311.22-.53.219-.22.53-.22.311 0 .53.22ZM.72 14.22l4.5-4.5' + | |
'q.219-.22.53-.22.311 0 .53.22.22.219.22.53 0 .311-.22.53l-4.5 4.5q-.219.22-.53.22-.311 0-.53-.22-.22-.219-.22-.53 0-.311.22-.53Z' | |
}, | |
svg('path'), { | |
d: 'm5.424 6.146-1.759.419q-.143.034-.183.175-.04.141.064.245l5.469 5.469q.104.104.245.064.141-.04.175-.183l.359-1.509q.072-.302.337-.465.264-.163.567-.091.302.072.465.337.162.264.09.567' + | |
'l-.359 1.509q-.238.999-1.226 1.278-.988.28-1.714-.446L2.485 8.046q-.726-.726-.446-1.714.279-.988 1.278-1.226l1.759-.419q.303-.072.567.091.265.163.337.465.072.302-.091.567-.163.264-.465.336Z' + | |
'M7.47 3.47q.155-.156.247-.355l.751-1.627Q8.851.659 9.75.498q.899-.16 1.544.486l3.722 3.722q.646.645.486 1.544-.161.899-.99 1.282l-1.627.751' + | |
'q-.199.092-.355.247-.219.22-.53.22-.311 0-.53-.22-.22-.219-.22-.53 0-.311.22-.53.344-.345.787-.549l1.627-.751q.118-.055.141-.183.023-.128-.069-.221' + | |
'l-3.722-3.722q-.092-.092-.221-.069-.128.023-.183.141l-.751 1.627q-.204.443-.549.787-.219.22-.53.22-.311 0-.53-.22-.22-.219-.22-.53 0-.311.22-.53Z' | |
} | |
] | |
]); | |
let toggleCurrentAction; | |
const HeaderIcon = ({ isPinned }) => jsx([ | |
'button', { | |
class: 'Button Button--iconOnly Button--secondary Button--medium', | |
style: 'margin-left: 4px', | |
'on:click': () => toggleCurrentAction(!isPinned) | |
}, [ | |
isPinned ? IconUnpin : IconPin | |
] | |
]); | |
const headerIconElem = HeaderIcon({ isPinned: pinnedActions.has(href) }); | |
let headerIconElemState = getReplaceState(headerIconElem); | |
toggleCurrentAction = (nextIsPinned) => { | |
if (nextIsPinned) { | |
pinAction(href, name); | |
} else { | |
unpinAction(href); | |
} | |
headerIconElemState = replaceWith(headerIconElemState, HeaderIcon({ isPinned: nextIsPinned })); | |
}; | |
pageHeader.querySelector('.PageHeader-titleBar').prepend(headerIconElem); | |
}; | |
const originalPushState = history.pushState; | |
history.pushState = function () { | |
originalPushState.apply(this, arguments); | |
actionPinning(); | |
}; | |
const originalReplaceState = history.replaceState; | |
history.replaceState = function () { | |
originalReplaceState.apply(this, arguments); | |
actionPinning(); | |
}; | |
window.addEventListener("popstate", actionPinning); | |
actionPinning(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment