💡 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

@@ -4,9 +4,62 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VoidRaft - An elegant text snippet recording tool designed for developers.</title>
<meta name="description" content="VoidRaft is an elegant text snippet recording tool designed for developers. Features multi-language code blocks, syntax highlighting, code formatting, custom themes, and more.">
<meta name="keywords" content="text editor, code snippets, developer tools, syntax highlighting, code formatting, multi-language, VoidRaft">
<meta name="author" content="VoidRaft Team">
<meta name="robots" content="index, follow">
<link rel="canonical" href="https://landaiqing.github.io/voidraft/">
<!-- Internationalization / hreflang -->
<link rel="alternate" hreflang="en" href="https://landaiqing.github.io/voidraft/">
<link rel="alternate" hreflang="zh" href="https://landaiqing.github.io/voidraft/?lang=zh">
<link rel="alternate" hreflang="x-default" href="https://landaiqing.github.io/voidraft/">
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website">
<meta property="og:url" content="https://landaiqing.github.io/voidraft/">
<meta property="og:title" content="VoidRaft - An elegant text snippet recording tool designed for developers">
<meta property="og:description" content="VoidRaft is an elegant text snippet recording tool designed for developers. Features multi-language code blocks, syntax highlighting, code formatting, custom themes, and more.">
<meta property="og:image" content="https://landaiqing.github.io/voidraft/img/screenshot-dark.png">
<meta property="og:site_name" content="VoidRaft">
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image">
<meta property="twitter:url" content="https://landaiqing.github.io/voidraft/">
<meta property="twitter:title" content="VoidRaft - An elegant text snippet recording tool designed for developers">
<meta property="twitter:description" content="VoidRaft is an elegant text snippet recording tool designed for developers. Features multi-language code blocks, syntax highlighting, code formatting, custom themes, and more.">
<meta property="twitter:image" content="https://landaiqing.github.io/voidraft/img/screenshot-dark.png">
<link rel="stylesheet" href="./css/styles.css">
<link rel="icon" href="./img/favicon.ico" type="image/x-icon">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- Structured Data -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"name": "VoidRaft",
"description": "An elegant text snippet recording tool designed for developers. Features multi-language code blocks, syntax highlighting, code formatting, custom themes, and more.",
"url": "https://landaiqing.github.io/voidraft/",
"downloadUrl": "https://github.com/landaiqing/voidraft/releases",
"author": {
"@type": "Organization",
"name": "VoidRaft"
},
"operatingSystem": ["Windows", "macOS", "Linux"],
"applicationCategory": "DeveloperApplication",
"offers": {
"@type": "Offer",
"price": "0",
"priceCurrency": "USD"
},
"screenshot": "https://landaiqing.github.io/voidraft/img/screenshot-dark.png",
"softwareVersion": "Latest",
"programmingLanguage": ["Go", "TypeScript", "Vue.js"],
"codeRepository": "https://github.com/landaiqing/voidraft"
}
</script>
</head>
<body class="theme-dark">
<div class="container">
@@ -200,4 +253,4 @@
<script src="js/script.js"></script>
</body>
</html>
</html>

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();
});

View File

