[HTML] 纯文本查看 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Pudiu 翻页在线时钟 - 永久免费的全屏专注桌面时钟</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@700&family=Oswald:wght@500;700&family=Share+Tech+Mono&display=swap" rel="stylesheet">
<style>
/* ================= 核心重置与变量 ================= */
* { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--text-main: #f5f5f7;
--text-secondary: #86868b;
--dock-bg: rgba(20, 20, 22, 0.65);
--clock-font: 'Inter', "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
--clock-color: #f5f5f7;
--card-bg-color: #1a1a1c;
--clock-scale: 1;
}
html, body {
width: 100%; height: 100%;
background-color: #000;
color: var(--text-main);
font-family: var(--clock-font);
-webkit-font-smoothing: antialiased;
overflow: hidden;
}
/* ================= 全局物理翻转层 ================= */
.app-root {
width: 100vw; height: 100vh;
transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1);
transform-origin: center center;
position: relative;
}
.app-root.mirror-x { transform: scaleX(-1); }
.app-root.mirror-y { transform: scaleY(-1); }
.app-root.mirror-xy { transform: scale(-1, -1); }
/* ================= 底层背景渲染 ================= */
#bg-layer {
position: absolute; top: 0; left: 0; width: 100vw; height: 100vh;
z-index: 0; background-color: #000; pointer-events: none;
}
#bg-layer img, #bg-layer video {
position: absolute; top: 0; left: 0;
width: 100%; height: 100%; object-fit: cover;
animation: fadeIn 0.8s ease forwards;
}
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
#bg-overlay {
position: absolute; top: 0; left: 0; width: 100vw; height: 100vh;
background: rgba(0, 0, 0, 0.35); z-index: 1; pointer-events: none;
transition: background 0.8s ease;
}
/* ================= 头部导航栏 ================= */
.app-header {
position: absolute; top: 0; left: 0; width: 100%; padding: 25px 40px;
display: flex; justify-content: space-between; align-items: center;
z-index: 50; transition: opacity 0.4s ease, transform 0.4s ease;
}
.header-logo { display: flex; align-items: center; gap: 12px; cursor: pointer; }
.header-logo svg { width: 28px; height: 28px; fill: #fff; }
.header-logo span { font-size: 1.2rem; font-weight: 700; letter-spacing: 2px; color: #fff; text-transform: uppercase; }
.header-lang {
background: rgba(255, 255, 255, 0.1); border: 1px solid rgba(255,255,255,0.2);
padding: 6px 14px; border-radius: 50px; color: #fff; font-size: 0.85rem; font-weight: 500;
cursor: pointer; backdrop-filter: blur(10px); transition: all 0.2s;
}
.header-lang:hover { background: rgba(255, 255, 255, 0.2); }
/* ================= 多国语言切换器 ================= */
.lang-switcher { position: relative; user-select: none; }
.lang-current {
display: flex; align-items: center; gap: 6px;
background: rgba(255, 255, 255, 0.1); border: 1px solid rgba(255,255,255,0.2);
padding: 7px 14px; border-radius: 50px; color: #fff; font-size: 0.85rem; font-weight: 500;
cursor: pointer; backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
transition: all 0.2s; font-family: inherit;
}
.lang-current:hover { background: rgba(255, 255, 255, 0.2); }
.lang-current .lang-globe { font-size: 1rem; line-height: 1; }
.lang-current .lang-caret { font-size: 0.65rem; opacity: 0.7; transition: transform 0.2s; }
.lang-switcher.open .lang-caret { transform: rotate(180deg); }
.lang-menu {
position: absolute; top: calc(100% + 8px); right: 0; min-width: 160px;
background: rgba(20, 20, 22, 0.92); backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px);
border: 1px solid rgba(255,255,255,0.15); border-radius: 12px; padding: 6px; margin: 0;
list-style: none; box-shadow: 0 10px 40px rgba(0,0,0,0.4);
opacity: 0; visibility: hidden; transform: translateY(-6px);
transition: opacity 0.18s ease, transform 0.18s ease, visibility 0.18s; z-index: 100;
}
.lang-switcher.open .lang-menu { opacity: 1; visibility: visible; transform: translateY(0); }
.lang-menu li {
display: flex; align-items: center; gap: 10px; padding: 9px 12px; border-radius: 8px;
color: #fff; font-size: 0.88rem; cursor: pointer; transition: background 0.15s;
}
.lang-menu li:hover { background: rgba(255,255,255,0.12); }
.lang-menu li.active { background: rgba(255,255,255,0.08); color: #8ab4f8; }
.lang-menu .lang-flag { font-size: 1.1rem; line-height: 1; }
/* ================= 悬浮快捷控制台 ================= */
.quick-controls {
position: absolute; left: 40px; bottom: 40px;
display: flex; gap: 15px; z-index: 100;
transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
}
.control-btn {
background: rgba(0, 0, 0, 0.4); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.15); color: #fff; font-size: 1.2rem; cursor: pointer; opacity: 0.7;
transition: all 0.3s cubic-bezier(0.2, 0.8, 0.2, 1);
width: 48px; height: 48px; display: flex; align-items: center; justify-content: center;
border-radius: 50%; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
}
.control-btn:hover, .control-btn.active { opacity: 1; background: rgba(0, 0, 0, 0.8); transform: scale(1.1); border-color: #0a84ff; }
/* ================= 全局主页面滚动 ================= */
.scroll-container {
position: relative; z-index: 5; width: 100vw; height: 100vh;
overflow-y: auto; overflow-x: hidden;
scroll-snap-type: y mandatory; scroll-behavior: smooth;
}
.scroll-container::-webkit-scrollbar { display: none; }
.screen-section {
width: 100vw; min-height: 100vh; scroll-snap-align: start;
position: relative; display: flex; flex-direction: column;
align-items: center; justify-content: center; padding: 20px;
}
/* ================= 第一屏:核心翻页时钟 ================= */
.widget-container {
display: flex; flex-direction: column; align-items: center; justify-content: center;
gap: 4vh; width: 100%; transition: transform 0.8s cubic-bezier(0.25, 1, 0.5, 1);
}
.clock-display { display: flex; align-items: center; justify-content: center; gap: clamp(10px, 3vw, 30px); transform: scale(var(--clock-scale)); transition: transform 0.3s ease; }
.time-group { display: flex; gap: clamp(3px, 1vw, 6px); }
.colon { font-size: clamp(4rem, 10vw, 8rem); color: rgba(255, 255, 255, 0.4); animation: pulse 2s infinite; padding-bottom: 2vh; font-family: 'Inter', sans-serif; }
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.15; } }
.flip-unit {
position: relative; width: clamp(45px, 10vw, 150px); height: clamp(70px, 16vw, 220px);
font-size: clamp(3.5rem, 13vw, 18rem); font-weight: 700; color: var(--clock-color);
border-radius: clamp(8px, 1.5vw, 16px); box-shadow: 0 20px 40px rgba(0,0,0,0.6); perspective: 1500px; background-color: var(--card-bg-color);
}
.flip-unit::after { content: ""; position: absolute; top: 50%; left: 0; width: 100%; height: 2px; background: rgba(0,0,0,0.8); z-index: 10; margin-top: -1px; box-shadow: 0 1px 1px rgba(255,255,255,0.1); }
.card { position: absolute; left: 0; width: 100%; height: 50%; overflow: hidden; backface-visibility: hidden; transform-style: preserve-3d; background-color: var(--card-bg-color); }
.card span { position: absolute; left: 0; width: 100%; height: 200%; display: flex; align-items: center; justify-content: center; line-height: 1; font-variant-numeric: tabular-nums; }
.top-bg, .top-flip { top: 0; transform-origin: bottom; border-top-left-radius: inherit; border-top-right-radius: inherit; }
.top-bg span, .top-flip span { top: 0; }
.bottom-bg, .bottom-flip { bottom: 0; transform-origin: top; border-bottom-left-radius: inherit; border-bottom-right-radius: inherit; }
.bottom-bg span, .bottom-flip span { bottom: 0; }
.top-bg { z-index: 1; } .bottom-bg { z-index: 1; } .top-flip { z-index: 3; } .bottom-flip { z-index: 2; transform: rotateX(90deg); }
.is-flipping .top-flip { animation: flipTop 0.4s cubic-bezier(0.3, 0, 0.8, 0.15) forwards; }
.is-flipping .bottom-flip { animation: flipBottom 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.15) 0.4s forwards; }
@keyframes flipTop { 0% { transform: rotateX(0deg); filter: brightness(1); } 100% { transform: rotateX(-90deg); filter: brightness(0.2); } }
@keyframes flipBottom { 0% { transform: rotateX(90deg); filter: brightness(0.2); } 100% { transform: rotateX(0deg); filter: brightness(1); } }
/* ================= 透明底座与文字信息 ================= */
.info-footer {
display: flex; flex-direction: column; align-items: center; gap: 6px;
background: transparent;
padding: 16px 40px; border-radius: 24px;
border: 1px solid rgba(255, 255, 255, 0.15);
transition: transform 0.3s ease;
position: relative;
}
.city-name { font-size: clamp(1.4rem, 2.5vw, 1.8rem); font-weight: 700; letter-spacing: 4px; text-transform: uppercase; display: flex; align-items: center; justify-content: center; gap: 10px; color: #fff; text-shadow: 0 4px 15px rgba(0,0,0,0.8); }
.ampm-badge { font-size: clamp(0.75rem, 1.2vw, 0.9rem); font-weight: 600; background: rgba(255,255,255,0.15); padding: 4px 10px; border-radius: 8px; letter-spacing: 1px; color: #fff; display: none; border: 1px solid rgba(255,255,255,0.1); }
.date-display { font-size: clamp(0.95rem, 1.5vw, 1.1rem); color: rgba(255, 255, 255, 0.9); font-weight: 600; letter-spacing: 2px; text-shadow: 0 2px 10px rgba(0,0,0,0.8); margin-top: 5px; }
/* ================= 时区 Dock 与全域选择中心 ================= */
.dock-wrapper {
position: absolute; bottom: 30px; left: 50%; transform: translateX(-50%);
display: flex; z-index: 10; transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
width: 100%; max-width: 800px; justify-content: center;
}
.dock-container {
display: flex; gap: 10px; background: var(--dock-bg); padding: 10px 14px;
border-radius: 100px; backdrop-filter: blur(25px); border: 1px solid rgba(255,255,255,0.08);
max-width: 95vw; overflow-x: auto;
}
.dock-container::-webkit-scrollbar { display: none; }
.dock-btn {
background: transparent; border: none; color: #aaa; font-size: 0.95rem; font-weight: 600;
padding: 10px 20px; border-radius: 100px; cursor: pointer; display: flex; align-items: center; gap: 8px;
white-space: nowrap; transition: all 0.3s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.dock-btn:hover { color: #fff; background: rgba(255, 255, 255, 0.1); }
.dock-btn.active { background: rgba(255,255,255,0.95); color: #000; box-shadow: 0 4px 15px rgba(255,255,255,0.2); }
.dock-btn.add-btn { border: 1px dashed rgba(255,255,255,0.3); color: #fff; }
.dock-btn.add-btn:hover { border-style: solid; background: rgba(255,255,255,0.15); }
.tz-modal {
position: fixed; top: 0; left: 0; width: 100vw; height: 100vh;
background: rgba(0, 0, 0, 0.65); backdrop-filter: blur(15px); -webkit-backdrop-filter: blur(15px);
display: flex; align-items: center; justify-content: center; z-index: 3000;
opacity: 0; pointer-events: none; transition: opacity 0.3s ease;
}
.tz-modal.show { opacity: 1; pointer-events: auto; }
.tz-content {
background: rgba(18, 18, 20, 0.9); border: 1px solid rgba(255,255,255,0.1);
width: 90%; max-width: 700px; max-height: 80vh; border-radius: 24px;
display: flex; flex-direction: column; box-shadow: 0 30px 60px rgba(0,0,0,0.6);
transform: scale(0.95) translateY(10px); transition: transform 0.3s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.tz-modal.show .tz-content { transform: scale(1) translateY(0); }
.tz-header {
padding: 25px 30px; border-bottom: 1px solid rgba(255,255,255,0.05);
display: flex; justify-content: space-between; align-items: center;
}
.tz-header h3 { color: #fff; font-size: 1.2rem; font-weight: 700; letter-spacing: 1px;}
.close-tz { background: transparent; border: none; color: #888; font-size: 1.8rem; cursor: pointer; transition: color 0.2s;}
.close-tz:hover { color: #fff; }
.tz-body { padding: 25px 30px; overflow-y: auto; flex: 1; }
.tz-body::-webkit-scrollbar { display: none; }
.tz-group-title { color: #888; font-size: 0.9rem; margin: 20px 0 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 2px;}
.tz-group-title:first-child { margin-top: 0; }
.tz-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 12px; }
.tz-item {
background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.05);
border-radius: 12px; padding: 12px 15px; color: #ddd; font-size: 0.95rem; font-weight: 500;
cursor: pointer; transition: all 0.2s; display: flex; align-items: center; gap: 8px;
}
.tz-item:hover { background: rgba(255,255,255,0.15); color: #fff; transform: translateY(-2px); border-color: rgba(255,255,255,0.2);}
.scroll-down-hint {
position: absolute; bottom: 100px; left: 50%; transform: translateX(-50%);
display: flex; flex-direction: column; align-items: center; opacity: 0.85;
animation: bounce 2s infinite; cursor: pointer; z-index: 150; transition: all 0.5s ease;
color: #fff; text-shadow: 0 2px 10px rgba(0,0,0,0.5);
}
.scroll-down-hint span { font-size: 0.85rem; letter-spacing: 2px; margin-bottom: 5px; text-transform: uppercase;}
.scroll-down-hint svg { width: 24px; height: 24px; fill: #fff; filter: drop-shadow(0 2px 5px rgba(0,0,0,0.5));}
@keyframes bounce { 0%, 20%, 50%, 80%, 100% { transform: translateY(0) translateX(-50%); } 40% { transform: translateY(-10px) translateX(-50%); } 60% { transform: translateY(-5px) translateX(-50%); } }
/* ================= 侧边抽屉设置 ================= */
.sidebar-wrapper {
position: absolute; top: 0; right: 0; width: 380px; height: 100vh;
transform: translateX(100%); transition: transform 0.5s cubic-bezier(0.2, 0.8, 0.2, 1);
z-index: 2000; will-change: transform; display: flex; align-items: center;
}
.sidebar-wrapper.open { transform: translateX(0); }
#settings-btn { position: absolute; left: -75px; bottom: 40px; width: 54px; height: 54px; font-size: 1.5rem; box-shadow: -5px 5px 20px rgba(0,0,0,0.5); }
.sidebar-panel {
width: 100%; height: 100%; padding: 40px 25px; overflow-y: auto; overflow-x: hidden;
background: rgba(18, 18, 20, 0.85); backdrop-filter: blur(25px); -webkit-backdrop-filter: blur(25px);
border-left: 1px solid rgba(255,255,255,0.1); box-shadow: -10px 0 40px rgba(0,0,0,0.6);
}
.sidebar-panel::-webkit-scrollbar { display: none; }
.sidebar-header-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; padding-bottom: 10px; border-bottom: 1px solid rgba(255,255,255,0.1); }
.sidebar-header-row .section-title { margin: 0; border: none; padding: 0; }
.sidebar-close-btn { background: transparent; border: none; color: #888; font-size: 2rem; cursor: pointer; transition: color 0.2s; line-height: 1; display: none; }
.sidebar-close-btn:hover { color: #fff; }
.section-title { font-size: 1.05rem; font-weight: 700; color: #fff; margin: 25px 0 15px; border-bottom: 1px solid rgba(255,255,255,0.1); padding-bottom: 8px; letter-spacing: 1px;}
.section-title:first-child { margin-top: 0; }
.form-group { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; background: rgba(255,255,255,0.03); padding: 12px 16px; border-radius: 12px; border: 1px solid rgba(255,255,255,0.02);}
.form-group label { font-size: 0.95rem; color: #ddd; font-weight: 500; }
.custom-select { background: #2a2a2d; color: #fff; border: 1px solid rgba(255,255,255,0.15); padding: 6px 10px; border-radius: 8px; font-size: 0.9rem; outline: none; font-family: inherit; cursor: pointer; width: 135px;}
.color-picker { -webkit-appearance: none; width: 28px; height: 28px; border: none; border-radius: 50%; cursor: pointer; background: transparent; padding: 0; }
.color-picker::-webkit-color-swatch-wrapper { padding: 0; }
.color-picker::-webkit-color-swatch { border: 2px solid rgba(255,255,255,0.2); border-radius: 50%; }
.switch { position: relative; display: inline-block; width: 44px; height: 24px; }
.switch input { opacity: 0; width: 0; height: 0; }
.slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #444; transition: .4s; border-radius: 34px; }
.slider:before { position: absolute; content: ""; height: 18px; width: 18px; left: 3px; bottom: 3px; background-color: white; transition: .4s; border-radius: 50%; }
input:checked + .slider { background-color: #0a84ff; }
input:checked + .slider:before { transform: translateX(20px); }
input[type=range] { -webkit-appearance: none; width: 110px; background: transparent; }
input[type=range]::-webkit-slider-thumb { -webkit-appearance: none; height: 16px; width: 16px; border-radius: 50%; background: #fff; cursor: pointer; margin-top: -6px; box-shadow: 0 2px 5px rgba(0,0,0,0.5); }
input[type=range]::-webkit-slider-runnable-track { width: 100%; height: 4px; cursor: pointer; background: #555; border-radius: 2px; }
.custom-file-btn { background: #2a2a2d; color: #fff; border: 1px solid rgba(255,255,255,0.15); padding: 6px 12px; border-radius: 8px; font-size: 0.85rem; cursor: pointer; transition: all 0.2s; font-weight: 500; display: inline-block; }
.custom-file-btn:hover { background: rgba(255,255,255,0.1); border-color: #0a84ff; }
/* 隐藏原生 file input,避免浏览器自带的“选择文件/未选择文件”文案不跟随页面语言切换 */
#custom-audio-upload { display: none; }
.playlist-group { flex-direction: column; align-items: flex-start !important; gap: 10px !important; }
.playlist-group > label:first-child { width: 100%; margin-bottom: 4px; }
.playlist-container {
width: 100%; max-height: 180px; overflow-y: auto;
border: 1px solid rgba(255,255,255,0.1); border-radius: 8px;
background: rgba(0,0,0,0.2); padding: 4px;
}
.playlist-container:empty { display: none; }
.playlist-item {
display: flex; align-items: center; gap: 8px;
padding: 8px 10px; border-radius: 6px;
font-size: 0.82rem; color: rgba(255,255,255,0.85);
cursor: pointer; transition: background 0.15s;
}
.playlist-item:hover { background: rgba(255,255,255,0.08); }
.playlist-item.active {
background: rgba(10,132,255,0.2); color: #8ab4f8;
}
.playlist-item .track-num {
width: 20px; text-align: center; font-size: 0.75rem; opacity: 0.5; flex-shrink: 0;
}
.playlist-item .track-name {
flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.playlist-item .track-del {
width: 20px; text-align: center; opacity: 0; transition: opacity 0.15s;
color: rgba(255,255,255,0.5); font-size: 0.9rem;
}
.playlist-item:hover .track-del { opacity: 1; }
.playlist-item .track-del:hover { color: #ff6b6b; }
.playlist-container::-webkit-scrollbar { width: 6px; }
.playlist-container::-webkit-scrollbar-track { background: transparent; }
.playlist-container::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.15); border-radius: 3px; }
.btn-reset { margin-top: 25px; width: 100%; padding: 14px; background: rgba(255, 59, 48, 0.1); border: 1px solid rgba(255, 59, 48, 0.4); color: #ff3b30; border-radius: 10px; font-weight: 600; cursor: pointer; transition: all 0.2s; font-size: 0.95rem; letter-spacing: 1px;}
.btn-reset:hover { background: rgba(255, 59, 48, 0.25); color: #fff; }
/* ================= 第二/三屏专属样式 ================= */
.media-panel, .about-panel {
width: 95%; max-width: 1400px; padding: clamp(30px, 5vw, 60px);
background: rgba(15, 15, 18, 0.75); backdrop-filter: blur(25px); -webkit-backdrop-filter: blur(25px);
border: 1px solid rgba(255,255,255,0.08); border-radius: 28px; box-shadow: 0 40px 80px rgba(0,0,0,0.6);
}
.gallery-title { font-size: clamp(1.4rem, 2.5vw, 1.8rem); font-weight: 700; color: #fff; margin-bottom: 30px; text-align: center; letter-spacing: 2px;}
.media-gallery { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 20px; }
.bg-card { background: #2a2a2d; border-radius: 16px; overflow: hidden; cursor: pointer; border: 2px solid transparent; transition: all 0.2s; position: relative; }
.bg-card:hover { transform: translateY(-5px); box-shadow: 0 15px 30px rgba(0,0,0,0.5); }
.bg-card.active { border-color: #0a84ff; box-shadow: 0 0 0 2px rgba(10, 132, 255, 0.4); }
.bg-cover { width: 100%; height: 140px; object-fit: cover; background: #111; display: block; }
.bg-info { padding: 12px; display: flex; flex-direction: column; gap: 4px; text-align: center; }
.bg-title { font-size: 1rem; color: #fff; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.bg-type-badge { position: absolute; top: 12px; left: 12px; background: rgba(0,0,0,0.7); color: #fff; font-size: 0.75rem; padding: 4px 10px; border-radius: 6px; backdrop-filter: blur(4px); }
/* 修复上传卡片变形问题 */
.upload-card {
border: 2px dashed rgba(255,255,255,0.3); background: rgba(0,0,0,0.2);
display: flex; flex-direction: column; align-items: center; justify-content: center;
color: #aaa; min-height: 185px; transition: all 0.3s ease;
cursor: pointer; text-align: center; padding: 20px;
}
.upload-card:hover { border-color: #0a84ff; color: #fff; background: rgba(10, 132, 255, 0.1); }
.upload-icon { font-size: 2.5rem; margin-bottom: 12px; color: inherit; }
.upload-text { font-size: 0.95rem; font-weight: 500; letter-spacing: 1px; color: inherit; }
/* 关于面板 */
.about-panel { display: flex; flex-direction: column; gap: 40px; }
.about-hero { text-align: center; max-width: 900px; margin: 0 auto; }
.about-hero h2 { font-size: clamp(2rem, 4vw, 2.8rem); color: #fff; margin-bottom: 20px; font-weight: 700; letter-spacing: 2px; }
.about-hero p { font-size: clamp(1rem, 1.5vw, 1.15rem); color: #ccc; line-height: 1.8; font-weight: 400; }
.about-hero .highlight { color: #fff; font-weight: 600; }
.feature-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 20px; }
.feature-card { background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.05); border-radius: 20px; padding: 25px; transition: all 0.3s ease; }
.feature-card:hover { transform: translateY(-5px); background: rgba(255,255,255,0.08); border-color: rgba(255,255,255,0.2); box-shadow: 0 10px 25px rgba(0,0,0,0.3); }
.feature-icon { font-size: 2.2rem; margin-bottom: 15px; display: inline-block;}
.feature-title { font-size: 1.1rem; color: #fff; font-weight: 600; margin-bottom: 8px; letter-spacing: 1px; }
.feature-desc { font-size: 0.9rem; color: #999; line-height: 1.6; }
.about-footer { text-align: center; padding-top: 20px; border-top: 1px solid rgba(255,255,255,0.1); margin-top: 20px; }
.about-footer p { color: #888; font-size: 0.95rem; letter-spacing: 1px; }
.friendly-links { display: flex; flex-wrap: wrap; justify-content: center; align-items: center; gap: 15px; margin-top: 20px; padding-top: 20px; border-top: 1px dotted rgba(255,255,255,0.1); }
.links-title { color: #666; font-size: 0.9rem; font-weight: 600; letter-spacing: 1px; }
.friendly-links a { color: #aaa; font-size: 0.9rem; text-decoration: none; transition: color 0.2s; font-family: 'Inter', sans-serif;}
.friendly-links a:hover { color: #fff; text-decoration: underline; }
/* ================= 移动端适配 ================= */
@media (max-width: 768px) {
.app-header { padding: 15px 20px; }
.header-logo span { font-size: 1rem; letter-spacing: 1px; }
.header-lang { padding: 4px 10px; font-size: 0.75rem; }
.lang-current { padding: 5px 10px; font-size: 0.75rem; }
.lang-menu { min-width: 140px; }
.quick-controls { left: 15px; bottom: 85px; gap: 10px; flex-wrap: wrap; max-width: 160px; }
.control-btn { width: 42px; height: 42px; font-size: 1.1rem; }
.dock-wrapper { bottom: 15px; padding: 0 10px; }
.dock-container { width: 100%; justify-content: flex-start; padding: 6px 10px; }
.dock-btn { font-size: 0.85rem; padding: 8px 14px; }
.sidebar-wrapper { width: 100vw; }
#settings-btn { left: -60px; bottom: 30px; width: 48px; height: 48px; }
#audio-controls { left: -60px !important; bottom: 30px !important; gap: 6px !important; flex-wrap: wrap; max-width: 250px; }
#audio-controls .control-btn { width: 40px !important; height: 40px !important; font-size: 1rem !important; }
#audio-volume-box { height: 40px !important; padding: 0 10px !important; }
#audio-volume-box input[type=range] { width: 70px !important; }
#setting-play-mode-inline { height: 40px !important; font-size: 0.8rem !important; padding: 0 8px !important; }
.sidebar-close-btn { display: block; }
.media-gallery { grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); }
.bg-cover { height: 100px; }
.upload-card { min-height: 145px; }
.scroll-hint { bottom: 95px; }
.feature-grid { grid-template-columns: 1fr; }
}
/* ================= 沉浸全屏模式与锁屏模式 (样式复用与隐藏) ================= */
body.fullscreen-mode .app-header, body.standby-mode .app-header { opacity: 0; transform: translateY(-30px); pointer-events: none; }
body.fullscreen-mode .quick-controls, body.standby-mode .quick-controls { opacity: 0; transform: translateY(30px); pointer-events: none; }
body.fullscreen-mode .dock-wrapper, body.standby-mode .dock-wrapper { opacity: 0; transform: translate(-50%, 30px); pointer-events: none; }
body.fullscreen-mode .scroll-hint, body.standby-mode .scroll-hint { opacity: 0; pointer-events: none; }
body.fullscreen-mode .sidebar-wrapper, body.standby-mode .sidebar-wrapper { opacity: 0; pointer-events: none; }
/* 修复后的独立底层锁屏面板 */
#standby-hint {
position: fixed; bottom: 10vh; left: 50%; transform: translateX(-50%) translateY(20px);
color: rgba(255, 255, 255, 0.9); font-size: 1.05rem; letter-spacing: 2px;
opacity: 0; pointer-events: none; transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1); z-index: 9999;
display: flex; flex-direction: column; align-items: center; gap: 8px;
font-weight: 500; font-family: "PingFang SC", sans-serif;
background: rgba(20, 20, 22, 0.6); padding: 18px 30px; border-radius: 16px;
backdrop-filter: blur(15px); -webkit-backdrop-filter: blur(15px);
border: 1px solid rgba(255,255,255,0.1); box-shadow: 0 10px 30px rgba(0,0,0,0.5);
}
body.standby-mode #standby-hint { opacity: 1; transform: translateX(-50%) translateY(0); pointer-events: auto; cursor: pointer; }
#standby-hint svg { width: 28px; height: 28px; fill: currentColor; animation: pulse-up 2s infinite cubic-bezier(0.4, 0, 0.2, 1); }
@keyframes pulse-up { 0%, 100% { transform: translateY(0); opacity: 0.6; } 50% { transform: translateY(-8px); opacity: 1; } }
body.standby-mode .scroll-container { overflow-y: hidden; }
body.standby-mode #bg-overlay { background: rgba(0, 0, 0, 0.5); backdrop-filter: blur(25px); -webkit-backdrop-filter: blur(25px); }
body.standby-mode .widget-container { transform: scale(0.9); }
</style>
</head>
<body>
<audio id="ambient-audio" loop preload="none">
<source src="https://cdn.pixabay.com/audio/2022/05/27/audio_1808fbf07a.mp3" type="audio/mpeg">
</audio>
<div id="app-root" class="app-root">
<div id="bg-layer"></div>
<div id="bg-overlay"></div>
<div id="standby-hint">
<svg viewBox="0 0 24 24"><path d="M12 2C9.24 2 7 4.24 7 7v3H6c-1.1 0-2 .9-2 2v8c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2v-8c0-1.1-.9-2-2-2h-1V7c0-2.76-2.24-5-5-5zm3 8H9V7c0-1.66 1.34-3 3-3s3 1.34 3 3v3zm-3 6c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2z"/></svg>
<span id="standby-hint-text">点击屏幕或按任意键解锁</span>
</div>
<div id="lock-password-modal" style="display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.8);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);z-index:9999;align-items:center;justify-content:center;">
<div style="background:#2a2a2d;border:1px solid rgba(255,255,255,0.15);border-radius:16px;padding:30px;width:320px;box-shadow:0 20px 60px rgba(0,0,0,0.5);">
<div style="text-align:center;margin-bottom:20px;">
<div style="font-size:3rem;margin-bottom:10px;">🔒</div>
<div style="color:#fff;font-size:1.2rem;font-weight:600;" id="lock-password-modal-title">输入锁屏密码</div>
</div>
<div style="margin-bottom:15px;">
<input type="password" id="lock-password-input" placeholder="密码" style="width:100%;padding:12px;border-radius:8px;border:1px solid rgba(255,255,255,0.15);background:#1a1a1c;color:#fff;font-size:1rem;text-align:center;outline:none;">
</div>
<div id="lock-password-error" style="color:#ff6b6b;font-size:0.85rem;text-align:center;margin-bottom:15px;min-height:20px;"></div>
<div style="display:flex;gap:10px;">
<button id="lock-password-cancel" class="btn-reset" style="flex:1;background:#3a3a3d;color:#aaa;">取消</button>
<button id="lock-password-submit" class="btn-reset" style="flex:1;">确认</button>
</div>
</div>
</div>
<header class="app-header" id="app-header">
<div class="header-logo">
<svg viewBox="0 0 24 24"><path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm.5-13H11v6l5.25 3.15.75-1.23-4.5-2.67z"/></svg>
<span id="site-title">Pudiu 翻页在线时钟</span>
</div>
<div class="lang-switcher" id="lang-switcher">
<button class="lang-current" id="lang-current-btn" type="button" aria-haspopup="listbox" aria-expanded="false">
<span class="lang-globe">🌐</span>
<span class="lang-current-name" id="lang-current-name">简体中文</span>
<span class="lang-caret">▾</span>
</button>
<ul class="lang-menu" id="lang-menu" role="listbox">
<li role="option" data-lang="zh"><span class="lang-flag">🇨🇳</span><span class="lang-name">简体中文</span></li>
<li role="option" data-lang="zh-tw"><span class="lang-flag">🇹🇼</span><span class="lang-name">繁體中文</span></li>
<li role="option" data-lang="en"><span class="lang-flag">🇬🇧</span><span class="lang-name">English</span></li>
<li role="option" data-lang="ja"><span class="lang-flag">🇯🇵</span><span class="lang-name">日本語</span></li>
<li role="option" data-lang="ko"><span class="lang-flag">🇰🇷</span><span class="lang-name">한국어</span></li>
</ul>
</div>
</header>
<div class="quick-controls" id="quick-controls">
<button id="btn-fullscreen" class="control-btn" title="全屏沉浸模式 (双击屏幕退出)">⛶</button>
<button id="btn-standby" class="control-btn" title="一键进入锁屏 (专注模式)">🔒</button>
<button id="btn-next-bg" class="control-btn" title="一键随机必应壁纸">🖼️</button>
<button id="btn-flip-x" class="control-btn" title="界面水平翻转 (镜像)">↔</button>
<button id="btn-flip-y" class="control-btn" title="界面垂直翻转 (倒影)">↕</button>
<button id="btn-share" class="control-btn" title="分享本站给朋友">🔗</button>
<button id="btn-favorite" class="control-btn" title="收藏本站">⭐</button>
<button id="btn-countdown" class="control-btn" title="倒计时模式"><svg t="1784116564885" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5245" width="20" height="20"><path d="M766.208 958.336H269.696c-112.768 0-204.032-91.392-204.032-204.032V257.792c0-112.64 91.264-204.032 204.032-204.032h496.512c112.64 0 204.032 91.392 204.032 204.032v496.512c0 112.768-91.392 204.032-204.032 204.032z" fill="#FF9429" p-id="5246"></path><path d="M705.408 743.04c-1.664-10.624-3.84-20.992-6.272-30.976-1.152-4.736-2.688-9.216-4.096-13.824-1.792-6.016-3.456-12.032-5.632-17.792-1.92-5.248-3.968-10.112-6.144-15.232-2.048-4.736-3.968-9.6-6.144-14.208-2.432-5.12-5.12-10.112-7.808-14.976-2.176-3.968-4.48-7.936-6.784-11.776-2.944-4.864-6.144-9.472-9.344-14.08-2.432-3.456-4.864-6.656-7.424-9.856-3.456-4.352-7.04-8.704-10.752-12.8-2.688-2.816-5.376-5.504-8.064-8.192-3.84-3.84-7.808-7.552-11.904-11.008-2.944-2.432-6.016-4.48-8.96-6.656-4.224-3.072-8.32-6.272-12.672-8.832-0.256-0.128-0.512-0.384-0.768-0.512-13.312-7.936-21.376-22.016-21.376-37.504v-17.536c0-15.488 8.192-29.568 21.376-37.504 0.256-0.128 0.512-0.384 0.768-0.512 4.352-2.688 8.448-5.76 12.672-8.832 3.072-2.176 6.016-4.224 8.96-6.656 4.096-3.456 7.936-7.168 11.904-11.008 2.688-2.688 5.504-5.376 8.192-8.192 3.712-4.096 7.296-8.32 10.752-12.672 2.56-3.2 4.992-6.528 7.424-9.856 3.2-4.48 6.4-9.216 9.344-14.08 2.432-3.84 4.608-7.808 6.784-11.776 2.688-4.864 5.376-9.856 7.808-14.976 2.176-4.608 4.224-9.344 6.144-14.208 2.048-4.992 4.224-9.984 6.144-15.232 2.048-5.76 3.84-11.776 5.632-17.792 1.408-4.608 2.944-9.088 4.096-13.824 2.56-10.112 4.608-20.48 6.272-30.976 0.128-0.896 0.384-1.664 0.512-2.56 4.096-26.752-16.384-51.072-43.52-51.072H373.376c-27.136 0-47.616 24.32-43.52 51.072 0.128 0.896 0.384 1.664 0.512 2.56 1.664 10.624 3.84 20.992 6.272 31.104 1.152 4.736 2.688 9.216 4.096 13.824 1.792 6.016 3.456 12.032 5.632 17.792 1.92 5.248 3.968 10.112 6.144 15.232 2.048 4.736 3.968 9.6 6.144 14.208 2.432 5.12 5.12 10.112 7.808 14.976 2.176 3.968 4.48 7.936 6.784 11.776 2.944 4.864 6.144 9.472 9.344 14.08 2.432 3.456 4.864 6.656 7.424 9.856 3.456 4.352 7.04 8.704 10.752 12.672 2.688 2.816 5.376 5.504 8.192 8.192 3.84 3.84 7.808 7.552 11.904 11.008 2.944 2.432 6.016 4.48 8.96 6.656 4.224 3.072 8.32 6.272 12.672 8.832 0.256 0.128 0.512 0.384 0.768 0.512 13.312 7.936 21.376 22.016 21.376 37.504v17.536c0 15.488-8.192 29.568-21.376 37.504-0.256 0.128-0.512 0.384-0.768 0.512-4.352 2.688-8.448 5.76-12.672 8.832-2.944 2.176-6.016 4.224-8.96 6.656-4.096 3.456-7.936 7.168-11.904 11.008-2.688 2.688-5.504 5.248-8.064 8.192-3.712 4.096-7.296 8.32-10.752 12.8-2.56 3.2-4.992 6.528-7.424 9.856-3.2 4.608-6.4 9.216-9.344 14.08-2.304 3.84-4.608 7.808-6.784 11.776-2.688 4.864-5.376 9.856-7.808 14.976-2.176 4.608-4.224 9.344-6.144 14.208-2.048 4.992-4.224 9.984-6.144 15.232-2.048 5.76-3.84 11.776-5.632 17.792-1.408 4.608-2.944 9.088-4.096 13.824-2.56 10.112-4.608 20.48-6.272 31.104-0.128 0.896-0.384 1.664-0.512 2.56-4.096 26.752 16.384 51.072 43.52 51.072h289.024c27.136 0 47.616-24.32 43.52-51.072-0.256-1.024-0.512-1.92-0.64-2.816z" fill="#FFFFFF" p-id="5247"></path><path d="M415.872 755.712h204.16c7.68 0 11.264-9.728 5.248-14.592l-93.952-73.216c-1.92-1.536-2.944-3.84-2.944-6.4V425.856c0-2.304 1.024-4.48 2.688-6.016l55.936-45.952c5.504-5.12 1.92-14.336-5.632-14.336H454.4c-7.552 0-11.136 9.216-5.632 14.336l55.296 45.696c1.664 1.536 2.688 3.712 2.688 6.016v235.648c0 2.432-1.152 4.736-2.944 6.4L410.496 741.12c-5.888 4.992-2.432 14.592 5.376 14.592z" fill="#FF9429" p-id="5248"></path></svg></button>
<button id="btn-timer" class="control-btn" title="计时器模式">🕐</button>
</div>
<div class="sidebar-wrapper" id="sidebar-wrapper">
<div id="audio-controls" style="position:absolute;left:-585px;bottom:40px;display:flex;gap:10px;align-items:center;">
<button id="audio-prev" class="control-btn" title="上一曲" style="width:48px;height:48px;font-size:1.2rem;">◀◀</button>
<button id="audio-play-pause" class="control-btn" title="播放/暂停" style="width:48px;height:48px;font-size:1.2rem;">▶</button>
<button id="audio-next" class="control-btn" title="下一曲" style="width:48px;height:48px;font-size:1.2rem;">▶▶</button>
<div id="audio-volume-box" style="display:flex;align-items:center;gap:8px;background:rgba(0,0,0,0.4);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border:1px solid rgba(255,255,255,0.15);border-radius:50px;padding:0 15px;height:48px;">
<span style="color:#fff;font-size:0.9rem;opacity:0.8;">🔊</span>
<input type="range" id="setting-volume-inline" min="0" max="1" step="0.05" value="0.5" style="width:100px;">
</div>
<select id="setting-play-mode-inline" class="custom-select" style="height:48px;border-radius:50px;padding:0 12px;width:auto;">
<option value="list">列表循环</option>
<option value="single">单曲循环</option>
<option value="random">随机播放</option>
</select>
</div>
<button id="settings-btn" class="control-btn" title="系统高级设置">⚙️</button>
<div class="sidebar-panel">
<div class="sidebar-header-row">
<div class="section-title">系统高级设置</div>
<button id="close-sidebar-btn" class="sidebar-close-btn" title="关闭设置">×</button>
</div>
<div class="form-group"><label>时钟部件可见</label><label class="switch"><input type="checkbox" id="setting-show-clock" checked><span class="slider"></span></label></div>
<div class="form-group">
<label>时间制式</label>
<select class="custom-select" id="setting-hour-format">
<option value="24">24 小时制</option>
<option value="12">12 小时制</option>
</select>
</div>
<div class="form-group"><label>整体缩放</label><input type="range" id="setting-scale" min="0.5" max="1.5" step="0.1" value="1"></div>
<div class="section-title">字体与UI色彩</div>
<div class="form-group">
<label>数字字体</label>
<select class="custom-select" id="setting-font">
<option value="'Inter', sans-serif">现代 (Inter)</option>
<option value="'Oswald', sans-serif">力量 (Oswald)</option>
<option value="'Bebas Neue', sans-serif">海报 (Bebas)</option>
<option value="'JetBrains Mono', monospace">极客 (JetBrains)</option>
</select>
</div>
<div class="form-group"><label>开启农历</label><label class="switch"><input type="checkbox" id="setting-show-lunar"><span class="slider"></span></label></div>
<div class="form-group"><label>卡片底板颜色</label><input type="color" class="color-picker" id="setting-card-bg" value="#1a1a1c"></div>
<div class="form-group"><label>翻页数字颜色</label><input type="color" class="color-picker" id="setting-font-color" value="#f5f5f7"></div>
<div class="section-title">壁纸设置</div>
<div class="form-group"><label>壁纸自动切换</label><label class="switch"><input type="checkbox" id="setting-bg-auto-switch"><span class="slider"></span></label></div>
<div class="form-group">
<label>自动切换时长</label>
<input type="number" id="setting-bg-switch-interval" min="1" max="1440" value="30" style="width:100px;padding:6px 10px;border-radius:8px;border:1px solid rgba(255,255,255,0.15);background:#2a2a2d;color:#fff;font-size:0.9rem;font-family:inherit;text-align:center;">
</div>
<div class="section-title">沉浸白噪音 (BGM)</div>
<div class="form-group"><label>开启背景音乐</label><label class="switch"><input type="checkbox" id="setting-audio-toggle"><span class="slider"></span></label></div>
<div class="form-group playlist-group" style="background: rgba(255,255,255,0.06);">
<label>播放列表</label>
<label for="custom-audio-upload" class="custom-file-btn">选择 MP3...</label>
<input type="file" id="custom-audio-upload" accept="audio/*" multiple>
<div class="playlist-container" id="playlist-container"></div>
</div>
<div class="section-title" id="section-lock-password">锁屏密码</div>
<div class="form-group"><label id="label-lock-password-enable">启用锁屏密码</label><label class="switch"><input type="checkbox" id="setting-lock-password-enable"><span class="slider"></span></label></div>
<div id="lock-password-panel" style="display:none;background:rgba(0,0,0,0.3);border-radius:12px;padding:15px;margin-top:8px;">
<div id="lock-password-set" style="display:none;">
<div class="form-group"><label id="label-lock-password-new">新密码</label><input type="password" id="input-lock-password-new" style="width:100%;padding:8px 12px;border-radius:8px;border:1px solid rgba(255,255,255,0.15);background:#2a2a2d;color:#fff;font-size:0.9rem;"></div>
<div class="form-group"><label id="label-lock-password-confirm">确认密码</label><input type="password" id="input-lock-password-confirm" style="width:100%;padding:8px 12px;border-radius:8px;border:1px solid rgba(255,255,255,0.15);background:#2a2a2d;color:#fff;font-size:0.9rem;"></div>
<button id="btn-lock-password-set" class="btn-reset" style="width:100%;margin-top:5px;">设置密码</button>
</div>
<div id="lock-password-change" style="display:none;">
<div class="form-group"><label id="label-lock-password-old">原密码</label><input type="password" id="input-lock-password-old" style="width:100%;padding:8px 12px;border-radius:8px;border:1px solid rgba(255,255,255,0.15);background:#2a2a2d;color:#fff;font-size:0.9rem;"></div>
<div class="form-group"><label id="label-lock-password-new2">新密码</label><input type="password" id="input-lock-password-new2" style="width:100%;padding:8px 12px;border-radius:8px;border:1px solid rgba(255,255,255,0.15);background:#2a2a2d;color:#fff;font-size:0.9rem;"></div>
<div class="form-group"><label id="label-lock-password-confirm2">确认密码</label><input type="password" id="input-lock-password-confirm2" style="width:100%;padding:8px 12px;border-radius:8px;border:1px solid rgba(255,255,255,0.15);background:#2a2a2d;color:#fff;font-size:0.9rem;"></div>
<div style="display:flex;gap:8px;margin-top:5px;">
<button id="btn-lock-password-change" class="btn-reset" style="flex:1;">修改密码</button>
<button id="btn-lock-password-remove" class="btn-reset" style="flex:1;background:#4a2a2a;color:#ff9999;">清除密码</button>
</div>
</div>
</div>
<button class="btn-reset" id="reset-btn">清空缓存并重置系统</button>
</div>
</div>
<div class="scroll-container" id="scroll-app">
<section class="screen-section" id="screen-clock">
<div class="widget-container" id="main-widget">
<div class="clock-display">
<div class="time-group">
<div class="flip-unit" id="hours-tens"><div class="card top-bg"><span>0</span></div><div class="card bottom-bg"><span>0</span></div><div class="card top-flip"><span>0</span></div><div class="card bottom-flip"><span>0</span></div></div>
<div class="flip-unit" id="hours-ones"><div class="card top-bg"><span>0</span></div><div class="card bottom-bg"><span>0</span></div><div class="card top-flip"><span>0</span></div><div class="card bottom-flip"><span>0</span></div></div>
</div>
<div class="colon">:</div>
<div class="time-group">
<div class="flip-unit" id="minutes-tens"><div class="card top-bg"><span>0</span></div><div class="card bottom-bg"><span>0</span></div><div class="card top-flip"><span>0</span></div><div class="card bottom-flip"><span>0</span></div></div>
<div class="flip-unit" id="minutes-ones"><div class="card top-bg"><span>0</span></div><div class="card bottom-bg"><span>0</span></div><div class="card top-flip"><span>0</span></div><div class="card bottom-flip"><span>0</span></div></div>
</div>
<div class="colon">:</div>
<div class="time-group">
<div class="flip-unit" id="seconds-tens"><div class="card top-bg"><span>0</span></div><div class="card bottom-bg"><span>0</span></div><div class="card top-flip"><span>0</span></div><div class="card bottom-flip"><span>0</span></div></div>
<div class="flip-unit" id="seconds-ones"><div class="card top-bg"><span>0</span></div><div class="card bottom-bg"><span>0</span></div><div class="card top-flip"><span>0</span></div><div class="card bottom-flip"><span>0</span></div></div>
</div>
</div>
<div class="info-footer">
<button id="timer-pause-btn" class="control-btn" title="暂停" style="display:none;position:absolute;left:-80px;top:50%;transform:translateY(-50%);width:48px;height:48px;font-size:1.2rem;">⏸</button>
<div class="city-name"><span id="display-city">定位中...</span><span class="ampm-badge" id="display-ampm">AM</span></div>
<div class="date-display" id="display-date">Loading...</div>
<div class="lunar-display" id="display-lunar" style="display:none;font-size:clamp(0.85rem,1.2vw,1rem);color:rgba(255,255,255,0.7);font-weight:500;letter-spacing:1px;margin-top:4px;"></div>
<button id="timer-stop-btn" class="control-btn" title="停止" style="display:none;position:absolute;right:-80px;top:50%;transform:translateY(-50%);width:48px;height:48px;font-size:1.2rem;">■</button>
</div>
</div>
<div class="dock-wrapper" id="dock-wrapper">
<div class="dock-container" id="dock-container">
</div>
</div>
<div class="scroll-hint">
<span>更换壁纸</span>
<svg viewBox="0 0 24 24"><path d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z"/></svg>
</div>
</section>
<section class="screen-section" id="screen-media">
<div class="media-panel">
<div class="gallery-title">视觉背景媒体库</div>
<div class="media-gallery" id="master-bg-grid">
<div class="bg-card upload-card">
<span class="upload-icon">+</span>
<span class="upload-text">自定义本地文件</span>
<input type="file" id="custom-upload" accept="image/*,video/*" style="display:none;">
</div>
</div>
</div>
<div class="scroll-hint" style="bottom: 40px;">
<span>探索全部功能</span>
<svg viewBox="0 0 24 24"><path d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z"/></svg>
</div>
</section>
<section class="screen-section" id="screen-about">
<div class="about-panel">
<div class="about-hero">
<h2>永久免费的极致美学与精准在线时间</h2>
<p>在快节奏的数字时代,Pudiu 翻页在线时钟不仅是一款获取精准时间的无广告工具,更是能够为您的桌面注入生命力的美学艺术品。无论您是需要跨时区办公的数字游民,还是渴望在繁杂事物中进入 <span class="highlight">沉浸状态</span> 的专注者,它都是您的免费首选。</p>
</div>
<div class="feature-grid">
<div class="feature-card"><span class="feature-icon">📱</span><div class="feature-title">全端跨平台兼容</div><div class="feature-desc">无缝适配个人电脑、智能手机或平板设备,支持全端弹性自适应排版。</div></div>
<div class="feature-card"><span class="feature-icon">✨</span><div class="feature-title">极简全屏美学</div><div class="feature-desc">高级透明悬浮视窗结合平滑物理翻页动画,为设备屏幕增添极致艺术质感。</div></div>
<div class="feature-card"><span class="feature-icon">🕒</span><div class="feature-title">精准直观在线时间</div><div class="feature-desc">经典的复古大字号翻页数字设计,让核心在线时间瞬间捕捉,一目了然。</div></div>
<div class="feature-card"><span class="feature-icon">🎨</span><div class="feature-title">动态壁纸深度定制</div><div class="feature-desc">内置海量 4K 动态视频与必应随机壁纸,搭配多种顶级字体随心打造专属空间。</div></div>
<div class="feature-card"><span class="feature-icon">🔲</span><div class="feature-title">专注沉浸式模式</div><div class="feature-desc">双击即可全屏,一键隐藏所有无关控制控件,让您进入心流状态保持极度专注。</div></div>
<div class="feature-card"><span class="feature-icon">🖼️</span><div class="feature-title">高端桌面装饰摆件</div><div class="feature-desc">进入深度锁屏状态后,时钟化身为极具品味的桌面动态摆件与奢华电子画框。</div></div>
<div class="feature-card"><span class="feature-icon">⚡</span><div class="feature-title">多功能效率中枢</div><div class="feature-desc">底座全域时区中心,一键掌控全球多地时间,满足远程办公、跨国沟通所需。</div></div>
<div class="feature-card"><span class="feature-icon">🎯</span><div class="feature-title">毫秒级精准走时</div><div class="feature-desc">系统底层实时智能同步在线时间与全球物理地理数据,确保每一秒运转分毫不差。</div></div>
<div class="feature-card"><span class="feature-icon">🔋</span><div class="feature-title">极致超低能耗</div><div class="feature-desc">纯前端原生 DOM 节点渲染引擎,拒绝框架冗余,最大程度节省移动设备的电量。</div></div>
<div class="feature-card"><span class="feature-icon">🍃</span><div class="feature-title">绿色环保理念</div><div class="feature-desc">纯粹的数字化呈现,实现完全 0 碳排放,是对传统实体机械手表的完美环保替代。</div></div>
<div class="feature-card"><span class="feature-icon">🚀</span><div class="feature-title">零门槛开箱即用</div><div class="feature-desc">无需下载软件,无需繁杂的安装操作步骤,打开网页即可立即获得丝滑无广体验。</div></div>
<div class="feature-card"><span class="feature-icon">💎</span><div class="feature-title">100% 永久免费</div><div class="feature-desc">Pudiu Flip Clock 的所有核心功能与高阶底层架构对全网用户永久免费无条件开放。</div></div>
</div>
<div class="about-footer">
<p>将网页添加至书签,随时随刻开启您的专注之旅。更多功能,敬请期待 Pudiu 团队更新!</p>
<div class="friendly-links">
<span class="links-title">🔗 友情链接:</span>
<a href="https://www.pudiu.com/" target="_blank">Pudiu Team</a>
<a href="https://ip.pudiu.com/" target="_blank" data-i18n="link2">IP Lookup Tool</a>
<a href="https://hunter.pudiu.com/" target="_blank" data-i18n="link3">Pudiu Resource Hunter</a>
<a href="https://url.pudiu.com/" target="_blank" data-i18n="link4">Pudiu-Extractor</a>
<a href="https://zy.lss90.com/" target="_blank" data-i18n="link5">山远资源网</a>
</div>
</div>
</div>
</section>
</div>
</div>
<div id="countdown-modal" class="tz-modal">
<div class="tz-content" style="max-width: 400px;">
<div class="tz-header">
<h3>设置倒计时</h3>
<button class="close-tz" id="close-countdown-btn">×</button>
</div>
<div class="tz-body">
<div style="display:flex;gap:15px;margin-bottom:20px;">
<div style="flex:1;">
<label style="display:block;color:#aaa;font-size:0.85rem;margin-bottom:8px;">分钟</label>
<input type="number" id="countdown-minutes" placeholder="0" style="width:100%;padding:12px;border-radius:12px;border:1px solid rgba(255,255,255,0.2);background:rgba(255,255,255,0.05);color:#fff;font-size:1.2rem;text-align:center;outline:none;font-family:inherit;" min="0" max="99">
</div>
<div style="flex:1;">
<label style="display:block;color:#aaa;font-size:0.85rem;margin-bottom:8px;">秒</label>
<input type="number" id="countdown-seconds" placeholder="0" style="width:100%;padding:12px;border-radius:12px;border:1px solid rgba(255,255,255,0.2);background:rgba(255,255,255,0.05);color:#fff;font-size:1.2rem;text-align:center;outline:none;font-family:inherit;" min="0" max="59">
</div>
</div>
<button id="countdown-start-btn" style="width:100%;padding:14px;border-radius:12px;background:#0a84ff;color:#fff;font-weight:600;border:none;cursor:pointer;font-size:1rem;transition:background 0.2s;">开始倒计时</button>
</div>
</div>
</div>
<div id="countdown-end-modal" class="tz-modal">
<div class="tz-content" style="max-width: 400px;">
<div class="tz-header">
<h3>倒计时已结束</h3>
<button class="close-tz" id="close-countdown-end-btn">×</button>
</div>
<div class="tz-body">
<p style="color:#fff;font-size:1.1rem;text-align:center;margin-bottom:20px;">⏰ 时间到!</p>
<button id="countdown-restart-btn" style="width:100%;padding:12px;border-radius:12px;background:#0a84ff;color:#fff;font-weight:600;border:none;cursor:pointer;font-size:0.95rem;transition:background 0.2s;margin-bottom:10px;">重新开始</button>
<button id="countdown-close-btn" style="width:100%;padding:12px;border-radius:12px;background:rgba(255,255,255,0.05);color:#aaa;font-weight:500;border:1px solid rgba(255,255,255,0.1);cursor:pointer;font-size:0.95rem;transition:background 0.2s;">关闭</button>
</div>
</div>
</div>
<div id="calendar-modal" class="tz-modal">
<div class="tz-content" style="max-width: 500px;">
<div class="tz-header">
<h3 id="calendar-title">万年历</h3>
<button class="close-tz" id="close-calendar-btn">×</button>
</div>
<div class="tz-body">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:20px;">
<button id="calendar-prev" style="background:rgba(255,255,255,0.1);border:none;color:#fff;padding:8px 16px;border-radius:8px;cursor:pointer;">上一月</button>
<div id="calendar-month-year" style="font-size:1.1rem;font-weight:600;color:#fff;"></div>
<button id="calendar-next" style="background:rgba(255,255,255,0.1);border:none;color:#fff;padding:8px 16px;border-radius:8px;cursor:pointer;">下一月</button>
</div>
<div id="calendar-grid" style="display:grid;grid-template-columns:repeat(7,1fr);gap:8px;"></div>
<div id="calendar-detail" style="margin-top:20px;padding-top:15px;border-top:1px solid rgba(255,255,255,0.1);text-align:center;"></div>
</div>
</div>
</div>
<div id="tz-modal" class="tz-modal">
<div class="tz-content">
<div class="tz-header">
<h3 id="tz-modal-title">添加全球城市时区</h3>
<button class="close-tz" id="close-tz-btn">×</button>
</div>
<div class="tz-body" id="tz-modal-body">
</div>
</div>
</div>
<script>
// ==========================================
// 0. 强化的分享与收藏引擎 (结合标题与链接)
// ==========================================
document.getElementById('btn-share').addEventListener('click', async () => {
const shareData = {
title: t('share_title'),
text: t('share_text'),
url: window.location.href
};
if (navigator.share) {
try { await navigator.share(shareData); } catch (err) { console.log('Share cancelled', err); }
} else {
const copyText = `${shareData.title}\n${shareData.text}\n${shareData.url}`;
const el = document.createElement('textarea');
el.value = copyText;
document.body.appendChild(el); el.select(); document.execCommand('copy'); document.body.removeChild(el);
alert(t('share_copied'));
}
});
document.getElementById('btn-favorite').addEventListener('click', () => {
alert(t('favorite_tip'));
});
// ==========================================
// 语言切换引擎
// ==========================================
const supportedLangs = ['zh', 'zh-tw', 'en', 'ja', 'ko'];
const langDisplayNames = { 'zh': '简体中文', 'zh-tw': '繁體中文', 'en': 'English', 'ja': '日本語', 'ko': '한국어' };
const langLocales = { 'zh': 'zh-CN', 'zh-tw': 'zh-TW', 'en': 'en-US', 'ja': 'ja-JP', 'ko': 'ko-KR' };
let currentLang = (() => {
const saved = localStorage.getItem('app_lang');
return supportedLangs.includes(saved) ? saved : 'zh';
})();
let currentMode = 'clock';
let isTimerPaused = false;
let lastDateStr = '';
let currentTz = Intl.DateTimeFormat().resolvedOptions().timeZone;
let localCityNameZh = "定位中...";
let localCityNameEn = "Locating...";
const i18n = {
'zh': {
'btn_fullscreen': '全屏沉浸模式 (双击屏幕退出)',
'btn_standby': '一键进入锁屏 (专注模式)',
'btn_next_bg': '一键随机必应壁纸',
'btn_flip_x': '界面水平翻转 (镜像)',
'btn_flip_y': '界面垂直翻转 (倒影)',
'btn_share': '分享本站给朋友',
'btn_favorite': '收藏本站',
'btn_countdown': '倒计时模式',
'btn_timer': '计时器模式',
'audio_prev': '上一曲',
'audio_play_pause': '播放/暂停',
'audio_next': '下一曲',
'settings_btn': '系统高级设置',
'close_sidebar': '关闭设置',
'show_clock': '时钟部件可见',
'hour_format': '时间制式',
'24h': '24 小时制',
'12h': '12 小时制',
'scale': '整体缩放',
'show_lunar': '开启农历',
'font_ui': '字体与UI色彩',
'font': '数字字体',
'font_modern': '现代 (Inter)',
'font_oswald': '力量 (Oswald)',
'font_bebas': '海报 (Bebas)',
'font_jetbrains': '极客 (JetBrains)',
'card_bg': '卡片底板颜色',
'font_color': '翻页数字颜色',
'bg_settings': '壁纸设置',
'bg_auto_switch': '壁纸自动切换',
'bg_switch_interval': '自动切换时长',
'bgm': '沉浸白噪音 (BGM)',
'audio_toggle': '开启背景音乐',
'volume': '音量大小控制',
'play_mode': '播放模式',
'mode_list': '列表循环',
'mode_single': '单曲循环',
'mode_random': '随机播放',
'add_audio': '播放列表',
'choose_mp3': '选择 MP3...',
'default_audio': '默认白噪音',
'delete': '删除',
'reset': '清空缓存并重置系统',
'change_bg': '更换壁纸',
'explore': '探索全部功能',
'gallery_title': '视觉背景媒体库',
'upload_text': '自定义本地文件',
'about_hero_h2': '永久免费的极致美学与精准在线时间',
'about_hero_p': '在快节奏的数字时代,Pudiu 翻页在线时钟不仅是一款获取精准时间的无广告工具,更是能够为您的桌面注入生命力的美学艺术品。无论您是需要跨时区办公的数字游民,还是渴望在繁杂事物中进入 <span class="highlight">沉浸状态</span> 的专注者,它都是您的免费首选。',
'f1_title': '全端跨平台兼容', 'f1_desc': '无缝适配个人电脑、智能手机或平板设备,支持全端弹性自适应排版。',
'f2_title': '极简全屏美学', 'f2_desc': '高级透明悬浮视窗结合平滑物理翻页动画,为设备屏幕增添极致艺术质感。',
'f3_title': '精准直观在线时间', 'f3_desc': '经典的复古大字号翻页数字设计,让核心在线时间瞬间捕捉,一目了然。',
'f4_title': '动态壁纸深度定制', 'f4_desc': '内置海量 4K 动态视频与必应随机壁纸,搭配多种顶级字体随心打造专属空间。',
'f5_title': '专注沉浸式模式', 'f5_desc': '双击即可全屏,一键隐藏所有无关控制控件,让您进入心流状态保持极度专注。',
'f6_title': '高端桌面装饰摆件', 'f6_desc': '进入深度锁屏状态后,时钟化身为极具品味的桌面动态摆件与奢华电子画框。',
'f7_title': '多功能效率中枢', 'f7_desc': '底座全域时区中心,一键掌控全球多地时间,满足远程办公、跨国沟通所需。',
'f8_title': '毫秒级精准走时', 'f8_desc': '系统底层实时智能同步在线时间与全球物理地理数据,确保每一秒运转分毫不差。',
'f9_title': '极致超低能耗', 'f9_desc': '纯前端原生 DOM 节点渲染引擎,拒绝框架冗余,最大程度节省移动设备的电量。',
'f10_title': '绿色环保理念', 'f10_desc': '纯粹的数字化呈现,实现完全 0 碳排放,是对传统实体机械手表的完美环保替代。',
'f11_title': '零门槛开箱即用', 'f11_desc': '无需下载软件,无需繁杂的安装操作步骤,打开网页即可立即获得丝滑无广体验。',
'f12_title': '100% 永久免费', 'f12_desc': 'Pudiu Flip Clock 的所有核心功能与高阶底层架构对全网用户永久免费无条件开放。',
'about_footer_p': '将网页添加至书签,随时随刻开启您的专注之旅。更多功能,敬请期待 Pudiu 团队更新!',
'links_title': '友情链接:',
'save': '保存',
'restore_default': '恢复默认',
'countdown_title': '设置倒计时',
'minutes': '分钟',
'seconds': '秒',
'hours': '小时',
'start_countdown': '开始倒计时',
'countdown_end_title': '倒计时已结束',
'time_up': '⏰ 时间到!',
'restart': '重新开始',
'close': '关闭',
'calendar_title': '万年历',
'prev_month': '上一月',
'next_month': '下一月',
'tz_title': '添加全球城市时区',
'dock_current': '当前',
'more_regions': '更多地区',
'timer_pause': '暂停',
'timer_resume': '继续',
'timer_stop': '停止',
'timer_close': '关闭',
'locating': '定位中...',
'loading': 'Loading...',
'countdown_label': '倒计时',
'timer_label': '计时器',
'confirm_reset': '系统提示:确认要清除所有自定义数据并恢复默认设置吗?',
'standby_unlock': '点击屏幕或按任意键解锁',
'lock_password': '锁屏密码',
'lock_password_enable': '启用锁屏密码',
'lock_password_set': '设置密码',
'lock_password_change': '修改密码',
'lock_password_old': '原密码',
'lock_password_new': '新密码',
'lock_password_confirm': '确认密码',
'lock_password_empty': '请输入密码',
'lock_password_mismatch': '两次输入不一致',
'lock_password_incorrect': '密码错误',
'lock_password_set_success': '密码设置成功',
'lock_password_change_success': '密码修改成功',
'lock_password_remove': '清除密码',
'lock_password_remove_confirm': '确认清除密码?',
'site_title': 'Pudiu 翻页在线时钟',
'share_title': 'Pudiu 翻页在线时钟',
'share_text': '发现一个免费好用的在线翻页时钟,无广告,支持全屏、沉浸白噪音与 4K 动态壁纸!',
'share_copied': '标题、简介及网址已成功复制到剪贴板,快去分享给朋友吧!',
'favorite_tip': '请按组合键 Ctrl + D (Mac 用户请按 Cmd + D) 将免费全屏时钟加入收藏夹'
},
'zh-tw': {
'btn_fullscreen': '全螢幕沉浸模式 (雙擊螢幕退出)',
'btn_standby': '一鍵進入鎖屏 (專注模式)',
'btn_next_bg': '一鍵隨機 Bing 桌布',
'btn_flip_x': '介面水平翻轉 (鏡像)',
'btn_flip_y': '介面垂直翻轉 (倒影)',
'btn_share': '分享本站給朋友',
'btn_favorite': '收藏本站',
'btn_countdown': '倒數計時模式',
'btn_timer': '計時器模式',
'audio_prev': '上一曲',
'audio_play_pause': '播放/暫停',
'audio_next': '下一曲',
'settings_btn': '系統進階設定',
'close_sidebar': '關閉設定',
'show_clock': '時鐘元件可見',
'hour_format': '時間制式',
'24h': '24 小時制',
'12h': '12 小時制',
'scale': '整體縮放',
'show_lunar': '開啟農曆',
'font_ui': '字體與 UI 色彩',
'font': '數字字體',
'font_modern': '現代 (Inter)',
'font_oswald': '力量 (Oswald)',
'font_bebas': '海報 (Bebas)',
'font_jetbrains': '極客 (JetBrains)',
'card_bg': '卡片底板顏色',
'font_color': '翻頁數字顏色',
'bg_settings': '桌布設定',
'bg_auto_switch': '桌布自動切換',
'bg_switch_interval': '自動切換時長',
'bgm': '沉浸白噪音 (BGM)',
'audio_toggle': '開啟背景音樂',
'volume': '音量大小控制',
'play_mode': '播放模式',
'mode_list': '列表循環',
'mode_single': '單曲循環',
'mode_random': '隨機播放',
'add_audio': '播放清單',
'choose_mp3': '選擇 MP3...',
'default_audio': '預設白噪音',
'delete': '刪除',
'reset': '清空快取並重設系統',
'change_bg': '更換桌布',
'explore': '探索全部功能',
'gallery_title': '視覺背景媒體庫',
'upload_text': '自訂本機檔案',
'about_hero_h2': '永久免費的極致美學與精準線上時間',
'about_hero_p': '在快節奏的數位時代,Pudiu 翻頁線上時鐘不僅是一款獲取精準時間的無廣告工具,更是能夠為您的桌面注入生命力的美學藝術品。無論您是需要跨時區辦公的數位遊牧民族,還是渴望在繁雜事物中進入 <span class="highlight">沉浸狀態</span> 的專注者,它都是您的免費首選。',
'f1_title': '全端跨平台相容', 'f1_desc': '無縫適應個人電腦、智慧型手機或平板裝置,支援全端彈性自適應排版。',
'f2_title': '極簡全螢幕美學', 'f2_desc': '高級透明懸浮視窗結合平滑物理翻頁動畫,為裝置螢幕增添極致藝術質感。',
'f3_title': '精準直觀線上時間', 'f3_desc': '經典的復古大字號翻頁數字設計,讓核心線上時間瞬間捕捉,一目了然。',
'f4_title': '動態桌布深度客製', 'f4_desc': '內建海量 4K 動態影片與 Bing 隨機桌布,搭配多種頂級字體隨心打造專屬空間。',
'f5_title': '專注沉浸式模式', 'f5_desc': '雙擊即可全螢幕,一鍵隱藏所有無關控制元件,讓您進入心流狀態保持極度專注。',
'f6_title': '高端桌面裝飾擺件', 'f6_desc': '進入深度鎖屏狀態後,時鐘化身為極具品味的桌面動態擺件與奢華電子畫框。',
'f7_title': '多功能效率中樞', 'f7_desc': '底座全域時區中心,一鍵掌控全球多地時間,滿足遠端辦公、跨國溝通所需。',
'f8_title': '毫秒級精準走時', 'f8_desc': '系統底層即時智慧同步線上時間與全球物理地理資料,確保每一秒咿D分毫不差。',
'f9_title': '極致超低能耗', 'f9_desc': '純前端原生 DOM 節點渲染引擎,拒絕框架冗餘,最大程度節省行動裝置的電量。',
'f10_title': '綠色環保理念', 'f10_desc': '純粹的數位化呈現,實現完全 0 碳排放,是對傳統實體機械手錶的完美環保替代。',
'f11_title': '零門檻開箱即用', 'f11_desc': '無需下載軟體,無需繁雜的安裝操作步驟,開啟網頁即可立即獲得絲滑無廣告體驗。',
'f12_title': '100% 永久免費', 'f12_desc': 'Pudiu Flip Clock 的所有核心功能與高階底層架構對全網用戶永久免費無條件開放。',
'about_footer_p': '將網頁加入書籤,隨時隨地開啟您的專注之旅。更多功能,敬請期待 Pudiu 團隊更新!',
'links_title': '友情連結:',
'save': '儲存',
'restore_default': '恢復預設',
'countdown_title': '設定倒數計時',
'minutes': '分鐘',
'seconds': '秒',
'hours': '小時',
'start_countdown': '開始倒數計時',
'countdown_end_title': '倒數計時已結束',
'time_up': '⏰ 時間到!',
'restart': '重新開始',
'close': '關閉',
'calendar_title': '萬年曆',
'prev_month': '上一月',
'next_month': '下一月',
'tz_title': '新增全球城市時區',
'dock_current': '目前',
'more_regions': '更多地區',
'timer_pause': '暫停',
'timer_resume': '繼續',
'timer_stop': '停止',
'timer_close': '關閉',
'locating': '定位中...',
'loading': '載入中...',
'countdown_label': '倒數計時',
'timer_label': '計時器',
'confirm_reset': '系統提示:確定要清除所有自訂資料並恢復預設設定嗎?',
'standby_unlock': '點擊螢幕或按任意鍵解鎖',
'lock_password': '鎖屏密碼',
'lock_password_enable': '啟用鎖屏密碼',
'lock_password_set': '設定密碼',
'lock_password_change': '修改密碼',
'lock_password_old': '原密碼',
'lock_password_new': '新密碼',
'lock_password_confirm': '確認密碼',
'lock_password_empty': '請輸入密碼',
'lock_password_mismatch': '兩次輸入不一致',
'lock_password_incorrect': '密碼錯誤',
'lock_password_set_success': '密碼設定成功',
'lock_password_change_success': '密碼修改成功',
'lock_password_remove': '清除密碼',
'lock_password_remove_confirm': '確認清除密碼?',
'share_title': 'Pudiu 翻頁線上時鐘',
'share_text': '發現一個免費好用的線上翻頁時鐘,無廣告,支援全螢幕、沉浸白噪音與 4K 動態桌布!',
'share_copied': '標題、簡介及網址已成功複製到剪貼簿,快去分享給朋友吧!',
'favorite_tip': '請按組合鍵 Ctrl + D (Mac 使用者請按 Cmd + D) 將免費全螢幕時鐘加入我的最愛',
'site_title': 'Pudiu 翻頁線上時鐘'
},
'en': {
'btn_fullscreen': 'Fullscreen Immersive Mode (Double-click to exit)',
'btn_standby': 'One-click Lock Screen (Focus Mode)',
'btn_next_bg': 'Random Bing Wallpaper',
'btn_flip_x': 'Horizontal Flip (Mirror)',
'btn_flip_y': 'Vertical Flip (Reflection)',
'btn_share': 'Share This Site',
'btn_favorite': 'Bookmark This Site',
'btn_countdown': 'Countdown Mode',
'btn_timer': 'Timer Mode',
'audio_prev': 'Previous',
'audio_play_pause': 'Play/Pause',
'audio_next': 'Next',
'settings_btn': 'Advanced Settings',
'close_sidebar': 'Close Settings',
'show_clock': 'Clock Widget Visible',
'hour_format': 'Time Format',
'24h': '24-Hour',
'12h': '12-Hour',
'scale': 'Overall Scale',
'show_lunar': 'Enable Lunar',
'font_ui': 'Font & UI Colors',
'font': 'Number Font',
'font_modern': 'Modern (Inter)',
'font_oswald': 'Bold (Oswald)',
'font_bebas': 'Poster (Bebas)',
'font_jetbrains': 'Geek (JetBrains)',
'card_bg': 'Card Background Color',
'font_color': 'Flip Number Color',
'bg_settings': 'Wallpaper Settings',
'bg_auto_switch': 'Auto-Switch Wallpaper',
'bg_switch_interval': 'Auto-switch Interval',
'bgm': 'Ambient White Noise (BGM)',
'audio_toggle': 'Enable Background Music',
'volume': 'Volume Control',
'play_mode': 'Play Mode',
'mode_list': 'List Loop',
'mode_single': 'Single Loop',
'mode_random': 'Shuffle',
'add_audio': 'Playlist',
'choose_mp3': 'Choose MP3...',
'default_audio': 'Default White Noise',
'delete': 'Delete',
'reset': 'Clear Cache & Reset System',
'change_bg': 'Change Wallpaper',
'explore': 'Explore All Features',
'gallery_title': 'Visual Background Media Library',
'upload_text': 'Custom Local File',
'about_hero_h2': 'Free Aesthetic & Precise Online Clock',
'about_hero_p': 'In the fast-paced digital era, Pudiu Flip Clock is not just an ad-free tool for precise time, but an aesthetic artwork that breathes life into your desktop. Whether you are a digital nomad working across time zones or a focus seeker longing for <span class="highlight">immersive state</span> in a busy world, it is your free top choice.',
'f1_title': 'Cross-Platform Compatible', 'f1_desc': 'Seamlessly adapts to desktops, smartphones, and tablets with responsive layout.',
'f2_title': 'Minimalist Full-Screen Aesthetics', 'f2_desc': 'Premium transparent floating window with smooth flip animation for artistic screen.',
'f3_title': 'Precise Online Time', 'f3_desc': 'Classic retro large flip numbers for instant and clear time reading.',
'f4_title': 'Dynamic Wallpaper Customization', 'f4_desc': 'Built-in 4K videos and Bing random wallpapers with premium fonts for personalization.',
'f5_title': 'Focus Immersive Mode', 'f5_desc': 'Double-click for fullscreen, hide all controls, and enter flow state.',
'f6_title': 'Desktop Decoration', 'f6_desc': 'In deep lock screen, the clock becomes a tasteful desktop ornament and digital frame.',
'f7_title': 'Multi-Function Hub', 'f7_desc': 'Global timezone center for remote work and cross-border communication.',
'f8_title': 'Millisecond Precision', 'f8_desc': 'Real-time sync with online time and global geographic data for perfect accuracy.',
'f9_title': 'Ultra-Low Power', 'f9_desc': 'Pure native DOM rendering engine, no framework overhead, saving device battery.',
'f10_title': 'Eco-Friendly', 'f10_desc': 'Pure digital presentation, zero carbon emission, a green alternative to mechanical clocks.',
'f11_title': 'Zero Setup', 'f11_desc': 'No download, no installation, open the webpage for instant ad-free experience.',
'f12_title': '100% Free Forever', 'f12_desc': 'All core features and architecture of Pudiu Flip Clock are free for all users.',
'about_footer_p': 'Bookmark this page to start your focus journey anytime. More features coming from Pudiu team!',
'links_title': 'Links:',
'save': 'Save',
'restore_default': 'Reset to Default',
'countdown_title': 'Set Countdown',
'minutes': 'Minutes',
'seconds': 'Seconds',
'hours': 'Hours',
'start_countdown': 'Start Countdown',
'countdown_end_title': 'Countdown Finished',
'time_up': '⏰ Time\'s Up!',
'restart': 'Restart',
'close': 'Close',
'calendar_title': 'Calendar',
'prev_month': 'Previous',
'next_month': 'Next',
'tz_title': 'Add Global City Timezone',
'dock_current': 'Current',
'more_regions': 'More Regions',
'timer_pause': 'Pause',
'timer_resume': 'Resume',
'timer_stop': 'Stop',
'timer_close': 'Close',
'locating': 'Locating...',
'loading': 'Loading...',
'countdown_label': 'Countdown',
'timer_label': 'Timer',
'confirm_reset': 'System: Are you sure you want to clear all custom data and restore default settings?',
'standby_unlock': 'Click screen or press any key to unlock',
'lock_password': 'Lock Screen Password',
'lock_password_enable': 'Enable Lock Screen Password',
'lock_password_set': 'Set Password',
'lock_password_change': 'Change Password',
'lock_password_old': 'Current Password',
'lock_password_new': 'New Password',
'lock_password_confirm': 'Confirm Password',
'lock_password_empty': 'Please enter password',
'lock_password_mismatch': 'Passwords do not match',
'lock_password_incorrect': 'Incorrect password',
'lock_password_set_success': 'Password set successfully',
'lock_password_change_success': 'Password changed successfully',
'lock_password_remove': 'Remove Password',
'lock_password_remove_confirm': 'Confirm password removal?',
'site_title': 'Pudiu Flip Clock',
'share_title': 'Pudiu Flip Online Clock',
'share_text': 'A free, ad-free online flip clock with fullscreen, ambient white noise, and 4K dynamic wallpapers!',
'share_copied': 'Title, description, and URL copied to clipboard. Share with your friends!',
'favorite_tip': 'Press Ctrl + D (Cmd + D on Mac) to bookmark this free fullscreen clock'
},
'ja': {
'btn_fullscreen': '全画面没入モード (ダブルクリックで終了)',
'btn_standby': 'ワンクリックロック画面 (集中モード)',
'btn_next_bg': 'Bingのランダム壁紙',
'btn_flip_x': '画面を左右反転 (ミラー)',
'btn_flip_y': '画面を上下反転 (反射)',
'btn_share': 'このサイトを共有',
'btn_favorite': 'このサイトをブックマーク',
'btn_countdown': 'カウントダウンモード',
'btn_timer': 'タイマーモード',
'audio_prev': '前の曲',
'audio_play_pause': '再生/一時停止',
'audio_next': '次の曲',
'settings_btn': 'システム詳細設定',
'close_sidebar': '設定を閉じる',
'show_clock': '時計ウィジェット表示',
'hour_format': '時間形式',
'24h': '24時間制',
'12h': '12時間制',
'scale': '全体スケール',
'show_lunar': '旧暦を表示',
'font_ui': 'フォントとUIカラー',
'font': '数字フォント',
'font_modern': 'モダン (Inter)',
'font_oswald': '力強い (Oswald)',
'font_bebas': 'ポスター (Bebas)',
'font_jetbrains': 'ギーク (JetBrains)',
'card_bg': 'カード背景色',
'font_color': 'フリップ数字の色',
'bg_settings': '壁紙設定',
'bg_auto_switch': '壁紙自動切替',
'bg_switch_interval': '自動切替間隔',
'bgm': '環境ホワイトノイズ (BGM)',
'audio_toggle': 'BGMを有効化',
'volume': '音量制御',
'play_mode': '再生モード',
'mode_list': 'リストループ',
'mode_single': 'シングルループ',
'mode_random': 'シャッフル',
'add_audio': 'プレイリスト',
'choose_mp3': 'MP3を選択...',
'default_audio': 'デフォルト環境音',
'delete': '削除',
'reset': 'キャッシュをクリアしてリセット',
'change_bg': '壁紙を変更',
'explore': 'すべての機能を探索',
'gallery_title': 'ビジュアル背景メディアライブラリ',
'upload_text': 'カスタムローカルファイル',
'about_hero_h2': '永久無料の究極美学と高精度オンライン時計',
'about_hero_p': '急速なデジタル時代において、Pudiuフリップオンライン時計は、正確な時間を取得するための広告なしツールであるだけでなく、デスクトップに生命を吹き込む美学アート作品です。複数のタイムゾーンで働くデジタルノマドであっても、忙しい世界で<span class="highlight">没入状態</span>を求める集中者であっても、無料の最適な選択です。',
'f1_title': 'クロスプラットフォーム対応', 'f1_desc': 'デスクトップ、スマートフォン、タブレットにシームレスに対応するレスポンシブレイアウト。',
'f2_title': 'ミニマル全画面美学', 'f2_desc': '高品質な透明フローティングウィンドウとスムーズなフリップアニメーションで芸術的な画面を実現。',
'f3_title': '正確なオンライン時間', 'f3_desc': 'クラシックなレトロ大型フリップ数字で、瞬時に明確な時間表示。',
'f4_title': 'ダイナミック壁紙カスタマイズ', 'f4_desc': '内蔵の4K動画とBingランダム壁紙、プレミアムフォントでパーソナライズ。',
'f5_title': '集中没入モード', 'f5_desc': 'ダブルクリックで全画面、すべてのコントロールを非表示にしてフロー状態へ。',
'f6_title': 'デスクトップ装飾', 'f6_desc': 'ディープロック画面で、時計は上品なデスクトップ装飾とデジタルフレームに。',
'f7_title': '多機能ハブ', 'f7_desc': 'グローバルタイムゾーンセンターでリモートワークと国際通信に対応。',
'f8_title': 'ミリ秒精度', 'f8_desc': 'オンライン時間とグローバル地理データをリアルタイム同期し、完璧な精度を実現。',
'f9_title': '超低消費電力', 'f9_desc': '純粋なネイティブDOMレンダリングエンジン、フレームワーク不要でバッテリーを節約。',
'f10_title': 'エコフレンドリー', 'f10_desc': '純粋なデジタル表示、ゼロカーボン排出で機械式時計の環境に優しい代替。',
'f11_title': 'ゼロセットアップ', 'f11_desc': 'ダウンロード不要、インストール不要、ページを開けば即座に広告なし体験。',
'f12_title': '100%永久無料', 'f12_desc': 'Pudiuフリップ時計のすべてのコア機能とアーキテクチャは全ユーザーに永久無料。',
'about_footer_p': 'このページをブックマークして、いつでも集中の旅を始めましょう。Pudiuチームからさらなる機能をお楽しみに!',
'links_title': 'リンク:',
'save': '保存',
'restore_default': 'デフォルトに戻す',
'countdown_title': 'カウントダウンを設定',
'minutes': '分',
'seconds': '秒',
'start_countdown': 'カウントダウン開始',
'countdown_end_title': 'カウントダウン終了',
'time_up': '⏰ 時間です!',
'restart': 'やり直す',
'close': '閉じる',
'calendar_title': 'カレンダー',
'prev_month': '前月',
'next_month': '翌月',
'tz_title': '世界都市タイムゾーンを追加',
'dock_current': '現在',
'more_regions': 'その他の地域',
'timer_pause': '一時停止',
'timer_resume': '再開',
'timer_stop': '停止',
'timer_close': '閉じる',
'locating': '位置情報取得中...',
'loading': '読み込み中...',
'countdown_label': 'カウントダウン',
'timer_label': 'タイマー',
'confirm_reset': 'システム:すべてのカスタムデータを消去し、デフォルト設定に戻しますか?',
'standby_unlock': '画面をクリックするか、任意のキーを押してロック解除',
'lock_password': 'ロック画面パスワード',
'lock_password_enable': 'ロック画面パスワードを有効にする',
'lock_password_set': 'パスワードを設定',
'lock_password_change': 'パスワードを変更',
'lock_password_old': '現在のパスワード',
'lock_password_new': '新しいパスワード',
'lock_password_confirm': 'パスワードを確認',
'lock_password_empty': 'パスワードを入力してください',
'lock_password_mismatch': 'パスワードが一致しません',
'lock_password_incorrect': 'パスワードが間違っています',
'lock_password_set_success': 'パスワードの設定に成功しました',
'lock_password_change_success': 'パスワードの変更に成功しました',
'lock_password_remove': 'パスワードを削除',
'lock_password_remove_confirm': 'パスワードを削除してもよろしいですか?',
'share_title': 'Pudiu フリップオンライン時計',
'share_text': '無料で使いやすいオンラインフリップ時計を発見!広告なし、フルスクリーン、環境ホワイトノイズ、4K動的壁紙をサポート!',
'share_copied': 'タイトル、概要、URLがクリップボードにコピーされました。友達に共有しましょう!',
'favorite_tip': 'Ctrl + D (MacはCmd + D)を押して、無料のフルスクリーン時計をブックマークに追加',
'site_title': 'Pudiu フリップ時計'
},
'ko': {
'btn_fullscreen': '전체 화면 몰입 모드 (더블클릭으로 종료)',
'btn_standby': '원클릭 잠금 화면 (집중 모드)',
'btn_next_bg': 'Bing 랜덤 배경화면',
'btn_flip_x': '화면 좌우 반전 (거울)',
'btn_flip_y': '화면 상하 반전 (반사)',
'btn_share': '이 사이트 공유',
'btn_favorite': '이 사이트 북마크',
'btn_countdown': '카운트다운 모드',
'btn_timer': '타이머 모드',
'audio_prev': '이전 곡',
'audio_play_pause': '재생/일시정지',
'audio_next': '다음 곡',
'settings_btn': '시스템 고급 설정',
'close_sidebar': '설정 닫기',
'show_clock': '시계 위젯 표시',
'hour_format': '시간 형식',
'24h': '24시간제',
'12h': '12시간제',
'scale': '전체 스케일',
'show_lunar': '음력 표시',
'font_ui': '글꼴 및 UI 색상',
'font': '숫자 글꼴',
'font_modern': '모던 (Inter)',
'font_oswald': '강렬 (Oswald)',
'font_bebas': '포스터 (Bebas)',
'font_jetbrains': '긱 (JetBrains)',
'card_bg': '카드 배경색',
'font_color': '플립 숫자 색상',
'bg_settings': '배경화면 설정',
'bg_auto_switch': '배경화면 자동 전환',
'bg_switch_interval': '자동 전환 간격',
'bgm': '환경 백색소음 (BGM)',
'audio_toggle': '배경 음악 활성화',
'volume': '음량 제어',
'play_mode': '재생 모드',
'mode_list': '리스트 반복',
'mode_single': '단곡 반복',
'mode_random': '셔플',
'add_audio': '재생 목록',
'choose_mp3': 'MP3 선택...',
'default_audio': '기본 백색소음',
'delete': '삭제',
'reset': '캐시 지우기 및 시스템 재설정',
'change_bg': '배경화면 변경',
'explore': '모든 기능 탐색',
'gallery_title': '비주얼 배경 미디어 라이브러리',
'upload_text': '사용자 정의 로컬 파일',
'about_hero_h2': '영구 무료의 극미학과 정확한 온라인 시계',
'about_hero_p': '빠른 속도의 디지털 시대에 Pudiu 플립 온라인 시계는 정확한 시간을 얻기 위한 광고 없는 도구일 뿐만 아니라, 데스크톱에 생명력을 불어넣는 미학 예술 작품입니다. 여러 시간대에서 일하는 디지털 노마드이든, 복잡한 세상에서 <span class="highlight">몰입 상태</span>를 갈망하는 집중자이든, 무료 최고의 선택입니다.',
'f1_title': '크로스 플랫폼 호환', 'f1_desc': '데스크톱, 스마트폰, 태블릿에 완벽하게 적응하는 반응형 레이아웃.',
'f2_title': '미니멀 풀스크린 미학', 'f2_desc': '고급 투명 플로팅 창과 부드러운 플립 애니메이션으로 예술적 화면 구현.',
'f3_title': '정확한 온라인 시간', 'f3_desc': '클래식 레트로 대형 플립 숫자로 즉각적이고 명확한 시간 표시.',
'f4_title': '다이나믹 배경화면 커스터마이징', 'f4_desc': '내장 4K 동영상과 Bing 랜덤 배경화면, 프리미엄 글꼴로 개인화.',
'f5_title': '집중 몰입 모드', 'f5_desc': '더블클릭으로 전체화면, 모든 컨트롤을 숨기고 플로우 상태로 진입.',
'f6_title': '데스크톱 장식', 'f6_desc': '딥 잠금 화면에서 시계는 우아한 데스크톱 장식과 디지털 프레임으로 변신.',
'f7_title': '다기능 허브', 'f7_desc': '글로벌 시간대 센터로 원격 근무와 국제 소통에 대응.',
'f8_title': '밀리초 정밀도', 'f8_desc': '온라인 시간과 글로벌 지리 데이터를 실시간 동기화하여 완벽한 정확도.',
'f9_title': '초저전력', 'f9_desc': '순수 네이티브 DOM 렌더링 엔진, 프레임워크 없이 배터리 절약.',
'f10_title': '친환경', 'f10_desc': '순수 디지털 표현, 제로 탄소 배출로 기계식 시계의 친환경 대안.',
'f11_title': '제로 설치', 'f11_desc': '다운로드 불필요, 설치 불필요, 페이지를 열면 즉시 광고 없는 경험.',
'f12_title': '100% 영구 무료', 'f12_desc': 'Pudiu 플립 시계의 모든 핵심 기능과 아키텍처는 모든 사용자에게 영구 무료.',
'about_footer_p': '이 페이지를 북마크하여 언제든 집중의 여정을 시작하세요. Pudiu 팀의 더 많은 기능을 기대해주세요!',
'links_title': '링크:',
'save': '저장',
'restore_default': '기본값으로 복원',
'countdown_title': '카운트다운 설정',
'minutes': '분',
'seconds': '초',
'start_countdown': '카운트다운 시작',
'countdown_end_title': '카운트다운 종료',
'time_up': '⏰ 시간 종료!',
'restart': '다시 시작',
'close': '닫기',
'calendar_title': '만년력',
'prev_month': '이전 달',
'next_month': '다음 달',
'tz_title': '세계 도시 시간대 추가',
'dock_current': '현재',
'more_regions': '더 많은 지역',
'timer_pause': '일시정지',
'timer_resume': '계속',
'timer_stop': '정지',
'timer_close': '닫기',
'locating': '위치 확인 중...',
'loading': '로딩 중...',
'countdown_label': '카운트다운',
'timer_label': '타이머',
'confirm_reset': '시스템:모든 사용자 정의 데이터를 지우고 기본 설정으로 복원하시겠습니까?',
'standby_unlock': '화면을 클릭하거나 아무 키나 눌러 잠금 해제',
'lock_password': '잠금 화면 비밀번호',
'lock_password_enable': '잠금 화면 비밀번호 활성화',
'lock_password_set': '비밀번호 설정',
'lock_password_change': '비밀번호 변경',
'lock_password_old': '현재 비밀번호',
'lock_password_new': '새 비밀번호',
'lock_password_confirm': '비밀번호 확인',
'lock_password_empty': '비밀번호를 입력하세요',
'lock_password_mismatch': '비밀번호가 일치하지 않습니다',
'lock_password_incorrect': '비밀번호가 틀렸습니다',
'lock_password_set_success': '비밀번호 설정 성공',
'lock_password_change_success': '비밀번호 변경 성공',
'lock_password_remove': '비밀번호 제거',
'lock_password_remove_confirm': '비밀번호를 제거하시겠습니까?',
'share_title': 'Pudiu 플립 온라인 시계',
'share_text': '무료로 사용하기 좋은 온라인 플립 시계를 발견! 광고 없음, 전체화면, 환경 백색소음, 4K 동적 배경화면 지원!',
'share_copied': '제목, 설명, URL이 클립보드에 복사되었습니다. 친구에게 공유하세요!',
'favorite_tip': 'Ctrl + D (Mac은 Cmd + D)를 눌러 무료 전체화면 시계를 북마크에 추가',
'site_title': 'Pudiu 플립 시계'
}
};
function t(key) { return i18n[currentLang][key] || key; }
function applyLanguage(lang) {
currentLang = lang;
localStorage.setItem('app_lang', lang);
const L = i18n[lang];
document.getElementById('btn-fullscreen').title = L.btn_fullscreen;
document.getElementById('btn-standby').title = L.btn_standby;
document.getElementById('btn-next-bg').title = L.btn_next_bg;
document.getElementById('btn-flip-x').title = L.btn_flip_x;
document.getElementById('btn-flip-y').title = L.btn_flip_y;
document.getElementById('btn-share').title = L.btn_share;
document.getElementById('btn-favorite').title = L.btn_favorite;
document.getElementById('btn-countdown').title = L.btn_countdown;
document.getElementById('btn-timer').title = L.btn_timer;
document.getElementById('audio-prev').title = L.audio_prev;
document.getElementById('audio-play-pause').title = L.audio_play_pause;
document.getElementById('audio-next').title = L.audio_next;
document.getElementById('settings-btn').title = L.settings_btn;
document.getElementById('close-sidebar-btn').title = L.close_sidebar;
document.querySelector('.sidebar-header-row .section-title').textContent = L.settings_btn;
document.querySelectorAll('.sidebar-panel .form-group label:not(.custom-file-btn):not(.switch)').forEach((lbl, i) => {
const labels = [L.show_clock, L.hour_format, L.scale, L.font, L.show_lunar, L.card_bg, L.font_color, L.bg_auto_switch, L.bg_switch_interval, L.audio_toggle, L.add_audio];
if (labels[i]) lbl.childNodes[0].textContent = labels[i];
});
const chooseMp3Btn = document.querySelector('.custom-file-btn');
if (chooseMp3Btn) chooseMp3Btn.textContent = L.choose_mp3;
document.querySelectorAll('.sidebar-panel .section-title').forEach((el, i) => {
if (i === 1) el.textContent = L.font_ui;
if (i === 2) el.textContent = L.bg_settings;
if (i === 3) el.textContent = L.bgm;
if (i === 4) el.textContent = L.lock_password;
});
const elSecPassword = document.getElementById('section-lock-password');
if (elSecPassword) elSecPassword.textContent = L.lock_password;
const elLabelEnable = document.getElementById('label-lock-password-enable');
if (elLabelEnable) elLabelEnable.textContent = L.lock_password_enable;
const elLabelNew = document.getElementById('label-lock-password-new');
if (elLabelNew) elLabelNew.textContent = L.lock_password_new;
const elLabelConfirm = document.getElementById('label-lock-password-confirm');
if (elLabelConfirm) elLabelConfirm.textContent = L.lock_password_confirm;
const elBtnSet = document.getElementById('btn-lock-password-set');
if (elBtnSet) elBtnSet.textContent = L.lock_password_set;
const elLabelOld = document.getElementById('label-lock-password-old');
if (elLabelOld) elLabelOld.textContent = L.lock_password_old;
const elLabelNew2 = document.getElementById('label-lock-password-new2');
if (elLabelNew2) elLabelNew2.textContent = L.lock_password_new;
const elLabelConfirm2 = document.getElementById('label-lock-password-confirm2');
if (elLabelConfirm2) elLabelConfirm2.textContent = L.lock_password_confirm;
const elBtnChange = document.getElementById('btn-lock-password-change');
if (elBtnChange) elBtnChange.textContent = L.lock_password_change;
const elBtnRemove = document.getElementById('btn-lock-password-remove');
if (elBtnRemove) elBtnRemove.textContent = L.lock_password_remove;
const elModalTitle = document.getElementById('lock-password-modal-title');
if (elModalTitle) elModalTitle.textContent = L.lock_password;
const elModalInput = document.getElementById('lock-password-input');
if (elModalInput) elModalInput.placeholder = L.lock_password;
const hourOpts = document.querySelectorAll('#setting-hour-format option');
if (hourOpts[0]) hourOpts[0].textContent = L['24h'];
if (hourOpts[1]) hourOpts[1].textContent = L['12h'];
const fontOpts = document.querySelectorAll('#setting-font option');
if (fontOpts[0]) fontOpts[0].textContent = L.font_modern;
if (fontOpts[1]) fontOpts[1].textContent = L.font_oswald;
if (fontOpts[2]) fontOpts[2].textContent = L.font_bebas;
if (fontOpts[3]) fontOpts[3].textContent = L.font_jetbrains;
const modeOpts = document.querySelectorAll('#setting-play-mode-inline option');
if (modeOpts[0]) modeOpts[0].textContent = L.mode_list;
if (modeOpts[1]) modeOpts[1].textContent = L.mode_single;
if (modeOpts[2]) modeOpts[2].textContent = L.mode_random;
document.querySelector('.custom-file-btn').textContent = L.choose_mp3;
document.getElementById('reset-btn').textContent = L.reset;
document.querySelectorAll('.scroll-hint span').forEach((el, i) => {
el.textContent = i === 0 ? L.change_bg : L.explore;
});
document.querySelector('.gallery-title').textContent = L.gallery_title;
document.querySelector('.upload-text').textContent = L.upload_text;
document.querySelector('.about-hero h2').textContent = L.about_hero_h2;
document.querySelector('.about-hero p').innerHTML = L.about_hero_p;
const featureCards = document.querySelectorAll('.feature-card');
const fKeys = ['f1','f2','f3','f4','f5','f6','f7','f8','f9','f10','f11','f12'];
featureCards.forEach((card, i) => {
card.querySelector('.feature-title').textContent = L[fKeys[i]+'_title'];
card.querySelector('.feature-desc').textContent = L[fKeys[i]+'_desc'];
});
document.querySelector('.about-footer p').textContent = L.about_footer_p;
document.querySelector('.links-title').innerHTML = '🔗 ' + L.links_title;
document.querySelector('#countdown-modal h3').textContent = L.countdown_title;
document.querySelectorAll('#countdown-modal label')[0].textContent = L.minutes;
document.querySelectorAll('#countdown-modal label')[1].textContent = L.seconds;
document.getElementById('countdown-start-btn').textContent = L.start_countdown;
document.querySelector('#countdown-end-modal h3').textContent = L.countdown_end_title;
document.querySelector('#countdown-end-modal p').textContent = L.time_up;
document.getElementById('countdown-restart-btn').textContent = L.restart;
document.getElementById('countdown-close-btn').textContent = L.close;
document.getElementById('calendar-title').textContent = L.calendar_title;
document.getElementById('calendar-prev').textContent = L.prev_month;
document.getElementById('calendar-next').textContent = L.next_month;
const standbyHintText = document.getElementById('standby-hint-text');
if (standbyHintText) standbyHintText.textContent = L.standby_unlock;
if (document.getElementById('tz-modal-title')) document.getElementById('tz-modal-title').textContent = L.tz_title;
try { renderTzModal(); } catch(e) {}
const pauseBtn = document.getElementById('timer-pause-btn');
const stopBtn = document.getElementById('timer-stop-btn');
if (currentMode === 'timer') {
pauseBtn.title = isTimerPaused ? L.timer_resume : L.timer_pause;
const closeTitles = ['关闭', 'Close', '閉じる', '닫기'];
stopBtn.title = closeTitles.includes(stopBtn.title) ? L.timer_close : L.timer_stop;
}
const dateStr = new Intl.DateTimeFormat(langLocales[lang] || 'en-US', { timeZone: currentTz, year: 'numeric', month: 'long', day: 'numeric', weekday: 'long' }).format(new Date());
lastDateStr = '';
const dateEl = document.getElementById('display-date');
if (dateEl) dateEl.innerText = dateStr;
const cityEl = document.getElementById('display-city');
if (cityEl && currentMode === 'clock') {
cityEl.innerText = getCityDisplay(localCityNameZh, localCityNameEn);
}
// 更新站点标题
const siteTitleEl = document.getElementById('site-title');
if (siteTitleEl) siteTitleEl.textContent = L.site_title;
document.title = `${L.site_title} - ${L.about_hero_h2}`;
// 同步语言切换器当前显示与高亮项
const langNameEl = document.getElementById('lang-current-name');
if (langNameEl) langNameEl.textContent = langDisplayNames[lang] || lang;
document.querySelectorAll('#lang-menu li').forEach(li => {
li.classList.toggle('active', li.dataset.lang === lang);
});
try { renderDock(); } catch(e) {}
try { if (window._renderPlaylist) window._renderPlaylist(); } catch(e) {}
try {
if (typeof updateTime === 'function') {
Object.keys(currentTimes).forEach(k => currentTimes[k] = -1);
updateTime();
}
} catch(e) {}
}
applyLanguage(currentLang);
// ==========================================
// 多国语言切换器交互
// ==========================================
(function initLangSwitcher() {
const switcher = document.getElementById('lang-switcher');
const currentBtn = document.getElementById('lang-current-btn');
const menu = document.getElementById('lang-menu');
if (!switcher || !currentBtn || !menu) return;
// 点击当前语言按钮:切换下拉菜单开/关
currentBtn.addEventListener('click', (e) => {
e.stopPropagation();
const isOpen = switcher.classList.toggle('open');
currentBtn.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
});
// 点击菜单项:切换语言
menu.querySelectorAll('li').forEach(li => {
li.addEventListener('click', () => {
const lang = li.dataset.lang;
if (lang && lang !== currentLang) {
applyLanguage(lang);
}
switcher.classList.remove('open');
currentBtn.setAttribute('aria-expanded', 'false');
});
});
// 点击页面其他区域:关闭下拉菜单
document.addEventListener('click', (e) => {
if (!switcher.contains(e.target)) {
switcher.classList.remove('open');
currentBtn.setAttribute('aria-expanded', 'false');
}
});
// 按 Esc 键关闭下拉菜单
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && switcher.classList.contains('open')) {
switcher.classList.remove('open');
currentBtn.setAttribute('aria-expanded', 'false');
currentBtn.focus();
}
});
})();
// ==========================================
// 1. 全屏与双击引擎
// ==========================================
function toggleFullScreen() {
const doc = document.documentElement;
if (!document.fullscreenElement && !document.webkitFullscreenElement && !document.mozFullScreenElement && !document.msFullscreenElement) {
if (doc.requestFullscreen) doc.requestFullscreen().catch(()=>{});
else if (doc.webkitRequestFullscreen) doc.webkitRequestFullscreen();
else if (doc.mozRequestFullScreen) doc.mozRequestFullScreen();
else if (doc.msRequestFullscreen) doc.msRequestFullscreen();
} else {
if (document.exitFullscreen) document.exitFullscreen().catch(()=>{});
else if (document.webkitExitFullscreen) document.webkitExitFullscreen();
else if (document.mozCancelFullScreen) document.mozCancelFullScreen();
else if (document.msExitFullscreen) document.msExitFullscreen();
}
}
function handleFullscreenChange() {
const isFull = document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement;
if (isFull) document.body.classList.add('fullscreen-mode');
else { document.body.classList.remove('fullscreen-mode'); requestAnimationFrame(() => { try { adjustBottomButtonsScale(); } catch(e) {} }); }
}
document.addEventListener('fullscreenchange', handleFullscreenChange);
document.addEventListener('webkitfullscreenchange', handleFullscreenChange);
document.addEventListener('mozfullscreenchange', handleFullscreenChange);
document.addEventListener('MSFullscreenChange', handleFullscreenChange);
document.getElementById('btn-fullscreen').addEventListener('click', (e) => {
e.stopPropagation(); toggleFullScreen();
});
document.addEventListener('dblclick', (e) => {
if (e.target.closest('.control-btn') || e.target.closest('.sidebar-panel') || e.target.closest('.scroll-hint') || e.target.closest('.dock-container') || e.target.closest('.tz-modal')) return;
toggleFullScreen();
});
// ==========================================
// 2. 动态时区底座库系统 (FIFO 排队)
// ==========================================
const dockContainer = document.getElementById('dock-container');
const tzModal = document.getElementById('tz-modal');
const tzModalBody = document.getElementById('tz-modal-body');
const tzModalTitle = document.getElementById('tz-modal-title');
const allTzCities = [
{ group: 'asia', groupZh: '亚洲', groupEn: 'Asia', cities: [
{ tz: 'Asia/Shanghai', city: '北京', cityEn: 'Beijing', code: 'BJS', flag: '🇨🇳' },
{ tz: 'Asia/Tokyo', city: '东京', cityEn: 'Tokyo', code: 'TYO', flag: '🇯🇵' },
{ tz: 'Asia/Seoul', city: '首尔', cityEn: 'Seoul', code: 'SEL', flag: '🇰🇷' },
{ tz: 'Asia/Singapore', city: '新加坡', cityEn: 'Singapore', code: 'SIN', flag: '🇸🇬' },
{ tz: 'Asia/Bangkok', city: '曼谷', cityEn: 'Bangkok', code: 'BKK', flag: '🇹🇭' },
{ tz: 'Asia/Dubai', city: '迪拜', cityEn: 'Dubai', code: 'DXB', flag: '🇦🇪' },
]},
{ group: 'europe', groupZh: '欧洲', groupEn: 'Europe', cities: [
{ tz: 'Europe/London', city: '伦敦', cityEn: 'London', code: 'LON', flag: '🇬🇧' },
{ tz: 'Europe/Paris', city: '巴黎', cityEn: 'Paris', code: 'PAR', flag: '🇫🇷' },
{ tz: 'Europe/Berlin', city: '柏林', cityEn: 'Berlin', code: 'BER', flag: '🇩🇪' },
{ tz: 'Europe/Rome', city: '罗马', cityEn: 'Rome', code: 'ROM', flag: '🇮🇹' },
{ tz: 'Europe/Moscow', city: '莫斯科', cityEn: 'Moscow', code: 'MOW', flag: '🇷🇺' },
]},
{ group: 'americas', groupZh: '美洲', groupEn: 'Americas', cities: [
{ tz: 'America/New_York', city: '纽约', cityEn: 'New York', code: 'NYC', flag: '🇺🇸' },
{ tz: 'America/Los_Angeles', city: '洛杉矶', cityEn: 'Los Angeles', code: 'LAX', flag: '🇺🇸' },
{ tz: 'America/Chicago', city: '芝加哥', cityEn: 'Chicago', code: 'CHI', flag: '🇺🇸' },
{ tz: 'America/Toronto', city: '多伦多', cityEn: 'Toronto', code: 'YTO', flag: '🇨🇦' },
{ tz: 'America/Sao_Paulo', city: '圣保罗', cityEn: 'São Paulo', code: 'SAO', flag: '🇧🇷' },
]},
{ group: 'oceania_africa', groupZh: '大洋洲与非洲', groupEn: 'Oceania & Africa', cities: [
{ tz: 'Australia/Sydney', city: '悉尼', cityEn: 'Sydney', code: 'SYD', flag: '🇦🇺' },
{ tz: 'Pacific/Auckland', city: '奥克兰', cityEn: 'Auckland', code: 'AKL', flag: '🇳🇿' },
{ tz: 'Africa/Cairo', city: '开罗', cityEn: 'Cairo', code: 'CAI', flag: '🇪🇬' },
{ tz: 'Africa/Johannesburg', city: '约翰内斯堡', cityEn: 'Johannesburg', code: 'JNB', flag: '🇿🇦' },
]}
];
function renderTzModal() {
tzModalBody.innerHTML = '';
allTzCities.forEach((group, idx) => {
const groupTitle = document.createElement('div');
groupTitle.className = 'tz-group-title';
if (idx > 0) groupTitle.style.marginTop = '25px';
if (currentLang === 'en') {
groupTitle.innerText = group.groupEn;
} else if (currentLang === 'ja') {
const jaGroups = { '国内热门': '国内の人気都市', '亚太地区': 'アジア太平洋地域', '欧美地区': '欧米地域', '其他地区': 'その他の地域' };
groupTitle.innerText = `${jaGroups[group.groupZh] || group.groupZh} (${group.groupEn})`;
} else if (currentLang === 'ko') {
const koGroups = { '国内热门': '국내 인기 도시', '亚太地区': '아시아 태평양 지역', '欧美地区': '유럽 미주 지역', '其他地区': '기타 지역' };
groupTitle.innerText = `${koGroups[group.groupZh] || group.groupZh} (${group.groupEn})`;
} else if (currentLang === 'zh-tw') {
const twGroups = { '国内热门': '國內熱門', '亚太地区': '亞太地區', '欧美地区': '歐美地區', '其他地区': '其他地區' };
groupTitle.innerText = `${twGroups[group.groupZh] || group.groupZh} (${group.groupEn})`;
} else {
groupTitle.innerText = `${group.groupZh} (${group.groupEn})`;
}
tzModalBody.appendChild(groupTitle);
const grid = document.createElement('div');
grid.className = 'tz-grid';
group.cities.forEach(city => {
const item = document.createElement('div');
item.className = 'tz-item';
item.dataset.tz = city.tz;
item.dataset.city = city.city;
item.dataset.cityEn = city.cityEn;
const cityName = getCityDisplay(city.city, city.cityEn);
item.innerHTML = `${city.flag} ${cityName} (${city.code})`;
item.onclick = () => {
const newTz = city.tz;
const newCity = city.city;
const newCityEn = city.cityEn;
const newIcon = city.flag;
if (!myDockCities.find(c => c.tz === newTz)) {
if (myDockCities.length >= 3) myDockCities.shift();
myDockCities.push({ tz: newTz, city: newCity, cityEn: newCityEn, icon: newIcon });
localStorage.setItem('my_dock_cities', JSON.stringify(myDockCities));
}
currentTz = newTz;
localCityNameZh = newCity;
localCityNameEn = newCityEn;
document.getElementById('display-city').innerText = getCityDisplay(localCityNameZh, localCityNameEn);
renderDock();
document.querySelectorAll('.dock-btn').forEach(b => b.classList.remove('active'));
const targetBtn = Array.from(document.querySelectorAll('.dock-btn')).find(b => b.dataset.tz === newTz);
if (targetBtn) targetBtn.classList.add('active');
Object.keys(currentTimes).forEach(k => currentTimes[k] = -1);
lastDateStr = '';
updateTime();
tzModal.classList.remove('show');
};
grid.appendChild(item);
});
tzModalBody.appendChild(grid);
});
}
const defaultDockCities = [
{ tz: 'America/New_York', city: '纽约', cityEn: 'New York', icon: '🇺🇸' },
{ tz: 'Europe/London', city: '伦敦', cityEn: 'London', icon: '🇬🇧' },
{ tz: 'Asia/Tokyo', city: '东京', cityEn: 'Tokyo', icon: '🇯🇵' }
];
function loadDockCities() {
try {
const saved = localStorage.getItem('my_dock_cities');
if (saved) {
const parsed = JSON.parse(saved);
if (Array.isArray(parsed) && parsed.length > 0) {
// 验证数据完整性:每个项必须有 tz 和 city
const valid = parsed.every(item => item && item.tz && item.city && item.cityEn && item.city !== item.cityEn);
if (valid) return parsed;
}
}
} catch (e) {}
return defaultDockCities;
}
let myDockCities = loadDockCities();
function renderDock() {
dockContainer.innerHTML = '';
const localBtn = document.createElement('button');
localBtn.className = `dock-btn ${currentTz === localBtn.dataset.tz || document.querySelectorAll('.dock-btn.active').length === 0 ? 'active' : ''}`;
localBtn.dataset.tz = currentTz;
localBtn.dataset.city = localCityNameZh;
localBtn.dataset.cityEn = localCityNameEn;
const currentCityName = getCityDisplay(localCityNameZh, localCityNameEn);
localBtn.innerHTML = `📍 ${t('dock_current')}: ${currentCityName}`;
dockContainer.appendChild(localBtn);
myDockCities.forEach((item) => {
// 跳过与定位城市相同的城市名,避免重复
const localName = getCityDisplay(localCityNameZh, localCityNameEn);
const itemName = getCityDisplay(item.city, item.cityEn || item.city);
if (localName === itemName) return;
const btn = document.createElement('button');
btn.className = `dock-btn ${currentTz === item.tz ? 'active' : ''}`;
btn.dataset.tz = item.tz;
btn.dataset.city = item.city;
btn.dataset.cityEn = item.cityEn || item.city;
btn.innerHTML = `${item.icon} ${itemName}`;
dockContainer.appendChild(btn);
});
const addBtn = document.createElement('button');
addBtn.className = 'dock-btn add-btn';
addBtn.innerHTML = `🌍 ${t('more_regions')}`;
addBtn.onclick = (e) => { e.stopPropagation(); renderTzModal(); tzModal.classList.add('show'); };
dockContainer.appendChild(addBtn);
// 等待 DOM 布局完成后调整底部按钮缩放,防止与时区栏重叠
requestAnimationFrame(() => { try { adjustBottomButtonsScale(); } catch(e) {} });
}
// 底部功能按钮与时区栏防重叠自动缩放
function adjustBottomButtonsScale() {
const dock = document.getElementById('dock-wrapper');
const qc = document.getElementById('quick-controls');
if (!dock || !qc) return;
// 全屏/锁屏模式下按钮由 CSS 隐藏,不设置内联 transform 以免覆盖 CSS 动画
if (document.body.classList.contains('fullscreen-mode') || document.body.classList.contains('standby-mode')) {
qc.style.transform = '';
const ac2 = document.getElementById('audio-controls');
if (ac2) ac2.style.transform = '';
return;
}
const dockRect = dock.getBoundingClientRect();
const margin = 16;
// 左侧快捷按钮防重叠
const qcRect = qc.getBoundingClientRect();
const qcNaturalWidth = qc.scrollWidth || qcRect.width;
const availableLeft = dockRect.left - margin - qcRect.left;
if (qcNaturalWidth > availableLeft && availableLeft > 40) {
const scale = Math.max(0.5, availableLeft / qcNaturalWidth);
qc.style.transformOrigin = 'left center';
qc.style.transform = `scale(${scale})`;
} else if (availableLeft <= 40) {
qc.style.transformOrigin = 'left center';
qc.style.transform = 'scale(0.5)';
} else {
qc.style.transform = '';
}
// 右侧音频控制防重叠
const ac = document.getElementById('audio-controls');
if (ac) {
const acRect = ac.getBoundingClientRect();
const acNaturalWidth = ac.scrollWidth || acRect.width;
const availableRight = acRect.right - (dockRect.right + margin);
if (acNaturalWidth > availableRight && availableRight > 40) {
const scale = Math.max(0.5, availableRight / acNaturalWidth);
ac.style.transformOrigin = 'right center';
ac.style.transform = `scale(${scale})`;
} else if (availableRight <= 40) {
ac.style.transformOrigin = 'right center';
ac.style.transform = 'scale(0.5)';
} else {
ac.style.transform = '';
}
}
}
dockContainer.addEventListener('click', (e) => {
const btn = e.target.closest('.dock-btn:not(.add-btn)');
if (!btn) return;
document.querySelectorAll('.dock-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
currentTz = btn.dataset.tz;
document.getElementById('display-city').innerText = getCityDisplay(btn.dataset.city, btn.dataset.cityEn || btn.dataset.city);
Object.keys(currentTimes).forEach(k => currentTimes[k] = -1); updateTime();
});
document.getElementById('close-tz-btn').addEventListener('click', () => tzModal.classList.remove('show'));
tzModal.addEventListener('click', (e) => { if(e.target === tzModal) tzModal.classList.remove('show'); });
const cityNameMap = {
'Beijing': '北京', 'Shanghai': '上海', 'Guangzhou': '广州', 'Shenzhen': '深圳',
'Hong Kong': '香港', 'Taipei': '台北', 'Macau': '澳门', 'Tianjin': '天津',
'Chongqing': '重庆', 'Chengdu': '成都', 'Hangzhou': '杭州', 'Wuhan': '武汉',
'Xian': '西安', 'Nanjing': '南京', 'Suzhou': '苏州', 'Zhengzhou': '郑州',
'Dongguan': '东莞', 'Qingdao': '青岛', 'Changsha': '长沙', 'Shenyang': '沈阳',
'Harbin': '哈尔滨', 'Foshan': '佛山', 'Hefei': '合肥', 'Jinan': '济南',
'Ningbo': '宁波', 'Xiamen': '厦门', 'Kunming': '昆明', 'Taiyuan': '太原',
'Urumqi': '乌鲁木齐', 'Lanzhou': '兰州', 'Xining': '西宁', 'Yinchuan': '银川',
'Hohhot': '呼和浩特', 'Nanchang': '南昌', 'Changchun': '长春', 'Dalian': '大连',
'Wuxi': '无锡', 'Wenzhou': '温州', 'Zhongshan': '中山', 'Zhuhai': '珠海',
'Tokyo': '东京', 'Osaka': '大阪', 'Sapporo': '札幌', 'Nagoya': '名古屋',
'Seoul': '首尔', 'Busan': '釜山', 'Singapore': '新加坡', 'Bangkok': '曼谷',
'Dubai': '迪拜', 'London': '伦敦', 'Paris': '巴黎', 'Berlin': '柏林',
'Rome': '罗马', 'Moscow': '莫斯科', 'New York': '纽约', 'Los Angeles': '洛杉矶',
'Chicago': '芝加哥', 'Toronto': '多伦多', 'Sao Paulo': '圣保罗',
'Sydney': '悉尼', 'Auckland': '奥克兰', 'Cairo': '开罗', 'Johannesburg': '约翰内斯堡'
};
function getChineseCityName(englishName, timezone) {
if (cityNameMap[englishName]) return cityNameMap[englishName];
const cityPart = timezone.split('/').pop().replace('_', ' ');
return cityNameMap[cityPart] || '本地';
}
const cityNameMapReverse = {};
Object.keys(cityNameMap).forEach(en => { cityNameMapReverse[cityNameMap[en]] = en; });
function getEnglishCityName(chineseName, timezone) {
if (cityNameMapReverse[chineseName]) return cityNameMapReverse[chineseName];
const cityPart = timezone.split('/').pop().replace('_', ' ');
return cityPart;
}
const cityNameJa = {
'北京': '北京', '上海': '上海', '广州': '広州', '深圳': '深セン',
'香港': '香港', '台北': '台北', '澳门': 'マカオ', '天津': '天津',
'重庆': '重慶', '成都': '成都', '杭州': '杭州', '武汉': '武漢',
'西安': '西安', '南京': '南京', '苏州': '蘇州', '郑州': '鄭州',
'东莞': '東莞', '青岛': '青島', '长沙': '長沙', '沈阳': '瀋陽',
'哈尔滨': 'ハルビン', '佛山': '仏山', '合肥': '合肥', '济南': '済南',
'宁波': '寧波', '厦门': 'アモイ', '昆明': '昆明', '太原': '太原',
'乌鲁木齐': 'ウルムチ', '兰州': '蘭州', '西宁': '西寧', '银川': '銀川',
'呼和浩特': 'フフホト', '南昌': '南昌', '长春': '長春', '大连': '大連',
'无锡': '無錫', '温州': '温州', '中山': '中山', '珠海': '珠海',
'东京': '東京', '大阪': '大阪', '札幌': '札幌', '名古屋': '名古屋',
'首尔': 'ソウル', '釜山': '釜山', '新加坡': 'シンガポール', '曼谷': 'バンコク',
'迪拜': 'ドバイ', '伦敦': 'ロンドン', '巴黎': 'パリ', '柏林': 'ベルリン',
'罗马': 'ローマ', '莫斯科': 'モスクワ', '纽约': 'ニューヨーク', '洛杉矶': 'ロサンゼルス',
'芝加哥': 'シカゴ', '多伦多': 'トロント', '圣保罗': 'サンパウロ',
'悉尼': 'シドニー', '奥克兰': 'オークランド', '开罗': 'カイロ', '约翰内斯堡': 'ヨハネスブルク',
'本地': '現在地'
};
const cityNameKo = {
'北京': '베이징', '上海': '상하이', '广州': '광저우', '深圳': '선전',
'香港': '홍콩', '台北': '타이베이', '澳门': '마카오', '天津': '톈진',
'重庆': '충칭', '成都': '청두', '杭州': '항저우', '武汉': '우한',
'西安': '시안', '南京': '난징', '苏州': '쑤저우', '郑州': '정저우',
'东莞': '둥관', '青岛': '칭다오', '长沙': '창사', '沈阳': '선양',
'哈尔滨': '하얼빈', '佛山': '포산', '合肥': '허페이', '济南': '지난',
'宁波': '닝보', '厦门': '샤먼', '昆明': '쿤밍', '太原': '타이위안',
'乌鲁木齐': '우루무치', '兰州': '란저우', '西宁': '시닝', '银川': '인촨',
'呼和浩特': '후허하오터', '南昌': '난창', '长春': '창춘', '大连': '대련',
'无锡': '우시', '温州': '원저우', '中山': '중산', '珠海': '주하이',
'东京': '도쿄', '大阪': '오사카', '札幌': '삿포로', '名古屋': '나고야',
'首尔': '서울', '釜山': '부산', '新加坡': '싱가포르', '曼谷': '방콕',
'迪拜': '두바이', '伦敦': '런던', '巴黎': '파리', '柏林': '베를린',
'罗马': '로마', '莫斯科': '모스크바', '纽约': '뉴욕', '洛杉矶': '로스앤젤레스',
'芝加哥': '시카고', '多伦多': '토론토', '圣保罗': '상파울루',
'悉尼': '시드니', '奥克兰': '오클랜드', '开罗': '카이로', '约翰内斯堡': '요하네스버그',
'本地': '현지'
};
const cityNameZhTw = {
'北京': '北京', '上海': '上海', '广州': '廣州', '深圳': '深圳',
'香港': '香港', '台北': '台北', '澳门': '澳門', '天津': '天津',
'重庆': '重慶', '成都': '成都', '杭州': '杭州', '武汉': '武漢',
'西安': '西安', '南京': '南京', '苏州': '蘇州', '郑州': '鄭州',
'东莞': '東莞', '青岛': '青島', '长沙': '長沙', '沈阳': '瀋陽',
'哈尔滨': '哈爾濱', '佛山': '佛山', '合肥': '合肥', '济南': '濟南',
'宁波': '寧波', '厦门': '廈門', '昆明': '昆明', '太原': '太原',
'乌鲁木齐': '烏魯木齊', '兰州': '蘭州', '西宁': '西寧', '银川': '銀川',
'呼和浩特': '呼和浩特', '南昌': '南昌', '长春': '長春', '大连': '大連',
'无锡': '無錫', '温州': '溫州', '中山': '中山', '珠海': '珠海',
'东京': '東京', '大阪': '大阪', '札幌': '札幌', '名古屋': '名古屋',
'首尔': '首爾', '釜山': '釜山', '新加坡': '新加坡', '曼谷': '曼谷',
'迪拜': '杜拜', '伦敦': '倫敦', '巴黎': '巴黎', '柏林': '柏林',
'罗马': '羅馬', '莫斯科': '莫斯科', '纽约': '紐約', '洛杉矶': '洛杉磯',
'芝加哥': '芝加哥', '多伦多': '多倫多', '圣保罗': '聖保羅',
'悉尼': '雪梨', '奥克兰': '奧克蘭', '开罗': '開羅', '约翰内斯堡': '約翰內斯堡',
'本地': '本地'
};
function getCityDisplay(zhName, enName) {
if (currentLang === 'en') return enName || zhName;
if (currentLang === 'zh') return zhName || enName;
if (currentLang === 'zh-tw') return cityNameZhTw[zhName] || zhName;
if (currentLang === 'ja') return cityNameJa[zhName] || zhName;
if (currentLang === 'ko') return cityNameKo[zhName] || zhName;
return zhName;
}
async function locateByIP() {
// 首次获取后缓存到 localStorage,下次直接读取(节省请求、加快首屏)
const cacheKey = 'saved_ip_location';
const cached = localStorage.getItem(cacheKey);
if (cached) {
try {
const data = JSON.parse(cached);
if (data && data.zhName && data.enName) {
if (data.tz) currentTz = data.tz;
localCityNameZh = data.zhName;
localCityNameEn = data.enName;
return;
}
} catch (e) { /* 缓存损坏,继续走在线获取 */ }
}
// 多 IP 定位 API 回退链(均为 HTTPS、免费、免密钥,按中文友好度排序)
const apis = [
{
// 返回原生中文城市名,精度高
url: 'https://ip.useragentinfo.com/json',
parse: d => d && d.code === 200 && d.city
? { city: d.city, tz: null } : null
},
{
url: 'https://ipwho.is/',
parse: d => d && d.success !== false && d.city
? { city: d.city, tz: d.timezone && d.timezone.id } : null
},
{
url: 'https://ipapi.co/json/',
parse: d => d && !d.error && d.city
? { city: d.city, tz: d.timezone } : null
}
];
for (const api of apis) {
try {
const res = await fetch(api.url);
const data = await res.json();
const parsed = api.parse(data);
if (parsed) {
if (parsed.tz) currentTz = parsed.tz;
// 若城市名已是中文直接使用,否则查映射表翻译
localCityNameZh = /[\u4e00-\u9fa5]/.test(parsed.city)
? parsed.city
: getChineseCityName(parsed.city, currentTz);
localCityNameEn = parsed.city;
// 保存到缓存,下次直接读取
localStorage.setItem(cacheKey, JSON.stringify({
zhName: localCityNameZh,
enName: localCityNameEn,
tz: currentTz
}));
return;
}
} catch (e) { /* 尝试下一个 API */ }
}
// 所有 API 均失败,使用时区兜底
localCityNameZh = getChineseCityName('', currentTz);
localCityNameEn = getEnglishCityName(localCityNameZh, currentTz);
}
async function initLocation() {
// 并行启动 IP 定位和浏览器地理定位,谁快用谁
// IP 定位通常更快(几百毫秒),地理定位精度更高但可能慢
let settled = false;
const finish = (zhName, enName) => {
if (settled) return;
settled = true;
localCityNameZh = zhName;
localCityNameEn = enName;
document.getElementById('display-city').innerText = getCityDisplay(localCityNameZh, localCityNameEn);
renderDock(); updateTime();
};
// 1. IP 定位(更快,通常 200-500ms)
locateByIP().then(() => {
finish(localCityNameZh, localCityNameEn);
}).catch(() => {});
// 2. 浏览器地理定位(更精确,作为后台升级)
if (navigator.geolocation) {
new Promise((resolve, reject) => {
navigator.geolocation.getCurrentPosition(resolve, reject, {
enableHighAccuracy: false,
timeout: 6000,
maximumAge: 600000
});
}).then(async (position) => {
const { latitude, longitude } = position.coords;
try {
const res = await fetch(`https://nominatim.openstreetmap.org/reverse?lat=${latitude}&lon=${longitude}&format=json&accept-language=zh`);
const data = await res.json();
if (data && data.address) {
const addr = data.address;
const city = addr.city || addr.town || addr.county || addr.state || addr.region || '';
if (city) {
// 地理定位更精确,如果已经有 IP 结果则静默升级
localCityNameZh = city;
localCityNameEn = getEnglishCityName(city, currentTz) || city;
document.getElementById('display-city').innerText = getCityDisplay(localCityNameZh, localCityNameEn);
renderDock();
}
}
} catch (e) {}
}).catch(() => {});
}
}
// ==========================================
// 农历工具函数
// ==========================================
const lunarMonthNames = ['正月','二月','三月','四月','五月','六月','七月','八月','九月','十月','冬月','腊月'];
const lunarDayNames = ['初一','初二','初三','初四','初五','初六','初七','初八','初九','初十','十一','十二','十三','十四','十五','十六','十七','十八','十九','二十','廿一','廿二','廿三','廿四','廿五','廿六','廿七','廿八','廿九','三十'];
const zodiacNames = ['鼠','牛','虎','兔','龙','蛇','马','羊','猴','鸡','狗','猪'];
const ganNames = ['甲','乙','丙','丁','戊','己','庚','辛','壬','癸'];
const zhiNames = ['子','丑','寅','卯','辰','巳','午','未','申','酉','戌','亥'];
const lunarInfo = [
0x04bd8,0x04ae0,0x0a570,0x054d5,0x0d260,0x0d950,0x16554,0x056a0,0x09ad0,0x055d2,
0x04ae0,0x0a5b6,0x0a4d0,0x0d250,0x1d255,0x0b540,0x0d6a0,0x0ada2,0x095b0,0x14977,
0x04970,0x0a4b0,0x0b4b5,0x06a50,0x06d40,0x1ab54,0x02b60,0x09570,0x052f2,0x04970,
0x06566,0x0d4a0,0x0ea50,0x06e95,0x05ad0,0x02b60,0x186e3,0x092e0,0x1c8d7,0x0c950,
0x0d4a0,0x1d8a6,0x0b550,0x056a0,0x1a5b4,0x025d0,0x092d0,0x0d2b2,0x0a950,0x0b557,
0x06ca0,0x0b550,0x15355,0x04da0,0x0a5d0,0x14573,0x052d0,0x0a9a8,0x0e950,0x06aa0,
0x0aea6,0x0ab50,0x04b60,0x0aae4,0x0a570,0x05260,0x0f263,0x0d950,0x05b57,0x056a0,
0x096d0,0x04dd5,0x04ad0,0x0a4d0,0x0d4d4,0x0d250,0x0d558,0x0b540,0x0b5a0,0x195a6,
0x095b0,0x049b0,0x0a974,0x0a4b0,0x0b27a,0x06a50,0x06d40,0x0af46,0x0ab60,0x09570,
0x04af5,0x04970,0x064b0,0x074a3,0x0ea50,0x06b58,0x055c0,0x0ab60,0x096d5,0x092e0,
0x0c960,0x0d954,0x0d4a0,0x0da50,0x07552,0x056a0,0x0abb7,0x025d0,0x092d0,0x0cab5,
0x0a950,0x0b4a0,0x0baa4,0x0ad50,0x055d9,0x04ba0,0x0a5b0,0x15176,0x052b0,0x0a930,
0x07954,0x06aa0,0x0ad50,0x05b52,0x04b60,0x0a6e6,0x0a4e0,0x0d260,0x0ea65,0x0d530,
0x05aa0,0x076a3,0x096d0,0x04bd7,0x04ad0,0x0a4d0,0x1d0b6,0x0d250,0x0d520,0x0dd45,
0x0b5a0,0x056d0,0x055b2,0x049b0,0x0a577,0x0a4b0,0x0aa50,0x1b255,0x06d20,0x0ada0
];
function lYearDays(y) {
let i, sum = 348;
for(i = 0x8000; i > 0x8; i >>= 1) sum += (lunarInfo[y - 1900] & i) ? 1 : 0;
return(sum + leapDays(y));
}
function leapMonth(y) { return(lunarInfo[y - 1900] & 0xf); }
function leapDays(y) {
if(leapMonth(y)) return((lunarInfo[y - 1900] & 0x10000) ? 30 : 29);
else return(0);
}
function monthDays(y, m) { return((lunarInfo[y - 1900] & (0x10000 >> m)) ? 30 : 29); }
function solarToLunar(year, month, day) {
const solarMonth = [31,28,31,30,31,30,31,31,30,31,30,31];
let i, leap = 0, temp = 0;
let baseYear = 1900;
let baseMonth = 1;
let baseDay = 31;
year < 1900 && (year += 1900);
let offset = (Date.UTC(year, month - 1, day) - Date.UTC(1900, 0, 31)) / 86400000;
for(i = 1900; i < 2101 && offset > 0; i++) {
temp = lYearDays(i);
offset -= temp;
}
if(offset < 0) {
offset += temp;
i--;
}
year = i;
leap = leapMonth(i);
let isLeap = false;
for(i = 1; i < 13 && offset > 0; i++) {
if(leap > 0 && i == (leap + 1) && isLeap == false) {
--i;
isLeap = true;
temp = leapDays(year);
} else {
temp = monthDays(year, i);
}
if(isLeap == true && i == (leap + 1)) isLeap = false;
offset -= temp;
}
if(offset == 0 && leap > 0 && i == leap + 1) {
if(isLeap) { isLeap = false; }
else { isLeap = true; --i; }
}
if(offset < 0) { offset += temp; --i; }
month = i;
day = offset + 1;
return { year, month, day, isLeap };
}
function getGanZhi(year) {
const ganIndex = (year - 4) % 10;
const zhiIndex = (year - 4) % 12;
return ganNames[ganIndex] + zhiNames[zhiIndex];
}
function getZodiac(year) {
return zodiacNames[(year - 4) % 12];
}
function formatLunar(date) {
const lunar = solarToLunar(date.getFullYear(), date.getMonth() + 1, date.getDate());
const yearGanZhi = getGanZhi(lunar.year);
const zodiac = getZodiac(lunar.year);
const monthStr = lunar.isLeap ? '闰' + lunarMonthNames[lunar.month - 1] : lunarMonthNames[lunar.month - 1];
const dayStr = lunarDayNames[lunar.day - 1];
return `${yearGanZhi}年 ${zodiac}年 ${monthStr} ${dayStr}`;
}
// ==========================================
// 3. 媒体画廊引擎
// ==========================================
let bgDataList = [
{ id: 'i1', type: 'image', title: '极简纯白', cover: 'https://images.unsplash.com/photo-1495195129352-aeb325a55b65?w=300', url: 'https://images.unsplash.com/photo-1495195129352-aeb325a55b65?q=80&w=2560&auto=format&fit=crop' },
{ id: 'v1', type: 'video', title: '深海潜游 (4K)', cover: 'https://images.unsplash.com/photo-1505118380757-91f5f5632de0?w=300', url: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4' },
{ id: 'i2', type: 'image', title: '赛博霓虹', cover: 'https://cdn.pixabay.com/video/2019/02/01/21118-315137091_tiny.jpg', url: 'https://cdn.pixabay.com/video/2019/02/01/21118-315137091_tiny.jpg' },
{ id: 'v2', type: 'video', title: '科技夜城', cover: 'https://images.unsplash.com/photo-1519501025264-65ba15a82390?w=300', url: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/TearsOfSteel.mp4' },
{ id: 'i3', type: 'image', title: '暗黑质感', cover: 'https://images.unsplash.com/photo-1550684848-fac1c5b4e853?w=300', url: 'https://images.unsplash.com/photo-1550684848-fac1c5b4e853?q=80&w=2560&auto=format&fit=crop' },
{ id: 'i4', type: 'image', title: '深空星系', cover: 'https://images.unsplash.com/photo-1462331940025-496dfbfc7564?w=300', url: 'https://images.unsplash.com/photo-1462331940025-496dfbfc7564?q=80&w=2560&auto=format&fit=crop' }
];
const bgLayer = document.getElementById('bg-layer');
const masterGrid = document.getElementById('master-bg-grid');
let currentMediaBlob = null, currentAudioBlob = null;
let currentBgIndex = 0;
function renderBgList() {
const uploadCard = masterGrid.firstElementChild;
masterGrid.innerHTML = '';
masterGrid.appendChild(uploadCard);
const currentBg = localStorage.getItem('pro_bg_id') || 'i1';
bgDataList.forEach((item, index) => {
if(item.id === currentBg) currentBgIndex = index;
const card = document.createElement('div');
card.className = `bg-card ${item.id === currentBg ? 'active' : ''}`;
card.innerHTML = `<div class="bg-type-badge">${item.type === 'video' ? '▶ 动态视频' : '🖼 高清图片'}</div><img class="bg-cover" src="${item.cover}" alt="cover"><div class="bg-info"><span class="bg-title">${item.title}</span></div>`;
card.onclick = () => {
document.querySelectorAll('.media-gallery .bg-card').forEach(c => c.classList.remove('active'));
card.classList.add('active');
localStorage.setItem('pro_bg_id', item.id);
currentBgIndex = index;
applyBackground(item);
document.getElementById('screen-clock').scrollIntoView();
};
masterGrid.appendChild(card);
});
}
function applyBackground(item) {
bgLayer.innerHTML = '';
if (item.type === 'image') {
const img = document.createElement('img'); img.src = item.url; bgLayer.appendChild(img);
} else if (item.type === 'video') {
const vid = document.createElement('video');
vid.src = item.url; vid.autoplay = true; vid.loop = true; vid.muted = true; vid.playsInline = true;
bgLayer.appendChild(vid);
}
}
document.getElementById('custom-upload').addEventListener('change', (e) => {
const file = e.target.files[0]; if (!file) return;
if(currentMediaBlob) URL.revokeObjectURL(currentMediaBlob);
currentMediaBlob = URL.createObjectURL(file);
const isVideo = file.type.startsWith('video/');
const customItem = { id: 'custom_' + Date.now(), type: isVideo ? 'video' : 'image', title: '自定义本地文件', cover: isVideo ? 'https://via.placeholder.com/300x150/2a2a2d/fff?text=Local+Video' : currentMediaBlob, url: currentMediaBlob };
bgDataList.unshift(customItem); currentBgIndex = 0; localStorage.setItem('pro_bg_id', customItem.id);
renderBgList(); applyBackground(customItem); document.getElementById('screen-clock').scrollIntoView();
});
function initBg() {
const savedId = localStorage.getItem('pro_bg_id') || 'i1';
if (savedId === 'bing_random') {
const savedUrl = localStorage.getItem('pro_bg_url') || `https://bing.img.run/rand.php?t=${new Date().getTime()}`;
applyBackground({ type: 'image', url: savedUrl });
document.querySelectorAll('.media-gallery .bg-card').forEach(c => c.classList.remove('active'));
} else {
const item = bgDataList.find(d => d.id === savedId) || bgDataList.find(d => d.id === 'i1');
applyBackground(item);
}
renderBgList();
}
let bgAutoSwitchInterval = null;
let bgAutoSwitchEnabled = false;
function switchToNextBg() {
if (bgDataList.length === 0) return;
const savedId = localStorage.getItem('pro_bg_id') || 'i1';
if (savedId === 'bing_random') {
const randUrl = `https://bing.img.run/rand.php?t=${new Date().getTime()}`;
localStorage.setItem('pro_bg_url', randUrl);
applyBackground({ type: 'image', url: randUrl });
} else {
currentBgIndex = (currentBgIndex + 1) % bgDataList.length;
const nextItem = bgDataList[currentBgIndex];
localStorage.setItem('pro_bg_id', nextItem.id);
applyBackground(nextItem);
document.querySelectorAll('.media-gallery .bg-card').forEach(c => c.classList.remove('active'));
const cards = document.querySelectorAll('.media-gallery .bg-card:not(.upload-card)');
if (cards[currentBgIndex]) cards[currentBgIndex].classList.add('active');
}
}
function startBgAutoSwitch() {
stopBgAutoSwitch();
const intervalInput = document.getElementById('setting-bg-switch-interval');
const minutes = parseInt(intervalInput.value) || 30;
const ms = minutes * 60 * 1000;
bgAutoSwitchInterval = setInterval(() => {
switchToNextBg();
}, ms);
bgAutoSwitchEnabled = true;
localStorage.setItem('bg_auto_switch', 'true');
localStorage.setItem('bg_switch_interval', minutes.toString());
}
function stopBgAutoSwitch() {
if (bgAutoSwitchInterval) {
clearInterval(bgAutoSwitchInterval);
bgAutoSwitchInterval = null;
}
bgAutoSwitchEnabled = false;
localStorage.setItem('bg_auto_switch', 'false');
}
function initBgAutoSwitch() {
const toggleEl = document.getElementById('setting-bg-auto-switch');
const intervalEl = document.getElementById('setting-bg-switch-interval');
const savedEnabled = localStorage.getItem('bg_auto_switch') === 'true';
const savedInterval = localStorage.getItem('bg_switch_interval');
if (savedInterval) intervalEl.value = savedInterval;
toggleEl.checked = savedEnabled;
if (savedEnabled) {
startBgAutoSwitch();
}
toggleEl.addEventListener('change', (e) => {
if (e.target.checked) {
startBgAutoSwitch();
} else {
stopBgAutoSwitch();
}
});
intervalEl.addEventListener('change', (e) => {
let val = parseInt(e.target.value);
if (isNaN(val) || val < 1) val = 1;
if (val > 1440) val = 1440;
e.target.value = val;
localStorage.setItem('bg_switch_interval', val.toString());
if (bgAutoSwitchEnabled) {
startBgAutoSwitch();
}
});
}
// ==========================================
// 4. 必应接口防缓存 & 全局翻转
// ==========================================
const appRoot = document.getElementById('app-root');
let isFlipX = false, isFlipY = false;
function updateGlobalMirror() {
appRoot.className = 'app-root';
if(isFlipX && isFlipY) appRoot.classList.add('mirror-xy');
else if(isFlipX) appRoot.classList.add('mirror-x');
else if(isFlipY) appRoot.classList.add('mirror-y');
document.getElementById('btn-flip-x').classList.toggle('active', isFlipX);
document.getElementById('btn-flip-y').classList.toggle('active', isFlipY);
}
document.getElementById('btn-flip-x').addEventListener('click', () => { isFlipX = !isFlipX; updateGlobalMirror(); });
document.getElementById('btn-flip-y').addEventListener('click', () => { isFlipY = !isFlipY; updateGlobalMirror(); });
document.getElementById('btn-next-bg').addEventListener('click', () => {
const randUrl = `https://bing.img.run/rand.php?t=${new Date().getTime()}`;
localStorage.setItem('pro_bg_id', 'bing_random');
localStorage.setItem('pro_bg_url', randUrl);
document.querySelectorAll('.media-gallery .bg-card').forEach(c => c.classList.remove('active'));
applyBackground({ type: 'image', url: randUrl });
});
// ==========================================
// 5. 设置引擎与侧边栏
// ==========================================
const sidebarWrapper = document.getElementById('sidebar-wrapper');
const settingsBtn = document.getElementById('settings-btn');
const closeSidebarBtn = document.getElementById('close-sidebar-btn');
settingsBtn.addEventListener('click', (e) => { e.stopPropagation(); sidebarWrapper.classList.toggle('open'); });
closeSidebarBtn.addEventListener('click', () => { sidebarWrapper.classList.remove('open'); });
document.addEventListener('click', (e) => {
if (!sidebarWrapper.contains(e.target) && e.target !== settingsBtn && sidebarWrapper.classList.contains('open')) {
sidebarWrapper.classList.remove('open');
}
});
const root = document.documentElement;
const elShowClock = document.getElementById('setting-show-clock');
const elScale = document.getElementById('setting-scale');
const elFont = document.getElementById('setting-font');
const elCardBg = document.getElementById('setting-card-bg');
const elFontColor = document.getElementById('setting-font-color');
const mainWidget = document.getElementById('main-widget');
const elHourFormat = document.getElementById('setting-hour-format');
const elShowLunar = document.getElementById('setting-show-lunar');
elShowClock.addEventListener('change', e => mainWidget.classList.toggle('hidden', !e.target.checked));
// 根据屏幕分辨率自动缩放
let userScaleOverride = localStorage.getItem('user_scale');
function autoScale() {
if (userScaleOverride) {
root.style.setProperty('--clock-scale', userScaleOverride);
elScale.value = userScaleOverride;
return;
}
const refWidth = 1920;
const scale = Math.max(0.5, Math.min(1.5, window.innerWidth / refWidth));
root.style.setProperty('--clock-scale', scale);
elScale.value = scale;
}
autoScale();
window.addEventListener('resize', autoScale);
window.addEventListener('resize', () => { try { adjustBottomButtonsScale(); } catch(e) {} });
elScale.addEventListener('input', e => {
userScaleOverride = e.target.value;
localStorage.setItem('user_scale', e.target.value);
root.style.setProperty('--clock-scale', e.target.value);
});
elFont.addEventListener('change', e => root.style.setProperty('--clock-font', e.target.value));
elCardBg.addEventListener('input', e => root.style.setProperty('--card-bg-color', e.target.value));
elFontColor.addEventListener('input', e => root.style.setProperty('--clock-color', e.target.value));
elShowLunar.addEventListener('change', (e) => {
const lunarEl = document.getElementById('display-lunar');
const infoFooter = document.querySelector('.info-footer');
if (e.target.checked) {
lunarEl.style.display = 'block';
lunarEl.innerText = formatLunar(new Date());
infoFooter.style.padding = '32px 40px';
} else {
lunarEl.style.display = 'none';
infoFooter.style.padding = '';
}
});
let is24Hour = true;
elHourFormat.addEventListener('change', (e) => {
is24Hour = e.target.value === '24';
Object.keys(currentTimes).forEach(k => currentTimes[k] = -1); updateTime();
});
const ambientAudio = document.getElementById('ambient-audio');
const audioToggle = document.getElementById('setting-audio-toggle');
const audioVol = document.getElementById('setting-volume-inline');
const playModeSelect = document.getElementById('setting-play-mode-inline');
if (ambientAudio && audioToggle && audioVol && playModeSelect) {
ambientAudio.volume = audioVol.value;
let audioPlaylist = [{ id: 'default', url: 'https://cdn.pixabay.com/audio/2022/05/27/audio_1808fbf07a.mp3', title: '__default_audio__' }];
let currentAudioIndex = 0;
let audioBlobs = [];
function renderPlaylist() {
const container = document.getElementById('playlist-container');
if (!container) return;
container.innerHTML = '';
audioPlaylist.forEach((item, index) => {
const div = document.createElement('div');
div.className = 'playlist-item' + (index === currentAudioIndex ? ' active' : '');
div.dataset.index = index;
const num = document.createElement('span');
num.className = 'track-num';
num.textContent = (index + 1);
const name = document.createElement('span');
name.className = 'track-name';
// 默认曲目标题跟随当前语言切换
const displayName = item.id === 'default' ? t('default_audio') : item.title;
name.textContent = displayName;
name.title = displayName;
const del = document.createElement('span');
del.className = 'track-del';
del.textContent = '✕';
del.title = t('delete');
del.addEventListener('click', (e) => {
e.stopPropagation();
removeFromPlaylist(index);
});
div.appendChild(num);
div.appendChild(name);
div.appendChild(del);
div.addEventListener('click', () => {
loadAudio(index);
});
container.appendChild(div);
});
}
// 已通过 window._renderPlaylist 暴露给 applyLanguage 在语言切换后刷新播放列表
function removeFromPlaylist(index) {
if (index < 0 || index >= audioPlaylist.length) return;
const item = audioPlaylist[index];
if (item.id !== 'default' && item.url && item.url.startsWith('blob:')) {
URL.revokeObjectURL(item.url);
}
audioPlaylist.splice(index, 1);
if (audioPlaylist.length === 0) {
audioPlaylist.push({ id: 'default', url: 'https://cdn.pixabay.com/audio/2022/05/27/audio_1808fbf07a.mp3', title: '__default_audio__' });
currentAudioIndex = 0;
loadAudio(0);
} else {
if (index === currentAudioIndex) {
currentAudioIndex = Math.min(index, audioPlaylist.length - 1);
loadAudio(currentAudioIndex);
} else if (index < currentAudioIndex) {
currentAudioIndex--;
}
}
renderPlaylist();
}
function loadAudio(index) {
if (index < 0 || index >= audioPlaylist.length) return;
currentAudioIndex = index;
const item = audioPlaylist[index];
ambientAudio.src = item.url;
if (audioToggle.checked) {
ambientAudio.play().catch(() => {});
}
renderPlaylist();
}
function playPrev() {
let newIndex;
const mode = playModeSelect.value;
if (mode === 'random') {
newIndex = Math.floor(Math.random() * audioPlaylist.length);
} else {
newIndex = currentAudioIndex - 1;
if (newIndex < 0) newIndex = audioPlaylist.length - 1;
}
loadAudio(newIndex);
}
function playNext() {
let newIndex;
const mode = playModeSelect.value;
if (mode === 'random') {
newIndex = Math.floor(Math.random() * audioPlaylist.length);
} else if (mode === 'single') {
newIndex = currentAudioIndex;
} else {
newIndex = (currentAudioIndex + 1) % audioPlaylist.length;
}
loadAudio(newIndex);
}
ambientAudio.addEventListener('ended', () => {
if (playModeSelect.value === 'single') {
ambientAudio.currentTime = 0;
ambientAudio.play().catch(() => {});
} else {
playNext();
}
});
audioToggle.addEventListener('change', (e) => {
if(e.target.checked) ambientAudio.play().catch(() => document.addEventListener('click', () => ambientAudio.play(), {once:true}));
else ambientAudio.pause();
});
audioVol.addEventListener('input', e => ambientAudio.volume = e.target.value);
document.getElementById('custom-audio-upload').addEventListener('change', (e) => {
const files = e.target.files; if(!files || files.length === 0) return;
const hadOnlyDefault = audioPlaylist.length === 1 && audioPlaylist[0].id === 'default';
let firstNewIndex = audioPlaylist.length;
Array.from(files).forEach(file => {
const blob = URL.createObjectURL(file);
audioBlobs.push(blob);
audioPlaylist.push({ id: 'custom_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9), url: blob, title: file.name });
});
renderPlaylist();
if (hadOnlyDefault) {
loadAudio(firstNewIndex);
}
audioToggle.checked = true;
e.target.value = '';
});
function updateAudioBtnState() {
const playPauseBtn = document.getElementById('audio-play-pause');
if (playPauseBtn) {
playPauseBtn.innerHTML = ambientAudio.paused ? '▶' : '⏸';
}
}
document.getElementById('audio-prev').addEventListener('click', () => playPrev());
document.getElementById('audio-next').addEventListener('click', () => playNext());
document.getElementById('audio-play-pause').addEventListener('click', () => {
if (ambientAudio.paused) {
ambientAudio.play().catch(() => {});
} else {
ambientAudio.pause();
}
updateAudioBtnState();
});
ambientAudio.addEventListener('pause', updateAudioBtnState);
ambientAudio.addEventListener('play', updateAudioBtnState);
window._renderPlaylist = renderPlaylist;
}
document.getElementById('reset-btn').addEventListener('click', () => {
if(confirm(t('confirm_reset'))) { localStorage.clear(); location.reload(); }
});
// ==========================================
// 6. 倒计时与计时器引擎
// ==========================================
let countdownInterval = null;
let timerInterval = null;
let countdownSeconds = 0;
let timerSeconds = 0;
let isCountdownRunning = false;
let isTimerRunning = false;
const countdownModal = document.getElementById('countdown-modal');
const countdownEndModal = document.getElementById('countdown-end-modal');
function showCountdownModal() {
countdownModal.classList.add('show');
}
function hideCountdownModal() {
countdownModal.classList.remove('show');
}
function showCountdownEndModal() {
countdownEndModal.classList.add('show');
}
function hideCountdownEndModal() {
countdownEndModal.classList.remove('show');
}
function switchToMode(mode) {
currentMode = mode;
const pauseBtn = document.getElementById('timer-pause-btn');
const stopBtn = document.getElementById('timer-stop-btn');
const cityEl = document.getElementById('display-city');
const dateEl = document.getElementById('display-date');
const ampmEl = document.getElementById('display-ampm');
const lunarEl = document.getElementById('display-lunar');
const infoFooter = document.querySelector('.info-footer');
if (mode === 'clock') {
clearInterval(countdownInterval);
clearInterval(timerInterval);
document.getElementById('btn-countdown').classList.remove('active');
document.getElementById('btn-timer').classList.remove('active');
cityEl.innerText = getCityDisplay(localCityNameZh, localCityNameEn);
cityEl.style.display = 'inline';
dateEl.innerText = '';
ampmEl.style.display = 'none';
pauseBtn.style.display = 'none';
stopBtn.style.display = 'none';
infoFooter.style.padding = '';
const showLunar = document.getElementById('setting-show-lunar')?.checked;
if (showLunar) {
lunarEl.style.display = 'block';
infoFooter.style.padding = '32px 40px';
} else {
lunarEl.style.display = 'none';
}
lastDateStr = '';
Object.keys(currentTimes).forEach(k => currentTimes[k] = -1);
updateTime();
} else if (mode === 'countdown') {
document.getElementById('btn-countdown').classList.add('active');
document.getElementById('btn-timer').classList.remove('active');
pauseBtn.style.display = 'none';
stopBtn.style.display = 'none';
lunarEl.style.display = 'none';
infoFooter.style.padding = '16px 40px';
} else if (mode === 'timer') {
document.getElementById('btn-timer').classList.add('active');
document.getElementById('btn-countdown').classList.remove('active');
pauseBtn.style.display = 'flex';
stopBtn.style.display = 'flex';
pauseBtn.innerHTML = '⏸';
pauseBtn.title = '暂停';
stopBtn.innerHTML = '■';
stopBtn.title = '停止';
lunarEl.style.display = 'none';
infoFooter.style.padding = '16px 40px';
}
}
// 格式化倒计时/计时器时长显示,根据当前语言返回紧凑格式
function formatDuration(h, m, s) {
if (currentLang === 'en') {
return h > 0 ? `${h}h ${m}m ${s}s` : `${m}m ${s}s`;
}
const units = {
'zh': { h: '小时', m: '分', s: '秒' },
'zh-tw': { h: '小時', m: '分', s: '秒' },
'ja': { h: '時間', m: '分', s: '秒' },
'ko': { h: '시간', m: '분', s: '초' }
};
const u = units[currentLang] || units.zh;
return h > 0 ? `${h}${u.h} ${m}${u.m} ${s}${u.s}` : `${m}${u.m} ${s}${u.s}`;
}
function updateCountdownDisplay(seconds) {
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = seconds % 60;
const hStr = h.toString().padStart(2, '0');
const mStr = m.toString().padStart(2, '0');
const sStr = s.toString().padStart(2, '0');
updateUnit('hours-tens', hStr[0]);
updateUnit('hours-ones', hStr[1]);
updateUnit('minutes-tens', mStr[0]);
updateUnit('minutes-ones', mStr[1]);
updateUnit('seconds-tens', sStr[0]);
updateUnit('seconds-ones', sStr[1]);
document.getElementById('display-city').innerText = t('countdown_label');
document.getElementById('display-date').innerText = formatDuration(0, m, s);
}
function updateTimerDisplay(seconds) {
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = seconds % 60;
const hStr = h.toString().padStart(2, '0');
const mStr = m.toString().padStart(2, '0');
const sStr = s.toString().padStart(2, '0');
updateUnit('hours-tens', hStr[0]);
updateUnit('hours-ones', hStr[1]);
updateUnit('minutes-tens', mStr[0]);
updateUnit('minutes-ones', mStr[1]);
updateUnit('seconds-tens', sStr[0]);
updateUnit('seconds-ones', sStr[1]);
document.getElementById('display-city').innerText = t('timer_label');
document.getElementById('display-date').innerText = formatDuration(h, m, s);
}
document.getElementById('btn-countdown').addEventListener('click', () => {
if (currentMode === 'countdown') {
switchToMode('clock');
} else {
showCountdownModal();
}
});
document.getElementById('close-countdown-btn').addEventListener('click', hideCountdownModal);
countdownModal.addEventListener('click', (e) => { if(e.target === countdownModal) hideCountdownModal(); });
document.getElementById('countdown-start-btn').addEventListener('click', () => {
const minutes = parseInt(document.getElementById('countdown-minutes').value) || 0;
const seconds = parseInt(document.getElementById('countdown-seconds').value) || 0;
countdownSeconds = minutes * 60 + seconds;
if (countdownSeconds <= 0) {
alert('请输入有效的倒计时时间');
return;
}
hideCountdownModal();
switchToMode('countdown');
isCountdownRunning = true;
updateCountdownDisplay(countdownSeconds);
countdownInterval = setInterval(() => {
countdownSeconds--;
updateCountdownDisplay(countdownSeconds);
if (countdownSeconds <= 0) {
clearInterval(countdownInterval);
isCountdownRunning = false;
updateCountdownDisplay(0);
showCountdownEndModal();
}
}, 1000);
});
document.getElementById('close-countdown-end-btn').addEventListener('click', () => {
hideCountdownEndModal();
switchToMode('clock');
});
document.getElementById('countdown-close-btn').addEventListener('click', () => {
hideCountdownEndModal();
switchToMode('clock');
});
document.getElementById('countdown-restart-btn').addEventListener('click', () => {
hideCountdownEndModal();
showCountdownModal();
});
document.getElementById('btn-timer').addEventListener('click', () => {
if (currentMode === 'timer') {
switchToMode('clock');
} else {
switchToMode('timer');
isTimerRunning = true;
isTimerPaused = false;
timerSeconds = 0;
updateTimerDisplay(0);
timerInterval = setInterval(() => {
if (!isTimerPaused) {
timerSeconds++;
updateTimerDisplay(timerSeconds);
}
}, 1000);
}
});
document.getElementById('timer-pause-btn').addEventListener('click', () => {
const pauseBtn = document.getElementById('timer-pause-btn');
if (isTimerPaused) {
isTimerPaused = false;
pauseBtn.innerHTML = '⏸';
pauseBtn.title = '暂停';
} else {
isTimerPaused = true;
pauseBtn.innerHTML = '▶';
pauseBtn.title = '继续';
}
});
document.getElementById('timer-stop-btn').addEventListener('click', () => {
const stopBtn = document.getElementById('timer-stop-btn');
if (stopBtn.title === '停止') {
clearInterval(timerInterval);
isTimerRunning = false;
stopBtn.innerHTML = '✕';
stopBtn.title = '关闭';
} else {
switchToMode('clock');
}
});
// ==========================================
// 万年历引擎
// ==========================================
const calendarModal = document.getElementById('calendar-modal');
const calendarGrid = document.getElementById('calendar-grid');
const calendarMonthYear = document.getElementById('calendar-month-year');
const calendarDetail = document.getElementById('calendar-detail');
let currentCalendarDate = new Date();
let selectedCalendarDay = null;
function showCalendar() {
calendarModal.classList.add('show');
selectedCalendarDay = null;
renderCalendar(currentCalendarDate);
}
function hideCalendar() {
calendarModal.classList.remove('show');
}
function renderCalendar(date) {
const year = date.getFullYear();
const month = date.getMonth();
const today = new Date();
calendarMonthYear.innerText = `${year}年 ${month + 1}月`;
const firstDay = new Date(year, month, 1).getDay();
const daysInMonth = new Date(year, month + 1, 0).getDate();
calendarGrid.innerHTML = '';
const weekDays = ['日', '一', '二', '三', '四', '五', '六'];
weekDays.forEach(day => {
const cell = document.createElement('div');
cell.style.textAlign = 'center';
cell.style.padding = '8px';
cell.style.color = '#888';
cell.style.fontWeight = '600';
cell.innerText = day;
calendarGrid.appendChild(cell);
});
for (let i = 0; i < firstDay; i++) {
const cell = document.createElement('div');
cell.style.height = '40px';
calendarGrid.appendChild(cell);
}
for (let i = 1; i <= daysInMonth; i++) {
const cell = document.createElement('div');
cell.style.textAlign = 'center';
cell.style.padding = '8px';
cell.style.height = '40px';
cell.style.display = 'flex';
cell.style.alignItems = 'center';
cell.style.justifyContent = 'center';
cell.style.cursor = 'pointer';
cell.style.borderRadius = '8px';
cell.style.color = '#fff';
cell.style.transition = 'all 0.2s ease';
cell.dataset.day = i;
const isToday = (year === today.getFullYear() && month === today.getMonth() && i === today.getDate());
if (isToday) {
cell.style.background = '#0a84ff';
}
if (selectedCalendarDay === i) {
cell.style.background = '#ff6b00';
cell.style.fontWeight = '700';
cell.style.transform = 'scale(1.1)';
}
cell.innerText = i;
cell.onclick = () => {
selectedCalendarDay = i;
renderCalendar(currentCalendarDate);
showDateDetail(year, month, i);
};
calendarGrid.appendChild(cell);
}
if (selectedCalendarDay) {
showDateDetail(year, month, selectedCalendarDay);
} else {
showDateDetail(year, month, date.getDate());
}
}
function showDateDetail(year, month, day) {
const date = new Date(year, month, day);
const lunar = solarToLunar(year, month + 1, day);
const yearGanZhi = getGanZhi(lunar.year);
const zodiac = getZodiac(lunar.year);
const monthStr = lunar.isLeap ? '闰' + lunarMonthNames[lunar.month - 1] : lunarMonthNames[lunar.month - 1];
const dayStr = lunarDayNames[lunar.day - 1];
calendarDetail.innerHTML = `
<div style="font-size:1.2rem;font-weight:600;color:#fff;margin-bottom:8px;">${year}年${month + 1}月${day}日</div>
<div style="font-size:1rem;color:rgba(255,255,255,0.8);">农历 ${yearGanZhi}年 ${zodiac}年 ${monthStr} ${dayStr}</div>
`;
}
document.getElementById('display-date').addEventListener('click', showCalendar);
document.getElementById('close-calendar-btn').addEventListener('click', hideCalendar);
calendarModal.addEventListener('click', (e) => { if(e.target === calendarModal) hideCalendar(); });
document.getElementById('calendar-prev').addEventListener('click', () => {
currentCalendarDate.setMonth(currentCalendarDate.getMonth() - 1);
renderCalendar(currentCalendarDate);
});
document.getElementById('calendar-next').addEventListener('click', () => {
currentCalendarDate.setMonth(currentCalendarDate.getMonth() + 1);
renderCalendar(currentCalendarDate);
});
// ==========================================
// 6. 核心翻页时钟引擎
// ==========================================
let currentTimes = { 'hours-tens': -1, 'hours-ones': -1, 'minutes-tens': -1, 'minutes-ones': -1, 'seconds-tens': -1, 'seconds-ones': -1 };
let flipTimeouts = {};
function updateTime() {
if (currentMode !== 'clock') return;
const now = new Date();
const parts = new Intl.DateTimeFormat('en-US', { timeZone: currentTz, hour12: !is24Hour, hour: '2-digit', minute: '2-digit', second: '2-digit' }).formatToParts(now);
let h = parts.find(p => p.type === 'hour').value;
const m = parts.find(p => p.type === 'minute').value;
const s = parts.find(p => p.type === 'second').value;
if (is24Hour && h === '24') h = '00'; h = h.padStart(2, '0');
updateUnit('hours-tens', h[0]); updateUnit('hours-ones', h[1]);
updateUnit('minutes-tens', m[0]); updateUnit('minutes-ones', m[1]);
updateUnit('seconds-tens', s[0]); updateUnit('seconds-ones', s[1]);
const ampmEl = document.getElementById('display-ampm');
const ampmPart = parts.find(p => p.type === 'dayPeriod');
if (!is24Hour && ampmPart) { ampmEl.style.display = 'inline-block'; ampmEl.innerText = ampmPart.value.toUpperCase(); }
else { ampmEl.style.display = 'none'; }
const dateStr = new Intl.DateTimeFormat(langLocales[currentLang] || 'en-US', { timeZone: currentTz, year: 'numeric', month: 'long', day: 'numeric', weekday: 'long' }).format(now);
if (dateStr !== lastDateStr) {
lastDateStr = dateStr;
document.getElementById('display-date').innerText = dateStr;
}
const lunarEl = document.getElementById('display-lunar');
if (lunarEl) {
const showLunar = document.getElementById('setting-show-lunar')?.checked;
if (showLunar) {
lunarEl.style.display = 'block';
lunarEl.innerText = formatLunar(now);
} else {
lunarEl.style.display = 'none';
}
}
}
function updateUnit(id, newVal) {
if (currentTimes[id] === newVal) return;
const unit = document.getElementById(id);
const topBg = unit.querySelector('.top-bg span'), bottomBg = unit.querySelector('.bottom-bg span');
const topFlip = unit.querySelector('.top-flip span'), bottomFlip = unit.querySelector('.bottom-flip span');
const oldVal = currentTimes[id];
if (oldVal === -1) {
currentTimes[id] = newVal; topBg.innerText = newVal; bottomBg.innerText = newVal;
topFlip.innerText = newVal; bottomFlip.innerText = newVal; return;
}
currentTimes[id] = newVal;
if (flipTimeouts[id]) clearTimeout(flipTimeouts[id]);
topBg.innerText = newVal; bottomBg.innerText = oldVal; topFlip.innerText = oldVal; bottomFlip.innerText = newVal;
unit.classList.remove('is-flipping'); void unit.offsetWidth; unit.classList.add('is-flipping');
flipTimeouts[id] = setTimeout(() => { topFlip.innerText = newVal; bottomBg.innerText = newVal; unit.classList.remove('is-flipping'); }, 850);
}
// ==========================================
// 7. 电脑级锁屏控制 (带密码保护)
// ==========================================
const standbyBtn = document.getElementById('btn-standby');
let isLocked = false;
function hashPassword(password) {
let hash = 0;
for (let i = 0; i < password.length; i++) {
const char = password.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}
return hash.toString(16);
}
function getStoredPasswordHash() {
return localStorage.getItem('lock_password_hash');
}
function isPasswordEnabled() {
return localStorage.getItem('lock_password_enabled') === 'true' && !!getStoredPasswordHash();
}
function showPasswordModal() {
const modal = document.getElementById('lock-password-modal');
const input = document.getElementById('lock-password-input');
const error = document.getElementById('lock-password-error');
modal.style.display = 'flex';
input.value = '';
error.textContent = '';
setTimeout(() => input.focus(), 100);
}
function hidePasswordModal() {
const modal = document.getElementById('lock-password-modal');
modal.style.display = 'none';
}
function checkPassword(password) {
const storedHash = getStoredPasswordHash();
return storedHash && hashPassword(password) === storedHash;
}
function unlockScreen() {
if (!isLocked) return;
if (isPasswordEnabled()) {
showPasswordModal();
return;
}
document.body.classList.remove('standby-mode');
isLocked = false;
requestAnimationFrame(() => { try { adjustBottomButtonsScale(); } catch(e) {} });
}
standbyBtn.addEventListener('click', (e) => {
e.stopPropagation();
if (!document.fullscreenElement) document.documentElement.requestFullscreen().catch(()=>{});
sidebarWrapper.classList.remove('open');
document.body.classList.add('standby-mode');
isLocked = true;
document.getElementById('screen-clock').scrollIntoView();
});
window.addEventListener('beforeunload', (e) => {
if (isLocked) {
e.preventDefault();
e.returnValue = '';
return '';
}
});
document.addEventListener('click', (e) => {
if (e.target.closest('.control-btn') || e.target.closest('.sidebar-panel') || e.target.closest('.scroll-hint') || e.target.closest('.tz-modal') || e.target.closest('#lock-password-modal')) return;
if (isLocked) unlockScreen();
});
window.addEventListener('keydown', (e) => {
if (!isLocked) return;
const modal = document.getElementById('lock-password-modal');
if (modal.style.display === 'flex') return;
unlockScreen();
});
document.getElementById('lock-password-submit').addEventListener('click', () => {
const input = document.getElementById('lock-password-input');
const error = document.getElementById('lock-password-error');
const password = input.value.trim();
if (!password) {
error.textContent = L.lock_password_empty;
return;
}
if (checkPassword(password)) {
hidePasswordModal();
document.body.classList.remove('standby-mode');
isLocked = false;
requestAnimationFrame(() => { try { adjustBottomButtonsScale(); } catch(e) {} });
} else {
error.textContent = L.lock_password_incorrect;
input.value = '';
}
});
document.getElementById('lock-password-cancel').addEventListener('click', () => {
hidePasswordModal();
});
document.getElementById('lock-password-input').addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
document.getElementById('lock-password-submit').click();
} else if (e.key === 'Escape') {
hidePasswordModal();
}
});
const elPasswordEnable = document.getElementById('setting-lock-password-enable');
const elPasswordPanel = document.getElementById('lock-password-panel');
const elPasswordSet = document.getElementById('lock-password-set');
const elPasswordChange = document.getElementById('lock-password-change');
function updatePasswordPanel() {
const hasPassword = !!getStoredPasswordHash();
const enabled = localStorage.getItem('lock_password_enabled') === 'true';
elPasswordEnable.checked = enabled;
elPasswordPanel.style.display = enabled ? 'block' : 'none';
if (enabled) {
elPasswordSet.style.display = hasPassword ? 'none' : 'block';
elPasswordChange.style.display = hasPassword ? 'block' : 'none';
}
}
elPasswordEnable.addEventListener('change', (e) => {
const isChecked = e.target.checked;
localStorage.setItem('lock_password_enabled', isChecked ? 'true' : 'false');
elPasswordPanel.style.display = isChecked ? 'block' : 'none';
if (isChecked) {
const hasPassword = !!getStoredPasswordHash();
elPasswordSet.style.display = hasPassword ? 'none' : 'block';
elPasswordChange.style.display = hasPassword ? 'block' : 'none';
}
});
document.getElementById('btn-lock-password-set').addEventListener('click', () => {
const newPwd = document.getElementById('input-lock-password-new').value.trim();
const confirmPwd = document.getElementById('input-lock-password-confirm').value.trim();
if (!newPwd) {
alert(L.lock_password_empty);
return;
}
if (newPwd !== confirmPwd) {
alert(L.lock_password_mismatch);
return;
}
localStorage.setItem('lock_password_hash', hashPassword(newPwd));
document.getElementById('input-lock-password-new').value = '';
document.getElementById('input-lock-password-confirm').value = '';
alert(L.lock_password_set_success);
updatePasswordPanel();
});
document.getElementById('btn-lock-password-change').addEventListener('click', () => {
const oldPwd = document.getElementById('input-lock-password-old').value.trim();
const newPwd = document.getElementById('input-lock-password-new2').value.trim();
const confirmPwd = document.getElementById('input-lock-password-confirm2').value.trim();
if (!oldPwd || !newPwd) {
alert(L.lock_password_empty);
return;
}
if (!checkPassword(oldPwd)) {
alert(L.lock_password_incorrect);
return;
}
if (newPwd !== confirmPwd) {
alert(L.lock_password_mismatch);
return;
}
localStorage.setItem('lock_password_hash', hashPassword(newPwd));
document.getElementById('input-lock-password-old').value = '';
document.getElementById('input-lock-password-new2').value = '';
document.getElementById('input-lock-password-confirm2').value = '';
alert(L.lock_password_change_success);
});
document.getElementById('btn-lock-password-remove').addEventListener('click', () => {
if (confirm(L.lock_password_remove_confirm)) {
localStorage.removeItem('lock_password_hash');
localStorage.setItem('lock_password_enabled', 'false');
updatePasswordPanel();
}
});
updatePasswordPanel();
// 初始化运行
initLocation();
initBg();
initBgAutoSwitch();
if (typeof window._renderPlaylist === 'function') {
window._renderPlaylist();
}
setInterval(updateTime, 1000);
</script>
</body></html>