// Set current year in footer document.getElementById('current-year').textContent = new Date().getFullYear(); // Default configuration let config = { server: localStorage.getItem('giteaServer') || '', port: localStorage.getItem('giteaPort') || '3000', sshPort: localStorage.getItem('giteaSSHPort') || '22', token: localStorage.getItem('giteaToken') || '' }; // DOM Elements const pluginsContainer = document.getElementById('plugins-container'); const searchInput = document.getElementById('search'); const filterButtons = document.querySelectorAll('.filter-btn'); const errorContainer = document.getElementById('error-container'); const toggleTokenBtn = document.getElementById('toggle-token'); // State let allRepos = []; let currentFilter = 'all'; // Initialize document.addEventListener('DOMContentLoaded', () => { loadConfig(); setupEventListeners(); // Toggle token visibility toggleTokenBtn.addEventListener('click', () => { const tokenInput = document.getElementById('access-token'); if (tokenInput.type === 'password') { tokenInput.type = 'text'; toggleTokenBtn.innerHTML = ''; } else { tokenInput.type = 'password'; toggleTokenBtn.innerHTML = ''; } }); // If we have a server configured, fetch repos immediately if (config.server) { fetchRepositories(); } else { // Show config panel if no server is configured document.getElementById('server-config').classList.remove('hidden'); } }); function loadConfig() { document.getElementById('server-address').value = config.server; document.getElementById('http-port').value = config.port; document.getElementById('ssh-port').value = config.sshPort; document.getElementById('access-token').value = config.token; } function setupEventListeners() { // Search functionality searchInput.addEventListener('input', debounce(filterRepositories, 300)); // Filter buttons filterButtons.forEach(button => { button.addEventListener('click', () => { filterButtons.forEach(btn => { btn.classList.remove('bg-indigo-600', 'text-white'); btn.classList.add('bg-dark-700', 'hover:bg-dark-600', 'text-white'); }); button.classList.remove('bg-dark-700', 'hover:bg-dark-600'); button.classList.add('bg-indigo-600', 'text-white'); currentFilter = button.dataset.filter; filterRepositories(); }); }); // Server configuration panel document.getElementById('show-config').addEventListener('click', () => { document.getElementById('server-config').classList.remove('hidden'); loadConfig(); }); document.getElementById('cancel-config').addEventListener('click', () => { document.getElementById('server-config').classList.add('hidden'); }); document.getElementById('save-config').addEventListener('click', () => { // Save new configuration config = { server: document.getElementById('server-address').value.trim(), port: document.getElementById('http-port').value.trim(), sshPort: document.getElementById('ssh-port').value.trim(), token: document.getElementById('access-token').value.trim() }; // Validate required fields if (!config.server) { showError('Server address is required'); return; } // Save to localStorage localStorage.setItem('giteaServer', config.server); localStorage.setItem('giteaPort', config.port); localStorage.setItem('giteaSSHPort', config.sshPort); localStorage.setItem('giteaToken', config.token); // Hide config panel document.getElementById('server-config').classList.add('hidden'); // Reload repositories pluginsContainer.innerHTML = `

Loading repositories...

