💡 Update docs

This commit is contained in:
2025-07-13 11:58:53 +08:00
parent 9f53d7421d
commit 6d8fdf62f1
5 changed files with 964 additions and 451 deletions

View File

@@ -1,211 +1,283 @@
/**
* VoidRaft - Changelog Script
* Fetches release information from GitHub API with Gitea fallback
* 从GitHub API获取发布信息支持Gitea备用源
*/
document.addEventListener('DOMContentLoaded', () => {
// Repository information
const REPOS = {
github: {
owner: 'landaiqing',
name: 'voidraft',
apiUrl: 'https://api.github.com/repos/landaiqing/voidraft/releases',
releasesUrl: 'https://github.com/landaiqing/voidraft/releases'
},
gitea: {
owner: 'landaiqing',
name: 'voidraft',
domain: 'git.landaiqing.cn',
apiUrl: 'https://git.landaiqing.cn/api/v1/repos/landaiqing/voidraft/releases',
releasesUrl: 'https://git.landaiqing.cn/landaiqing/voidraft/releases'
}
};
// Error messages with i18n support
const MESSAGES = {
loading: {
en: 'Loading releases...',
zh: '正在加载版本信息...'
},
noReleases: {
en: 'No release information found',
zh: '没有找到版本发布信息'
},
fetchError: {
en: 'Failed to load release information. Please try again later.',
zh: '无法获取版本信息,请稍后再试'
},
githubApiError: {
en: 'GitHub API returned an error status: ',
zh: 'GitHub API返回错误状态: '
},
giteaApiError: {
en: 'Gitea API returned an error status: ',
zh: 'Gitea API返回错误状态: '
},
dataSource: {
en: 'Data source: ',
zh: '数据来源: '
},
downloads: {
en: 'Downloads',
zh: '下载资源'
},
download: {
en: 'Download',
zh: '下载'
},
preRelease: {
en: 'Pre-release',
zh: '预发布'
}
};
// Element references
const elements = {
loading: document.getElementById('loading'),
changelog: document.getElementById('changelog'),
error: document.getElementById('error-message')
};
// Initialize
init();
/**
* Initialize the changelog
*/
function init() {
// Try GitHub API first
fetchReleases('github')
.catch(() => fetchReleases('gitea'))
.catch(error => {
elements.loading.style.display = 'none';
showError(MESSAGES.fetchError[getCurrentLang()]);
});
/**
* 仓库配置类
*/
class RepositoryConfig {
constructor() {
this.repos = {
github: {
owner: 'landaiqing',
name: 'voidraft',
apiUrl: 'https://api.github.com/repos/landaiqing/voidraft/releases',
releasesUrl: 'https://github.com/landaiqing/voidraft/releases'
},
gitea: {
owner: 'landaiqing',
name: 'voidraft',
domain: 'git.landaiqing.cn',
apiUrl: 'https://git.landaiqing.cn/api/v1/repos/landaiqing/voidraft/releases',
releasesUrl: 'https://git.landaiqing.cn/landaiqing/voidraft/releases'
}
};
}
/**
* Get current language
* 获取仓库配置
* @param {string} source - 'github' 或 'gitea'
*/
function getCurrentLang() {
getRepo(source) {
return this.repos[source];
}
/**
* 获取所有仓库配置
*/
getAllRepos() {
return this.repos;
}
}
/**
* 国际化消息管理类
*/
class I18nMessages {
constructor() {
this.messages = {
loading: {
en: 'Loading releases...',
zh: '正在加载版本信息...'
},
noReleases: {
en: 'No release information found',
zh: '没有找到版本发布信息'
},
fetchError: {
en: 'Failed to load release information. Please try again later.',
zh: '无法获取版本信息,请稍后再试'
},
githubApiError: {
en: 'GitHub API returned an error status: ',
zh: 'GitHub API返回错误状态: '
},
giteaApiError: {
en: 'Gitea API returned an error status: ',
zh: 'Gitea API返回错误状态: '
},
dataSource: {
en: 'Data source: ',
zh: '数据来源: '
},
downloads: {
en: 'Downloads',
zh: '下载资源'
},
download: {
en: 'Download',
zh: '下载'
},
preRelease: {
en: 'Pre-release',
zh: '预发布'
}
};
}
/**
* 获取消息
* @param {string} key - 消息键
* @param {string} lang - 语言代码
*/
getMessage(key, lang = 'en') {
return this.messages[key] && this.messages[key][lang] || this.messages[key]['en'] || '';
}
/**
* 获取当前语言
*/
getCurrentLang() {
return window.currentLang || 'en';
}
}
/**
* API客户端类
*/
class APIClient {
constructor(repositoryConfig, i18nMessages) {
this.repositoryConfig = repositoryConfig;
this.i18nMessages = i18nMessages;
}
/**
* Fetch releases from specified source
* @param {string} source - 'github' or 'gitea'
* 从指定源获取发布信息
* @param {string} source - 'github' 'gitea'
*/
async function fetchReleases(source) {
const apiUrl = REPOS[source].apiUrl;
async fetchReleases(source) {
const repo = this.repositoryConfig.getRepo(source);
const errorMessageKey = source === 'github' ? 'githubApiError' : 'giteaApiError';
// Setup timeout for GitHub
const options = {
headers: { 'Accept': 'application/json' }
};
if (source === 'github') {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout
options.signal = controller.signal;
options.headers['Accept'] = 'application/vnd.github.v3+json';
try {
const response = await fetch(apiUrl, options);
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`${MESSAGES[errorMessageKey][getCurrentLang()]}${response.status}`);
}
const releases = await response.json();
if (!releases || releases.length === 0) {
throw new Error(MESSAGES.noReleases[getCurrentLang()]);
}
// Display releases
elements.loading.style.display = 'none';
displayReleases(releases, source);
elements.changelog.style.display = 'block';
return releases;
} catch (error) {
clearTimeout(timeoutId);
throw error;
}
return this.fetchFromGitHub(repo, options, errorMessageKey);
} else {
const response = await fetch(apiUrl, options);
return this.fetchFromGitea(repo, options, errorMessageKey);
}
}
/**
* 从GitHub获取数据
* @param {Object} repo - 仓库配置
* @param {Object} options - 请求选项
* @param {string} errorMessageKey - 错误消息键
*/
async fetchFromGitHub(repo, options, errorMessageKey) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000);
options.signal = controller.signal;
options.headers['Accept'] = 'application/vnd.github.v3+json';
try {
const response = await fetch(repo.apiUrl, options);
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`${MESSAGES[errorMessageKey][getCurrentLang()]}${response.status}`);
throw new Error(`${this.i18nMessages.getMessage(errorMessageKey, this.i18nMessages.getCurrentLang())}${response.status}`);
}
const releases = await response.json();
// Hide loading indicator
elements.loading.style.display = 'none';
if (!releases || releases.length === 0) {
throw new Error(MESSAGES.noReleases[getCurrentLang()]);
throw new Error(this.i18nMessages.getMessage('noReleases', this.i18nMessages.getCurrentLang()));
}
// Display releases
displayReleases(releases, source);
elements.changelog.style.display = 'block';
return releases;
} catch (error) {
clearTimeout(timeoutId);
throw error;
}
}
/**
* Show error message
* 从Gitea获取数据
* @param {Object} repo - 仓库配置
* @param {Object} options - 请求选项
* @param {string} errorMessageKey - 错误消息键
*/
function showError(message) {
const errorMessageElement = elements.error.querySelector('p');
errorMessageElement.textContent = message;
elements.error.style.display = 'block';
async fetchFromGitea(repo, options, errorMessageKey) {
const response = await fetch(repo.apiUrl, options);
if (!response.ok) {
throw new Error(`${this.i18nMessages.getMessage(errorMessageKey, this.i18nMessages.getCurrentLang())}${response.status}`);
}
const releases = await response.json();
if (!releases || releases.length === 0) {
throw new Error(this.i18nMessages.getMessage('noReleases', this.i18nMessages.getCurrentLang()));
}
return releases;
}
}
/**
* UI管理类
*/
class UIManager {
constructor(i18nMessages) {
this.i18nMessages = i18nMessages;
this.elements = {
loading: document.getElementById('loading'),
changelog: document.getElementById('changelog'),
error: document.getElementById('error-message')
};
}
/**
* Display releases
* @param {Array} releases - Array of release objects
* @param {string} source - 'github' or 'gitea'
* 显示加载状态
*/
function displayReleases(releases, source) {
// Clear existing content
elements.changelog.innerHTML = '';
showLoading() {
this.elements.loading.style.display = 'block';
this.elements.error.style.display = 'none';
this.elements.changelog.innerHTML = '';
}
/**
* 隐藏加载状态
*/
hideLoading() {
this.elements.loading.style.display = 'none';
}
/**
* 显示错误消息
* @param {string} message - 错误消息
*/
showError(message) {
const errorMessageElement = this.elements.error.querySelector('p');
if (errorMessageElement) {
errorMessageElement.textContent = message;
} else {
this.elements.error.textContent = message;
}
this.elements.error.style.display = 'block';
this.hideLoading();
}
/**
* 显示发布信息
* @param {Array} releases - 发布信息数组
* @param {string} source - 数据源
*/
displayReleases(releases, source) {
this.hideLoading();
// Add data source indicator
const sourceElement = createSourceElement(source);
elements.changelog.appendChild(sourceElement);
// 清除现有内容
this.elements.changelog.innerHTML = '';
// Create release elements
// 创建数据源元素
const sourceElement = this.createSourceElement(source);
this.elements.changelog.appendChild(sourceElement);
// 创建发布信息元素
releases.forEach(release => {
const releaseElement = createReleaseElement(release, source);
elements.changelog.appendChild(releaseElement);
const releaseElement = this.createReleaseElement(release, source);
this.elements.changelog.appendChild(releaseElement);
});
this.elements.changelog.style.display = 'block';
}
/**
* Create source element
* 创建数据源元素
* @param {string} source - 数据源
*/
function createSourceElement(source) {
createSourceElement(source) {
const sourceElement = document.createElement('div');
sourceElement.className = 'data-source';
// Create source label with i18n support
// 创建带有国际化支持的源标签
const sourceLabel = document.createElement('span');
sourceLabel.setAttribute('data-en', MESSAGES.dataSource.en);
sourceLabel.setAttribute('data-zh', MESSAGES.dataSource.zh);
sourceLabel.textContent = MESSAGES.dataSource[getCurrentLang()];
sourceLabel.setAttribute('data-en', this.i18nMessages.getMessage('dataSource', 'en'));
sourceLabel.setAttribute('data-zh', this.i18nMessages.getMessage('dataSource', 'zh'));
sourceLabel.textContent = this.i18nMessages.getMessage('dataSource', this.i18nMessages.getCurrentLang());
// Create link
const sourceLink = document.createElement('a');
sourceLink.href = REPOS[source].releasesUrl;
sourceLink.textContent = source === 'github' ? 'GitHub' : 'Gitea';
sourceLink.target = '_blank';
// 创建链接
const sourceLink = document.createElement('a');
const repositoryConfig = new RepositoryConfig();
sourceLink.href = repositoryConfig.getRepo(source).releasesUrl;
sourceLink.textContent = source === 'github' ? 'GitHub' : 'Gitea';
sourceLink.target = '_blank';
// Assemble elements
// 组装元素
sourceElement.appendChild(sourceLabel);
sourceElement.appendChild(sourceLink);
@@ -213,34 +285,34 @@ document.addEventListener('DOMContentLoaded', () => {
}
/**
* Create release element
* @param {Object} release - Release data
* @param {string} source - 'github' or 'gitea'
* 创建发布信息元素
* @param {Object} release - 发布信息对象
* @param {string} source - 数据源
*/
function createReleaseElement(release, source) {
createReleaseElement(release, source) {
const releaseElement = document.createElement('div');
releaseElement.className = 'release';
// Format release date
// 格式化发布日期
const releaseDate = new Date(release.published_at || release.created_at);
const formattedDate = formatDate(releaseDate);
const formattedDate = DateFormatter.formatDate(releaseDate);
// Create header
const headerElement = createReleaseHeader(release, formattedDate);
// 创建头部
const headerElement = this.createReleaseHeader(release, formattedDate);
releaseElement.appendChild(headerElement);
// Add release description
// 添加发布说明
if (release.body) {
const descriptionElement = document.createElement('div');
descriptionElement.className = 'release-description markdown-content';
descriptionElement.innerHTML = parseMarkdown(release.body);
descriptionElement.innerHTML = MarkdownParser.parseMarkdown(release.body);
releaseElement.appendChild(descriptionElement);
}
// Add download assets
const assets = getAssetsFromRelease(release, source);
// 添加下载资源
const assets = AssetManager.getAssetsFromRelease(release, source);
if (assets && assets.length > 0) {
const assetsElement = createAssetsElement(assets);
const assetsElement = this.createAssetsElement(assets);
releaseElement.appendChild(assetsElement);
}
@@ -248,32 +320,32 @@ document.addEventListener('DOMContentLoaded', () => {
}
/**
* Create release header
* 创建发布信息头部
*/
function createReleaseHeader(release, formattedDate) {
createReleaseHeader(release, formattedDate) {
const headerElement = document.createElement('div');
headerElement.className = 'release-header';
// Version element
// 版本元素
const versionElement = document.createElement('div');
versionElement.className = 'release-version';
// Version text
// 版本文本
const versionText = document.createElement('span');
versionText.textContent = release.name || release.tag_name;
versionElement.appendChild(versionText);
// Pre-release badge
// 预发布标记
if (release.prerelease) {
const preReleaseTag = document.createElement('span');
preReleaseTag.className = 'release-badge pre-release';
preReleaseTag.setAttribute('data-en', MESSAGES.preRelease.en);
preReleaseTag.setAttribute('data-zh', MESSAGES.preRelease.zh);
preReleaseTag.textContent = MESSAGES.preRelease[getCurrentLang()];
preReleaseTag.setAttribute('data-en', this.i18nMessages.getMessage('preRelease', 'en'));
preReleaseTag.setAttribute('data-zh', this.i18nMessages.getMessage('preRelease', 'zh'));
preReleaseTag.textContent = this.i18nMessages.getMessage('preRelease', this.i18nMessages.getCurrentLang());
versionElement.appendChild(preReleaseTag);
}
// Date element
// 日期元素
const dateElement = document.createElement('div');
dateElement.className = 'release-date';
dateElement.textContent = formattedDate;
@@ -285,16 +357,94 @@ document.addEventListener('DOMContentLoaded', () => {
}
/**
* Get assets from release based on source
* 创建资源文件元素
* @param {Array} assets - 资源文件数组
*/
function getAssetsFromRelease(release, source) {
createAssetsElement(assets) {
const assetsElement = document.createElement('div');
assetsElement.className = 'release-assets';
// 资源标题
const assetsTitle = document.createElement('div');
assetsTitle.className = 'release-assets-title';
assetsTitle.setAttribute('data-en', this.i18nMessages.getMessage('downloads', 'en'));
assetsTitle.setAttribute('data-zh', this.i18nMessages.getMessage('downloads', 'zh'));
assetsTitle.textContent = this.i18nMessages.getMessage('downloads', this.i18nMessages.getCurrentLang());
// 资源列表
const assetList = document.createElement('ul');
assetList.className = 'asset-list';
// 添加每个资源
assets.forEach(asset => {
const assetItem = this.createAssetItem(asset);
assetList.appendChild(assetItem);
});
assetsElement.appendChild(assetsTitle);
assetsElement.appendChild(assetList);
return assetsElement;
}
/**
* 创建资源文件项
* @param {Object} asset - 资源文件对象
*/
createAssetItem(asset) {
const assetItem = document.createElement('li');
assetItem.className = 'asset-item';
// 文件图标
const iconElement = document.createElement('i');
iconElement.className = `asset-icon fas fa-${FileIconHelper.getFileIcon(asset.name)}`;
// 文件名
const nameElement = document.createElement('span');
nameElement.className = 'asset-name';
nameElement.textContent = asset.name;
// 文件大小
const sizeElement = document.createElement('span');
sizeElement.className = 'asset-size';
sizeElement.textContent = FileSizeFormatter.formatFileSize(asset.size);
// 下载链接
const downloadLink = document.createElement('a');
downloadLink.className = 'download-btn';
downloadLink.href = asset.browser_download_url;
downloadLink.target = '_blank';
downloadLink.setAttribute('data-en', this.i18nMessages.getMessage('download', 'en'));
downloadLink.setAttribute('data-zh', this.i18nMessages.getMessage('download', 'zh'));
downloadLink.textContent = this.i18nMessages.getMessage('download', this.i18nMessages.getCurrentLang());
// 组装资源项
assetItem.appendChild(iconElement);
assetItem.appendChild(nameElement);
assetItem.appendChild(sizeElement);
assetItem.appendChild(downloadLink);
return assetItem;
}
}
/**
* 资源管理器类
*/
class AssetManager {
/**
* 从发布信息中获取资源文件
* @param {Object} release - 发布信息对象
* @param {string} source - 数据源
*/
static getAssetsFromRelease(release, source) {
let assets = [];
if (source === 'github') {
assets = release.assets || [];
} else { // Gitea
assets = release.assets || [];
// Check for Gitea-specific asset structure
// 检查Gitea特定的资源结构
if (!assets.length && release.attachments) {
assets = release.attachments.map(attachment => ({
name: attachment.name,
@@ -306,131 +456,93 @@ document.addEventListener('DOMContentLoaded', () => {
return assets;
}
}
/**
* 文件图标助手类
*/
class FileIconHelper {
/**
* Create assets element
* 根据文件扩展名获取图标
* @param {string} filename - 文件名
*/
function createAssetsElement(assets) {
const assetsElement = document.createElement('div');
assetsElement.className = 'release-assets';
static getFileIcon(filename) {
const extension = filename.split('.').pop().toLowerCase();
// Assets title
const assetsTitle = document.createElement('div');
assetsTitle.className = 'release-assets-title';
assetsTitle.setAttribute('data-en', MESSAGES.downloads.en);
assetsTitle.setAttribute('data-zh', MESSAGES.downloads.zh);
assetsTitle.textContent = MESSAGES.downloads[getCurrentLang()];
const iconMap = {
'exe': 'download',
'msi': 'download',
'dmg': 'download',
'pkg': 'download',
'deb': 'download',
'rpm': 'download',
'tar': 'file-archive',
'gz': 'file-archive',
'zip': 'file-archive',
'7z': 'file-archive',
'rar': 'file-archive',
'pdf': 'file-pdf',
'txt': 'file-alt',
'md': 'file-alt',
'json': 'file-code',
'xml': 'file-code',
'yml': 'file-code',
'yaml': 'file-code'
};
// Asset list
const assetList = document.createElement('ul');
assetList.className = 'asset-list';
// Add each asset
assets.forEach(asset => {
const assetItem = createAssetItem(asset);
assetList.appendChild(assetItem);
});
assetsElement.appendChild(assetsTitle);
assetsElement.appendChild(assetList);
return assetsElement;
return iconMap[extension] || 'file';
}
}
/**
* 文件大小格式化器类
*/
class FileSizeFormatter {
/**
* Create asset item
* 格式化文件大小
* @param {number} bytes - 字节数
*/
function createAssetItem(asset) {
const assetItem = document.createElement('li');
assetItem.className = 'asset-item';
static formatFileSize(bytes) {
if (!bytes) return '';
// File icon
const iconElement = document.createElement('i');
iconElement.className = `asset-icon fas fa-${getFileIcon(asset.name)}`;
// File name
const nameElement = document.createElement('span');
nameElement.className = 'asset-name';
nameElement.textContent = asset.name;
// File size
const sizeElement = document.createElement('span');
sizeElement.className = 'asset-size';
sizeElement.textContent = formatFileSize(asset.size);
// Download link
const downloadLink = document.createElement('a');
downloadLink.className = 'download-btn';
downloadLink.href = asset.browser_download_url;
downloadLink.target = '_blank';
downloadLink.setAttribute('data-en', MESSAGES.download.en);
downloadLink.setAttribute('data-zh', MESSAGES.download.zh);
downloadLink.textContent = MESSAGES.download[getCurrentLang()];
// Assemble asset item
assetItem.appendChild(iconElement);
assetItem.appendChild(nameElement);
assetItem.appendChild(sizeElement);
assetItem.appendChild(downloadLink);
return assetItem;
}
/**
* Get file icon based on extension
*/
function getFileIcon(filename) {
const ext = filename.split('.').pop().toLowerCase();
switch (ext) {
case 'zip':
case 'gz':
case 'tar':
case '7z':
return 'file-archive';
case 'exe':
return 'file-code';
case 'dmg':
return 'apple';
case 'deb':
case 'rpm':
return 'linux';
case 'json':
case 'xml':
return 'file-alt';
default:
return 'file';
}
}
/**
* Format file size
*/
function formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return parseFloat((bytes / Math.pow(1024, i)).toFixed(2)) + ' ' + sizes[i];
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
}
}
/**
* 日期格式化器类
*/
class DateFormatter {
/**
* Format date
* 格式化日期
* @param {Date} date - 日期对象
*/
function formatDate(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
static formatDate(date) {
const options = {
year: 'numeric',
month: 'long',
day: 'numeric'
};
return `${year}-${month}-${day}`;
const lang = window.currentLang || 'en';
const locale = lang === 'zh' ? 'zh-CN' : 'en-US';
return date.toLocaleDateString(locale, options);
}
}
/**
* Markdown解析器类
*/
class MarkdownParser {
/**
* Simple Markdown parser
* Note: This is a very basic implementation that handles only common Markdown syntax
* 简单的Markdown解析
* @param {string} markdown - Markdown文本
*/
function parseMarkdown(markdown) {
static parseMarkdown(markdown) {
if (!markdown) return '';
// 预处理:保留原始换行符,用特殊标记替换
@@ -529,24 +641,65 @@ document.addEventListener('DOMContentLoaded', () => {
return markdown;
}
// Update translations when language changes
window.addEventListener('languageChanged', updateUI);
// Initial UI update based on current language
updateUI();
/**
* Update UI elements with current language
*/
function updateUI() {
const lang = getCurrentLang();
}
/**
* 更新日志主应用类
*/
class ChangelogApp {
constructor() {
this.repositoryConfig = new RepositoryConfig();
this.i18nMessages = new I18nMessages();
this.apiClient = new APIClient(this.repositoryConfig, this.i18nMessages);
this.uiManager = new UIManager(this.i18nMessages);
// Update all i18n elements
document.querySelectorAll('[data-en][data-zh]').forEach(el => {
if (el.hasAttribute(`data-${lang}`)) {
el.textContent = el.getAttribute(`data-${lang}`);
this.init();
}
/**
* 初始化应用
*/
init() {
this.uiManager.showLoading();
// 首先尝试GitHub API
this.apiClient.fetchReleases('github')
.then(releases => {
this.uiManager.displayReleases(releases, 'github');
})
.catch(() => {
// GitHub失败时尝试Gitea
return this.apiClient.fetchReleases('gitea')
.then(releases => {
this.uiManager.displayReleases(releases, 'gitea');
});
})
.catch(error => {
console.error('获取发布信息失败:', error);
this.uiManager.showError(this.i18nMessages.getMessage('fetchError', this.i18nMessages.getCurrentLang()));
});
// 监听语言变化事件
document.addEventListener('languageChanged', () => this.updateUI());
}
/**
* 更新UI元素当语言变化时
*/
updateUI() {
const elementsToUpdate = document.querySelectorAll('[data-en][data-zh]');
const currentLang = this.i18nMessages.getCurrentLang();
elementsToUpdate.forEach(element => {
const text = element.getAttribute(`data-${currentLang}`);
if (text) {
element.textContent = text;
}
});
}
});
}
// 当DOM加载完成时初始化应用
document.addEventListener('DOMContentLoaded', () => {
new ChangelogApp();
});