Add service mode set-list view with screen wake lock
This commit is contained in:
171
frontend/app.js
171
frontend/app.js
@@ -1384,6 +1384,7 @@ const Player = {
|
|||||||
updateNowPlayingActions();
|
updateNowPlayingActions();
|
||||||
markPlayingCard();
|
markPlayingCard();
|
||||||
showMiniBar();
|
showMiniBar();
|
||||||
|
updateServiceMode();
|
||||||
// Media Session API — OS media keys + lock screen
|
// Media Session API — OS media keys + lock screen
|
||||||
if ('mediaSession' in navigator) {
|
if ('mediaSession' in navigator) {
|
||||||
navigator.mediaSession.metadata = new MediaMetadata({
|
navigator.mediaSession.metadata = new MediaMetadata({
|
||||||
@@ -1711,10 +1712,19 @@ const Player = {
|
|||||||
// faster than the 1s watchdog interval, which matters for a jarring "audio
|
// faster than the 1s watchdog interval, which matters for a jarring "audio
|
||||||
// paused, video kept playing" UX.
|
// paused, video kept playing" UX.
|
||||||
document.addEventListener('visibilitychange', () => {
|
document.addEventListener('visibilitychange', () => {
|
||||||
if (document.visibilityState === 'visible') Player.resumeIfNeeded();
|
if (document.visibilityState === 'visible') {
|
||||||
|
Player.resumeIfNeeded();
|
||||||
|
if (isServiceModeOpen()) acquireWakeLock();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
window.addEventListener('pageshow', () => {
|
||||||
|
Player.resumeIfNeeded();
|
||||||
|
if (isServiceModeOpen()) acquireWakeLock();
|
||||||
|
});
|
||||||
|
window.addEventListener('focus', () => {
|
||||||
|
Player.resumeIfNeeded();
|
||||||
|
if (isServiceModeOpen()) acquireWakeLock();
|
||||||
});
|
});
|
||||||
window.addEventListener('pageshow', () => Player.resumeIfNeeded());
|
|
||||||
window.addEventListener('focus', () => Player.resumeIfNeeded());
|
|
||||||
|
|
||||||
function showSpinner(on) {
|
function showSpinner(on) {
|
||||||
els.spinner.classList.toggle('hidden', !on);
|
els.spinner.classList.toggle('hidden', !on);
|
||||||
@@ -1839,6 +1849,8 @@ function wirePlayerEvents() {
|
|||||||
function updatePlayBtn() {
|
function updatePlayBtn() {
|
||||||
els.playBtn.textContent = Player.master.paused ? '▶' : '⏸';
|
els.playBtn.textContent = Player.master.paused ? '▶' : '⏸';
|
||||||
$('miniPlayBtn').textContent = Player.master.paused ? '▶' : '⏸';
|
$('miniPlayBtn').textContent = Player.master.paused ? '▶' : '⏸';
|
||||||
|
const spBtn = $('servicePlayBtn');
|
||||||
|
if (spBtn) spBtn.textContent = Player.master.paused ? '▶' : '⏸';
|
||||||
}
|
}
|
||||||
// Reflect cache state on the now-playing Save button.
|
// Reflect cache state on the now-playing Save button.
|
||||||
function updateNowPlayingActions() {
|
function updateNowPlayingActions() {
|
||||||
@@ -1878,6 +1890,7 @@ function updateProgress() {
|
|||||||
els.durTime.textContent = fmtTime(dur);
|
els.durTime.textContent = fmtTime(dur);
|
||||||
if (dur) els.seek.value = String((cur / dur) * 1000);
|
if (dur) els.seek.value = String((cur / dur) * 1000);
|
||||||
updateMiniBar();
|
updateMiniBar();
|
||||||
|
updateServiceProgress(cur, dur);
|
||||||
// A-B loop: passing B behaves like the track ending. Loop the A-B segment only
|
// A-B loop: passing B behaves like the track ending. Loop the A-B segment only
|
||||||
// when "loop current" is on; otherwise B is the effective end of the track and
|
// when "loop current" is on; otherwise B is the effective end of the track and
|
||||||
// we advance per the repeat settings (next track, or wrap when "repeat list").
|
// we advance per the repeat settings (next track, or wrap when "repeat list").
|
||||||
@@ -2029,6 +2042,7 @@ function playQueue(index = 0) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderUpNext() {
|
function renderUpNext() {
|
||||||
|
updateServiceUpNext();
|
||||||
const upcoming = queue.slice(queueIndex + 1);
|
const upcoming = queue.slice(queueIndex + 1);
|
||||||
if (!upcoming.length) { $('upnext').classList.add('hidden'); return; }
|
if (!upcoming.length) { $('upnext').classList.add('hidden'); return; }
|
||||||
$('upnext').classList.remove('hidden');
|
$('upnext').classList.remove('hidden');
|
||||||
@@ -2113,6 +2127,131 @@ function playPrev() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Service mode (set-list view) with screen wake lock
|
||||||
|
// ============================================================================
|
||||||
|
let serviceWakeLock = null;
|
||||||
|
|
||||||
|
function isServiceModeOpen() {
|
||||||
|
const el = $('serviceMode');
|
||||||
|
return !!(el && !el.classList.contains('hidden'));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function acquireWakeLock() {
|
||||||
|
if (!isServiceModeOpen()) return;
|
||||||
|
if (!('wakeLock' in navigator) || typeof navigator.wakeLock.request !== 'function') {
|
||||||
|
serviceWakeLock = null;
|
||||||
|
updateWakeStateUI();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (serviceWakeLock && !serviceWakeLock.released) {
|
||||||
|
try { await serviceWakeLock.release(); } catch {}
|
||||||
|
}
|
||||||
|
const lock = await navigator.wakeLock.request('screen');
|
||||||
|
serviceWakeLock = lock;
|
||||||
|
lock.addEventListener('release', () => {
|
||||||
|
if (serviceWakeLock === lock) {
|
||||||
|
serviceWakeLock = null;
|
||||||
|
updateWakeStateUI();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
serviceWakeLock = null;
|
||||||
|
}
|
||||||
|
updateWakeStateUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function releaseWakeLock() {
|
||||||
|
if (serviceWakeLock) {
|
||||||
|
try {
|
||||||
|
if (!serviceWakeLock.released) {
|
||||||
|
await serviceWakeLock.release();
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
serviceWakeLock = null;
|
||||||
|
}
|
||||||
|
updateWakeStateUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateWakeStateUI() {
|
||||||
|
const el = $('serviceWakeState');
|
||||||
|
if (!el) return;
|
||||||
|
const isAwake = !!(serviceWakeLock && !serviceWakeLock.released);
|
||||||
|
el.textContent = isAwake ? 'Screen will stay awake' : 'Screen may sleep';
|
||||||
|
el.classList.toggle('awake', isAwake);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateServiceUpNext() {
|
||||||
|
const upNextEl = $('serviceUpNext');
|
||||||
|
if (!upNextEl) return;
|
||||||
|
const upcoming = (queue && queueIndex >= 0) ? queue.slice(queueIndex + 1) : [];
|
||||||
|
if (upcoming.length > 0) {
|
||||||
|
upNextEl.textContent = 'Up next: ' + (upcoming[0].title || 'Untitled');
|
||||||
|
} else {
|
||||||
|
upNextEl.textContent = 'Last song';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateServiceProgress(cur, dur) {
|
||||||
|
const curEl = $('serviceCurTime');
|
||||||
|
const durEl = $('serviceDurTime');
|
||||||
|
if (curEl) curEl.textContent = fmtTime(cur || 0);
|
||||||
|
if (durEl) durEl.textContent = fmtTime(dur || 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateServiceMode() {
|
||||||
|
if (!isServiceModeOpen()) return;
|
||||||
|
const meta = current ? current.meta : null;
|
||||||
|
const titleEl = $('serviceTitle');
|
||||||
|
const chanEl = $('serviceChannel');
|
||||||
|
if (titleEl) titleEl.textContent = meta ? (meta.title || '') : 'No track playing';
|
||||||
|
if (chanEl) chanEl.textContent = meta ? (meta.channel || '') : '';
|
||||||
|
|
||||||
|
// Current track note (Section 5): read entry.notes, degrade gracefully if absent
|
||||||
|
const currentEntry = (queue && queueIndex >= 0 && queueIndex < queue.length) ? queue[queueIndex] : meta;
|
||||||
|
const note = (currentEntry && currentEntry.notes) ? String(currentEntry.notes).trim() : '';
|
||||||
|
const noteEl = $('serviceNote');
|
||||||
|
if (noteEl) {
|
||||||
|
if (note) {
|
||||||
|
noteEl.textContent = note;
|
||||||
|
noteEl.classList.remove('hidden');
|
||||||
|
} else {
|
||||||
|
noteEl.textContent = '';
|
||||||
|
noteEl.classList.add('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateServiceUpNext();
|
||||||
|
|
||||||
|
const cur = Player.master ? (Player.master.currentTime || 0) : 0;
|
||||||
|
const dur = Player.master ? (Player.master.duration || (meta ? meta.duration : 0) || 0) : 0;
|
||||||
|
updateServiceProgress(cur, dur);
|
||||||
|
|
||||||
|
const spBtn = $('servicePlayBtn');
|
||||||
|
if (spBtn && Player.master) {
|
||||||
|
spBtn.textContent = Player.master.paused ? '▶' : '⏸';
|
||||||
|
}
|
||||||
|
|
||||||
|
updateWakeStateUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
function enterServiceMode(pl) {
|
||||||
|
if (pl && pl.videos && pl.videos.length) {
|
||||||
|
playFromList(pl.videos, 0, 'playlist:' + pl.id);
|
||||||
|
}
|
||||||
|
const el = $('serviceMode');
|
||||||
|
if (el) el.classList.remove('hidden');
|
||||||
|
updateServiceMode();
|
||||||
|
acquireWakeLock();
|
||||||
|
}
|
||||||
|
|
||||||
|
function exitServiceMode() {
|
||||||
|
const el = $('serviceMode');
|
||||||
|
if (el) el.classList.add('hidden');
|
||||||
|
releaseWakeLock();
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Channel view — list a channel's uploads with quick actions
|
// Channel view — list a channel's uploads with quick actions
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -2375,6 +2514,13 @@ function renderList() {
|
|||||||
playFromList(pl.videos, 0, 'playlist:' + pl.id, { playFull: true });
|
playFromList(pl.videos, 0, 'playlist:' + pl.id, { playFull: true });
|
||||||
toast('Playing in full — stops after the last video');
|
toast('Playing in full — stops after the last video');
|
||||||
};
|
};
|
||||||
|
const serviceBtn = document.createElement('button');
|
||||||
|
serviceBtn.textContent = '🎤 Service mode';
|
||||||
|
serviceBtn.title = 'Set-list view with screen wake lock';
|
||||||
|
serviceBtn.onclick = () => {
|
||||||
|
if (!pl.videos || !pl.videos.length) { toast('Playlist is empty'); return; }
|
||||||
|
enterServiceMode(pl);
|
||||||
|
};
|
||||||
const queueAll = document.createElement('button');
|
const queueAll = document.createElement('button');
|
||||||
queueAll.textContent = '+ Queue';
|
queueAll.textContent = '+ Queue';
|
||||||
queueAll.onclick = () => { pl.videos.forEach((v) => addToQueue(v, { quiet: true })); toast('Added playlist to queue'); };
|
queueAll.onclick = () => { pl.videos.forEach((v) => addToQueue(v, { quiet: true })); toast('Added playlist to queue'); };
|
||||||
@@ -2398,7 +2544,7 @@ function renderList() {
|
|||||||
pinBtn.title = isPinned ? 'Pinned for offline keep — click to unpin' : 'Pin to keep all videos in this playlist available offline';
|
pinBtn.title = isPinned ? 'Pinned for offline keep — click to unpin' : 'Pin to keep all videos in this playlist available offline';
|
||||||
pinBtn.onclick = () => togglePinPlaylist(pl);
|
pinBtn.onclick = () => togglePinPlaylist(pl);
|
||||||
|
|
||||||
const buttons = [playAll, playFull, queueAll, offlineBtn, pinBtn];
|
const buttons = [playAll, playFull, serviceBtn, queueAll, offlineBtn, pinBtn];
|
||||||
if (pl.videos && pl.videos.length >= 1) {
|
if (pl.videos && pl.videos.length >= 1) {
|
||||||
const share = document.createElement('button');
|
const share = document.createElement('button');
|
||||||
share.textContent = '🔗 Share';
|
share.textContent = '🔗 Share';
|
||||||
@@ -4136,15 +4282,24 @@ document.querySelectorAll('.chip').forEach((c) => {
|
|||||||
else if (e.key === 'b') { if (current) setAbB(); }
|
else if (e.key === 'b') { if (current) setAbB(); }
|
||||||
else if (e.key === '?') toggleShortcutHelp();
|
else if (e.key === '?') toggleShortcutHelp();
|
||||||
else if (e.key === 'Escape') {
|
else if (e.key === 'Escape') {
|
||||||
const app = document.querySelector('.app');
|
if (isServiceModeOpen()) exitServiceMode();
|
||||||
if (app && app.classList.contains('sidebar-open')) closeSidebar();
|
else {
|
||||||
else if (!$('shortcutHelp').classList.contains('hidden')) $('shortcutHelp').classList.add('hidden');
|
const app = document.querySelector('.app');
|
||||||
else if (selectMode) { exitSelectMode(); renderList(); }
|
if (app && app.classList.contains('sidebar-open')) closeSidebar();
|
||||||
|
else if (!$('shortcutHelp').classList.contains('hidden')) $('shortcutHelp').classList.add('hidden');
|
||||||
|
else if (selectMode) { exitSelectMode(); renderList(); }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$('modal').addEventListener('click', (e) => { if (e.target.id === 'modal') closeModal(); });
|
$('modal').addEventListener('click', (e) => { if (e.target.id === 'modal') closeModal(); });
|
||||||
|
|
||||||
|
// Service mode controls
|
||||||
|
$('servicePlayBtn').addEventListener('click', () => Player.toggle());
|
||||||
|
$('serviceNextBtn').addEventListener('click', playNext);
|
||||||
|
$('servicePrevBtn').addEventListener('click', playPrev);
|
||||||
|
$('serviceExitBtn').addEventListener('click', exitServiceMode);
|
||||||
|
|
||||||
// Mini now-playing bar
|
// Mini now-playing bar
|
||||||
$('miniPlayBtn').addEventListener('click', (e) => { e.stopPropagation(); Player.toggle(); });
|
$('miniPlayBtn').addEventListener('click', (e) => { e.stopPropagation(); Player.toggle(); });
|
||||||
$('miniBar').addEventListener('click', () => {
|
$('miniBar').addEventListener('click', () => {
|
||||||
|
|||||||
@@ -256,6 +256,32 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Service mode (set-list view) overlay -->
|
||||||
|
<section id="serviceMode" class="service-mode hidden" aria-label="Service Mode">
|
||||||
|
<div class="service-topbar">
|
||||||
|
<div id="serviceWakeState" class="service-wake-state">Screen may sleep</div>
|
||||||
|
<button id="serviceExitBtn" class="service-exit-btn" type="button" aria-label="Exit Service Mode">Exit</button>
|
||||||
|
</div>
|
||||||
|
<div class="service-center">
|
||||||
|
<div class="service-meta">
|
||||||
|
<h1 id="serviceTitle" class="service-title"></h1>
|
||||||
|
<div id="serviceChannel" class="service-channel"></div>
|
||||||
|
<div id="serviceNote" class="service-note hidden"></div>
|
||||||
|
</div>
|
||||||
|
<div id="serviceUpNext" class="service-upnext">Last song</div>
|
||||||
|
</div>
|
||||||
|
<div class="service-bottom">
|
||||||
|
<div id="serviceTime" class="service-time">
|
||||||
|
<span id="serviceCurTime">0:00</span> / <span id="serviceDurTime">0:00</span>
|
||||||
|
</div>
|
||||||
|
<div class="service-controls">
|
||||||
|
<button id="servicePrevBtn" class="service-btn" type="button" aria-label="Previous track">⏮</button>
|
||||||
|
<button id="servicePlayBtn" class="service-btn service-btn-play" type="button" aria-label="Play or Pause">▶</button>
|
||||||
|
<button id="serviceNextBtn" class="service-btn" type="button" aria-label="Next track">⏭</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<div id="toastContainer" class="toast-container"></div>
|
<div id="toastContainer" class="toast-container"></div>
|
||||||
|
|
||||||
<!-- Mini now-playing bar -->
|
<!-- Mini now-playing bar -->
|
||||||
|
|||||||
@@ -2458,3 +2458,271 @@ input[type="range"]::-webkit-slider-thumb:hover { transform: scale(1.25); }
|
|||||||
}
|
}
|
||||||
.ve-marker-a { left: 0; }
|
.ve-marker-a { left: 0; }
|
||||||
.ve-marker-b { right: 0; }
|
.ve-marker-b { right: 0; }
|
||||||
|
|
||||||
|
/* ===================== Service Mode (Set-List View) ===================== */
|
||||||
|
.service-mode {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
height: 100dvh;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
z-index: 110;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: calc(env(safe-area-inset-top, 0px) + 24px)
|
||||||
|
calc(env(safe-area-inset-right, 0px) + 28px)
|
||||||
|
calc(env(safe-area-inset-bottom, 0px) + 28px)
|
||||||
|
calc(env(safe-area-inset-left, 0px) + 28px);
|
||||||
|
box-sizing: border-box;
|
||||||
|
animation: fade 0.2s var(--ease);
|
||||||
|
overflow-y: auto;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-topbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 0 auto;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-wake-state {
|
||||||
|
font-family: var(--ui);
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-dim);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
transition: color 0.2s var(--ease);
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-wake-state::before {
|
||||||
|
content: "";
|
||||||
|
display: inline-block;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--line);
|
||||||
|
transition: background 0.2s var(--ease), box-shadow 0.2s var(--ease);
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-wake-state.awake {
|
||||||
|
color: var(--text-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-wake-state.awake::before {
|
||||||
|
background: #34d399;
|
||||||
|
box-shadow: 0 0 10px rgba(52, 211, 153, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-exit-btn {
|
||||||
|
background: var(--bg-3);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
color: var(--text);
|
||||||
|
padding: 9px 18px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
font-family: var(--ui);
|
||||||
|
font-size: 13.5px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 40px;
|
||||||
|
touch-action: manipulation;
|
||||||
|
transition: all 0.16s var(--ease);
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-exit-btn:hover {
|
||||||
|
background: var(--bg-2);
|
||||||
|
border-color: var(--accent);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-exit-btn:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-center {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
max-width: 900px;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 24px 0;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-meta {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-title {
|
||||||
|
margin: 0 0 12px;
|
||||||
|
font-family: var(--display);
|
||||||
|
font-weight: 800;
|
||||||
|
font-size: clamp(28px, 5.5vw, 56px);
|
||||||
|
line-height: 1.15;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
color: var(--text);
|
||||||
|
max-width: 100%;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-channel {
|
||||||
|
font-family: var(--ui);
|
||||||
|
font-size: clamp(16px, 2.5vw, 22px);
|
||||||
|
color: var(--text-2);
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-note {
|
||||||
|
background: var(--bg-2);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
padding: 14px 20px;
|
||||||
|
margin: 12px 0 0;
|
||||||
|
color: var(--text);
|
||||||
|
font-family: var(--ui);
|
||||||
|
font-size: clamp(14px, 2vw, 17px);
|
||||||
|
line-height: 1.5;
|
||||||
|
max-width: 680px;
|
||||||
|
width: calc(100% - 32px);
|
||||||
|
text-align: left;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-upnext {
|
||||||
|
margin-top: 24px;
|
||||||
|
font-family: var(--ui);
|
||||||
|
font-size: clamp(15px, 2.2vw, 20px);
|
||||||
|
color: var(--text-dim);
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
max-width: 100%;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-bottom {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-time {
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: clamp(17px, 2.8vw, 24px);
|
||||||
|
color: var(--text-2);
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: clamp(18px, 4vw, 36px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-btn {
|
||||||
|
background: var(--bg-2);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
color: var(--text);
|
||||||
|
border-radius: 50%;
|
||||||
|
width: clamp(56px, 12vw, 70px);
|
||||||
|
height: clamp(56px, 12vw, 70px);
|
||||||
|
font-size: clamp(20px, 4vw, 26px);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
touch-action: manipulation;
|
||||||
|
transition: all 0.16s var(--ease);
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-btn:hover {
|
||||||
|
background: var(--bg-3);
|
||||||
|
border-color: var(--text-dim);
|
||||||
|
transform: scale(1.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-btn:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-btn-play {
|
||||||
|
width: clamp(72px, 15vw, 92px);
|
||||||
|
height: clamp(72px, 15vw, 92px);
|
||||||
|
font-size: clamp(28px, 5.5vw, 38px);
|
||||||
|
background: linear-gradient(145deg, var(--accent-bright), var(--accent-deep));
|
||||||
|
border: none;
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: 0 10px 28px -6px var(--accent-glow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-btn-play:hover {
|
||||||
|
background: linear-gradient(145deg, var(--accent-bright), var(--accent-deep));
|
||||||
|
box-shadow: 0 14px 34px -4px var(--accent-glow);
|
||||||
|
transform: scale(1.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-btn-play:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-height: 520px) {
|
||||||
|
.service-mode {
|
||||||
|
padding: 12px 20px;
|
||||||
|
}
|
||||||
|
.service-center {
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
.service-title {
|
||||||
|
font-size: clamp(22px, 4.5vh, 32px);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.service-channel {
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.service-upnext {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.service-bottom {
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.service-time {
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
.service-btn {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
.service-btn-play {
|
||||||
|
width: 58px;
|
||||||
|
height: 58px;
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user