`; errorContainer.innerHTML = ''; fetchRepositories(); }); } async function fetchRepositories() { if (!config.server) { showError('Server address is not configured. Please configure your Gitea server first.'); return; } try { const apiBase = `${window.location.protocol}//${config.server}:${config.port}/api/v1`; const fetchOptions = { method: 'GET', headers: { 'Accept': 'application/json', ...(config.token ? { 'Authorization': `token ${config.token}` } : {}) } }; // First get all repositories (paginated) let allRepositories = []; let page = 1; let hasMore = true; while (hasMore) { let reposUrl = `${apiBase}/repos/search?limit=50&page=${page}`; if (config.token) { reposUrl += '&mode=all'; // Include private repos if authenticated } const response = await fetch(reposUrl, fetchOptions); if (!response.ok) { throw new Error(`API request failed: ${response.status} ${response.statusText}`); } const data = await response.json(); if (!data.ok || !Array.isArray(data.data)) { throw new Error('Invalid response format from Gitea API'); } if (data.data.length === 0) { hasMore = false; } else { allRepositories = allRepositories.concat(data.data); page++; } } if (allRepositories.length === 0) { throw new Error('No repositories found on this Gitea instance.'); } // Process repositories in batches to avoid overwhelming the browser const batchSize = 5; allRepos = []; for (let i = 0; i < allRepositories.length; i += batchSize) { const batch = allRepositories.slice(i, i + batchSize); const processedBatch = await Promise.all(batch.map(repo => processRepository(repo, apiBase, fetchOptions))); // Filter out null values and add to allRepos allRepos = allRepos.concat(processedBatch.filter(repo => repo !== null)); // Update UI with progress renderProgress(allRepositories.length, allRepos.length); } if (allRepos.length === 0) { throw new Error('No Minecraft/Paper repositories found. Try adjusting your search criteria or check your access token.'); } renderRepositories(allRepos); } catch (error) { console.error('Error fetching repositories:', error); showError(`Failed to fetch repositories: ${error.message}`); } } async function processRepository(repo, apiBase, fetchOptions) { try { // Check if this repository has "minecraft" or "paper" as a topic const hasMinecraftTopic = await checkRepositoryTopics(repo, ['minecraft', 'paper'], apiBase, fetchOptions); if (!hasMinecraftTopic) return null; // Get README for description let description = repo.description || 'No description available'; try { const readmeContent = await getReadmeContent(repo, apiBase, fetchOptions); if (readmeContent) { description = readmeContent; } } catch (error) { console.error(`Error getting README for ${repo.full_name}:`, error); } // Get build status if Actions are enabled let buildStatus = null; let latestArtifactUrl = null; let buildTimestamp = null; // Only check actions if we have a token (required for private repos) if (config.token) { try { const runsUrl = `${apiBase}/repos/${repo.owner.login || repo.owner.username}/${repo.name}/actions/runs`; const runsResponse = await fetch(runsUrl, fetchOptions); if (runsResponse.ok) { const runsData = await runsResponse.json(); if (runsData.workflow_runs && runsData.workflow_runs.length > 0) { const latestRun = runsData.workflow_runs[0]; buildStatus = latestRun.status === 'completed' ? (latestRun.conclusion || null) : latestRun.status; buildTimestamp = latestRun.updated_at || latestRun.created_at; if (buildStatus === 'success') { const artifactsUrl = `${apiBase}/repos/${repo.owner.login || repo.owner.username}/${repo.name}/actions/runs/${latestRun.id}/artifacts`; const artifactsResponse = await fetch(artifactsUrl, fetchOptions); if (artifactsResponse.ok) { const artifactsData = await artifactsResponse.json(); if (artifactsData.artifacts && artifactsData.artifacts.length > 0) { latestArtifactUrl = `${apiBase}/repos/${repo.owner.login || repo.owner.username}/${repo.name}/actions/artifacts/${artifactsData.artifacts[0].id}/zip`; } } } } } } catch (error) { console.error(`Error checking workflows for ${repo.name}:`, error); } } return { ...repo, buildStatus, latestArtifactUrl, buildTimestamp, description, cloneUrl: `ssh://git@${config.server}:${config.sshPort}/${repo.owner.login || repo.owner.username}/${repo.name}.git`, httpCloneUrl: `${window.location.protocol}//${config.server}:${config.port}/${repo.owner.login || repo.owner.username}/${repo.name}.git`, html_url: repo.html_url || `${window.location.protocol}//${config.server}:${config.port}/${repo.owner.login || repo.owner.username}/${repo.name}` }; } catch (error) { console.error(`Error processing repository ${repo.name}:`, error); return null; } } async function checkRepositoryTopics(repo, topics, apiBase, fetchOptions) { try { const topicsUrl = `${apiBase}/repos/${repo.owner.login || repo.owner.username}/${repo.name}/topics`; const response = await fetch(topicsUrl, fetchOptions); if (!response.ok) { return false; } const data = await response.json(); if (!data.topics || !Array.isArray(data.topics)) { return false; } // Check if any of the required topics are present const hasTopic = topics.some(topic => data.topics.includes(topic)); // Add topics to repo object for filtering later repo.topics = data.topics; return hasTopic; } catch (error) { console.error(`Error checking topics for ${repo.name}:`, error); return false; } } async function getReadmeContent(repo, apiBase, fetchOptions) { try { const contentsUrl = `${apiBase}/repos/${repo.owner.login || repo.owner.username}/${repo.name}/contents`; const contentsResponse = await fetch(contentsUrl, fetchOptions); if (!contentsResponse.ok) return null; const contentsData = await contentsResponse.json(); const readmeItem = contentsData.find(item => item.name.toLowerCase().startsWith('readme') ); if (!readmeItem) return null; const readmeResponse = await fetch(readmeItem.url, fetchOptions); if (!readmeResponse.ok) return null; const readmeData = await readmeResponse.json(); if (!readmeData.content || readmeData.encoding !== 'base64') return null; const decodedContent = atob(readmeData.content); const firstParagraph = decodedContent.split('\n\n')[0] .replace(/^#+ .*$/m, '') .replace(/\[.*?\]\(.*?\)/g, '') // Remove markdown links .replace(/`.*?`/g, '') // Remove code tags .replace(/\*\*.*?\*\*/g, '') // Remove bold text .trim(); return firstParagraph && firstParagraph.length > 10 ? firstParagraph : null; } catch (error) { console.error(`Error getting README for ${repo.name}:`, error); return null; } } function renderProgress(total, processed) { if (processed === 0) return; const percent = Math.round((processed / total) * 100); pluginsContainer.innerHTML = `

