diff --git a/gradle.properties b/gradle.properties index 456764f..9b61fe8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,7 +6,7 @@ minecraft_version=1.21.4 yarn_mappings=1.21.4+build.8 loader_version=0.16.10 # Mod Properties -mod_version=1.14.514.128 +mod_version=1.14.514.129 maven_group=org.example1 archives_base_name=playerOnlineTimeTrackerMod # Dependencies diff --git a/src/main/java/com/example/playertime/ModConfig.java b/src/main/java/com/example/playertime/ModConfig.java index 9577d13..e59a1f5 100644 --- a/src/main/java/com/example/playertime/ModConfig.java +++ b/src/main/java/com/example/playertime/ModConfig.java @@ -9,6 +9,7 @@ public class ModConfig { private final Path configPath; private int webPort = 60048; private String language = "zh_cn"; + private long autoSaveSeconds = 300; public ModConfig(Path configDir) { this.configPath = configDir.resolve("playertime-config.json"); @@ -17,7 +18,7 @@ public class ModConfig { private void loadConfig() { if (!Files.exists(configPath)) { - PlayerTimeMod.LOGGER.info("[在线时间] 配置文件未找到,正在创建默认配置"); // 使用英文日志,或者也本地化日志?这里先用英文 + PlayerTimeMod.LOGGER.info("[在线时间] 配置文件未找到,正在创建默认配置"); saveConfig(); return; } @@ -35,6 +36,9 @@ public class ModConfig { if (json.has("webPort")) { webPort = json.get("webPort").getAsInt(); + } else { + PlayerTimeMod.LOGGER.info("[在线时间] 配置文件缺少“webPort”字段,添加默认值'%s'并保存", webPort); + saveConfig(); } if (json.has("language")) { language = json.get("language").getAsString(); @@ -42,6 +46,12 @@ public class ModConfig { PlayerTimeMod.LOGGER.info("[在线时间] 配置文件缺少“language”字段,添加默认值'%s'并保存", language); saveConfig(); } + if (json.has("autoSaveSeconds")) { + autoSaveSeconds = json.get("autoSaveSeconds").getAsLong(); + } else { + PlayerTimeMod.LOGGER.info("[在线时间] 配置文件缺少“autoSaveSeconds”字段,添加默认值'%s'并保存", autoSaveSeconds); + saveConfig(); + } } catch (Exception e) { PlayerTimeMod.LOGGER.error("[在线时间] 加载配置文件失败,使用默认配置", e); @@ -54,6 +64,7 @@ public class ModConfig { JsonObject json = new JsonObject(); json.addProperty("webPort", webPort); json.addProperty("language", language); + json.addProperty("autoSaveSeconds", autoSaveSeconds); try { Files.createDirectories(configPath.getParent()); @@ -75,4 +86,9 @@ public class ModConfig { public String getLanguage() { return language; } + + // 获取保存间隔 + public long getSeconds() { + return autoSaveSeconds; + } } diff --git a/src/main/java/com/example/playertime/PlayerTimeMod.java b/src/main/java/com/example/playertime/PlayerTimeMod.java index 04bc05d..cef7e4a 100644 --- a/src/main/java/com/example/playertime/PlayerTimeMod.java +++ b/src/main/java/com/example/playertime/PlayerTimeMod.java @@ -35,13 +35,13 @@ public class PlayerTimeMod implements ModInitializer { // TODO 定时保存配置文件没整,暂时硬编码 private ScheduledExecutorService scheduler; private ScheduledFuture> saveTask; - private static final long AUTO_SAVE_INTERVAL_SECONDS = 5 * 60; + private static long AUTO_SAVE_INTERVAL_SECONDS; @Override public void onInitialize() { config = new ModConfig(FabricLoader.getInstance().getConfigDir()); - localizationManager = new LocalizationManager(config.getLanguage()); // 初始化本地化管理器 + localizationManager = new LocalizationManager(config.getLanguage()); scheduler = Executors.newSingleThreadScheduledExecutor(); @@ -49,6 +49,8 @@ public class PlayerTimeMod implements ModInitializer { try { LOGGER.info("[在线时间] 初始化 玩家在线时长视奸Mod"); + AUTO_SAVE_INTERVAL_SECONDS = config.getSeconds(); + ServerLifecycleEvents.SERVER_STARTING.register(server -> { timeTracker = new PlayerTimeTracker(server); try { @@ -102,7 +104,7 @@ public class PlayerTimeMod implements ModInitializer { } } catch (InterruptedException e) { LOGGER.error("[在线时间] 调度程序终止被中断", e); - Thread.currentThread().interrupt(); // Restore interrupted status + Thread.currentThread().interrupt(); } LOGGER.info("[在线时间] 调度程序关闭"); } diff --git a/src/main/resources/assets/playertime/web/js/app.js b/src/main/resources/assets/playertime/web/js/app.js index 1b74570..82c8602 100644 --- a/src/main/resources/assets/playertime/web/js/app.js +++ b/src/main/resources/assets/playertime/web/js/app.js @@ -1,17 +1,17 @@ document.addEventListener('DOMContentLoaded', function() { - // 1. 首先初始化所有变量 + // 初始化变量 const tpsHistory = Array(30).fill(20); const msptHistory = Array(30).fill(50); let memoryChart = null; let performanceChart = null; - let lang = {}; // 新增:存储语言文件内容 + let lang = {}; - // 2. 获取DOM元素(带安全检查) + // 获取语言 const elements = { refreshBtn: document.getElementById('refresh-btn'), themeToggle: document.getElementById('theme-toggle'), - statsTableBody: document.getElementById('stats-table')?.getElementsByTagName('tbody')[0], // Changed ID for clarity - statsTableHeader: document.getElementById('stats-table')?.getElementsByTagName('thead')[0], // Added for header localization + statsTableBody: document.getElementById('stats-table')?.getElementsByTagName('tbody')[0], + statsTableHeader: document.getElementById('stats-table')?.getElementsByTagName('thead')[0], whitelistPlayers: document.getElementById('whitelist-players'), nonWhitelistPlayers: document.getElementById('non-whitelist-players'), totalCount: document.getElementById('total-count'), @@ -20,30 +20,27 @@ document.addEventListener('DOMContentLoaded', function() { memoryUsed: document.getElementById('memory-used'), memoryFree: document.getElementById('memory-free'), memoryPercent: document.getElementById('memory-percent'), - avgTick: document.getElementById('avg-tick'), // This element seems unused in updateServerStatus now, can remove if not needed + avgTick: document.getElementById('avg-tick'), uptime: document.getElementById('uptime'), tpsValue: document.getElementById('tps-value'), msptValue: document.getElementById('mspt-value') }; - // 3. 初始化主题 const savedTheme = localStorage.getItem('theme') || 'light'; document.documentElement.setAttribute('data-theme', savedTheme); - // 4. 初始加载语言文件,然后加载其他数据并初始化图表 + // 加载语言 loadLanguage().then(() => { - translatePage(); // 本地化页面静态文本 - initCharts(); // 初始化图表(需要语言文本) - loadAllData(); // 加载动态数据 - // 7. 设置定时刷新(10秒) + translatePage(); + initCharts(); + loadAllData(); + // 定时刷新 const refreshInterval = setInterval(loadAllData, 10000); }).catch(error => { console.error('Failed to load language or initial data:', error); - showError('Failed to load language or initial data.'); // Fallback error message + showError('Failed to load language or initial data.'); }); - - // 5. 事件监听器 if (elements.themeToggle) { elements.themeToggle.addEventListener('click', toggleTheme); } @@ -52,9 +49,8 @@ document.addEventListener('DOMContentLoaded', function() { elements.refreshBtn.addEventListener('click', handleRefresh); } - /*** 功能函数 ***/ - // 新增:加载语言文件 + // 后端加载语言 async function loadLanguage() { try { const response = await fetch('/api/lang'); @@ -63,18 +59,15 @@ document.addEventListener('DOMContentLoaded', function() { console.log('Language file loaded.'); } catch (error) { console.error('Failed to load language file:', error); - // Fallback to English keys if language file fails to load lang = {}; - throw error; // Propagate error to stop further loading if language is critical + throw error; } } - // 新增:根据data-lang-key属性本地化页面元素 function translatePage() { document.querySelectorAll('[data-lang-key]').forEach(element => { const key = element.getAttribute('data-lang-key'); if (lang[key]) { - // Special handling for title tag if (element.tagName === 'TITLE') { document.title = lang[key]; } else { @@ -86,11 +79,9 @@ document.addEventListener('DOMContentLoaded', function() { }); } - // 新增:获取本地化字符串 function getLangString(key, ...args) { - const pattern = lang[key] || key; // Use key as fallback + const pattern = lang[key] || key; try { - // Simple placeholder replacement {0}, {1}, etc. let result = pattern; args.forEach((arg, index) => { result = result.replace(new RegExp('\\{' + index + '\\}', 'g'), arg); @@ -98,19 +89,18 @@ document.addEventListener('DOMContentLoaded', function() { return result; } catch (e) { console.error(`Failed to format string for key: ${key}`, e); - return pattern; // Return unformatted pattern on error + return pattern; } } + // 图表 function initCharts() { - // 内存图表 const memoryCtx = document.getElementById('memory-chart')?.getContext('2d'); if (memoryCtx) { memoryChart = new Chart(memoryCtx, { type: 'doughnut', data: { - // Use localized labels labels: [getLangString('playertime.web.chart.memory_used'), getLangString('playertime.web.chart.memory_free')], datasets: [{ data: [0, 100], @@ -129,7 +119,6 @@ document.addEventListener('DOMContentLoaded', function() { }); } - // 性能图表 const perfCtx = document.getElementById('performance-chart')?.getContext('2d'); if (perfCtx) { performanceChart = new Chart(perfCtx, { @@ -138,7 +127,6 @@ document.addEventListener('DOMContentLoaded', function() { labels: Array(30).fill(''), datasets: [ { - // Use localized labels label: getLangString('playertime.web.chart.tps'), data: tpsHistory, borderColor: '#4CAF50', @@ -150,7 +138,6 @@ document.addEventListener('DOMContentLoaded', function() { yAxisID: 'y' }, { - // Use localized labels label: getLangString('playertime.web.chart.mspt'), data: msptHistory, borderColor: '#FF5722', @@ -203,7 +190,6 @@ document.addEventListener('DOMContentLoaded', function() { position: 'left', title: { display: true, - // Use localized labels text: getLangString('playertime.web.chart.tps'), font: { weight: 'bold' @@ -221,7 +207,6 @@ document.addEventListener('DOMContentLoaded', function() { position: 'right', title: { display: true, - // Use localized labels text: getLangString('playertime.web.chart.mspt'), font: { weight: 'bold' @@ -246,10 +231,10 @@ document.addEventListener('DOMContentLoaded', function() { } function handleRefresh() { - if (this.classList.contains('loading')) return; // Prevent multiple clicks + if (this.classList.contains('loading')) return; this.classList.add('loading'); - loadAllData().finally(() => { // Use finally to ensure loading class is removed - setTimeout(() => { // Add a small delay for visual feedback + loadAllData().finally(() => { + setTimeout(() => { this.classList.remove('loading'); }, 500); }); @@ -257,7 +242,6 @@ document.addEventListener('DOMContentLoaded', function() { async function loadAllData() { try { - // Fetch data concurrently const [statsData, onlinePlayersData, playerCountsData, serverStatusData] = await Promise.all([ fetchData('/api/stats'), fetchData('/api/online-players'), @@ -265,7 +249,6 @@ document.addEventListener('DOMContentLoaded', function() { fetchData('/api/server-status') ]); - // Update UI with fetched data updateTable(statsData); updateOnlinePlayers(onlinePlayersData); updatePlayerCounts(playerCountsData); @@ -273,14 +256,13 @@ document.addEventListener('DOMContentLoaded', function() { } catch (error) { console.error('加载数据出错:', error); - showError(getLangString('playertime.web.error.load_failed')); // Use localized error message + showError(getLangString('playertime.web.error.load_failed')); } } async function fetchData(url) { const response = await fetch(url); if (!response.ok) { - // Attempt to read error body if available const errorBody = await response.text().catch(() => 'Unknown Error'); throw new Error(`HTTP error! status: ${response.status}, URL: ${url}, Body: ${errorBody}`); } @@ -290,9 +272,7 @@ document.addEventListener('DOMContentLoaded', function() { function updateTable(data) { if (!elements.statsTableBody || !elements.statsTableHeader) return; - elements.statsTableBody.innerHTML = ''; // Clear existing rows - - // Update table headers using localization + elements.statsTableBody.innerHTML = ''; const headers = elements.statsTableHeader.querySelectorAll('th[data-lang-key]'); headers.forEach(th => { const key = th.getAttribute('data-lang-key'); @@ -304,25 +284,19 @@ document.addEventListener('DOMContentLoaded', function() { const sortedPlayers = Object.entries(data) .map(([name, statString]) => { - // Parse the localized stat string const stats = {}; - // Split by " | " first statString.split(" | ").forEach(part => { - // Find the first colon to split label and value const firstColonIndex = part.indexOf(':'); if (firstColonIndex > 0) { const label = part.substring(0, firstColonIndex).trim(); const value = part.substring(firstColonIndex + 1).trim(); stats[label] = value; } else { - // Handle cases without colon if necessary, or log a warning console.warn(`Could not parse stat part: ${part}`); } }); - // Map localized labels back to internal keys for sorting const internalStats = {}; - // This mapping assumes the order in the format string is consistent const formatParts = getLangString('playertime.stats.format').split(" | ").map(p => p.split(":")[0].trim()); if (formatParts.length >= 3) { @@ -331,7 +305,6 @@ document.addEventListener('DOMContentLoaded', function() { internalStats['7Days'] = stats[formatParts[2]]; } else { console.error("Unexpected format string structure:", getLangString('playertime.stats.format')); - // Fallback to using the raw statString if parsing fails internalStats.totalTime = statString.split(" | ")[0]?.split(": ")[1] || "0h 00m"; internalStats['30Days'] = statString.split(" | ")[1]?.split(": ")[1] || "0h 00m"; internalStats['7Days'] = statString.split(" | ")[2]?.split(": ")[1] || "0h 00m"; @@ -345,8 +318,8 @@ document.addEventListener('DOMContentLoaded', function() { if (sortedPlayers.length === 0) { const row = elements.statsTableBody.insertRow(); const cell = row.insertCell(0); - cell.colSpan = 4; // Span across all columns - cell.textContent = getLangString('playertime.web.stats_table.empty'); // Use localized empty message + cell.colSpan = 4; + cell.textContent = getLangString('playertime.web.stats_table.empty'); cell.style.textAlign = 'center'; cell.style.fontStyle = 'italic'; return; @@ -374,7 +347,6 @@ document.addEventListener('DOMContentLoaded', function() { element.appendChild(li); }); } else { - // Use localized empty message element.innerHTML = `