@@ -1,136 +1,443 @@
/**
* VoidRaft - Website Script
*/
document.addEventListener('DOMContentLoaded', () => {
// Initialize components
initThemeToggle();
initLanguageToggle();
initCardEffects();
// Console branding
console.log('%c VoidRaft', 'color: #ff006e; font-size: 20px; font-family: "Space Mono", monospace;');
console.log('%c An elegant text snippet recording tool designed for developers.', 'color: #073B4C; font-family: "Space Mono", monospace;');
});
/**
* Initialize theme toggle functionality
* 主题管理类
*/
function initThemeToggle() {
const themeToggle = document.getElementById('theme-toggle');
if (!themeToggle) return;
// Get initial theme from local storage or system preference
const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)');
const savedTheme = localStorage.getItem('theme');
const theme = savedTheme || (prefersDarkScheme.matches ? 'dark' : 'light');
// Apply initial theme
setTheme(theme);
// Add click handler
themeToggle.addEventListener('click', () => {
class ThemeManager {
constructor() {
this.themeToggle = document.getElementById('theme-toggle');
this.currentTheme = this.getInitialTheme();
this.init();
}
/**
* 获取初始主题
*/
getInitialTheme() {
const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)');
const savedTheme = localStorage.getItem('theme');
return savedTheme || (prefersDarkScheme.matches ? 'dark' : 'light');
}
/**
* 初始化主题管理器
*/
init() {
if (!this.themeToggle) return;
this.setTheme(this.currentTheme);
this.bindEvents();
}
/**
* 绑定事件
*/
bindEvents() {
this.themeToggle.addEventListener('click', () => {
this.toggleTheme();
});
}
/**
* 切换主题
*/
toggleTheme() {
document.body.classList.add('theme-transition');
const currentTheme = document.body.classList.contains('theme-dark') ? 'dark' : 'light';
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
const newTheme = this.currentTheme === 'dark' ? 'light' : 'dark';
this.setTheme(newTheme);
this.saveTheme(newTheme);
setTheme(newTheme);
localStorage.setItem('theme', newTheme);
// Remove transition class after animation completes
setTimeout(() => document.body.classList.remove('theme-transition'), 300);
});
}
/**
* 设置主题
* @param {string} theme - 'dark' 或 'light'
*/
setTheme(theme) {
this.currentTheme = theme;
const isDark = theme === 'dark';
document.body.classList.toggle('theme-dark', isDark);
document.body.classList.toggle('theme-light', !isDark);
this.updateToggleIcon(isDark);
}
/**
* 更新切换按钮图标
* @param {boolean} isDark - 是否为暗色主题
*/
updateToggleIcon(isDark) {
if (this.themeToggle) {
const icon = this.themeToggle.querySelector('i');
if (icon) {
icon.className = isDark ? 'fas fa-sun' : 'fas fa-moon';
}
}
}
/**
* 保存主题到本地存储
* @param {string} theme - 主题名称
*/
saveTheme(theme) {
localStorage.setItem('theme', theme);
}
}
/**
* Set theme to dark or light
* @param {string} theme - 'dark' or 'light'
* 语言管理类
*/
function setTheme(theme) {
const isDark = theme === 'dark';
// Update body class
document.body.classList.toggle('theme-dark', isDark);
document.body.classList.toggle('theme-light', !isDark);
// Update toggle icon
const themeToggle = document.getElementById('theme-toggle');
if (themeToggle) {
const icon = themeToggle.querySelector('i');
if (icon) {
icon.className = isDark ? 'fas fa-sun' : 'fas fa-moon';
class LanguageManager {
constructor() {
this.langToggle = document.getElementById('lang-toggle');
this.currentLang = this.getInitialLanguage();
this.init();
}
/**
* 获取初始语言
*/
getInitialLanguage() {
const urlParams = new URLSearchParams(window.location.search);
const urlLang = urlParams.get('lang');
const savedLang = localStorage.getItem('lang');
const browserLang = navigator.language.startsWith('zh') ? 'zh' : 'en';
return urlLang || savedLang || browserLang;
}
/**
* 初始化语言管理器
*/
init() {
if (!this.langToggle) return;
window.currentLang = this.currentLang;
this.setLanguage(this.currentLang);
this.bindEvents();
}
/**
* 绑定事件
*/
bindEvents() {
this.langToggle.addEventListener('click', () => {
this.toggleLanguage();
});
}
/**
* 切换语言
*/
toggleLanguage() {
document.body.classList.add('lang-transition');
const newLang = this.currentLang === 'zh' ? 'en' : 'zh';
this.setLanguage(newLang);
this.saveLanguage(newLang);
this.updateURL(newLang);
this.notifyLanguageChange(newLang);
setTimeout(() => document.body.classList.remove('lang-transition'), 300);
}
/**
* 设置页面语言
* @param {string} lang - 'zh' 或 'en'
*/
setLanguage(lang) {
this.currentLang = lang;
window.currentLang = lang;
this.updatePageElements(lang);
this.updateHTMLLang(lang);
this.updateToggleButton(lang);
}
/**
* 更新页面元素文本
* @param {string} lang - 语言代码
*/
updatePageElements(lang) {
document.querySelectorAll('[data-zh][data-en]').forEach(el => {
el.textContent = el.getAttribute(`data-${lang}`);
});
}
/**
* 更新HTML语言属性
* @param {string} lang - 语言代码
*/
updateHTMLLang(lang) {
document.documentElement.lang = lang === 'zh' ? 'zh-CN' : 'en';
}
/**
* 更新切换按钮文本
* @param {string} lang - 语言代码
*/
updateToggleButton(lang) {
if (this.langToggle) {
const text = lang === 'zh' ? 'EN/中' : '中/EN';
this.langToggle.innerHTML = `<i class="fas fa-language"></i> ${text}`;
}
}
/**
* 保存语言到本地存储
* @param {string} lang - 语言代码
*/
saveLanguage(lang) {
localStorage.setItem('lang', lang);
}
/**
* 更新URL参数
* @param {string} lang - 语言代码
*/
updateURL(lang) {
const newUrl = new URL(window.location);
if (lang === 'zh') {
newUrl.searchParams.set('lang', 'zh');
} else {
newUrl.searchParams.delete('lang');
}
window.history.replaceState({}, '', newUrl);
}
/**
* 通知语言变更
* @param {string} lang - 语言代码
*/
notifyLanguageChange(lang) {
window.dispatchEvent(new CustomEvent('languageChanged', { detail: { lang } }));
}
/**
* 获取当前语言
*/
getCurrentLanguage() {
return this.currentLang;
}
}
/**
* SEO管理类
*/
class SEOManager {
constructor(languageManager) {
this.languageManager = languageManager;
this.metaTexts = {
en: {
description: 'VoidRaft is an elegant text snippet recording tool designed for developers. Features multi-language code blocks, syntax highlighting, code formatting, custom themes, and more.',
title: 'VoidRaft - An elegant text snippet recording tool designed for developers.',
ogTitle: 'VoidRaft - An elegant text snippet recording tool designed for developers'
},
zh: {
description: 'VoidRaft 是专为开发者打造的优雅文本片段记录工具。支持多语言代码块、语法高亮、代码格式化、自定义主题等功能。',
title: 'VoidRaft - 专为开发者打造的优雅文本片段记录工具',
ogTitle: 'VoidRaft - 专为开发者打造的优雅文本片段记录工具'
}
};
this.init();
}
/**
* 初始化SEO管理器
*/
init() {
this.bindEvents();
this.updateMetaTags(this.languageManager.getCurrentLanguage());
}
/**
* 绑定事件
*/
bindEvents() {
window.addEventListener('languageChanged', (event) => {
this.updateMetaTags(event.detail.lang);
});
}
/**
* 更新SEO元标签
* @param {string} lang - 当前语言
*/
updateMetaTags(lang) {
const texts = this.metaTexts[lang];
this.updateMetaDescription(texts.description);
this.updateOpenGraphTags(texts.ogTitle, texts.description);
this.updateTwitterCardTags(texts.ogTitle, texts.description);
this.updatePageTitle(texts.title);
}
/**
* 更新meta描述
* @param {string} description - 描述文本
*/
updateMetaDescription(description) {
const metaDesc = document.querySelector('meta[name="description"]');
if (metaDesc) {
metaDesc.content = description;
}
}
/**
* 更新Open Graph标签
* @param {string} title - 标题
* @param {string} description - 描述
*/
updateOpenGraphTags(title, description) {
const ogTitle = document.querySelector('meta[property="og:title"]');
const ogDesc = document.querySelector('meta[property="og:description"]');
if (ogTitle) ogTitle.content = title;
if (ogDesc) ogDesc.content = description;
}
/**
* 更新Twitter Card标签
* @param {string} title - 标题
* @param {string} description - 描述
*/
updateTwitterCardTags(title, description) {
const twitterTitle = document.querySelector('meta[property="twitter:title"]');
const twitterDesc = document.querySelector('meta[property="twitter:description"]');
if (twitterTitle) twitterTitle.content = title;
if (twitterDesc) twitterDesc.content = description;
}
/**
* 更新页面标题
* @param {string} title - 标题
*/
updatePageTitle(title) {
document.title = title;
}
}
/**
* UI效果管理类
*/
class UIEffects {
constructor() {
this.init();
}
/**
* 初始化UI效果
*/
init() {
this.initCardEffects();
}
/**
* 初始化卡片悬停效果
*/
initCardEffects() {
const cards = document.querySelectorAll('.feature-card');
cards.forEach(card => {
card.addEventListener('mouseenter', () => {
this.animateCardHover(card, true);
});
card.addEventListener('mouseleave', () => {
this.animateCardHover(card, false);
});
});
}
/**
* 卡片悬停动画
* @param {Element} card - 卡片元素
* @param {boolean} isHover - 是否悬停
*/
animateCardHover(card, isHover) {
if (isHover) {
card.style.transform = 'translateY(-8px)';
card.style.boxShadow = '7px 7px 0 var(--shadow-color)';
} else {
card.style.transform = 'translateY(0)';
card.style.boxShadow = '5px 5px 0 var(--shadow-color)';
}
}
}
/**
* Initialize language toggle functionality
* VoidRaft主应用类
*/
function initLanguageToggle() {
const langToggle = document.getElementById('lang-toggle');
if (!langToggle) return;
// Get initial language from local storage or default to English
const savedLang = localStorage.getItem('lang');
const lang = savedLang || 'en';
// Set current language and apply it
window.currentLang = lang;
setLanguage(lang);
// Add click handler
langToggle.addEventListener('click', () => {
document.body.classList.add('lang-transition');
const newLang = window.currentLang === 'zh' ? 'en' : 'zh';
setLanguage(newLang);
window.currentLang = newLang;
localStorage.setItem('lang', newLang);
// Notify other components about language change
window.dispatchEvent(new CustomEvent('languageChanged', { detail: { lang: newLang } }));
// Remove transition class after animation completes
setTimeout(() => document.body.classList.remove('lang-transition'), 300);
});
}
class VoidRaftApp {
constructor() {
this.themeManager = null;
this.languageManager = null;
this.seoManager = null;
this.uiEffects = null;
this.init();
}
/**
* Set page language
* @param {string} lang - 'zh' or 'en'
*/
function setLanguage(lang) {
// Update all elements with data-zh and data-en attributes
document.querySelectorAll('[data-zh][data-en]').forEach(el => {
el.textContent = el.getAttribute(`data-${lang}`);
});
// Update HTML language attribute
document.documentElement.lang = lang === 'zh' ? 'zh-CN' : 'en';
// Update toggle button text
const langToggle = document.getElementById('lang-toggle');
if (langToggle) {
const text = lang === 'zh' ? 'EN/中' : '中/EN';
langToggle.innerHTML = `<i class="fas fa-language"></i> ${text}`;
/**
* 初始化应用
*/
init() {
this.initializeManagers();
this.showConsoleBranding();
}
/**
* 初始化各个管理器
*/
initializeManagers() {
this.themeManager = new ThemeManager();
this.languageManager = new LanguageManager();
this.seoManager = new SEOManager(this.languageManager);
this.uiEffects = new UIEffects();
}
/**
* 显示控制台品牌信息
*/
showConsoleBranding() {
console.log('%c VoidRaft', 'color: #ff006e; font-size: 20px; font-family: "Space Mono", monospace;');
console.log('%c An elegant text snippet recording tool designed for developers.', 'color: #073B4C; font-family: "Space Mono", monospace;');
}
/**
* 获取主题管理器
*/
getThemeManager() {
return this.themeManager;
}
/**
* 获取语言管理器
*/
getLanguageManager() {
return this.languageManager;
}
/**
* 获取SEO管理器
*/
getSEOManager() {
return this.seoManager;
}
/**
* 获取UI效果管理器
*/
getUIEffects() {
return this.uiEffects;
}
}
/**
* Initialize card hover effects
*/
function initCardEffects() {
const cards = document.querySelectorAll('.feature-card');
cards.forEach(card => {
card.addEventListener('mouseenter', () => {
card.style.transform = 'translateY(-8px)';
card.style.boxShadow = '7px 7px 0 var(--shadow-color)';
});
card.addEventListener('mouseleave', () => {
card.style.transform = 'translateY(0)';
card.style.boxShadow = '5px 5px 0 var(--shadow-color)';
});
});
}
// 当DOM加载完成时初始化应用
document.addEventListener('DOMContentLoaded', () => {
window.voidRaftApp = new VoidRaftApp();
});