GitZip Lite

Download selected files and folders from GitHub repositories.

  1. // ==UserScript==
  2. // @name GitZip Lite
  3. // @icon https://www.google.com/s2/favicons?sz=64&domain=github.com
  4. // @namespace https://github.com/tizee-tampermonkey-scripts/tampermonkey-gitzip-lite
  5. // @version 1.6.3
  6. // @description Download selected files and folders from GitHub repositories.
  7. // @author tizee
  8. // @match https://github.com/*/*
  9. // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js
  10. // @require https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js
  11. // @require https://unpkg.com/powerglitch@2.4.0/dist/powerglitch.min.js
  12. // @grant GM_xmlhttpRequest
  13. // @grant GM_setValue
  14. // @grant GM_getValue
  15. // @grant GM_addStyle
  16. // @grant GM_registerMenuCommand
  17. // @connect api.github.com
  18. // @connect raw.githubusercontent.com
  19. // @run-at document-end
  20. // @license MIT
  21. // ==/UserScript==
  22.  
  23. (function () {
  24. "use strict";
  25.  
  26. const itemCollectSelector =
  27. "div.js-navigation-item, table tbody tr.react-directory-row > td[class$='cell-large-screen']";
  28. const tokenKey = "githubApiToken";
  29.  
  30. const { parseRepoURL, getGitURL, getInfoURL } = {
  31. parseRepoURL: (repoUrl) => {
  32. const repoExp = new RegExp(
  33. "^https://github.com/([^/]+)/([^/]+)(/(tree|blob)/([^/]+)(/(.*))?)?"
  34. );
  35. const matches = repoUrl.match(repoExp);
  36.  
  37. if (!matches || matches.length === 0) return null;
  38.  
  39. const author = matches[1];
  40. const project = matches[2];
  41. const branch = matches[5];
  42. const type = matches[4];
  43. const path = matches[7] || "";
  44.  
  45. const rootUrl = branch
  46. ? `https://github.com/${author}/${project}/tree/${branch}`
  47. : `https://github.com/${author}/${project}`;
  48.  
  49. if (!type && repoUrl.length - rootUrl.length > 1) {
  50. return null;
  51. }
  52.  
  53. return {
  54. author,
  55. project,
  56. branch,
  57. type,
  58. path,
  59. inputUrl: repoUrl,
  60. rootUrl,
  61. };
  62. },
  63. getGitURL: (author, project, type, sha) => {
  64. if (type === "blob" || type === "tree") {
  65. const pluralType = type + "s";
  66. return `https://api.github.com/repos/${author}/${project}/git/${pluralType}/${sha}`;
  67. }
  68. return null;
  69. },
  70. getInfoURL: (author, project, path, branch) => {
  71. let url = `https://api.github.com/repos/${author}/${project}/contents/${path}`;
  72. if (branch) {
  73. url += `?ref=${branch}`;
  74. }
  75. return url;
  76. },
  77. };
  78.  
  79. // --- GitZip Functions ---
  80.  
  81. function base64toBlob(base64Data, contentType) {
  82. contentType = contentType || "";
  83. const sliceSize = 1024;
  84. const byteCharacters = atob(base64Data);
  85. const bytesLength = byteCharacters.length;
  86. const slicesCount = Math.ceil(bytesLength / sliceSize);
  87. const byteArrays = new Array(slicesCount);
  88.  
  89. for (let sliceIndex = 0; sliceIndex < slicesCount; ++sliceIndex) {
  90. const begin = sliceIndex * sliceSize;
  91. const end = Math.min(begin + sliceSize, bytesLength);
  92.  
  93. const bytes = new Array(end - begin);
  94. for (let offset = begin, i = 0; offset < end; ++i, ++offset) {
  95. bytes[i] = byteCharacters[offset].charCodeAt(0);
  96. }
  97. byteArrays[sliceIndex] = new Uint8Array(bytes);
  98. }
  99. return new Blob(byteArrays, { type: contentType });
  100. }
  101.  
  102. function callAjax(url, token) {
  103. return new Promise(function (resolve, reject) {
  104. GM_xmlhttpRequest({
  105. method: "GET",
  106. url: url,
  107. headers: {
  108. Authorization: token ? "token " + token : undefined,
  109. Accept: "application/json",
  110. },
  111. onload: function (response) {
  112. if (response.status >= 200 && response.status < 300) {
  113. try {
  114. const jsonResponse = JSON.parse(response.responseText);
  115. resolve({ response: jsonResponse });
  116. } catch (e) {
  117. console.debug("Error parsing JSON:", e);
  118. reject(e);
  119. }
  120. } else {
  121. console.debug("Request failed with status:", response.status);
  122. logMessage("ERROR", `Request failed with status: ${response.status}`);
  123. reject(response);
  124. }
  125. },
  126. onerror: function (error) {
  127. logMessage("ERROR", error);
  128. reject(error);
  129. },
  130. });
  131. });
  132. }
  133.  
  134. // New dedicated function for binary downloads
  135. function downloadFile(url, token) {
  136. return new Promise(function (resolve, reject) {
  137. GM_xmlhttpRequest({
  138. method: "GET",
  139. url: url,
  140. responseType: "arraybuffer",
  141. headers: {
  142. Authorization: token ? "token " + token : undefined,
  143. Accept: "application/octet-stream",
  144. },
  145. onload: function (response) {
  146. if (response.status >= 200 && response.status < 300) {
  147. resolve(new Uint8Array(response.response));
  148. } else {
  149. reject(new Error(`Download failed: ${response.status}`));
  150. }
  151. },
  152. onerror: reject,
  153. });
  154. });
  155. }
  156.  
  157. // --- End GitZip Functions ---
  158.  
  159. function addCheckboxes() {
  160. const fileRows = document.querySelectorAll(itemCollectSelector);
  161. fileRows.forEach((row) => {
  162. if (row.querySelector(".gitziplite-check-wrap")) return;
  163.  
  164. // Ensure the row is relatively positioned
  165. row.style.position = "relative";
  166.  
  167. const checkboxContainer = document.createElement("div");
  168. checkboxContainer.classList.add("gitziplite-check-wrap");
  169. checkboxContainer.style.position = "absolute";
  170. checkboxContainer.style.left = "4px";
  171. checkboxContainer.style.top = "50%";
  172. checkboxContainer.style.transform = "translateY(-50%)";
  173. checkboxContainer.style.display = "flex";
  174. checkboxContainer.style.alignItems = "center";
  175. checkboxContainer.style.height = "100%";
  176. checkboxContainer.style.display = "none";
  177.  
  178. const checkbox = document.createElement("input");
  179. checkbox.type = "checkbox";
  180. checkbox.classList.add("gitziplite-checkbox");
  181.  
  182. checkboxContainer.appendChild(checkbox);
  183.  
  184. // Find the first element to insert before. Handles both file and directory rows.
  185. const insertBeforeElement = row.firstChild;
  186. if (insertBeforeElement) {
  187. row.insertBefore(checkboxContainer, insertBeforeElement);
  188. } else {
  189. row.appendChild(checkboxContainer); // Fallback if no children exist
  190. }
  191.  
  192. // Add event listeners for hover
  193. row.addEventListener("mouseenter", () => {
  194. checkboxContainer.style.display = "flex";
  195. });
  196.  
  197. row.addEventListener("mouseleave", () => {
  198. if (!checkbox.checked) {
  199. checkboxContainer.style.display = "none";
  200. }
  201. });
  202.  
  203. row.addEventListener("dblclick", () => {
  204. console.debug("double click", row, checkbox);
  205. if (checkbox.checked) {
  206. checkboxContainer.style.display = "none";
  207. } else {
  208. checkboxContainer.style.display = "flex";
  209. }
  210. checkbox.checked = !checkbox.checked;
  211. checkbox.dispatchEvent(new Event("change"));
  212. });
  213.  
  214. // Add event listener for checkbox change
  215. checkbox.addEventListener("change", () => {
  216. let link;
  217. if (row.tagName === "TD") {
  218. link = row.querySelector("a[href]");
  219. } else {
  220. link = row.querySelector("a[href]");
  221. }
  222.  
  223. if (link) {
  224. const title = link.textContent.trim();
  225. const command = checkbox.checked ? "SELECT" : "UNSELECT";
  226. logMessage(command, title);
  227. }
  228. });
  229. });
  230. }
  231.  
  232. let logWindow;
  233. let logToggleButton;
  234. let downloadButton;
  235. let mainContainer;
  236. let stickerButton;
  237.  
  238. // Add global styles
  239. GM_addStyle(`
  240. /* Container Styles */
  241. .gitziplite-container {
  242. position: fixed;
  243. bottom: 1rem;
  244. right: 1rem;
  245. z-index: 1000;
  246. width: 480px;
  247. background-color: rgba(28, 28, 30, 0.95);
  248. border-radius: 16px;
  249. box-shadow: 0 4px 24px rgba(0, 0, 0, 0.2);
  250. padding: 1.25rem;
  251. backdrop-filter: blur(20px);
  252. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
  253. border: 1px solid rgba(255, 255, 255, 0.08);
  254. display: none; /* Hide window by default */
  255. }
  256.  
  257. /* Sidebar sticker button */
  258. .gitziplite-sticker-button {
  259. position: fixed;
  260. right: 0;
  261. top: 30%;
  262. background-color: rgba(28, 28, 30, 0.95);
  263. color: white;
  264. border-radius: 8px 0 0 8px;
  265. padding: 10px;
  266. cursor: pointer;
  267. z-index: 999;
  268. box-shadow: -2px 0 8px rgba(0, 0, 0, 0.2);
  269. transition: all 0.2s ease;
  270. border: 1px solid rgba(255, 255, 255, 0.08);
  271. border-right: none;
  272. }
  273.  
  274. .gitziplite-sticker-button:hover {
  275. background-color: rgba(40, 40, 45, 0.95);
  276. transform: translateX(-2px);
  277. }
  278.  
  279. /* Hide button for the container */
  280. .gitziplite-hide-button {
  281. position: absolute;
  282. top: -14px;
  283. right: -14px;
  284. width: 28px;
  285. height: 28px;
  286. border-radius: 14px;
  287. background-color: rgba(28, 28, 30, 0.95);
  288. border: 1px solid rgba(255, 255, 255, 0.08);
  289. color: white;
  290. font-size: 16px;
  291. cursor: pointer;
  292. display: flex;
  293. align-items: center;
  294. justify-content: center;
  295. transition: background-color 0.2s ease;
  296. z-index: 1001;
  297. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
  298. }
  299.  
  300. .gitziplite-hide-button:hover {
  301. background-color: rgba(40, 40, 45, 0.95);
  302. }
  303.  
  304. /* Log Window Styles */
  305. .gitziplite-log {
  306. width: 100%;
  307. height: 16rem;
  308. margin-bottom: 0.75rem;
  309. overflow-y: auto;
  310. border-radius: 12px;
  311. background-color: rgba(0, 0, 0, 0.25);
  312. color: #E4E4E4;
  313. font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, monospace;
  314. font-size: 12px;
  315. line-height: 1.5;
  316. padding: 0.75rem;
  317. scrollbar-width: thin;
  318. scrollbar-color: rgba(255, 255, 255, 0.2) transparent;
  319. border: 1px solid rgba(255, 255, 255, 0.06);
  320. }
  321.  
  322. /* Scrollbar Styles */
  323. .gitziplite-log::-webkit-scrollbar {
  324. width: 6px;
  325. height: 6px;
  326. }
  327.  
  328. .gitziplite-log::-webkit-scrollbar-track {
  329. background: transparent;
  330. }
  331.  
  332. .gitziplite-log::-webkit-scrollbar-thumb {
  333. background: rgba(255, 255, 255, 0.2);
  334. border-radius: 3px;
  335. }
  336.  
  337. .gitziplite-log::-webkit-scrollbar-thumb:hover {
  338. background: rgba(255, 255, 255, 0.3);
  339. }
  340.  
  341. /* Log Entry Styles */
  342. .gitziplite-log-entry {
  343. padding: 0.25rem 0;
  344. display: flex;
  345. align-items: center;
  346. gap: 0.5rem;
  347. opacity: 0;
  348. transform: translateY(10px);
  349. animation: gitziplite-fadeIn 0.2s ease-out forwards;
  350. }
  351.  
  352. .gitziplite-log-timestamp {
  353. color: #8E8E93;
  354. min-width: 5.5rem;
  355. font-feature-settings: "tnum";
  356. font-variant-numeric: tabular-nums;
  357. }
  358.  
  359. .gitziplite-log-command {
  360. min-width: 5rem;
  361. padding: 0.125rem 0.5rem;
  362. border-radius: 6px;
  363. font-weight: 500;
  364. text-align: center;
  365. backdrop-filter: blur(8px);
  366. }
  367.  
  368. .gitziplite-log-content {
  369. color: #E4E4E4;
  370. flex: 1;
  371. }
  372.  
  373. /* Button Container */
  374. .gitziplite-buttons {
  375. display: flex;
  376. gap: 0.75rem;
  377. justify-content: space-between;
  378. align-items: center;
  379. }
  380.  
  381. /* Button Styles */
  382. .gitziplite-button {
  383. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
  384. font-size: 13px;
  385. font-weight: 510;
  386. padding: 0.625rem 1rem;
  387. border-radius: 8px;
  388. cursor: pointer;
  389. transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
  390. border: none;
  391. outline: none;
  392. white-space: nowrap;
  393. user-select: none;
  394. position: relative;
  395. box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
  396. }
  397.  
  398. .gitziplite-button-primary {
  399. background-color: #0A84FF;
  400. color: white;
  401. }
  402.  
  403. .gitziplite-button-primary:hover {
  404. background-color: #007AFF;
  405. transform: translateY(-1px);
  406. box-shadow: 0 4px 12px rgba(10, 132, 255, 0.3);
  407. }
  408.  
  409. .gitziplite-button-primary:active {
  410. transform: translateY(0);
  411. background-color: #0062CC;
  412. box-shadow: 0 1px 2px rgba(10, 132, 255, 0.2);
  413. }
  414.  
  415. .gitziplite-button-secondary {
  416. background-color: rgba(255, 255, 255, 0.1);
  417. color: #FFFFFF;
  418. border: 1px solid rgba(255, 255, 255, 0.1);
  419. }
  420.  
  421. .gitziplite-button-secondary:hover {
  422. background-color: rgba(255, 255, 255, 0.15);
  423. transform: translateY(-1px);
  424. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
  425. }
  426.  
  427. .gitziplite-button-secondary:active {
  428. transform: translateY(0);
  429. background-color: rgba(255, 255, 255, 0.05);
  430. box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
  431. }
  432.  
  433. /* Animation */
  434. @keyframes gitziplite-fadeIn {
  435. to {
  436. opacity: 1;
  437. transform: translateY(0);
  438. }
  439. }
  440. `);
  441.  
  442. function createDownloadButton() {
  443. // Create sticker button for the sidebar
  444. stickerButton = document.createElement("div");
  445. stickerButton.className = "gitziplite-sticker-button";
  446. stickerButton.innerHTML = `
  447. <div style="display: flex; flex-direction: column; align-items: center;">
  448. <svg width="16" height="16" viewBox="0 0 16 16" style="margin-bottom: 8px;">
  449. <path fill="currentColor" d="M8 12l-4.5-4.5 1.5-1.5L7 8.25V2h2v6.25L11 6l1.5 1.5L8 12zm-6 2v-2h12v2H2z"></path>
  450. </svg>
  451. <div style="writing-mode: vertical-lr; transform: rotate(180deg); font-size: 12px; letter-spacing: 1px; margin-top: 5px;">GitZip</div>
  452. </div>
  453. `;
  454. stickerButton.setAttribute("title", "Show GitZip Download Window");
  455. stickerButton.addEventListener("click", () => {
  456. mainContainer.style.display = "block";
  457. stickerButton.style.display = "none";
  458. });
  459. document.body.appendChild(stickerButton);
  460.  
  461. // Main container
  462. mainContainer = document.createElement("div");
  463. mainContainer.className = "gitziplite-container";
  464.  
  465. // Hide button
  466. const hideButton = document.createElement("button");
  467. hideButton.className = "gitziplite-hide-button";
  468. hideButton.innerHTML = "✕";
  469. hideButton.setAttribute("title", "Hide Download Window");
  470. hideButton.addEventListener("click", () => {
  471. mainContainer.style.display = "none";
  472. stickerButton.style.display = "block";
  473. });
  474. mainContainer.appendChild(hideButton);
  475.  
  476. // Log Window Container
  477. logWindow = document.createElement("div");
  478. logWindow.setAttribute("aria-label", "Log Window");
  479. logWindow.className = "gitziplite-log";
  480. logWindow.style.display = "none";
  481.  
  482. // Button Container
  483. const buttonContainer = document.createElement("div");
  484. buttonContainer.className = "gitziplite-buttons";
  485.  
  486. // Log Toggle Button
  487. logToggleButton = document.createElement("button");
  488. logToggleButton.textContent = "Show Log";
  489. logToggleButton.className = "gitziplite-button gitziplite-button-secondary";
  490. logToggleButton.addEventListener("click", () => {
  491. logWindow.style.display =
  492. logWindow.style.display === "none" ? "block" : "none";
  493. logToggleButton.textContent =
  494. logWindow.style.display === "none" ? "Show Log" : "Hide Log";
  495. });
  496.  
  497. // Download Button
  498. downloadButton = document.createElement("button");
  499. downloadButton.textContent = "Download Selected";
  500. downloadButton.className = "gitziplite-button gitziplite-button-primary";
  501. downloadButton.addEventListener("click", downloadSelected);
  502.  
  503. // Assemble the UI
  504. buttonContainer.appendChild(logToggleButton);
  505. buttonContainer.appendChild(downloadButton);
  506. mainContainer.appendChild(logWindow);
  507. mainContainer.appendChild(buttonContainer);
  508. document.body.appendChild(mainContainer);
  509.  
  510. // Hide the window by default
  511. mainContainer.style.display = "none";
  512. stickerButton.style.display = "block";
  513. }
  514.  
  515. function logMessage(command, content) {
  516. const now = new Date();
  517. const timestamp = `${String(now.getHours()).padStart(2, "0")}:${String(
  518. now.getMinutes()
  519. ).padStart(2, "0")}:${String(now.getSeconds()).padStart(2, "0")}`;
  520.  
  521. const commandColors = {
  522. ERROR: { bg: "#FF453A20", color: "#FF453A" },
  523. SUCCESS: { bg: "#32D74B20", color: "#32D74B" },
  524. PROCESS: { bg: "#0A84FF20", color: "#0A84FF" },
  525. SELECT: { bg: "#FFD60A20", color: "#FFD60A" },
  526. UNSELECT: { bg: "#FFD60A20", color: "#FFD60A" },
  527. INFO: { bg: "#64D2FF20", color: "#64D2FF" },
  528. };
  529.  
  530. const colorScheme =
  531. commandColors[command.toUpperCase()] || commandColors.INFO;
  532.  
  533. const logEntry = document.createElement("div");
  534. logEntry.className = "gitziplite-log-entry";
  535. logEntry.innerHTML = `
  536. <span class="gitziplite-log-timestamp">${timestamp}</span>
  537. <span class="gitziplite-log-command" style="background: ${colorScheme.bg}; color: ${colorScheme.color}">
  538. ${command}
  539. </span>
  540. <span class="gitziplite-log-content">${content}</span>
  541. `;
  542.  
  543. logWindow.appendChild(logEntry);
  544. logWindow.scrollTop = logWindow.scrollHeight;
  545. }
  546.  
  547. /**
  548. * Collects selected files and folders from the DOM.
  549. * @returns {{files: [], folders: []}} - An object containing arrays of selected files and folders.
  550. */
  551. function collectSelectedItems() {
  552. const selectedFiles = [];
  553. const selectedFolders = [];
  554. const checkboxes = document.querySelectorAll(
  555. ".gitziplite-checkbox:checked"
  556. );
  557.  
  558. checkboxes.forEach((checkbox) => {
  559. const row = checkbox.parentNode.parentNode; // Direct parent access
  560. if (!row) {
  561. console.warn("Could not find a parent row for a selected checkbox.");
  562. return; // Skip to the next checkbox
  563. }
  564. console.debug(row);
  565. let link;
  566.  
  567. if (row.tagName === "TD") {
  568. link = row.querySelector("a[href]");
  569. } else {
  570. link = row.querySelector("a[href]");
  571. }
  572.  
  573. if (link) {
  574. const href = link.href;
  575. const title = link.textContent.trim();
  576. const resolved = parseRepoURL(href);
  577. if (resolved && resolved.type === "blob") {
  578. selectedFiles.push({ href: href, title: title });
  579. } else if (resolved && resolved.type === "tree") {
  580. selectedFolders.push({ href: href, title: title });
  581. }
  582. }
  583. });
  584.  
  585. return { files: selectedFiles, folders: selectedFolders };
  586. }
  587.  
  588. /**
  589. * Zips the given contents and triggers a download.
  590. * @param {Array<{path: string, content: string}>} allContents - Array of file contents to zip.
  591. * @param {object} resolvedUrl - Parsed URL information of the repository.
  592. */
  593. function zipAndDownload(allContents, resolvedUrl) {
  594. if (allContents.length === 1) {
  595. // Handle single file download
  596. const singleItem = allContents[0];
  597. console.debug(singleItem);
  598. if (singleItem.isBinary) {
  599. // Create Blob directly from Uint8Array
  600. const blob = new Blob([singleItem.content], {
  601. type: "application/octet-stream",
  602. });
  603. saveAs(blob, singleItem.path);
  604. } else {
  605. // Handle base64 encoded text files
  606. const blob = base64toBlob(singleItem.content, "");
  607. saveAs(blob, singleItem.path);
  608. }
  609. } else {
  610. // Handle zip archive creation
  611. try {
  612. const currDate = new Date();
  613. const dateWithOffset = new Date(
  614. currDate.getTime() - currDate.getTimezoneOffset() * 60000
  615. );
  616. window.JSZip.defaults.date = dateWithOffset;
  617.  
  618. const zip = new window.JSZip();
  619. allContents.forEach((item) => {
  620. if (item.isBinary) {
  621. // Add binary file as Uint8Array
  622. zip.file(item.path, item.content, {
  623. createFolders: true,
  624. binary: true,
  625. date: dateWithOffset,
  626. });
  627. } else {
  628. // Add base64 encoded file
  629. zip.file(item.path, item.content, {
  630. createFolders: true,
  631. base64: true,
  632. date: dateWithOffset,
  633. });
  634. }
  635. });
  636.  
  637. zip.generateAsync({ type: "blob" }).then((content) => {
  638. saveAs(
  639. content,
  640. [resolvedUrl.project]
  641. .concat(resolvedUrl.path.split("/"))
  642. .join("-") + ".zip"
  643. );
  644. });
  645. } catch (error) {
  646. console.debug("Error zipping files:", error);
  647. logMessage("ERROR", "zipping files.");
  648. }
  649. }
  650. }
  651.  
  652. async function downloadSelected() {
  653. const { files: selectedFiles, folders: selectedFolders } =
  654. collectSelectedItems();
  655.  
  656. if (selectedFiles.length === 0 && selectedFolders.length === 0) {
  657. logMessage("ERROR", "No files or folders selected.");
  658. return;
  659. }
  660.  
  661. const resolvedUrl = parseRepoURL(window.location.href);
  662. if (!resolvedUrl) {
  663. logMessage("ERROR", "Could not resolve repository URL.");
  664. return;
  665. }
  666.  
  667. const githubToken = GM_getValue(tokenKey);
  668.  
  669. const allContents = [];
  670.  
  671. async function processFolder(folder, pathPrefix = "") {
  672. logMessage("PROCESS", `${folder.title}`);
  673. const folderResolvedUrl = parseRepoURL(folder.href);
  674. const apiUrl = getInfoURL(
  675. folderResolvedUrl.author,
  676. folderResolvedUrl.project,
  677. folderResolvedUrl.path,
  678. folderResolvedUrl.branch
  679. );
  680.  
  681. try {
  682. const xmlResponse = await callAjax(apiUrl, githubToken);
  683. const folderContents = xmlResponse.response;
  684.  
  685. for (const item of folderContents) {
  686. const itemPath = pathPrefix + "/" + item.name;
  687. if (item.type === "file") {
  688. logMessage("PROCESS", `${itemPath}`);
  689. const fileInfoUrl = getInfoURL(
  690. folderResolvedUrl.author,
  691. folderResolvedUrl.project,
  692. folderResolvedUrl.path + "/" + item.name,
  693. folderResolvedUrl.branch
  694. );
  695. const fileXmlResponse = await callAjax(fileInfoUrl, githubToken);
  696. const fileContent = fileXmlResponse.response;
  697. allContents.push({
  698. path: itemPath,
  699. content: fileContent.content,
  700. });
  701. } else if (item.type === "dir") {
  702. await processFolder(
  703. { href: folder.href + "/" + item.name, title: item.name },
  704. itemPath
  705. );
  706. }
  707. }
  708. } catch (error) {
  709. console.debug("Error fetching folder:", folder.title, error);
  710. logMessage("ERROR", `Error fetching folder: ${folder.title}`);
  711. }
  712. }
  713.  
  714. for (const folder of selectedFolders) {
  715. await processFolder(folder, folder.title);
  716. }
  717.  
  718. for (const file of selectedFiles) {
  719. logMessage("PROCESS", `${file.title}`);
  720. const fileResolvedUrl = parseRepoURL(file.href);
  721. const infoUrl = getInfoURL(
  722. fileResolvedUrl.author,
  723. fileResolvedUrl.project,
  724. fileResolvedUrl.path,
  725. fileResolvedUrl.branch
  726. );
  727. logMessage("PROCESS", `${infoUrl}`);
  728. console.debug(`file info url: ${infoUrl}`);
  729. try {
  730. const xmlResponse = await callAjax(infoUrl, githubToken);
  731. const fileContent = xmlResponse.response;
  732.  
  733. if (fileContent.encoding === "base64" && fileContent.content) {
  734. allContents.push({
  735. path: file.title,
  736. content: fileContent.content,
  737. isBinary: false,
  738. });
  739. } else if (fileContent.download_url) {
  740. // Handle binary file with dedicated download function
  741. const binaryData = await downloadFile(
  742. fileContent.download_url,
  743. githubToken
  744. );
  745. allContents.push({
  746. path: file.title,
  747. content: binaryData,
  748. isBinary: true,
  749. });
  750. }
  751. } catch (error) {
  752. console.debug("Error fetching file:", file.title, error);
  753. logMessage("ERROR", `fetching file: ${file.title}`);
  754. return;
  755. }
  756. }
  757.  
  758. zipAndDownload(allContents, resolvedUrl);
  759. logMessage("SUCCESS", "Download complete.");
  760. }
  761.  
  762. // Register menu command for setting token
  763. GM_registerMenuCommand("Set GitHub API Token", () => {
  764. const token = prompt("Enter your GitHub API token:");
  765. if (token) {
  766. GM_setValue(tokenKey, token);
  767. alert("Token saved successfully!");
  768. }
  769. });
  770.  
  771. function onDomLoaded() {
  772. addCheckboxes();
  773. createDownloadButton();
  774. }
  775.  
  776. function onUrlChange() {
  777. addCheckboxes();
  778. }
  779.  
  780. // Initialize
  781. onDomLoaded();
  782. // Glitch Animation
  783. PowerGlitch.glitch(logToggleButton, {
  784. playMode: "click",
  785. timing: {
  786. duration: 400,
  787. easing: "ease-in-out",
  788. },
  789. shake: {
  790. velocity: 20,
  791. amplitudeX: 0,
  792. amplitudeY: 0.1,
  793. },
  794. });
  795. PowerGlitch.glitch(downloadButton, {
  796. playMode: "click",
  797. timing: {
  798. duration: 400,
  799. easing: "ease-in-out",
  800. },
  801. });
  802.  
  803. // Observe GitHub repository page URL changes (e.g., navigating into a new directory)
  804. const observer = new MutationObserver(onUrlChange);
  805. observer.observe(document.body, { childList: true, subtree: true });
  806. })();

QingJ © 2025

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