💡 Update docs
This commit is contained in:
@@ -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>
|
@@ -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();
|
||||
});
|
@@ -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();
|
||||
});
|
Reference in New Issue
Block a user