├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE ├── README.md ├── dist ├── download-git-userscript.meta.js └── download-git-userscript.user.js ├── eslintrc.json ├── package.json ├── screenshot.png ├── src ├── index.ts └── utils.ts ├── tsconfig.json ├── webpack.config.js └── yarn.lock /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [oe] 4 | ko_fi: esaiya 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | download-git-userscript.proxy.user.js 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Saiya 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Download Github Sub-folder User script 2 | 3 | [中文说明](#中文说明) 4 | 5 | > You can create your own userscript power by webpack/es6/typescript/etc by starting from this [template](https://github.com/oe/webpack-userscript-template) 6 | 7 | If you have any issues with this script, please create an issue on [Github](https://github.com/oe/download-git-userscript/issues) 8 | 9 | ## Features 10 | * **download github source code online**: allow you to download whole repo, a sub-folder of a repo or a single file online without `git clone` locally 11 | * **seamless integration**: seamless integrated with Github(click file icon in file list to download), and works great with [octotree](https://github.com/ovity/octotree). 12 | 13 | ![Download Github screenshot](./screenshot.png) 14 | 15 | ## Usage 16 | 17 | ### Install a user script manager 18 | To use user scripts you need to first install a user script manager. Which user script manager you can use depends on which browser you use. 19 | 20 | * Chrome: [Tampermonkey](https://chrome.google.com/webstore/detail/tampermonkey/dhdgffkkebhmkfjojejmpbldmpobfkfo) or [Violentmonkey](https://chrome.google.com/webstore/detail/violent-monkey/jinjaccalgkegednnccohejagnlnfdag) 21 | * Firefox: [Greasemonkey](https://addons.mozilla.org/firefox/addon/greasemonkey/), [Tampermonkey](https://addons.mozilla.org/firefox/addon/tampermonkey/), or [Violentmonkey](https://addons.mozilla.org/firefox/addon/violentmonkey/) 22 | * Safari: [Tampermonkey](http://tampermonkey.net/?browser=safari) or [Userscripts](https://apps.apple.com/app/userscripts/id1463298887) 23 | * Microsoft Edge: [Tampermonkey](https://www.microsoft.com/store/p/tampermonkey/9nblggh5162s) 24 | * Opera: [Tampermonkey](https://addons.opera.com/extensions/details/tampermonkey-beta/) or [Violentmonkey](https://violentmonkey.github.io/get-it/) 25 | * Maxthon: [Violentmonkey](http://extension.maxthon.com/detail/index.php?view_id=1680) 26 | * Dolphin: [Tampermonkey](https://play.google.com/store/apps/details?id=net.tampermonkey.dolphin) 27 | * UC: [Tampermonkey](https://www.tampermonkey.net/?browser=ucweb&ext=dhdg) 28 | 29 | ### Install this user script 30 | 31 | * [install script via greasyfork](https://greasyfork.org/scripts/411834-download-github-repo-sub-folder/code/Download%20github%20repo%20sub-folder.user.js), [greasyfork home page](https://greasyfork.org/scripts/411834-download-github-repo-sub-folder) 32 | * [install script via openuserjs](https://openuserjs.org/install/oe/Download_github_repo_online.user.js), [openuserjs home page](https://openuserjs.org/scripts/oe/Download_github_repo_online) 33 | 34 | 35 | ### Configure DownGit web app(not required) 36 | Due to github api requests rate limit, if you use [DownGit](https://downgit.evecalm.com/) frequently, you may failed to download files with it. Then you can click ***Github Auth*** button in the center of [DownGit](https://downgit.evecalm.com/) to auth your account with this app. 37 | 38 | 39 | ### Credits 40 | This script use [Downgit](https://downgit.evecalm.com/)([sourcecode](https://github.com/oe/DownGit/)) to download github sub-folder. DownGit is forked from [MinhasKamal](https://github.com/MinhasKamal/DownGit), I just added Github Auth feature. Thanks to [MinhasKamal](https://github.com/MinhasKamal/) 41 | 42 | 43 | # 中文说明 44 | 45 | 无需克隆GitHub仓库, 一键在线下载 Github仓库子文件夹; 同时还能在源码详情页一键复制源码. 46 | 47 | > 你也可以使用模版 [template](https://github.com/oe/webpack-userscript-template)使用 webpack/es6/typescript/等技术=来创建你自己的 userscript. 48 | 49 | 如果你使用中遇到任何问题, 欢迎在[Github](https://github.com/oe/download-git-userscript/issues) 上提交 issue 50 | ## 功能特性 51 | * **在线下载Github仓库源码**: 你可以在线下载整个仓库、仓库的某个文件夹、单个文件的代码, 无需在机器上使用`git clone`命令下载完整仓库 52 | * **无缝集成**: 与 GitHub 无缝集成(点击文件列表左侧图标即可直接下载), 看起来就像是原生功能, 与 Github 增强扩展 [octotree](https://github.com/ovity/octotree) 也能无缝配合 53 | 54 | 55 | ![Download Github screenshot](./screenshot.png) 56 | 57 | 在线下载Github仓库的文件夹功能使用开源项目 [DownGit](https://downgit.evecalm.com/)([源码](https://github.com/oe/DownGit/)) 实现. 该项目fork自[MinhasKamal](https://github.com/MinhasKamal/DownGit), 本人增加了GitHub auth授权功能, auth 授权后, downgit则拥有更多的Github API调用频次, 即可以用于下载更多github文件. 58 | 59 | 60 | ## 使用说明 61 | 62 | ### 安装脚本管理器 63 | 64 | Chrome 用户推荐安装浏览器扩展: [Tampermonkey](https://chrome.google.com/webstore/detail/tampermonkey/dhdgffkkebhmkfjojejmpbldmpobfkfo)  65 | 66 | 其他选择可参考: [安装一个用户脚本管理器](https://greasyfork.org/zh-CN#home-step-1) 67 | 68 | ### 安装脚本 69 | 70 | [点此来安装脚本](https://greasyfork.org/scripts/411834-download-github-repo-sub-folder/code/Download%20github%20repo%20sub-folder.user.js) 71 | * [从 greasyfork 安装脚本](https://greasyfork.org/scripts/411834-download-github-repo-sub-folder/code/Download%20github%20repo%20sub-folder.user.js), [greasyfork 主页](https://greasyfork.org/scripts/411834-download-github-repo-sub-folder) 72 | * [从 openuserjs 安装脚本](https://openuserjs.org/install/oe/Download_github_repo_online.user.js), [openuserjs 主页](https://openuserjs.org/scripts/oe/Download_github_repo_online) 73 | 74 | ### DownGit 网站配置(非必须) 75 | 因为Github对第三方应用调用API频率有限制, 如果你经常使用[DownGit](https://downgit.evecalm.com/)下载文件, 则可能出现下载失败的情况. 76 | 77 | 此时就建议你点击网站中间的 ***Github Auth*** 按钮进行 Auth 授权, 这样 DownGit 可以拥有更多api调用次数, 能下载更多的文件. 78 | 79 | ## develop steps 80 | 81 | ### change settings of chrome 82 | 83 | 1. navigate to `chrome://flags/#allow-insecure-localhost`, enable insecure localhost 84 | 2. navigate to `chrome://extensions/?id=dhdgffkkebhmkfjojejmpbldmpobfkfo`(Chrome manage extensions page of `Tampermonkey`) and enable `Allow access to file URLs` (you need to manual reload page when dev userscript, see [#475](https://github.com/Tampermonkey/tampermonkey/issues/475#issuecomment-348594785) for more detail) 85 | 86 | ### dev 87 | 88 | 1. `yarn` 89 | 2. `yarn dev` 90 | 3. open in browser(click `Advanced` -> `proceed` if it shows a security warning ) to install the proxy script 91 | 4. dev code, reload github.com webpage after userscript changed 92 | 93 | 94 | 95 | ## references 96 | 1. [Tampermonkey docs](https://www.tampermonkey.net/documentation.php) 97 | -------------------------------------------------------------------------------- /dist/download-git-userscript.meta.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Download github repo sub-folder 3 | // @name:zh-CN 在线下载Github仓库文件夹 4 | // @description download github sub-folder via one click, copy the single file's source code easily 5 | // @description:zh-CN 无需克隆GitHub仓库, 一键在线下载 Github仓库子文件夹; 同时还能在源码详情页一键复制源码 6 | // @version 0.7.2 7 | // @author Saiya 8 | // @supportURL https://github.com/oe/download-git-userscript/issues 9 | // @match https://github.com/* 10 | // @match https://gist.github.com/* 11 | // @connect cdn.jsdelivr.net 12 | // @grant GM_setClipboard 13 | // @grant GM_xmlhttpRequest 14 | // @homepageURL https://github.com/oe/download-git-userscript 15 | // @icon https://github.githubassets.com/pinned-octocat.svg 16 | // @namespace https://app.evecalm.com 17 | // @noframes 18 | // ==/UserScript== 19 | -------------------------------------------------------------------------------- /dist/download-git-userscript.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Download github repo sub-folder 3 | // @name:zh-CN 在线下载Github仓库文件夹 4 | // @description download github sub-folder via one click, copy the single file's source code easily 5 | // @description:zh-CN 无需克隆GitHub仓库, 一键在线下载 Github仓库子文件夹; 同时还能在源码详情页一键复制源码 6 | // @version 0.7.2 7 | // @author Saiya 8 | // @supportURL https://github.com/oe/download-git-userscript/issues 9 | // @match https://github.com/* 10 | // @match https://gist.github.com/* 11 | // @connect cdn.jsdelivr.net 12 | // @grant GM_setClipboard 13 | // @grant GM_xmlhttpRequest 14 | // @homepageURL https://github.com/oe/download-git-userscript 15 | // @icon https://github.githubassets.com/pinned-octocat.svg 16 | // @namespace https://app.evecalm.com 17 | // @noframes 18 | // ==/UserScript== 19 | 20 | /******/ (() => { // webpackBootstrap 21 | /******/ "use strict"; 22 | /******/ var __webpack_modules__ = ({ 23 | 24 | /***/ 607: 25 | /***/ (function(__unused_webpack_module, exports, __webpack_require__) { 26 | 27 | 28 | var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { 29 | if (k2 === undefined) k2 = k; 30 | var desc = Object.getOwnPropertyDescriptor(m, k); 31 | if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { 32 | desc = { enumerable: true, get: function() { return m[k]; } }; 33 | } 34 | Object.defineProperty(o, k2, desc); 35 | }) : (function(o, m, k, k2) { 36 | if (k2 === undefined) k2 = k; 37 | o[k2] = m[k]; 38 | })); 39 | var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { 40 | Object.defineProperty(o, "default", { enumerable: true, value: v }); 41 | }) : function(o, v) { 42 | o["default"] = v; 43 | }); 44 | var __importStar = (this && this.__importStar) || function (mod) { 45 | if (mod && mod.__esModule) return mod; 46 | var result = {}; 47 | if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); 48 | __setModuleDefault(result, mod); 49 | return result; 50 | }; 51 | Object.defineProperty(exports, "__esModule", ({ value: true })); 52 | const utils = __importStar(__webpack_require__(593)); 53 | (function () { 54 | const DOWNLOAD_BTN_ID = 'xiu-download-btn'; 55 | const STYLE_ELEMENT_ID = 'xiu-style-element'; 56 | let tid = 0; 57 | main(); 58 | // observe body change 59 | const observer = new MutationObserver(onBodyChanged); 60 | observer.observe(document, { subtree: true, childList: true }); 61 | function main() { 62 | if (!utils.isRepo()) 63 | return; 64 | addDownloadBtn(); 65 | addDownload2FileList(); 66 | } 67 | function onBodyChanged() { 68 | // debounce, avoid frequent call 69 | clearTimeout(tid); 70 | // @ts-ignore 71 | tid = setTimeout(main, 100); 72 | } 73 | function addDownloadBtn() { 74 | let $navi = utils.isRepoRootDir() && document.querySelector('#branch-picker-repos-header-ref-selector'); 75 | if ($navi) { 76 | $navi = $navi.parentElement.parentElement.nextElementSibling; 77 | } 78 | else { 79 | $navi = document.querySelector('#StickyHeader .js-github-dev-new-tab-shortcut'); 80 | if (!$navi) { 81 | $navi = document.querySelector('[data-testid="tree-overflow-menu-anchor"]'); 82 | if (!$navi) 83 | return; 84 | } 85 | $navi = $navi.parentElement; 86 | } 87 | if (!$navi) 88 | return; 89 | const downloadBtn = getDownloadBtn(); 90 | if (!downloadBtn || $navi.contains(downloadBtn)) 91 | return; 92 | $navi.appendChild(downloadBtn); 93 | } 94 | function getDownloadBtn() { 95 | let downloadBtn = document.getElementById(DOWNLOAD_BTN_ID); 96 | if (!downloadBtn) { 97 | downloadBtn = document.createElement('a'); 98 | downloadBtn.id = DOWNLOAD_BTN_ID; 99 | } 100 | const isRoot = utils.isRepoRootDir(); 101 | downloadBtn.className = `btn d-none d-md-block ${isRoot ? 'ml-0' : ''}`; 102 | downloadBtn.target = '_blank'; 103 | let url = ''; 104 | if (isRoot) { 105 | try { 106 | // @ts-ignore 107 | const repoInfo = JSON.parse(document.querySelector('[partial-name="repos-overview"] [data-target="react-partial.embeddedData"]').innerText); 108 | const zipUrl = repoInfo.props.initialPayload.overview.codeButton.local.platformInfo.zipballUrl; 109 | url = new URL(zipUrl, location.href).href; 110 | } 111 | catch (error) { 112 | console.warn('unable to get zip url', error); 113 | return; 114 | } 115 | } 116 | else { 117 | url = utils.getGithubDownloadUrl(utils.getCurrentUrlPath()); 118 | } 119 | downloadBtn.textContent = 'Download'; 120 | downloadBtn.href = url; 121 | return downloadBtn; 122 | } 123 | function addDownload2FileList() { 124 | if (document.getElementById(STYLE_ELEMENT_ID)) 125 | return; 126 | const style = document.createElement('style'); 127 | style.id = STYLE_ELEMENT_ID; 128 | const styleContent = ` 129 | .react-directory-filename-column { position: relative; } 130 | .react-directory-filename-column:has(a[aria-label*="File"]):after, 131 | .react-directory-filename-column:has(a[aria-label*="Directory"]):after, 132 | .Box .Box-row > [role="gridcell"]:first-child:after { 133 | position: absolute; 134 | left: 20px; 135 | top: 10px; 136 | opacity: 0.6; 137 | pointer-events: none; 138 | content: '↓'; 139 | font-size: 0.8em; 140 | z-index: 11; 141 | } 142 | 143 | .react-directory-filename-column:has(a[aria-label*="File"]):after, 144 | .react-directory-filename-column:has(a[aria-label*="Directory"]):after{ 145 | left: 4px; 146 | top: 12px; 147 | color: white; 148 | } 149 | 150 | 151 | [data-color-mode="light"] .react-directory-filename-column:after { 152 | color: black; 153 | } 154 | @media (prefers-color-scheme: light) { 155 | [data-color-mode=auto][data-light-theme*=light] .react-directory-filename-column:after { 156 | color: black; 157 | } 158 | } 159 | 160 | .react-directory-filename-column:has(a[aria-label*="File"]) svg, 161 | .react-directory-filename-column:has(a[aria-label*="Directory"]) svg{ 162 | cursor: pointer; 163 | } 164 | `; 165 | style.textContent = styleContent; 166 | document.head.appendChild(style); 167 | addEvent2FileIcon(); 168 | } 169 | function addEvent2FileIcon() { 170 | document.documentElement.addEventListener('click', (e) => { 171 | var _a, _b, _c, _d, _e, _f, _g; 172 | // @ts-ignore 173 | const target = (e.target && e.target.ownerSVGElement || e.target); 174 | if (!target || (target.tagName || '').toLowerCase() !== 'svg') 175 | return; 176 | const label = target.getAttribute('aria-label') || ''; 177 | let url = ''; 178 | let isFile = false; 179 | if (['Directory', 'File'].includes(label)) { 180 | url = (_d = (_c = (_b = (_a = target.parentElement) === null || _a === void 0 ? void 0 : _a.nextElementSibling) === null || _b === void 0 ? void 0 : _b.querySelector) === null || _c === void 0 ? void 0 : _c.call(_b, 'a')) === null || _d === void 0 ? void 0 : _d.href; 181 | isFile = label === 'File'; 182 | } 183 | else if ((_e = target.parentElement) === null || _e === void 0 ? void 0 : _e.classList.contains('react-directory-filename-column')) { 184 | const anchor = (_g = (_f = target.nextElementSibling) === null || _f === void 0 ? void 0 : _f.querySelector) === null || _g === void 0 ? void 0 : _g.call(_f, 'a'); 185 | if (!anchor) 186 | return; 187 | const label = anchor.getAttribute('aria-label') || ''; 188 | if (!label.includes('Directory') && !label.includes('File')) 189 | return; 190 | url = anchor.href; 191 | console.warn("url", url); 192 | isFile = target.classList.contains('color-fg-muted'); 193 | } 194 | else { 195 | return; 196 | } 197 | if (!url) 198 | return; 199 | utils.openLink(utils.getGithubDownloadUrl(url, isFile)); 200 | }, { 201 | capture: true, 202 | passive: true, 203 | }); 204 | } 205 | })(); 206 | 207 | 208 | /***/ }), 209 | 210 | /***/ 593: 211 | /***/ ((__unused_webpack_module, exports) => { 212 | 213 | 214 | Object.defineProperty(exports, "__esModule", ({ value: true })); 215 | exports.getGithubDownloadUrl = exports.openLink = exports.getCurrentUrlPath = exports.getRawBtn = exports.getUrlTextResponse = exports.isTextBasedSinglePage = exports.isRepoRootDir = exports.isPrivateRepo = exports.isRepo = exports.isGist = void 0; 216 | /** 217 | * is gist website 218 | */ 219 | function isGist() { 220 | return location.hostname === 'gist.github.com'; 221 | } 222 | exports.isGist = isGist; 223 | function isRepo() { 224 | if (!document.querySelector('.repository-content, #js-repo-pjax-container')) 225 | return false; 226 | const meta = document.querySelector('meta[name="selected-link"]'); 227 | if (meta && meta.getAttribute('value') === 'repo_commits') 228 | return false; 229 | if (document.querySelector('.js-navigation-container>.TimelineItem')) 230 | return false; 231 | return true; 232 | } 233 | exports.isRepo = isRepo; 234 | function isPrivateRepo() { 235 | const label = document.querySelector('#js-repo-pjax-container .hide-full-screen .Label'); 236 | return label && label.textContent === 'Private'; 237 | } 238 | exports.isPrivateRepo = isPrivateRepo; 239 | function isRepoRootDir() { 240 | return !!document.querySelector('.repository-content [partial-name="repos-overview"]'); 241 | } 242 | exports.isRepoRootDir = isRepoRootDir; 243 | function isTextBasedSinglePage() { 244 | if (!getRawBtn()) 245 | return; 246 | if (document.getElementById('readme')) 247 | return true; 248 | const boxBody = document.querySelector('table.highlight'); 249 | if (boxBody) 250 | return true; 251 | return false; 252 | } 253 | exports.isTextBasedSinglePage = isTextBasedSinglePage; 254 | function getUrlTextResponse(url) { 255 | // https://github.com/oe/search/raw/gh-pages/app-icon-retina.f492fc13.png 256 | // https://cdn.jsdelivr.net/gh/oe/search@gh-pages/app-icon-retina.f492fc13.png 257 | // https://github.com/oe/search/raw/master/CNAME 258 | let apiUrl = url 259 | .replace('github.com/', 'cdn.jsdelivr.net/gh/') 260 | .replace('/raw/', '@'); 261 | return new Promise((resolve, reject) => { 262 | // @ts-ignore 263 | GM_xmlhttpRequest({ 264 | url: apiUrl, 265 | method: 'GET', 266 | onload: (s) => { 267 | resolve(s.responseText); 268 | } 269 | }); 270 | }); 271 | } 272 | exports.getUrlTextResponse = getUrlTextResponse; 273 | // if is single file page, then it has a raw btn 274 | function getRawBtn() { 275 | return document.getElementById('raw-url'); 276 | } 277 | exports.getRawBtn = getRawBtn; 278 | // remove qeurystring & hash 279 | function getCurrentUrlPath() { 280 | const url = location.origin + location.pathname; 281 | return url.replace(/\/$/, ''); 282 | } 283 | exports.getCurrentUrlPath = getCurrentUrlPath; 284 | function openLink(url) { 285 | const link = document.createElement('a'); 286 | link.target = '_blank'; 287 | link.href = url; 288 | link.click(); 289 | } 290 | exports.openLink = openLink; 291 | function getGithubDownloadUrl(url, isFile) { 292 | if (isFile) { 293 | try { 294 | const u = new URL(url); 295 | let paths = u.pathname.split('/'); 296 | paths[3] = 'raw'; 297 | u.pathname = paths.join('/'); 298 | return u.href; 299 | } 300 | catch (error) { } 301 | } 302 | return `https://downgit.evecalm.com/#/home?url=${encodeURIComponent(url)}`; 303 | } 304 | exports.getGithubDownloadUrl = getGithubDownloadUrl; 305 | 306 | 307 | /***/ }) 308 | 309 | /******/ }); 310 | /************************************************************************/ 311 | /******/ // The module cache 312 | /******/ var __webpack_module_cache__ = {}; 313 | /******/ 314 | /******/ // The require function 315 | /******/ function __webpack_require__(moduleId) { 316 | /******/ // Check if module is in cache 317 | /******/ var cachedModule = __webpack_module_cache__[moduleId]; 318 | /******/ if (cachedModule !== undefined) { 319 | /******/ return cachedModule.exports; 320 | /******/ } 321 | /******/ // Create a new module (and put it into the cache) 322 | /******/ var module = __webpack_module_cache__[moduleId] = { 323 | /******/ // no module.id needed 324 | /******/ // no module.loaded needed 325 | /******/ exports: {} 326 | /******/ }; 327 | /******/ 328 | /******/ // Execute the module function 329 | /******/ __webpack_modules__[moduleId].call(module.exports, module, module.exports, __webpack_require__); 330 | /******/ 331 | /******/ // Return the exports of the module 332 | /******/ return module.exports; 333 | /******/ } 334 | /******/ 335 | /************************************************************************/ 336 | /******/ 337 | /******/ // startup 338 | /******/ // Load entry module and return exports 339 | /******/ // This entry module is referenced by other modules so it can't be inlined 340 | /******/ var __webpack_exports__ = __webpack_require__(607); 341 | /******/ 342 | /******/ })() 343 | ; -------------------------------------------------------------------------------- /eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2020": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/recommended" 10 | ], 11 | "ignorePatterns": [ 12 | "node_modules/", 13 | "dist/" 14 | ], 15 | "parser": "@typescript-eslint/parser", 16 | "parserOptions": { 17 | "ecmaVersion": 11, 18 | "sourceType": "module" 19 | }, 20 | "plugins": [ 21 | "@typescript-eslint" 22 | ], 23 | "rules": { 24 | "indent": ["warn", 2, { 25 | "SwitchCase": 1 26 | }], 27 | "no-constant-condition": ["error", { 28 | "checkLoops": false 29 | }], 30 | "no-undef": "off", 31 | "semi": "off", 32 | "@typescript-eslint/explicit-module-boundary-types": "off", 33 | "@typescript-eslint/member-delimiter-style": ["error", { 34 | "multiline": { 35 | "delimiter": "semi", 36 | "requireLast": true 37 | } 38 | }], 39 | "@typescript-eslint/semi": ["error"] 40 | }, 41 | "overrides": [ 42 | { 43 | "files": ["*.ts"], 44 | "rules": { 45 | "@typescript-eslint/explicit-module-boundary-types": ["warn"] 46 | } 47 | } 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "download-git-userscript", 3 | "version": "0.7.2", 4 | "scripts": { 5 | "dev": "cross-env NODE_ENV=development webpack-dev-server --host 127.0.0.1", 6 | "lint": "eslint .", 7 | "lint:fix": "eslint . --fix", 8 | "build": "cross-env NODE_ENV=production webpack --progress" 9 | }, 10 | "devDependencies": { 11 | "@babel/core": "^7.11.6", 12 | "@babel/preset-env": "^7.11.5", 13 | "@typescript-eslint/eslint-plugin": "^4.1.1", 14 | "@typescript-eslint/parser": "^4.1.1", 15 | "babel-loader": "^8.1.0", 16 | "clean-webpack-plugin": "^4.0.0", 17 | "cross-env": "^7.0.2", 18 | "eslint": "^7.9.0", 19 | "ts-loader": "^9.5.1", 20 | "typescript": "^4.0.2", 21 | "webpack": "^5.89.0", 22 | "webpack-cli": "^5.1.4", 23 | "webpack-dev-server": "^4.15.1", 24 | "webpack-userscript": "^3.2.2" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oe/download-git-userscript/f32d46ca150ab99f87b6b385583176067eda297f/screenshot.png -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import * as utils from './utils' 2 | 3 | (function () { 4 | const DOWNLOAD_BTN_ID = 'xiu-download-btn' 5 | const STYLE_ELEMENT_ID = 'xiu-style-element' 6 | let tid = 0 7 | main() 8 | 9 | // observe body change 10 | const observer = new MutationObserver(onBodyChanged); 11 | observer.observe(document, { subtree: true, childList: true }); 12 | 13 | function main() { 14 | if (!utils.isRepo()) return 15 | addDownloadBtn() 16 | addDownload2FileList() 17 | } 18 | 19 | function onBodyChanged() { 20 | // debounce, avoid frequent call 21 | clearTimeout(tid) 22 | // @ts-ignore 23 | tid = setTimeout(main, 100); 24 | } 25 | 26 | function addDownloadBtn() { 27 | let $navi = utils.isRepoRootDir() && document.querySelector('#branch-picker-repos-header-ref-selector') as HTMLElement 28 | if ($navi) { 29 | $navi = $navi.parentElement!.parentElement!.nextElementSibling as HTMLElement 30 | } else { 31 | $navi = document.querySelector('#StickyHeader .js-github-dev-new-tab-shortcut') as HTMLElement 32 | if (!$navi) { 33 | $navi = document.querySelector('[data-testid="tree-overflow-menu-anchor"]') as HTMLElement 34 | if (!$navi) return 35 | } 36 | $navi = $navi.parentElement as HTMLElement 37 | } 38 | if (!$navi) return 39 | const downloadBtn = getDownloadBtn() 40 | if (!downloadBtn || $navi.contains(downloadBtn)) return 41 | $navi.appendChild(downloadBtn) 42 | } 43 | 44 | function getDownloadBtn() { 45 | let downloadBtn = document.getElementById(DOWNLOAD_BTN_ID) as HTMLAnchorElement | null 46 | if (!downloadBtn) { 47 | downloadBtn = document.createElement('a') 48 | downloadBtn.id = DOWNLOAD_BTN_ID 49 | } 50 | const isRoot = utils.isRepoRootDir() 51 | downloadBtn.className = `btn d-none d-md-block ${isRoot ? 'ml-0' : ''}` 52 | downloadBtn.target = '_blank' 53 | let url = '' 54 | if (isRoot) { 55 | try { 56 | // @ts-ignore 57 | const repoInfo = JSON.parse(document.querySelector('[partial-name="repos-overview"] [data-target="react-partial.embeddedData"]')!.innerText) 58 | const zipUrl = repoInfo.props.initialPayload.overview.codeButton.local.platformInfo.zipballUrl 59 | url = new URL(zipUrl, location.href).href 60 | } catch (error) { 61 | console.warn('unable to get zip url', error) 62 | return 63 | } 64 | } else { 65 | url = utils.getGithubDownloadUrl(utils.getCurrentUrlPath()) 66 | } 67 | downloadBtn.textContent = 'Download' 68 | downloadBtn.href = url 69 | return downloadBtn 70 | } 71 | 72 | function addDownload2FileList() { 73 | if (document.getElementById(STYLE_ELEMENT_ID)) return 74 | const style = document.createElement('style') 75 | style.id = STYLE_ELEMENT_ID 76 | 77 | const styleContent = ` 78 | .react-directory-filename-column { position: relative; } 79 | .react-directory-filename-column:has(a[aria-label*="File"]):after, 80 | .react-directory-filename-column:has(a[aria-label*="Directory"]):after, 81 | .Box .Box-row > [role="gridcell"]:first-child:after { 82 | position: absolute; 83 | left: 20px; 84 | top: 10px; 85 | opacity: 0.6; 86 | pointer-events: none; 87 | content: '↓'; 88 | font-size: 0.8em; 89 | z-index: 11; 90 | } 91 | 92 | .react-directory-filename-column:has(a[aria-label*="File"]):after, 93 | .react-directory-filename-column:has(a[aria-label*="Directory"]):after{ 94 | left: 4px; 95 | top: 12px; 96 | color: white; 97 | } 98 | 99 | 100 | [data-color-mode="light"] .react-directory-filename-column:after { 101 | color: black; 102 | } 103 | @media (prefers-color-scheme: light) { 104 | [data-color-mode=auto][data-light-theme*=light] .react-directory-filename-column:after { 105 | color: black; 106 | } 107 | } 108 | 109 | .react-directory-filename-column:has(a[aria-label*="File"]) svg, 110 | .react-directory-filename-column:has(a[aria-label*="Directory"]) svg{ 111 | cursor: pointer; 112 | } 113 | ` 114 | style.textContent = styleContent 115 | document.head.appendChild(style) 116 | addEvent2FileIcon() 117 | } 118 | 119 | function addEvent2FileIcon() { 120 | document.documentElement.addEventListener('click', (e: MouseEvent) => { 121 | // @ts-ignore 122 | const target = (e.target && e.target.ownerSVGElement || e.target) as HTMLElement 123 | if (!target || (target.tagName || '').toLowerCase() !== 'svg') return 124 | const label = target.getAttribute('aria-label') || '' 125 | let url: string | undefined = '' 126 | let isFile = false 127 | 128 | if (['Directory', 'File'].includes(label)) { 129 | url = target.parentElement?.nextElementSibling?.querySelector?.('a')?.href 130 | isFile = label === 'File' 131 | } else if (target.parentElement?.classList.contains('react-directory-filename-column')) { 132 | const anchor = target.nextElementSibling?.querySelector?.('a') 133 | if (!anchor) return 134 | const label = anchor.getAttribute('aria-label') || '' 135 | if (!label.includes('Directory') && !label.includes('File')) return 136 | url = anchor.href 137 | console.warn("url", url) 138 | isFile = target.classList.contains('color-fg-muted') 139 | } else { 140 | return 141 | } 142 | if (!url) return 143 | utils.openLink(utils.getGithubDownloadUrl(url, isFile)) 144 | }, { 145 | capture: true, 146 | passive: true, 147 | }) 148 | } 149 | })() 150 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * is gist website 3 | */ 4 | export function isGist() { 5 | return location.hostname === 'gist.github.com' 6 | } 7 | 8 | export function isRepo() { 9 | if (!document.querySelector('.repository-content, #js-repo-pjax-container')) return false 10 | const meta = document.querySelector('meta[name="selected-link"]') as HTMLMetaElement 11 | if (meta && meta.getAttribute('value') === 'repo_commits') return false 12 | if (document.querySelector('.js-navigation-container>.TimelineItem')) return false 13 | return true 14 | } 15 | 16 | export function isPrivateRepo() { 17 | const label = document.querySelector('#js-repo-pjax-container .hide-full-screen .Label') 18 | return label && label.textContent === 'Private' 19 | } 20 | 21 | export function isRepoRootDir() { 22 | return !!document.querySelector('.repository-content [partial-name="repos-overview"]') 23 | } 24 | 25 | export function isTextBasedSinglePage() { 26 | if (!getRawBtn()) return 27 | if (document.getElementById('readme')) return true 28 | const boxBody = document.querySelector('table.highlight') 29 | if (boxBody) return true 30 | return false 31 | } 32 | 33 | export function getUrlTextResponse(url: string): Promise { 34 | // https://github.com/oe/search/raw/gh-pages/app-icon-retina.f492fc13.png 35 | // https://cdn.jsdelivr.net/gh/oe/search@gh-pages/app-icon-retina.f492fc13.png 36 | // https://github.com/oe/search/raw/master/CNAME 37 | let apiUrl = url 38 | .replace('github.com/', 'cdn.jsdelivr.net/gh/') 39 | .replace('/raw/', '@') 40 | return new Promise((resolve, reject) => { 41 | // @ts-ignore 42 | GM_xmlhttpRequest({ 43 | url: apiUrl, 44 | method: 'GET', 45 | onload: (s: any) => { 46 | resolve(s.responseText) 47 | } 48 | }) 49 | }) 50 | } 51 | 52 | // if is single file page, then it has a raw btn 53 | export function getRawBtn() { 54 | return document.getElementById('raw-url') 55 | } 56 | 57 | // remove qeurystring & hash 58 | export function getCurrentUrlPath() { 59 | const url = location.origin + location.pathname 60 | return url.replace(/\/$/, '') 61 | } 62 | 63 | export function openLink(url: string){ 64 | const link = document.createElement('a') 65 | link.target = '_blank' 66 | link.href = url 67 | link.click() 68 | } 69 | 70 | export function getGithubDownloadUrl(url: string, isFile?: boolean) { 71 | if (isFile) { 72 | try { 73 | const u = new URL(url) 74 | let paths = u.pathname.split('/') 75 | paths[3] = 'raw' 76 | u.pathname = paths.join('/') 77 | return u.href 78 | } catch (error) {} 79 | } 80 | return `https://downgit.evecalm.com/#/home?url=${encodeURIComponent(url)}` 81 | } 82 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "strict": true, 7 | "target": "es2018" 8 | }, 9 | "include": ["src/*"], 10 | "exclude": [ 11 | "node_modules/" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { UserscriptPlugin } = require('webpack-userscript') 3 | const { CleanWebpackPlugin } = require('clean-webpack-plugin') 4 | const pkg = require('./package.json') 5 | 6 | const outputPath = path.resolve(__dirname, 'dist') 7 | const isDev = process.env.NODE_ENV === 'development' 8 | const PORT = 8080 9 | const enableHTTPS = true 10 | 11 | module.exports = { 12 | mode: 'production', 13 | entry: path.join(__dirname, 'src/index.ts'), 14 | output: { 15 | path: outputPath, 16 | filename: `${pkg.name}.js` 17 | }, 18 | target: 'web', 19 | module: { 20 | rules: [ 21 | { 22 | test: /\.js$/, 23 | use: { 24 | loader: 'babel-loader', 25 | options: { 26 | presets: [ 27 | ['@babel/preset-env'] 28 | ] 29 | } 30 | } 31 | }, 32 | { 33 | test: /\.tsx?$/, 34 | use: 'ts-loader', 35 | exclude: /node_modules/, 36 | }, 37 | ], 38 | }, 39 | resolve: { 40 | extensions: [ '.tsx', '.ts', '.js' ], 41 | }, 42 | optimization: { 43 | minimize: false 44 | }, 45 | devServer: { 46 | server: { 47 | type: enableHTTPS ? 'https' : 'http', 48 | }, 49 | client: { 50 | overlay: false, 51 | progress: false, 52 | }, 53 | static: false, 54 | webSocketServer: false, 55 | port: PORT, 56 | hot: false, 57 | magicHtml: true, 58 | liveReload: false 59 | }, 60 | plugins: [ 61 | new CleanWebpackPlugin(), 62 | new UserscriptPlugin({ 63 | headers ({ name, version }) { 64 | return { 65 | name: 'Download github repo sub-folder', 66 | version: isDev ? `${version}-beta.${Date.now()}` : version, 67 | author: 'Saiya', 68 | namespace: 'https://app.evecalm.com', 69 | description: 70 | "download github sub-folder via one click, copy the single file's source code easily", 71 | homepageURL: 'https://github.com/oe/download-git-userscript', 72 | // licence: 'MIT', 73 | icon: 'https://github.githubassets.com/pinned-octocat.svg', 74 | supportURL: 'https://github.com/oe/download-git-userscript/issues', 75 | connect: ['cdn.jsdelivr.net'], 76 | match: ['https://github.com/*', 'https://gist.github.com/*'], 77 | grant: ['GM_setClipboard', 'GM_xmlhttpRequest'], 78 | noframes: true 79 | }; 80 | }, 81 | i18n: { 82 | 'zh-CN': { 83 | name: '在线下载Github仓库文件夹', 84 | description: 85 | '无需克隆GitHub仓库, 一键在线下载 Github仓库子文件夹; 同时还能在源码详情页一键复制源码' 86 | } 87 | }, 88 | proxyScript: { 89 | baseURL: enableHTTPS ? `https://localhost:${PORT}` : `http://localhost:${PORT}`, 90 | enable: isDev 91 | }, 92 | pretty: isDev 93 | }) 94 | ] 95 | } 96 | --------------------------------------------------------------------------------