Processing repositories... ${percent}% (${processed}/${total})

Checking for Minecraft/Paper topics and build status

`; } function renderRepositories(repositories) { pluginsContainer.innerHTML = ''; if (repositories.length === 0) { pluginsContainer.innerHTML = `

No Minecraft/Paper repositories found matching your criteria.

`; return; } repositories.forEach(repo => { const repoCard = document.createElement('div'); repoCard.className = 'glass-panel rounded-xl overflow-hidden fade-in hover:shadow-lg transition-shadow'; let statusBadge = ''; // Only show status badge if we have build status information if (repo.buildStatus) { let statusColor = 'bg-yellow-500'; let statusIcon = 'fa-question-circle'; let statusText = 'Unknown'; if (repo.buildStatus === 'success') { statusColor = 'bg-green-500'; statusIcon = 'fa-check-circle'; statusText = 'Success'; } else if (repo.buildStatus === 'failure') { statusColor = 'bg-red-500'; statusIcon = 'fa-times-circle'; statusText = 'Failed'; } else if (repo.buildStatus === 'running') { statusColor = 'bg-blue-500 badge-pulse'; statusIcon = 'fa-sync-alt fa-spin'; statusText = 'Running'; } statusBadge = ` ${statusText} `; } const lastUpdated = repo.buildTimestamp ? new Date(repo.buildTimestamp).toLocaleDateString() : new Date(repo.updated_at || repo.created_at).toLocaleDateString(); repoCard.innerHTML = `

${repo.name}

${statusBadge}

${repo.owner.login || repo.owner.username}

${repo.description}

${repo.stargazers_count || repo.stars_count || 0}
${lastUpdated}
View ${repo.latestArtifactUrl ? ` Download ` : ``}
`; pluginsContainer.appendChild(repoCard); }); } function filterRepositories() { const searchTerm = searchInput.value.toLowerCase(); const filteredRepos = allRepos.filter(repo => { const matchesSearch = repo.name.toLowerCase().includes(searchTerm) || (repo.description && repo.description.toLowerCase().includes(searchTerm)) || (repo.owner.login || repo.owner.username).toLowerCase().includes(searchTerm); const matchesFilter = (currentFilter === 'all') || (currentFilter === 'success' && repo.buildStatus === 'success') || (currentFilter === 'failure' && repo.buildStatus === 'failure') || (currentFilter === 'minecraft' && repo.topics?.includes('minecraft')) || (currentFilter === 'paper' && repo.topics?.includes('paper')); return matchesSearch && matchesFilter; }); renderRepositories(filteredRepos); } function copyCloneUrl(url) { navigator.clipboard.writeText(url) .then(() => { showToast('Clone URL copied to clipboard!', 'success'); }) .catch(err => { console.error('Error copying text: ', err); showToast('Failed to copy URL. Please copy it manually.', 'error'); }); } function showToast(message, type = 'info') { const toast = document.createElement('div'); let bgColor = 'bg-indigo-600'; if (type === 'success') bgColor = 'bg-green-600'; if (type === 'error') bgColor = 'bg-red-600'; toast.className = `fixed bottom-4 right-4 ${bgColor} text-white px-4 py-2 rounded-lg shadow-lg flex items-center gap-2 z-50`; toast.innerHTML = ` ${message} `; document.body.appendChild(toast); setTimeout(() => { toast.classList.add('opacity-0', 'transition-opacity', 'duration-300'); setTimeout(() => toast.remove(), 300); }, 3000); } function showError(message) { errorContainer.innerHTML = `

Error

${message}

  • Verify that your Gitea server (${config.server || 'not configured'}) is running
  • Check if authentication is required (you may need an access token)
  • Ensure CORS is properly configured on your Gitea server
`; pluginsContainer.innerHTML = `

Failed to load repositories.

`; } function retryFetch() { errorContainer.innerHTML = ''; pluginsContainer.innerHTML = `

Loading repositories...

`; fetchRepositories(); } // Utility function to debounce rapid events function debounce(func, wait) { let timeout; return function() { const context = this, args = arguments; clearTimeout(timeout); timeout = setTimeout(() => func.apply(context, args), wait); }; } // Make functions available globally window.copyCloneUrl = copyCloneUrl; window.retryFetch = retryFetch;