// ==UserScript==
// @name GiteeTree
// @namespace http://tampermonkey.net/
// @version 1.0
// @description Gitee目录树生成与卡片化分享
// @author Azad-sl
// @match https://gitee.com/*
// @grant GM_addStyle
// @grant GM_xmlhttpRequest
// @grant GM_setClipboard
// @grant unsafeWindow
// @connect gitee.com
// @require https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js
// @resource fontAwesome https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css
// @license MIT
// @homepageURL https://gitee.com/azad-sl/GiteeTree
// @supportURL https://gitee.com/azad-sl/GiteeTree/issues
// ==/UserScript==
(function() {
'use strict';
GM_addStyle(`
@import url("https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css");
.gitee-tree-btn {
background-color: #4CAF50 !important;
color: white !important;
border: none !important;
padding: 6px 12px !important;
border-radius: 4px !important;
cursor: pointer !important;
font-weight: 500 !important;
display: inline-flex !important;
align-items: center !important;
transition: background-color 0.2s !important;
font-size: 14px !important;
height: 32px !important;
box-shadow: 0 2px 5px rgba(0,0,0,0.2) !important;
}
.gitee-tree-btn:hover {
background-color: #3d8b40 !important;
transform: translateY(-1px) !important;
box-shadow: 0 4px 8px rgba(0,0,0,0.2) !important;
}
.gitee-tree-btn-fixed {
position: fixed !important;
top: 70px !important;
right: 20px !important;
z-index: 9999 !important;
}
.gitee-tree-modal {
display: none;
position: fixed;
z-index: 10000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(5px);
}
.gitee-tree-modal-content {
background-color: white;
margin: 5% auto;
padding: 20px;
border-radius: 16px;
width: 90%;
max-width: 900px;
max-height: 85vh;
overflow-y: auto;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.3);
}
@media (max-width: 768px) {
.gitee-tree-modal-content {
width: 95%;
max-width: 95%;
margin: 10% auto;
padding: 15px;
}
}
@media (max-width: 480px) {
.gitee-tree-modal-content {
width: 98%;
max-width: 98%;
margin: 15% auto;
padding: 10px;
}
}
.gitee-tree-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
}
.gitee-tree-modal-title {
font-size: 1.5rem;
font-weight: 600;
color: #1a202c;
display: flex;
align-items: center;
}
.gitee-tree-close {
color: #aaa;
font-size: 28px;
font-weight: bold;
cursor: pointer;
transition: color 0.3s ease;
}
.gitee-tree-close:hover {
color: #667eea;
}
.gitee-tree-logo {
width: 28px;
height: 28px;
margin-right: 10px;
fill: #c71d23;
display: inline-block;
vertical-align: middle;
}
.gitee-tree-input-field {
background: rgba(255, 255, 255, 0.9);
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 8px;
padding: 0.75rem 1rem;
font-size: 1rem;
transition: all 0.3s ease;
width: 100%;
margin-bottom: 10px;
}
.gitee-tree-input-field:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.gitee-tree-btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 8px;
padding: 0.75rem 1.5rem;
font-weight: 500;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
cursor: pointer;
}
.gitee-tree-btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
}
.gitee-tree-btn-secondary {
background: rgba(255, 255, 255, 0.9);
color: #4b5563;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 8px;
padding: 0.5rem 1rem;
font-weight: 500;
transition: all 0.3s ease;
cursor: pointer;
margin-right: 8px;
margin-bottom: 8px;
}
.gitee-tree-btn-secondary:hover {
background: rgba(255, 255, 255, 1);
transform: translateY(-1px);
}
.gitee-tree-section-title {
font-size: 1.2rem;
font-weight: 600;
color: #1a202c;
margin-bottom: 1rem;
display: flex;
align-items: center;
}
.gitee-tree-result-container {
background: rgba(255, 255, 255, 0.9);
border-radius: 12px;
padding: 1.5rem;
margin-top: 1.5rem;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05);
}
.gitee-tree-directory-tree-container {
background: rgba(249, 250, 251, 0.8);
border-radius: 8px;
padding: 1rem;
max-height: 400px;
overflow-y: auto;
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
line-height: 1.6;
}
.gitee-tree-advanced-options {
background-color: #f9fafb;
border-radius: 12px;
padding: 16px;
margin-top: 16px;
margin-bottom: 16px;
}
.gitee-tree-advanced-options-title {
font-weight: 600;
margin-bottom: 12px;
color: #4b5563;
display: flex;
align-items: center;
}
.gitee-tree-advanced-options-content {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
}
.gitee-tree-option-group {
display: flex;
flex-direction: column;
}
.gitee-tree-option-label {
font-size: 0.875rem;
font-weight: 500;
color: #4b5563;
margin-bottom: 6px;
}
.gitee-tree-loader-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 2rem;
}
.gitee-tree-loader {
width: 40px;
height: 40px;
position: relative;
}
.gitee-tree-loader-circle {
position: absolute;
width: 100%;
height: 100%;
border-radius: 50%;
border: 3px solid transparent;
border-top-color: #667eea;
animation: spin 1.5s cubic-bezier(0.68, -0.55, 0.265, 1.55) infinite;
}
.gitee-tree-loader-circle:nth-child(2) {
width: 80%;
height: 80%;
top: 10%;
left: 10%;
border-top-color: #764ba2;
animation-delay: 0.2s;
}
.gitee-tree-loader-circle:nth-child(3) {
width: 60%;
height: 60%;
top: 20%;
left: 20%;
border-top-color: #9f7aea;
animation-delay: 0.4s;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.gitee-tree-loader-text {
margin-top: 1rem;
color: #6b7280;
font-size: 0.875rem;
}
.gitee-tree-loader-dots {
display: inline-flex;
gap: 4px;
}
.gitee-tree-loader-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: #667eea;
animation: bounce 1.4s infinite ease-in-out both;
}
.gitee-tree-loader-dot:nth-child(1) { animation-delay: -0.32s; }
.gitee-tree-loader-dot:nth-child(2) { animation-delay: -0.16s; }
@keyframes bounce {
0%, 80%, 100% { transform: scale(0); }
40% { transform: scale(1); }
}
.gitee-tree-error-section {
margin-top: 1rem;
background: #fee;
border: 1px solid #fcc;
border-radius: 8px;
padding: 1rem;
color: #c33;
}
.gitee-tree-flex {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 16px;
}
.gitee-tree-dropdown {
position: relative;
display: inline-block;
}
.gitee-tree-dropdown-content {
display: none;
position: absolute;
background-color: #f9f9f9;
min-width: 120px;
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
z-index: 1;
border-radius: 8px;
overflow: hidden;
}
.gitee-tree-dropdown-content a {
color: #333;
padding: 8px 12px;
text-decoration: none;
display: block;
}
.gitee-tree-dropdown-content a:hover {
background-color: #f1f1f1;
}
.gitee-tree-dropdown:hover .gitee-tree-dropdown-content {
display: block;
}
.gitee-tree-mac-button-red {
background-color: #ff5f57 !important;
}
.gitee-tree-mac-button-yellow {
background-color: #ffbd2e !important;
}
.gitee-tree-mac-button-green {
background-color: #28ca42 !important;
}
.gitee-tree-mac-card {
background: linear-gradient(145deg, #2d3748, #1a202c);
border-radius: 16px;
overflow: hidden;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.3);
backdrop-filter: blur(10px);
width: 400px;
max-width: 90%;
margin: 0 auto;
}
.gitee-tree-mac-header {
background: linear-gradient(145deg, #4a5568, #2d3748);
padding: 10px 16px;
display: flex;
align-items: center;
gap: 8px;
}
.gitee-tree-mac-button {
width: 12px;
height: 12px;
border-radius: 50%;
}
.gitee-tree-mac-content {
padding: 20px;
display: flex;
gap: 20px;
align-items: flex-start;
}
.gitee-tree-mac-info {
flex: 1;
color: #e2e8f0;
}
.gitee-tree-mac-title {
font-size: 1.25rem;
font-weight: 600;
color: #f7fafc;
margin-bottom: 8px;
display: flex;
align-items: center;
}
.gitee-tree-mac-desc {
font-size: 0.875rem;
color: #cbd5e0;
margin-bottom: 16px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
}
.gitee-tree-mac-stats {
display: flex;
gap: 16px;
font-size: 0.875rem;
}
.gitee-tree-gitee-card {
background: #ffffff;
border: 1px solid #d0d7de;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
width: 400px;
max-width: 90%;
margin: 0 auto;
}
.gitee-tree-gitee-header {
background: #f6f8fa;
padding: 10px 16px;
border-bottom: 1px solid #d0d7de;
display: flex;
align-items: center;
gap: 8px;
}
.gitee-tree-gitee-title {
font-size: 1.25rem;
font-weight: 600;
color: #0969da;
display: flex;
align-items: center;
}
.gitee-tree-gitee-badge {
background: #ddf4ff;
color: #0969da;
padding: 2px 8px;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 500;
margin-left: auto;
}
.gitee-tree-gitee-content {
padding: 16px;
display: flex;
gap: 20px;
}
.gitee-tree-gitee-info {
flex: 1;
}
.gitee-tree-gitee-desc {
color: #656d76;
margin-bottom: 16px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
}
.gitee-tree-gitee-stats {
display: flex;
gap: 16px;
font-size: 0.875rem;
color: #656d76;
}
.gitee-tree-material-card {
background: #ffffff;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
overflow: hidden;
width: 400px;
max-width: 90%;
margin: 0 auto;
}
.gitee-tree-material-content {
padding: 20px;
}
.gitee-tree-material-top {
display: flex;
gap: 20px;
align-items: flex-start;
margin-bottom: 16px;
}
.gitee-tree-material-info {
flex: 1;
}
.gitee-tree-material-title {
font-size: 1.5rem;
font-weight: 400;
color: #202124;
margin-bottom: 8px;
display: flex;
align-items: center;
}
.gitee-tree-material-desc {
color: #5f6368;
line-height: 1.5;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
}
.gitee-tree-material-divider {
height: 1px;
background: #e8eaed;
margin: 16px 0;
}
.gitee-tree-material-stats {
display: flex;
justify-content: space-between;
align-items: center;
color: #5f6368;
}
.gitee-tree-material-stats-left {
display: flex;
gap: 20px;
}
.gitee-tree-modern-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 16px;
color: white;
overflow: hidden;
box-shadow: 0 10px 30px rgba(102, 126, 234, 0.3);
width: 400px;
max-width: 90%;
margin: 0 auto;
}
.gitee-tree-modern-content {
padding: 20px 28px;
}
.gitee-tree-modern-title {
font-size: 1.5rem;
font-weight: 600;
margin-bottom: 8px;
display: flex;
align-items: center;
}
.gitee-tree-modern-desc {
opacity: 0.9;
margin-bottom: 20px;
line-height: 1.5;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
}
.gitee-tree-modern-footer {
display: flex;
justify-content: space-between;
align-items: flex-end;
}
.gitee-tree-modern-stats {
display: flex;
flex-direction: column;
gap: 8px;
}
.gitee-tree-modern-stat {
opacity: 0.9;
font-size: 0.9rem;
}
.gitee-tree-qr-container {
background: white;
padding: 8px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.gitee-tree-mac-tree-container {
background: linear-gradient(145deg, #2d3748, #1a202c);
border-radius: 16px;
overflow: hidden;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.3);
width: 900px;
color: #e2e8f0;
}
.gitee-tree-mac-tree-header {
background: linear-gradient(145deg, #4a5568, #2d3748);
padding: 12px 16px;
display: flex;
align-items: center;
gap: 8px;
}
.gitee-tree-mac-tree-content {
padding: 20px;
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
line-height: 1.6;
white-space: pre;
overflow: visible;
font-size: 14px;
}
.gitee-tree-token-status {
margin-top: 8px;
color: #28a745;
font-size: 0.875rem;
display: none;
}
.gitee-tree-token-status.active {
display: block;
}
.fas, .far, .fab {
display: inline-block;
font-style: normal;
font-variant: normal;
text-rendering: auto;
line-height: 1;
}
`);
function createTreeButton() {
const treeButton = document.createElement('button');
treeButton.className = 'gitee-tree-btn gitee-tree-btn-fixed';
treeButton.innerHTML = '<i class="fas fa-tree"></i> GiteeTree';
treeButton.title = '生成Gitee项目目录树';
document.body.appendChild(treeButton);
treeButton.addEventListener('click', openTreeModal);
}
function createTreeModal() {
const modal = document.createElement('div');
modal.className = 'gitee-tree-modal';
modal.id = 'giteeTreeModal';
const modalContent = document.createElement('div');
modalContent.className = 'gitee-tree-modal-content';
const modalHeader = document.createElement('div');
modalHeader.className = 'gitee-tree-modal-header';
const modalTitle = document.createElement('h2');
modalTitle.className = 'gitee-tree-modal-title';
modalTitle.innerHTML = `
<svg class="gitee-tree-logo" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg">
<path d="M512 1024C229.2224 1024 0 794.7776 0 512S229.2224 0 512 0s512 229.2224 512 512-229.2224 512-512 512z m259.1488-568.8832H480.4096a25.2928 25.2928 0 0 0-25.2928 25.2928l-0.0256 63.2064c0 13.952 11.3152 25.2928 25.2672 25.2928h177.024c13.9776 0 25.2928 11.3152 25.2928 25.2672v12.6464a75.8528 75.8528 0 0 1-75.8528 75.8528H366.592a25.2928 25.2928 0 0 1-25.2672-25.2928v-240.1792a75.8528 75.8528 0 0 1 75.8272-75.8528h353.9456a25.2928 25.2928 0 0 0 25.2672-25.2928l0.0768-63.2064a25.2928 25.2928 0 0 0-25.2672-25.2928H417.152a189.6192 189.6192 0 0 0-189.6192 189.6448v353.9456c0 13.9776 11.3152 25.2928 25.2928 25.2928h372.9408a170.6496 170.6496 0 0 0 170.6496-170.6496v-145.408a25.2928 25.2928 0 0 0-25.2928-25.2672z" fill="currentColor"></path>
</svg>
GiteeTree - Gitee目录树生成与卡片化分享
`;
const closeButton = document.createElement('span');
closeButton.className = 'gitee-tree-close';
closeButton.innerHTML = '×';
closeButton.addEventListener('click', closeTreeModal);
modalHeader.appendChild(modalTitle);
modalHeader.appendChild(closeButton);
const modalBody = document.createElement('div');
modalBody.className = 'gitee-tree-modal-body';
modalBody.innerHTML = `
<div class="gitee-tree-section">
<details>
<summary class="cursor-pointer flex justify-between items-center text-lg font-semibold text-gray-700 hover:text-gray-900 transition duration-200">
<span><i class="fas fa-key mr-2"></i>API 访问令牌设置</span>
<i class="fas fa-chevron-down transform transition-transform duration-200"></i>
</summary>
<div class="mt-4 space-y-4 pt-4 border-t border-gray-100">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Gitee 个人访问令牌</label>
<div class="flex space-x-2">
<input type="password" id="giteeTreeTokenInput" placeholder="输入你的 Gitee 个人访问令牌" class="flex-1 gitee-tree-input-field">
<button id="giteeTreeSaveTokenBtn" class="gitee-tree-btn-secondary">保存</button>
</div>
</div>
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
<p class="text-sm text-blue-700">
<i class="fas fa-info-circle mr-2"></i>
<strong>如何获取访问令牌:</strong><br>
请访问 <a href="https://gitee.com/profile/personal_access_tokens" target="_blank" class="font-semibold underline hover:text-blue-800">Gitee 个人访问令牌页面 <i class="fas fa-external-link-alt text-xs"></i></a> 生成新令牌,确保勾选 <code class="bg-blue-100 px-1 rounded">user_info</code> 和 <code class="bg-blue-100 px-1 rounded">projects</code> 权限。
</p>
</div>
<div id="giteeTreeTokenStatus" class="gitee-tree-token-status">
<div class="flex items-center space-x-2 text-green-600">
<i class="fas fa-check-circle"></i>
<span class="text-sm">访问令牌已保存</span>
</div>
</div>
</div>
</details>
</div>
<div class="gitee-tree-section">
<h2 class="gitee-tree-section-title">
<i class="fas fa-tools mr-2"></i>项目地址
</h2>
<div class="space-y-4">
<input type="text" id="giteeTreeProjectInput" placeholder="例如:gitee.com/owner/repo 或 owner/repo" class="gitee-tree-input-field">
<div class="gitee-tree-advanced-options">
<div class="gitee-tree-advanced-options-title">
<i class="fas fa-cog mr-2"></i>高级选项
</div>
<div class="gitee-tree-advanced-options-content">
<div class="gitee-tree-option-group">
<label class="gitee-tree-option-label">目录深度</label>
<select id="giteeTreeDepthSelect" class="gitee-tree-input-field">
<option value="1">1层(仅根目录)</option>
<option value="2">2层</option>
<option value="3">3层</option>
<option value="4">4层</option>
<option value="5">5层</option>
<option value="0">全部(无限制)</option>
</select>
</div>
<div class="gitee-tree-option-group">
<label class="gitee-tree-option-label">显示内容</label>
<select id="giteeTreeViewTypeSelect" class="gitee-tree-input-field">
<option value="all">完整视图(文件和文件夹)</option>
<option value="folders">仅文件夹</option>
</select>
</div>
</div>
</div>
<div class="gitee-tree-flex">
<button id="giteeTreeGenerateDirBtn" class="gitee-tree-btn-primary flex items-center justify-center space-x-2 disabled:opacity-50 disabled:cursor-not-allowed" disabled>
<i class="fas fa-folder-tree"></i>
<span>生成目录树</span>
</button>
<button id="giteeTreeGenerateCardBtn" class="gitee-tree-btn-primary flex items-center justify-center space-x-2 disabled:opacity-50 disabled:cursor-not-allowed" disabled>
<i class="fas fa-id-card"></i>
<span>生成分享卡片</span>
</button>
</div>
</div>
<div id="giteeTreeDirLoadingSection" class="gitee-tree-loader-container" style="display: none;">
<div class="gitee-tree-loader">
<div class="gitee-tree-loader-circle"></div>
<div class="gitee-tree-loader-circle"></div>
<div class="gitee-tree-loader-circle"></div>
</div>
<div class="gitee-tree-loader-text">
正在获取项目目录结构
<div class="gitee-tree-loader-dots">
<div class="gitee-tree-loader-dot"></div>
<div class="gitee-tree-loader-dot"></div>
<div class="gitee-tree-loader-dot"></div>
</div>
</div>
</div>
<div id="giteeTreeDirResultSection" style="display: none;">
<div class="gitee-tree-result-container">
<div class="gitee-tree-flex justify-between items-start mb-4 gap-4">
<h3 class="text-lg font-semibold text-gray-700">
<i class="fas fa-folder-tree mr-2"></i>项目目录树
</h3>
<div class="gitee-tree-flex">
<button id="giteeTreeCopyTextBtn" class="gitee-tree-btn-secondary">
<i class="fas fa-copy mr-1"></i>复制文本
</button>
<button id="giteeTreeCopyMarkdownBtn" class="gitee-tree-btn-secondary">
<i class="fas fa-code mr-1"></i>复制Markdown
</button>
<button id="giteeTreeDownloadImageBtn" class="gitee-tree-btn-secondary">
<i class="fas fa-image mr-1"></i>导出图片
</button>
<div class="gitee-tree-dropdown">
<button id="giteeTreeDownloadScriptBtn" class="gitee-tree-btn-secondary">
<i class="fas fa-file-code mr-1"></i>下载脚本 <i class="fas fa-caret-down ml-1"></i>
</button>
<div class="gitee-tree-dropdown-content">
<a id="giteeTreeDownloadBatBtn" href="#">Windows (.bat)</a>
<a id="giteeTreeDownloadShBtn" href="#">Linux/Mac (.sh)</a>
</div>
</div>
</div>
</div>
<div id="giteeTreeDirectoryTreeContainer" class="gitee-tree-directory-tree-container"></div>
</div>
</div>
<div id="giteeTreeCardLoadingSection" class="gitee-tree-loader-container" style="display: none;">
<div class="gitee-tree-loader">
<div class="gitee-tree-loader-circle"></div>
<div class="gitee-tree-loader-circle"></div>
<div class="gitee-tree-loader-circle"></div>
</div>
<div class="gitee-tree-loader-text">
正在生成项目分享卡片
<div class="gitee-tree-loader-dots">
<div class="gitee-tree-loader-dot"></div>
<div class="gitee-tree-loader-dot"></div>
<div class="gitee-tree-loader-dot"></div>
</div>
</div>
</div>
<div id="giteeTreeCardResultSection" style="display: none;">
<div class="gitee-tree-result-container">
<div class="gitee-tree-flex justify-between items-start mb-4 gap-4">
<h3 class="text-lg font-semibold text-gray-700">
<i class="fas fa-id-card mr-2"></i>项目分享卡片
</h3>
<div class="gitee-tree-flex">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">卡片风格</label>
<select id="giteeTreeCardStyleSelect" class="gitee-tree-input-field text-sm">
<option value="mac">macOS 风格</option>
<option value="gitee">Gitee 风格</option>
<option value="material">Material 风格</option>
<option value="modern">现代风格</option>
</select>
</div>
<button id="giteeTreeDownloadCardBtn" class="gitee-tree-btn-secondary self-end">
<i class="fas fa-download mr-1"></i>下载卡片
</button>
</div>
</div>
<div class="flex justify-center">
<div id="giteeTreeShareCardContainer" class="w-full max-w-md"></div>
</div>
</div>
</div>
<div id="giteeTreeErrorSection" class="gitee-tree-error-section" style="display: none;">
<div class="flex items-center">
<i class="fas fa-exclamation-circle text-red-600 text-xl mr-3"></i>
<div>
<h3 class="text-lg font-semibold text-red-800">获取失败</h3>
<p class="text-red-600 mt-1" id="giteeTreeErrorMessage"></p>
</div>
</div>
</div>
</div>
`;
modalContent.appendChild(modalHeader);
modalContent.appendChild(modalBody);
modal.appendChild(modalContent);
document.body.appendChild(modal);
initializeEventListeners();
}
function initializeEventListeners() {
document.getElementById('giteeTreeSaveTokenBtn').addEventListener('click', () => {
const token = document.getElementById('giteeTreeTokenInput').value.trim();
if (token) {
localStorage.setItem('gitee_token', token);
document.getElementById('giteeTreeTokenStatus').classList.add('active');
document.getElementById('giteeTreeGenerateDirBtn').disabled = false;
document.getElementById('giteeTreeGenerateCardBtn').disabled = false;
}
});
document.getElementById('giteeTreeGenerateDirBtn').addEventListener('click', async () => {
const input = document.getElementById('giteeTreeProjectInput').value.trim();
if (!input) {
showError('请输入项目地址或路径');
return;
}
const depth = parseInt(document.getElementById('giteeTreeDepthSelect').value);
const viewType = document.getElementById('giteeTreeViewTypeSelect').value;
try {
const { owner, repo } = parseProjectPath(input);
document.getElementById('giteeTreeDirLoadingSection').style.display = 'flex';
document.getElementById('giteeTreeDirResultSection').style.display = 'none';
document.getElementById('giteeTreeErrorSection').style.display = 'none';
const [projectInfo, directory] = await Promise.all([
getProjectInfo(owner, repo),
getProjectDirectoryRecursive(owner, repo, '', 0, depth)
]);
currentProjectInfo = projectInfo;
currentDirectoryItems = directory;
fullDirectoryTree = `${projectInfo.name}\n${generateDirectoryTreeText(directory, '', 'all')}`;
currentDirectoryTree = `${projectInfo.name}\n${generateDirectoryTreeText(directory, '', viewType)}`;
document.getElementById('giteeTreeDirectoryTreeContainer').innerHTML = `<pre>${currentDirectoryTree}</pre>`;
document.getElementById('giteeTreeDirLoadingSection').style.display = 'none';
document.getElementById('giteeTreeDirResultSection').style.display = 'block';
} catch (error) {
showError(error.message);
}
});
document.getElementById('giteeTreeGenerateCardBtn').addEventListener('click', async () => {
const input = document.getElementById('giteeTreeProjectInput').value.trim();
if (!input) {
showError('请输入项目地址或路径');
return;
}
try {
const { owner, repo } = parseProjectPath(input);
document.getElementById('giteeTreeCardLoadingSection').style.display = 'flex';
document.getElementById('giteeTreeCardResultSection').style.display = 'none';
document.getElementById('giteeTreeErrorSection').style.display = 'none';
currentProjectInfo = await getProjectInfo(owner, repo);
const container = document.getElementById('giteeTreeShareCardContainer');
container.innerHTML = '';
const card = generateShareCard(currentProjectInfo, selectedCardStyle);
container.appendChild(card);
document.getElementById('giteeTreeCardLoadingSection').style.display = 'none';
document.getElementById('giteeTreeCardResultSection').style.display = 'block';
} catch (error) {
showError(error.message);
}
});
document.getElementById('giteeTreeCopyTextBtn').addEventListener('click', () => {
if (currentDirectoryTree) {
GM_setClipboard(currentDirectoryTree);
showButtonFeedback(document.getElementById('giteeTreeCopyTextBtn'));
}
});
document.getElementById('giteeTreeCopyMarkdownBtn').addEventListener('click', () => {
if (currentDirectoryTree) {
const markdown = `\`\`\`\n${currentDirectoryTree}\`\`\``;
GM_setClipboard(markdown);
showButtonFeedback(document.getElementById('giteeTreeCopyMarkdownBtn'));
}
});
document.getElementById('giteeTreeDownloadImageBtn').addEventListener('click', async () => {
if (!fullDirectoryTree || !currentProjectInfo) return;
const tempContainer = document.createElement('div');
tempContainer.style.position = 'absolute';
tempContainer.style.left = '-9999px';
const macTreeContainer = document.createElement('div');
macTreeContainer.className = 'gitee-tree-mac-tree-container';
const macHeader = document.createElement('div');
macHeader.className = 'gitee-tree-mac-tree-header';
macHeader.innerHTML = `
<div class="gitee-tree-mac-button gitee-tree-mac-button-red"></div>
<div class="gitee-tree-mac-button gitee-tree-mac-button-yellow"></div>
<div class="gitee-tree-mac-button gitee-tree-mac-button-green"></div>
<div style="margin-left: 10px; display: flex; align-items: center;">
<svg class="gitee-tree-logo" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg">
<path d="M512 1024C229.2224 1024 0 794.7776 0 512S229.2224 0 512 0s512 229.2224 512 512-229.2224 512-512 512z m259.1488-568.8832H480.4096a25.2928 25.2928 0 0 0-25.2928 25.2928l-0.0256 63.2064c0 13.952 11.3152 25.2928 25.2672 25.2928h177.024c13.9776 0 25.2928 11.3152 25.2928 25.2672v12.6464a75.8528 75.8528 0 0 1-75.8528 75.8528H366.592a25.2928 25.2928 0 0 1-25.2672-25.2928v-240.1792a75.8528 75.8528 0 0 1 75.8272-75.8528h353.9456a25.2928 25.2928 0 0 0 25.2672-25.2928l0.0768-63.2064a25.2928 25.2928 0 0 0-25.2672-25.2928H417.152a189.6192 189.6192 0 0 0-189.6192 189.6448v353.9456c0 13.9776 11.3152 25.2928 25.2928 25.2928h372.9408a170.6496 170.6496 0 0 0 170.6496-170.6496v-145.408a25.2928 25.2928 0 0 0-25.2928-25.2672z" fill="currentColor"></path>
</svg>
<span style="color: #f7fafc; font-weight: 600;">${currentProjectInfo.full_name}</span>
</div>
`;
const macContent = document.createElement('div');
macContent.className = 'gitee-tree-mac-tree-content';
macContent.textContent = fullDirectoryTree;
macTreeContainer.appendChild(macHeader);
macTreeContainer.appendChild(macContent);
tempContainer.appendChild(macTreeContainer);
document.body.appendChild(tempContainer);
try {
const contentHeight = macContent.scrollHeight;
macTreeContainer.style.height = (contentHeight + 80) + 'px'; // 80px是头部高度
const canvas = await html2canvas(macTreeContainer, {
backgroundColor: null,
scale: 2,
height: contentHeight + 80,
windowHeight: contentHeight + 80
});
const link = document.createElement('a');
link.download = `${currentProjectInfo.full_name.replace('/', '-')}-directory.png`;
link.href = canvas.toDataURL('image/png');
link.click();
showButtonFeedback(document.getElementById('giteeTreeDownloadImageBtn'), '已下载!');
} catch (error) {
console.error('生成图片失败:', error);
} finally {
document.body.removeChild(tempContainer);
}
});
document.getElementById('giteeTreeDownloadBatBtn').addEventListener('click', (e) => {
e.preventDefault();
if (!currentDirectoryItems || !currentProjectInfo) return;
let scriptContent = `@echo off\necho Creating directory structure for ${currentProjectInfo.full_name}\necho.\n`;
function generateBatScript(items, path = '') {
items.forEach(item => {
const itemPath = path ? `${path}\\${item.name}` : item.name;
if (item.type === 'dir') {
scriptContent += `mkdir "${itemPath}"\n`;
if (item.children && item.children.length > 0) {
generateBatScript(item.children, itemPath);
}
} else {
scriptContent += `echo. > "${itemPath}"\n`;
}
});
}
generateBatScript(currentDirectoryItems);
scriptContent += `\necho Directory structure created successfully!\npause`;
const blob = new Blob([scriptContent], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.download = `${currentProjectInfo.full_name.replace('/', '-')}-structure.bat`;
link.href = url;
link.click();
URL.revokeObjectURL(url);
});
document.getElementById('giteeTreeDownloadShBtn').addEventListener('click', (e) => {
e.preventDefault();
if (!currentDirectoryItems || !currentProjectInfo) return;
let scriptContent = `#!/bin/bash\necho "Creating directory structure for ${currentProjectInfo.full_name}"\necho\n`;
function generateShScript(items, path = '') {
items.forEach(item => {
const itemPath = path ? `${path}/${item.name}` : item.name;
if (item.type === 'dir') {
scriptContent += `mkdir -p "${itemPath}"\n`;
if (item.children && item.children.length > 0) {
generateShScript(item.children, itemPath);
}
} else {
scriptContent += `touch "${itemPath}"\n`;
}
});
}
generateShScript(currentDirectoryItems);
scriptContent += `\necho "Directory structure created successfully!"`;
const blob = new Blob([scriptContent], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.download = `${currentProjectInfo.full_name.replace('/', '-')}-structure.sh`;
link.href = url;
link.click();
URL.revokeObjectURL(url);
});
document.getElementById('giteeTreeDownloadCardBtn').addEventListener('click', async () => {
if (!currentProjectInfo) return;
try {
const cardElement = document.querySelector('#giteeTreeShareCardContainer > div');
const canvas = await html2canvas(cardElement, {
backgroundColor: null,
scale: 2
});
const link = document.createElement('a');
link.download = `${currentProjectInfo.full_name.replace('/', '-')}-card.png`;
link.href = canvas.toDataURL('image/png');
link.click();
} catch (error) {
console.error('生成卡片图片失败:', error);
}
});
document.getElementById('giteeTreeCardStyleSelect').addEventListener('change', (e) => {
selectedCardStyle = e.target.value;
if (currentProjectInfo) {
const container = document.getElementById('giteeTreeShareCardContainer');
container.innerHTML = '';
const card = generateShareCard(currentProjectInfo, selectedCardStyle);
container.appendChild(card);
}
});
document.getElementById('giteeTreeProjectInput').addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
document.getElementById('giteeTreeGenerateDirBtn').click();
}
});
document.querySelector('details').addEventListener('toggle', function() {
const icon = this.querySelector('i.fa-chevron-down');
if (this.open) {
icon.style.transform = 'rotate(180deg)';
} else {
icon.style.transform = 'rotate(0deg)';
}
});
}
function openTreeModal() {
const modal = document.getElementById('giteeTreeModal');
if (!modal) {
createTreeModal();
setTimeout(() => {
document.getElementById('giteeTreeModal').style.display = 'block';
const savedToken = localStorage.getItem('gitee_token');
if (savedToken) {
document.getElementById('giteeTreeTokenInput').value = savedToken;
document.getElementById('giteeTreeTokenStatus').classList.add('active');
document.getElementById('giteeTreeGenerateDirBtn').disabled = false;
document.getElementById('giteeTreeGenerateCardBtn').disabled = false;
}
const pathParts = window.location.pathname.split('/').filter(part => part);
if (pathParts.length >= 2) {
const owner = pathParts[0];
const repo = pathParts[1];
document.getElementById('giteeTreeProjectInput').value = `${owner}/${repo}`;
}
}, 100);
} else {
modal.style.display = 'block';
}
}
function closeTreeModal() {
document.getElementById('giteeTreeModal').style.display = 'none';
}
function showError(message) {
document.getElementById('giteeTreeErrorMessage').textContent = message;
document.getElementById('giteeTreeDirLoadingSection').style.display = 'none';
document.getElementById('giteeTreeCardLoadingSection').style.display = 'none';
document.getElementById('giteeTreeDirResultSection').style.display = 'none';
document.getElementById('giteeTreeCardResultSection').style.display = 'none';
document.getElementById('giteeTreeErrorSection').style.display = 'block';
}
function showButtonFeedback(button, text = '已复制!') {
const originalContent = button.innerHTML;
button.innerHTML = `<i class="fas fa-check mr-1"></i>${text}`;
button.disabled = true;
setTimeout(() => {
button.innerHTML = originalContent;
button.disabled = false;
}, 1500);
}
function parseProjectPath(input) {
input = input.replace(/^https?:\/\//, '');
input = input.replace(/^gitee\.com\//, '');
input = input.replace(/\/$/, '');
const parts = input.split('/');
if (parts.length !== 2) {
throw new Error('请输入正确的项目路径,格式:owner/repo');
}
return { owner: parts[0], repo: parts[1] };
}
async function getProjectInfo(owner, repo) {
const savedToken = localStorage.getItem('gitee_token');
if (!savedToken) {
throw new Error('请先设置 Gitee 访问令牌');
}
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
url: `https://gitee.com/api/v5/repos/${owner}/${repo}`,
headers: {
'Authorization': `token ${savedToken}`,
'Accept': 'application/json'
},
onload: function(response) {
if (response.status === 200) {
resolve(JSON.parse(response.responseText));
} else {
if (response.status === 401) {
reject(new Error('访问令牌无效或已过期,请重新设置'));
} else if (response.status === 403) {
reject(new Error('访问令牌权限不足,请确保已勾选相应权限'));
} else if (response.status === 404) {
reject(new Error('项目不存在或无权访问'));
} else {
reject(new Error(`获取项目信息失败 (${response.status})`));
}
}
},
onerror: function(error) {
reject(new Error('网络请求失败'));
}
});
});
}
async function getProjectDirectoryRecursive(owner, repo, path = '', depth = 0, maxDepth = 0) {
if (maxDepth > 0 && depth >= maxDepth) {
return [];
}
const savedToken = localStorage.getItem('gitee_token');
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
url: `https://gitee.com/api/v5/repos/${owner}/${repo}/contents/${path}`,
headers: {
'Authorization': `token ${savedToken}`,
'Accept': 'application/json'
},
onload: async function(response) {
if (response.status === 200) {
const items = JSON.parse(response.responseText);
for (const item of items) {
if (item.type === 'dir') {
item.children = await getProjectDirectoryRecursive(owner, repo, item.path, depth + 1, maxDepth);
}
}
resolve(items);
} else {
resolve([]);
}
},
onerror: function(error) {
reject(new Error('网络请求失败'));
}
});
});
}
function generateDirectoryTreeText(items, prefix = '', viewType = 'all') {
let text = '';
items.forEach((item, index) => {
if (viewType === 'folders' && item.type !== 'dir') {
return;
}
const isLast = index === items.length - 1;
const connector = isLast ? '└── ' : '├── ';
const icon = item.type === 'dir' ? '📁' : '📄';
text += `${prefix}${connector}${icon} ${item.name}\n`;
if (item.type === 'dir' && item.children && item.children.length > 0) {
const newPrefix = prefix + (isLast ? ' ' : '│ ');
text += generateDirectoryTreeText(item.children, newPrefix, viewType);
}
});
return text;
}
function generateShareCard(projectInfo, style) {
const cardDiv = document.createElement('div');
const giteeIcon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
giteeIcon.setAttribute('viewBox', '0 0 1024 1024');
giteeIcon.setAttribute('width', '16');
giteeIcon.setAttribute('height', '16');
giteeIcon.classList.add('gitee-tree-logo');
const giteePath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
giteePath.setAttribute('d', 'M512 1024C229.2224 1024 0 794.7776 0 512S229.2224 0 512 0s512 229.2224 512 512-229.2224 512-512 512z m259.1488-568.8832H480.4096a25.2928 25.2928 0 0 0-25.2928 25.2928l-0.0256 63.2064c0 13.952 11.3152 25.2928 25.2672 25.2928h177.024c13.9776 0 25.2928 11.3152 25.2928 25.2672v12.6464a75.8528 75.8528 0 0 1-75.8528 75.8528H366.592a25.2928 25.2928 0 0 1-25.2672-25.2928v-240.1792a75.8528 75.8528 0 0 1 75.8272-75.8528h353.9456a25.2928 25.2928 0 0 0 25.2672-25.2928l0.0768-63.2064a25.2928 25.2928 0 0 0-25.2672-25.2928H417.152a189.6192 189.6192 0 0 0-189.6192 189.6448v353.9456c0 13.9776 11.3152 25.2928 25.2928 25.2928h372.9408a170.6496 170.6496 0 0 0 170.6496-170.6496v-145.408a25.2928 25.2928 0 0 0-25.2928-25.2672z');
giteePath.setAttribute('fill', 'currentColor');
giteeIcon.appendChild(giteePath);
switch(style) {
case 'mac':
cardDiv.className = 'gitee-tree-mac-card';
cardDiv.innerHTML = `
<div class="gitee-tree-mac-header">
<div class="gitee-tree-mac-button gitee-tree-mac-button-red"></div>
<div class="gitee-tree-mac-button gitee-tree-mac-button-yellow"></div>
<div class="gitee-tree-mac-button gitee-tree-mac-button-green"></div>
</div>
<div class="gitee-tree-mac-content">
<div class="gitee-tree-mac-info">
<h3 class="gitee-tree-mac-title"></h3>
<p class="gitee-tree-mac-desc">${projectInfo.description || '暂无描述'}</p>
<div class="gitee-tree-mac-stats">
<span><i class="fas fa-star text-yellow-400 mr-1"></i>${projectInfo.stargazers_count || 0}</span>
<span><i class="fas fa-code-branch text-green-400 mr-1"></i>${projectInfo.forks_count || 0}</span>
<span><i class="fas fa-circle text-blue-400 mr-1 text-xs"></i>${projectInfo.language || '未知'}</span>
</div>
</div>
<div class="gitee-tree-qr-container">
<div id="giteeTreeQrcode"></div>
</div>
</div>
`;
const macTitle = cardDiv.querySelector('.gitee-tree-mac-title');
macTitle.appendChild(giteeIcon.cloneNode(true));
macTitle.appendChild(document.createTextNode(projectInfo.full_name));
break;
case 'gitee':
cardDiv.className = 'gitee-tree-gitee-card';
cardDiv.innerHTML = `
<div class="gitee-tree-gitee-header">
<h3 class="gitee-tree-gitee-title"></h3>
<span class="gitee-tree-gitee-badge">Public</span>
</div>
<div class="gitee-tree-gitee-content">
<div class="gitee-tree-gitee-info">
<p class="gitee-tree-gitee-desc">${projectInfo.description || '暂无描述'}</p>
<div class="gitee-tree-gitee-stats">
<span><i class="fas fa-star mr-1"></i>${projectInfo.stargazers_count || 0}</span>
<span><i class="fas fa-code-branch mr-1"></i>${projectInfo.forks_count || 0}</span>
<span><i class="fas fa-circle mr-1 text-xs"></i>${projectInfo.language || '未知'}</span>
</div>
</div>
<div class="gitee-tree-qr-container">
<div id="giteeTreeQrcode"></div>
</div>
</div>
`;
const giteeTitle = cardDiv.querySelector('.gitee-tree-gitee-title');
giteeTitle.appendChild(giteeIcon.cloneNode(true));
giteeTitle.appendChild(document.createTextNode(projectInfo.full_name));
break;
case 'material':
cardDiv.className = 'gitee-tree-material-card';
cardDiv.innerHTML = `
<div class="gitee-tree-material-content">
<div class="gitee-tree-material-top">
<div class="gitee-tree-material-info">
<h3 class="gitee-tree-material-title"></h3>
<p class="gitee-tree-material-desc">${projectInfo.description || '暂无描述'}</p>
</div>
<div class="gitee-tree-qr-container">
<div id="giteeTreeQrcode"></div>
</div>
</div>
<div class="gitee-tree-material-divider"></div>
<div class="gitee-tree-material-stats">
<div class="gitee-tree-material-stats-left">
<span><i class="fas fa-star mr-2"></i>${projectInfo.stargazers_count || 0}</span>
<span><i class="fas fa-code-branch mr-2"></i>${projectInfo.forks_count || 0}</span>
</div>
<span><i class="fas fa-circle mr-2 text-xs"></i>${projectInfo.language || '未知'}</span>
</div>
</div>
`;
const materialTitle = cardDiv.querySelector('.gitee-tree-material-title');
materialTitle.appendChild(giteeIcon.cloneNode(true));
materialTitle.appendChild(document.createTextNode(projectInfo.full_name));
break;
case 'modern':
cardDiv.className = 'gitee-tree-modern-card';
cardDiv.innerHTML = `
<div class="gitee-tree-modern-content">
<div>
<h3 class="gitee-tree-modern-title"></h3>
<p class="gitee-tree-modern-desc">${projectInfo.description || '暂无描述'}</p>
</div>
<div class="gitee-tree-modern-footer">
<div class="gitee-tree-modern-stats">
<div class="gitee-tree-modern-stat"><i class="fas fa-star mr-2"></i>${projectInfo.stargazers_count || 0} Stars</div>
<div class="gitee-tree-modern-stat"><i class="fas fa-code-branch mr-2"></i>${projectInfo.forks_count || 0} Forks</div>
<div class="gitee-tree-modern-stat"><i class="fas fa-circle mr-2 text-xs"></i>${projectInfo.language || '未知'}</div>
</div>
<div class="gitee-tree-qr-container">
<div id="giteeTreeQrcode"></div>
</div>
</div>
</div>
`;
const modernTitle = cardDiv.querySelector('.gitee-tree-modern-title');
modernTitle.appendChild(giteeIcon.cloneNode(true));
modernTitle.appendChild(document.createTextNode(projectInfo.full_name));
break;
}
setTimeout(() => {
const qrcodeDiv = cardDiv.querySelector('#giteeTreeQrcode');
if (qrcodeDiv) {
new QRCode(qrcodeDiv, {
text: projectInfo.html_url,
width: 96,
height: 96,
colorDark: "#000000",
colorLight: "#ffffff",
correctLevel: QRCode.CorrectLevel.H
});
}
}, 100);
return cardDiv;
}
let savedToken = localStorage.getItem('gitee_token') || '';
let selectedCardStyle = 'mac';
let currentDirectoryTree = '';
let currentProjectInfo = null;
let currentDirectoryItems = [];
let fullDirectoryTree = '';
window.addEventListener('load', () => {
setTimeout(() => {
createTreeButton();
window.addEventListener('click', (e) => {
const modal = document.getElementById('giteeTreeModal');
if (e.target === modal) {
closeTreeModal();
}
});
}, 1000);
});
let lastUrl = location.href;
new MutationObserver(() => {
const url = location.href;
if (url !== lastUrl) {
lastUrl = url;
setTimeout(createTreeButton, 1000);
}
}).observe(document, { subtree: true, childList: true });
})();