diff --git a/public/css/style.css b/public/css/style.css
index 378fc44..a6e8646 100644
--- a/public/css/style.css
+++ b/public/css/style.css
@@ -728,6 +728,11 @@ header p {
line-height: 1;
}
+.square-btn {
+ letter-spacing: -0.1em;
+ font-size: 0.82rem;
+}
+
.control-btn:hover {
transform: translateY(-1px);
background: rgba(51, 65, 85, 0.95);
@@ -740,6 +745,37 @@ header p {
transform: none;
}
+.control-popover {
+ position: relative;
+ display: inline-flex;
+ align-items: center;
+}
+
+.popover-panel {
+ position: absolute;
+ left: 50%;
+ bottom: calc(100% + 0.55rem);
+ transform: translateX(-50%) translateY(6px);
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity 0.18s ease, transform 0.18s ease;
+ padding: 0.55rem 0.6rem;
+ border-radius: 12px;
+ border: 1px solid rgba(148, 163, 184, 0.18);
+ background: rgba(15, 23, 42, 0.96);
+ box-shadow: 0 12px 24px rgba(2, 6, 23, 0.34);
+ backdrop-filter: blur(12px);
+ z-index: 20;
+ white-space: nowrap;
+}
+
+.control-popover:hover .popover-panel,
+.control-popover:focus-within .popover-panel {
+ opacity: 1;
+ pointer-events: auto;
+ transform: translateX(-50%) translateY(0);
+}
+
.volume-control {
display: flex;
align-items: center;
@@ -760,23 +796,11 @@ header p {
font-variant-numeric: tabular-nums;
}
-.progress-chip {
- display: inline-flex;
- align-items: center;
- padding: 0.34rem 0.55rem;
- border-radius: 999px;
- background: rgba(15, 23, 42, 0.7);
- border: 1px solid rgba(148, 163, 184, 0.16);
- color: rgba(226, 232, 240, 0.92);
- font-size: 0.76rem;
- font-weight: 600;
- white-space: nowrap;
-}
-
.speed-control {
- display: inline-flex;
- align-items: center;
- gap: 0.4rem;
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 0.45rem;
color: rgba(226, 232, 240, 0.88);
font-size: 0.8rem;
font-weight: 600;
@@ -803,11 +827,21 @@ header p {
cursor: pointer;
}
-.seek-bar-progress {
+.seek-bar-transcode {
+ position: absolute;
+ inset: 0 auto 0 0;
width: 0%;
height: 100%;
border-radius: 999px;
- background: linear-gradient(90deg, #38bdf8, #2563eb);
+ background: linear-gradient(90deg, rgba(59, 130, 246, 0.45), rgba(37, 99, 235, 0.82));
+}
+
+.seek-bar-progress {
+ position: relative;
+ width: 0%;
+ height: 100%;
+ border-radius: 999px;
+ background: linear-gradient(90deg, #f8fafc, #dbeafe);
}
.seek-bar-handle {
diff --git a/public/index.html b/public/index.html
index 21292f9..2809741 100644
--- a/public/index.html
+++ b/public/index.html
@@ -132,35 +132,43 @@
Paused
-
Transcode 0%
- Play 0%
-
-
-
-
+
+
+
+
+
+
+
diff --git a/public/js/main.js b/public/js/main.js
index 1f8d1d2..8edaaa2 100644
--- a/public/js/main.js
+++ b/public/js/main.js
@@ -26,13 +26,12 @@ document.addEventListener('DOMContentLoaded', () => {
const controlPlayToggle = document.getElementById('control-play-toggle');
const controlMuteToggle = document.getElementById('control-mute-toggle');
const controlFullscreenToggle = document.getElementById('control-fullscreen-toggle');
+ const controlSpeedToggle = document.getElementById('control-speed-toggle');
const volumeSlider = document.getElementById('volume-slider');
const volumeValue = document.getElementById('volume-value');
const playbackStatus = document.getElementById('playback-status');
const playbackStatusText = document.getElementById('playback-status-text');
const playbackSpeed = document.getElementById('playback-speed');
- const transcodeProgressChip = document.getElementById('transcode-progress-chip');
- const playbackProgressChip = document.getElementById('playback-progress-chip');
// Download phase elements
const downloadPhase = document.getElementById('download-phase');
@@ -53,6 +52,7 @@ document.addEventListener('DOMContentLoaded', () => {
// Custom seek bar elements
const customSeekContainer = document.getElementById('custom-seek-container');
const seekBar = document.getElementById('seek-bar');
+ const seekBarTranscode = document.getElementById('seek-bar-transcode');
const seekBarProgress = document.getElementById('seek-bar-progress');
const seekBarHandle = document.getElementById('seek-bar-handle');
const seekCurrentTime = document.getElementById('seek-current-time');
@@ -159,27 +159,28 @@ document.addEventListener('DOMContentLoaded', () => {
volumeValue.textContent = `${Math.round(effectiveVolume * 100)}%`;
}
if (controlMuteToggle) {
- controlMuteToggle.textContent = effectiveVolume === 0 ? 'Unmute' : 'Mute';
+ controlMuteToggle.textContent = effectiveVolume === 0 ? 'x' : 'o';
+ controlMuteToggle.setAttribute('aria-label', effectiveVolume === 0 ? 'Unmute' : 'Mute');
}
};
const updateFullscreenControls = () => {
if (controlFullscreenToggle) {
- controlFullscreenToggle.textContent = document.fullscreenElement ? 'Exit Fullscreen' : 'Fullscreen';
+ controlFullscreenToggle.textContent = '[]';
+ controlFullscreenToggle.setAttribute('aria-label', document.fullscreenElement ? 'Exit Fullscreen' : 'Fullscreen');
}
};
- const updatePlaybackProgressChip = (absoluteTime = 0) => {
- if (!playbackProgressChip) return;
- const safeDuration = videoDuration > 0 ? videoDuration : 0;
- const percent = safeDuration > 0 ? Math.min(Math.max(Math.round((absoluteTime / safeDuration) * 100), 0), 100) : 0;
- playbackProgressChip.textContent = `Play ${percent}%`;
+ const updateSpeedControls = () => {
+ if (controlSpeedToggle && playbackSpeed) {
+ controlSpeedToggle.textContent = playbackSpeed.value === '1' ? '1x' : playbackSpeed.value;
+ }
};
- const updateTranscodeProgressChip = (percent = 0, label = 'Transcode') => {
- if (!transcodeProgressChip) return;
+ const updateTranscodeProgressBar = (percent = 0) => {
+ if (!seekBarTranscode) return;
const safePercent = Math.min(Math.max(Math.round(percent || 0), 0), 100);
- transcodeProgressChip.textContent = `${label} ${safePercent}%`;
+ seekBarTranscode.style.width = `${safePercent}%`;
};
const showCustomControls = () => {
@@ -254,7 +255,6 @@ document.addEventListener('DOMContentLoaded', () => {
if (seekTotalTime) seekTotalTime.textContent = formatTime(videoDuration);
showSeekBar();
updateSeekBarPosition(seekOffset + (videoPlayer.currentTime || 0));
- updatePlaybackProgressChip(seekOffset + (videoPlayer.currentTime || 0));
}
if (message.type === 'progress') {
handleProgress(message.progress);
@@ -275,7 +275,7 @@ document.addEventListener('DOMContentLoaded', () => {
if (status === 'downloading') {
showDownloadPhase();
const percent = Math.min(Math.max(Math.round(progress.percent || 0), 0), 100);
- updateTranscodeProgressChip(percent, 'Download');
+ updateTranscodeProgressBar(percent);
const downloaded = formatBytes(progress.downloadedBytes || 0);
const total = formatBytes(progress.totalBytes || 0);
downloadSizeText.textContent = `${downloaded} / ${total}`;
@@ -283,7 +283,7 @@ document.addEventListener('DOMContentLoaded', () => {
downloadProgressFill.style.width = `${percent}%`;
} else if (status === 'downloaded') {
showDownloadPhase();
- updateTranscodeProgressChip(100, 'Download');
+ updateTranscodeProgressBar(100);
const downloaded = formatBytes(progress.downloadedBytes || progress.totalBytes || 0);
const total = formatBytes(progress.totalBytes || 0);
downloadSizeText.textContent = `${downloaded} / ${total} — 下载完成`;
@@ -295,7 +295,7 @@ document.addEventListener('DOMContentLoaded', () => {
} else if (status === 'transcoding') {
showTranscodePhase();
const percent = Math.min(Math.max(Math.round(progress.percent || 0), 0), 100);
- updateTranscodeProgressChip(percent, 'Transcode');
+ updateTranscodeProgressBar(percent);
transcodeProgressText.textContent = `${percent}%`;
transcodeProgressFill.style.width = `${percent}%`;
transcodeDetailText.textContent = progress.details || 'FFmpeg 转码中...';
@@ -308,16 +308,16 @@ document.addEventListener('DOMContentLoaded', () => {
}
} else if (status === 'finished') {
showTranscodePhase();
- updateTranscodeProgressChip(100, 'Transcode');
+ updateTranscodeProgressBar(100);
transcodeProgressText.textContent = '100%';
transcodeProgressFill.style.width = '100%';
transcodeDetailText.textContent = '转码完成';
} else if (status === 'failed') {
- updateTranscodeProgressChip(progress.percent || 0, 'Failed');
+ updateTranscodeProgressBar(progress.percent || 0);
transcodeDetailText.textContent = `失败: ${progress.details || '未知错误'}`;
transcodeProgressFill.style.background = 'linear-gradient(90deg, #dc2626, #b91c1c)';
} else if (status === 'cancelled') {
- updateTranscodeProgressChip(0, 'Stopped');
+ updateTranscodeProgressBar(0);
transcodeDetailText.textContent = '已取消';
transcodeProgressFill.style.width = '0%';
}
@@ -350,8 +350,7 @@ document.addEventListener('DOMContentLoaded', () => {
if (statFps) statFps.textContent = '';
if (statBitrate) statBitrate.textContent = '';
if (statTime) statTime.textContent = '';
- updateTranscodeProgressChip(0, 'Transcode');
- updatePlaybackProgressChip(0);
+ updateTranscodeProgressBar(0);
if (stopTranscodeBtn) {
stopTranscodeBtn.classList.add('hidden');
@@ -377,7 +376,6 @@ document.addEventListener('DOMContentLoaded', () => {
seekBarProgress.style.width = `${ratio * 100}%`;
seekBarHandle.style.left = `${ratio * 100}%`;
seekCurrentTime.textContent = formatTime(absoluteTime);
- updatePlaybackProgressChip(absoluteTime);
};
// Track playback position in the custom seek bar
@@ -1048,6 +1046,7 @@ document.addEventListener('DOMContentLoaded', () => {
playbackSpeed.addEventListener('change', (event) => {
const nextRate = Math.max(0.25, Math.min(4, parseFloat(event.target.value) || 1));
videoPlayer.playbackRate = nextRate;
+ updateSpeedControls();
revealPlaybackChrome();
schedulePlaybackChromeHide();
});
@@ -1103,8 +1102,8 @@ document.addEventListener('DOMContentLoaded', () => {
updatePlayControls();
updateVolumeControls();
updateFullscreenControls();
- updateTranscodeProgressChip(0, 'Transcode');
- updatePlaybackProgressChip(0);
+ updateSpeedControls();
+ updateTranscodeProgressBar(0);
// Bind events
refreshBtn.addEventListener('click', () => fetchVideos(selectedBucket));