mirror of
https://github.com/fishtank-dashboard/fishtank-dashboard.git
synced 2026-04-30 09:12:04 -04:00
misc changes
This commit is contained in:
committed by
GitHub
parent
55882b2ed4
commit
3d98da66cc
@@ -161,9 +161,9 @@
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: grid;
|
||||
grid-template-columns: 320px 1fr 320px;
|
||||
grid-template-columns: 320px 1fr 300px;
|
||||
grid-template-rows: 320px 1fr;
|
||||
grid-template-areas: "poll stocks camman" "tts cameras cameras";
|
||||
grid-template-areas: "poll stocks chat" "tts cameras chat";
|
||||
gap: 1px;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
@@ -173,24 +173,24 @@
|
||||
}
|
||||
|
||||
.main.stocks-collapsed {
|
||||
grid-template-columns: 320px 1fr 0px;
|
||||
grid-template-areas: "poll cameras cameras" "tts cameras cameras";
|
||||
grid-template-columns: 320px 1fr 300px;
|
||||
grid-template-areas: "poll cameras chat" "tts cameras chat";
|
||||
}
|
||||
.main.stocks-collapsed .stocks-hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.main.left-collapsed {
|
||||
grid-template-columns: 0px 1fr 320px;
|
||||
grid-template-areas: "stocks stocks camman" "cameras cameras camman";
|
||||
grid-template-columns: 0px 1fr 300px;
|
||||
grid-template-areas: "stocks stocks chat" "cameras cameras chat";
|
||||
}
|
||||
.main.left-collapsed .left-hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.main.left-collapsed.stocks-collapsed {
|
||||
grid-template-columns: 0px 1fr 0px;
|
||||
grid-template-areas: "cameras cameras cameras" "cameras cameras cameras";
|
||||
grid-template-columns: 0px 1fr 300px;
|
||||
grid-template-areas: "cameras cameras chat" "cameras cameras chat";
|
||||
}
|
||||
.main.left-collapsed.stocks-collapsed .stocks-hide {
|
||||
display: none;
|
||||
@@ -998,6 +998,139 @@
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* Inline chat panel */
|
||||
.chat-panel {
|
||||
grid-area: chat;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--panel);
|
||||
border-left: 1px solid var(--border);
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.main.chat-collapsed .chat-panel {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.main.chat-collapsed {
|
||||
grid-template-columns: 320px 1fr 0px !important;
|
||||
grid-template-areas: "poll stocks ." "tts cameras ." !important;
|
||||
}
|
||||
.main.chat-collapsed.stocks-collapsed {
|
||||
grid-template-areas: "poll cameras ." "tts cameras ." !important;
|
||||
}
|
||||
.main.chat-collapsed.left-collapsed {
|
||||
grid-template-columns: 0px 1fr 0px !important;
|
||||
grid-template-areas: "stocks stocks ." "cameras cameras ." !important;
|
||||
}
|
||||
.main.chat-collapsed.left-collapsed.stocks-collapsed {
|
||||
grid-template-columns: 0px 1fr 0px !important;
|
||||
grid-template-areas: "cameras cameras cameras" "cameras cameras cameras" !important;
|
||||
}
|
||||
|
||||
#inlineChatFeed {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 6px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
#inlineChatFeed::-webkit-scrollbar { width: 4px; }
|
||||
#inlineChatFeed::-webkit-scrollbar-track { background: transparent; }
|
||||
#inlineChatFeed::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
|
||||
|
||||
#inlineChatFeed .msg {
|
||||
padding: 3px 6px;
|
||||
border-radius: 3px;
|
||||
font-size: 11px;
|
||||
line-height: 1.4;
|
||||
border-left: 2px solid transparent;
|
||||
}
|
||||
|
||||
#inlineChatFeed .msg.fish { border-left-color: var(--accent3); background: rgba(255,230,0,0.04); }
|
||||
#inlineChatFeed .msg.admin { border-left-color: var(--accent2); background: rgba(255,61,113,0.06); }
|
||||
#inlineChatFeed .msg.mod { border-left-color: var(--green); background: rgba(0,255,136,0.05); }
|
||||
|
||||
#inlineChatFeed .msg-header {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 4px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
#inlineChatFeed .msg-user {
|
||||
font-family: 'Share Tech Mono', monospace;
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
#inlineChatFeed .msg-endorsement {
|
||||
font-family: 'Share Tech Mono', monospace;
|
||||
font-size: 9px;
|
||||
font-weight: bold;
|
||||
letter-spacing: 1px;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
#inlineChatFeed .msg-time {
|
||||
font-family: 'Share Tech Mono', monospace;
|
||||
font-size: 9px;
|
||||
color: var(--muted);
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
#inlineChatFeed .msg-text {
|
||||
color: var(--text);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
#inlineChatFeed .badge {
|
||||
font-family: 'Share Tech Mono', monospace;
|
||||
font-size: 8px;
|
||||
padding: 1px 4px;
|
||||
border-radius: 2px;
|
||||
font-weight: bold;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
#inlineChatFeed .badge.admin { background: var(--accent2); color: #fff; }
|
||||
#inlineChatFeed .badge.mod { background: var(--green); color: #000; }
|
||||
#inlineChatFeed .badge.fish { background: var(--accent3); color: #000; }
|
||||
#inlineChatFeed .badge.gm { background: var(--accent); color: #000; }
|
||||
|
||||
.chat-footer {
|
||||
padding: 4px 8px;
|
||||
font-family: 'Share Tech Mono', monospace;
|
||||
font-size: 9px;
|
||||
color: var(--muted);
|
||||
border-top: 1px solid var(--border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
#inlineChatScrollBtn {
|
||||
display: none;
|
||||
position: absolute;
|
||||
bottom: 28px;
|
||||
right: 8px;
|
||||
background: var(--accent);
|
||||
color: var(--bg);
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
font-family: 'Share Tech Mono', monospace;
|
||||
font-size: 9px;
|
||||
padding: 3px 8px;
|
||||
cursor: pointer;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* Viewer count badge */
|
||||
.cam-viewers {
|
||||
position: absolute;
|
||||
@@ -1272,6 +1405,139 @@
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* Inline chat panel */
|
||||
.chat-panel {
|
||||
grid-area: chat;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--panel);
|
||||
border-left: 1px solid var(--border);
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.main.chat-collapsed .chat-panel {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.main.chat-collapsed {
|
||||
grid-template-columns: 320px 1fr 0px !important;
|
||||
grid-template-areas: "poll stocks ." "tts cameras ." !important;
|
||||
}
|
||||
.main.chat-collapsed.stocks-collapsed {
|
||||
grid-template-areas: "poll cameras ." "tts cameras ." !important;
|
||||
}
|
||||
.main.chat-collapsed.left-collapsed {
|
||||
grid-template-columns: 0px 1fr 0px !important;
|
||||
grid-template-areas: "stocks stocks ." "cameras cameras ." !important;
|
||||
}
|
||||
.main.chat-collapsed.left-collapsed.stocks-collapsed {
|
||||
grid-template-columns: 0px 1fr 0px !important;
|
||||
grid-template-areas: "cameras cameras cameras" "cameras cameras cameras" !important;
|
||||
}
|
||||
|
||||
#inlineChatFeed {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 6px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
#inlineChatFeed::-webkit-scrollbar { width: 4px; }
|
||||
#inlineChatFeed::-webkit-scrollbar-track { background: transparent; }
|
||||
#inlineChatFeed::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
|
||||
|
||||
#inlineChatFeed .msg {
|
||||
padding: 3px 6px;
|
||||
border-radius: 3px;
|
||||
font-size: 11px;
|
||||
line-height: 1.4;
|
||||
border-left: 2px solid transparent;
|
||||
}
|
||||
|
||||
#inlineChatFeed .msg.fish { border-left-color: var(--accent3); background: rgba(255,230,0,0.04); }
|
||||
#inlineChatFeed .msg.admin { border-left-color: var(--accent2); background: rgba(255,61,113,0.06); }
|
||||
#inlineChatFeed .msg.mod { border-left-color: var(--green); background: rgba(0,255,136,0.05); }
|
||||
|
||||
#inlineChatFeed .msg-header {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 4px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
#inlineChatFeed .msg-user {
|
||||
font-family: 'Share Tech Mono', monospace;
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
#inlineChatFeed .msg-endorsement {
|
||||
font-family: 'Share Tech Mono', monospace;
|
||||
font-size: 9px;
|
||||
font-weight: bold;
|
||||
letter-spacing: 1px;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
#inlineChatFeed .msg-time {
|
||||
font-family: 'Share Tech Mono', monospace;
|
||||
font-size: 9px;
|
||||
color: var(--muted);
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
#inlineChatFeed .msg-text {
|
||||
color: var(--text);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
#inlineChatFeed .badge {
|
||||
font-family: 'Share Tech Mono', monospace;
|
||||
font-size: 8px;
|
||||
padding: 1px 4px;
|
||||
border-radius: 2px;
|
||||
font-weight: bold;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
#inlineChatFeed .badge.admin { background: var(--accent2); color: #fff; }
|
||||
#inlineChatFeed .badge.mod { background: var(--green); color: #000; }
|
||||
#inlineChatFeed .badge.fish { background: var(--accent3); color: #000; }
|
||||
#inlineChatFeed .badge.gm { background: var(--accent); color: #000; }
|
||||
|
||||
.chat-footer {
|
||||
padding: 4px 8px;
|
||||
font-family: 'Share Tech Mono', monospace;
|
||||
font-size: 9px;
|
||||
color: var(--muted);
|
||||
border-top: 1px solid var(--border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
#inlineChatScrollBtn {
|
||||
display: none;
|
||||
position: absolute;
|
||||
bottom: 28px;
|
||||
right: 8px;
|
||||
background: var(--accent);
|
||||
color: var(--bg);
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
font-family: 'Share Tech Mono', monospace;
|
||||
font-size: 9px;
|
||||
padding: 3px 8px;
|
||||
cursor: pointer;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* Viewer count badge */
|
||||
.cam-viewers {
|
||||
position: absolute;
|
||||
@@ -1472,7 +1738,6 @@
|
||||
setApiStatus('none');
|
||||
}
|
||||
initCameras();
|
||||
initCamerman();
|
||||
// Apply saved thumbnail interval from dropdown
|
||||
const iv = document.getElementById('intervalSelect');
|
||||
if (iv && parseInt(iv.value) !== 30000) changeInterval(iv.value);
|
||||
@@ -1487,67 +1752,6 @@
|
||||
}, 500);
|
||||
});
|
||||
|
||||
function initCamerman() {
|
||||
const video = document.getElementById('cammanVideo');
|
||||
if (!video) return;
|
||||
const idx = CAMERAS.findIndex(([,s]) => s === 'cameraman2-5');
|
||||
if (typeof Hls !== 'undefined' && Hls.isSupported()) {
|
||||
if (hlsInstances['camman']) hlsInstances['camman'].destroy();
|
||||
const hls = new Hls({ lowLatencyMode: true, maxBufferLength: 8 });
|
||||
hls.loadSource('http://localhost:3000/cam/cameraman2-5/index.m3u8');
|
||||
hls.attachMedia(video);
|
||||
hls.on(Hls.Events.MANIFEST_PARSED, () => video.play().catch(() => {}));
|
||||
hls.on(Hls.Events.ERROR, (e, d) => {
|
||||
if (d.fatal) {
|
||||
const label = document.getElementById('cammanLabel');
|
||||
if (label) label.textContent = 'CAMERAMAN · RECONNECTING...';
|
||||
hls.destroy();
|
||||
setTimeout(() => {
|
||||
if (video.isConnected) initCamerman();
|
||||
}, 3000);
|
||||
}
|
||||
});
|
||||
hlsInstances['camman'] = hls;
|
||||
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
|
||||
video.src = 'http://localhost:3000/cam/cameraman2-5/index.m3u8';
|
||||
video.play().catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// Update cameraman panel label when it gets swapped with featured
|
||||
const _origSetFeatured = setFeatured;
|
||||
setFeatured = function(i) {
|
||||
_origSetFeatured(i);
|
||||
// If cameraman is now in featured, show director in camman panel
|
||||
// If cameraman is what's being swapped back, restore its own stream
|
||||
const cammanIdx = CAMERAS.findIndex(([,s]) => s === 'cameraman2-5');
|
||||
const cammanVideo = document.getElementById('cammanVideo');
|
||||
const cammanLabel = document.getElementById('cammanLabel');
|
||||
if (!cammanVideo) return;
|
||||
|
||||
if (i === cammanIdx) {
|
||||
// Cameraman just went to featured - show director in camman panel
|
||||
const dirSlug = CAMERAS[DEFAULT_IDX][1];
|
||||
if (hlsInstances['camman']) hlsInstances['camman'].destroy();
|
||||
const hls = new Hls({ lowLatencyMode: true, maxBufferLength: 8 });
|
||||
hls.loadSource('http://localhost:3000/cam/' + dirSlug + '/index.m3u8');
|
||||
hls.attachMedia(cammanVideo);
|
||||
hls.on(Hls.Events.MANIFEST_PARSED, () => { cammanVideo.muted = true; cammanVideo.play().catch(() => {}); });
|
||||
hlsInstances['camman'] = hls;
|
||||
cammanLabel.textContent = 'DIRECTOR MODE';
|
||||
} else if (directorCell === cammanIdx) {
|
||||
// Director is in camman panel cell — keep it
|
||||
} else {
|
||||
// Restore camman panel to cameraman stream
|
||||
if (hlsInstances['camman']) hlsInstances['camman'].destroy();
|
||||
const hls = new Hls({ lowLatencyMode: true, maxBufferLength: 8 });
|
||||
hls.loadSource('http://localhost:3000/cam/cameraman2-5/index.m3u8');
|
||||
hls.attachMedia(cammanVideo);
|
||||
hls.on(Hls.Events.MANIFEST_PARSED, () => { cammanVideo.muted = true; cammanVideo.play().catch(() => {}); });
|
||||
hlsInstances['camman'] = hls;
|
||||
cammanLabel.textContent = 'CAMERAMAN';
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/hls.js/1.4.12/hls.min.js">
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
@@ -1561,7 +1765,6 @@
|
||||
setApiStatus('none');
|
||||
}
|
||||
initCameras();
|
||||
initCamerman();
|
||||
// Apply saved thumbnail interval from dropdown
|
||||
const iv = document.getElementById('intervalSelect');
|
||||
if (iv && parseInt(iv.value) !== 30000) changeInterval(iv.value);
|
||||
@@ -1576,67 +1779,6 @@
|
||||
}, 500);
|
||||
});
|
||||
|
||||
function initCamerman() {
|
||||
const video = document.getElementById('cammanVideo');
|
||||
if (!video) return;
|
||||
const idx = CAMERAS.findIndex(([,s]) => s === 'cameraman2-5');
|
||||
if (typeof Hls !== 'undefined' && Hls.isSupported()) {
|
||||
if (hlsInstances['camman']) hlsInstances['camman'].destroy();
|
||||
const hls = new Hls({ lowLatencyMode: true, maxBufferLength: 8 });
|
||||
hls.loadSource('http://localhost:3000/cam/cameraman2-5/index.m3u8');
|
||||
hls.attachMedia(video);
|
||||
hls.on(Hls.Events.MANIFEST_PARSED, () => video.play().catch(() => {}));
|
||||
hls.on(Hls.Events.ERROR, (e, d) => {
|
||||
if (d.fatal) {
|
||||
const label = document.getElementById('cammanLabel');
|
||||
if (label) label.textContent = 'CAMERAMAN · RECONNECTING...';
|
||||
hls.destroy();
|
||||
setTimeout(() => {
|
||||
if (video.isConnected) initCamerman();
|
||||
}, 3000);
|
||||
}
|
||||
});
|
||||
hlsInstances['camman'] = hls;
|
||||
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
|
||||
video.src = 'http://localhost:3000/cam/cameraman2-5/index.m3u8';
|
||||
video.play().catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// Update cameraman panel label when it gets swapped with featured
|
||||
const _origSetFeatured = setFeatured;
|
||||
setFeatured = function(i) {
|
||||
_origSetFeatured(i);
|
||||
// If cameraman is now in featured, show director in camman panel
|
||||
// If cameraman is what's being swapped back, restore its own stream
|
||||
const cammanIdx = CAMERAS.findIndex(([,s]) => s === 'cameraman2-5');
|
||||
const cammanVideo = document.getElementById('cammanVideo');
|
||||
const cammanLabel = document.getElementById('cammanLabel');
|
||||
if (!cammanVideo) return;
|
||||
|
||||
if (i === cammanIdx) {
|
||||
// Cameraman just went to featured - show director in camman panel
|
||||
const dirSlug = CAMERAS[DEFAULT_IDX][1];
|
||||
if (hlsInstances['camman']) hlsInstances['camman'].destroy();
|
||||
const hls = new Hls({ lowLatencyMode: true, maxBufferLength: 8 });
|
||||
hls.loadSource('http://localhost:3000/cam/' + dirSlug + '/index.m3u8');
|
||||
hls.attachMedia(cammanVideo);
|
||||
hls.on(Hls.Events.MANIFEST_PARSED, () => { cammanVideo.muted = true; cammanVideo.play().catch(() => {}); });
|
||||
hlsInstances['camman'] = hls;
|
||||
cammanLabel.textContent = 'DIRECTOR MODE';
|
||||
} else if (directorCell === cammanIdx) {
|
||||
// Director is in camman panel cell — keep it
|
||||
} else {
|
||||
// Restore camman panel to cameraman stream
|
||||
if (hlsInstances['camman']) hlsInstances['camman'].destroy();
|
||||
const hls = new Hls({ lowLatencyMode: true, maxBufferLength: 8 });
|
||||
hls.loadSource('http://localhost:3000/cam/cameraman2-5/index.m3u8');
|
||||
hls.attachMedia(cammanVideo);
|
||||
hls.on(Hls.Events.MANIFEST_PARSED, () => { cammanVideo.muted = true; cammanVideo.play().catch(() => {}); });
|
||||
hlsInstances['camman'] = hls;
|
||||
cammanLabel.textContent = 'CAMERAMAN';
|
||||
}
|
||||
};
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
@@ -1659,7 +1801,7 @@
|
||||
<div style="display:flex;align-items:center;gap:8px;">
|
||||
<button class="stocks-collapse-btn" onclick="toggleLeftPanels()" id="leftCollapseBtn">◀ POLL / TTS</button>
|
||||
<button class="stocks-collapse-btn" onclick="toggleStocks()" id="stocksCollapseBtn">▲ STOCKS</button>
|
||||
<button class="stocks-collapse-btn" onclick="window.open('http://localhost:3000/chat','fishtank-chat','width=400,height=700,resizable=yes')" title="Open chat popout">💬 CHAT</button>
|
||||
<button class="stocks-collapse-btn" id="chatCollapseBtn" onclick="toggleChat()" title="Hide chat panel">💬 CHAT</button>
|
||||
</div>
|
||||
|
||||
<div id="apiControl" style="display:flex;align-items:center;gap:8px;">
|
||||
@@ -1716,12 +1858,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Cameraman Panel -->
|
||||
<div class="panel stocks-hide" style="grid-area:camman;overflow:hidden;background:#000;padding:0;cursor:pointer;" onclick="setFeatured(CAMERAS.findIndex(([,s])=>s==='cameraman2-5'))" title="Click to feature Cameraman">
|
||||
<div style="position:relative;width:100%;height:100%;">
|
||||
<video id="cammanVideo" muted playsinline autoplay style="width:100%;height:100%;object-fit:contain;display:block;"></video>
|
||||
<div class="cam-label" id="cammanLabel" style="position:absolute;bottom:0;left:0;right:0;">CAMERAMAN</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- TTS + SFX Panel -->
|
||||
<div class="panel left-hide" style="grid-area:tts;">
|
||||
@@ -1740,6 +1877,19 @@
|
||||
</div>
|
||||
|
||||
<!-- Camera Panel -->
|
||||
<div class="chat-panel" id="chatPanel" style="position:relative;">
|
||||
<div id="inlineChatFeed">
|
||||
<div class="empty"><div class="empty-icon">💬</div><span>Waiting for messages...</span></div>
|
||||
</div>
|
||||
<button id="inlineChatScrollBtn" onclick="inlineChatScrollToBottom()">▼ NEW</button>
|
||||
<div class="chat-footer">
|
||||
<div class="dot" id="inlineChatDot"></div>
|
||||
<span id="inlineChatStatus">CONNECTING</span>
|
||||
<span id="inlineChatViewers" style="color:var(--muted);">—</span>
|
||||
<span style="margin-left:auto;" id="inlineChatCount">0 messages</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cameras-panel" style="grid-area:cameras;">
|
||||
<div class="panel-header">
|
||||
<div class="panel-title" style="color:var(--accent)">CAMERAS</div>
|
||||
@@ -2428,6 +2578,7 @@
|
||||
function setThumbStream(gridIdx, slug, labelOverride) {
|
||||
const cell = document.getElementById('cam-' + gridIdx);
|
||||
if (!cell) return;
|
||||
cell.dataset.slug = slug; // keep slug in sync for viewer count sorting
|
||||
const label = cell.querySelector('.cam-label');
|
||||
label.textContent = (labelOverride || CAMERAS[gridIdx][0]).toUpperCase();
|
||||
if (thumbMode) {
|
||||
@@ -2456,7 +2607,7 @@
|
||||
featuredIdx = idx;
|
||||
// Restart buffer for new camera — reset audio context too (new video element)
|
||||
stopBuffer();
|
||||
bufferAudioCtx = null;
|
||||
if (bufferAudioCtx) { try { bufferAudioCtx.close(); } catch(e) {} bufferAudioCtx = null; }
|
||||
if (bufferCanvasInterval) { clearInterval(bufferCanvasInterval); bufferCanvasInterval = null; }
|
||||
if (bufferCanvas) { bufferCanvas.remove(); bufferCanvas = null; }
|
||||
setTimeout(() => ensureBuffer(), 500);
|
||||
@@ -2543,22 +2694,26 @@
|
||||
const featVideo = wrap.querySelector('video');
|
||||
if (!featVideo) return null;
|
||||
|
||||
// Always use a 960x540 canvas for the buffer — fixes background tab throttling
|
||||
// and ensures consistent 540p output regardless of source resolution
|
||||
// Always use a 1280x720 canvas for the buffer — fixes background tab throttling
|
||||
// and ensures consistent 720p output regardless of source resolution
|
||||
if (!bufferCanvas || !bufferCanvas.isConnected) {
|
||||
if (bufferCanvas) bufferCanvas.remove();
|
||||
bufferCanvas = document.createElement('canvas');
|
||||
bufferCanvas.width = 960;
|
||||
bufferCanvas.height = 540;
|
||||
bufferCanvas.width = 1280;
|
||||
bufferCanvas.height = 720;
|
||||
bufferCanvas.style.cssText = 'position:fixed;top:-9999px;';
|
||||
document.body.appendChild(bufferCanvas);
|
||||
}
|
||||
const ctx = bufferCanvas.getContext('2d');
|
||||
if (bufferCanvasInterval) clearInterval(bufferCanvasInterval);
|
||||
// Cancel any previous rVFC loop by invalidating it via a shared token
|
||||
if (window._bufferDrawToken) window._bufferDrawToken.cancelled = true;
|
||||
const drawToken = { cancelled: false };
|
||||
window._bufferDrawToken = drawToken;
|
||||
|
||||
function drawFrame() {
|
||||
if (!bufferCanvas || !bufferCanvas.isConnected) return;
|
||||
if (featVideo.readyState >= 2) ctx.drawImage(featVideo, 0, 0, 960, 540);
|
||||
if (drawToken.cancelled || !bufferCanvas || !bufferCanvas.isConnected) return;
|
||||
if (featVideo.readyState >= 2) ctx.drawImage(featVideo, 0, 0, 1280, 720);
|
||||
if (typeof featVideo.requestVideoFrameCallback === 'function') {
|
||||
featVideo.requestVideoFrameCallback(drawFrame);
|
||||
}
|
||||
@@ -2567,7 +2722,7 @@
|
||||
featVideo.requestVideoFrameCallback(drawFrame);
|
||||
} else {
|
||||
bufferCanvasInterval = setInterval(() => {
|
||||
if (featVideo.readyState >= 2) ctx.drawImage(featVideo, 0, 0, 960, 540);
|
||||
if (featVideo.readyState >= 2) ctx.drawImage(featVideo, 0, 0, 1280, 720);
|
||||
}, 1000 / 30);
|
||||
}
|
||||
|
||||
@@ -2681,7 +2836,8 @@
|
||||
// Reset clip button
|
||||
if (clipBtn) { clipBtn.style.borderColor = ''; clipBtn.style.color = ''; clipBtn.title = ''; }
|
||||
// Restart buffer fresh when returning — old frames are degraded
|
||||
setTimeout(() => { stopBuffer(); bufferAudioCtx = null; setTimeout(startBuffer, 300); }, 100);
|
||||
// Keep bufferAudioCtx alive — createMediaElementSource can only be called once per element
|
||||
setTimeout(() => { stopBuffer(); setTimeout(startBuffer, 300); }, 100);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2821,12 +2977,14 @@
|
||||
inSlider.addEventListener('input', () => {
|
||||
if (parseFloat(inSlider.value) >= parseFloat(outSlider.value) - 1)
|
||||
inSlider.value = (parseFloat(outSlider.value) - 1).toFixed(1);
|
||||
if (!video.paused) stopPlayback();
|
||||
updateTrimUI(true);
|
||||
});
|
||||
|
||||
outSlider.addEventListener('input', () => {
|
||||
if (parseFloat(outSlider.value) <= parseFloat(inSlider.value) + 1)
|
||||
outSlider.value = (parseFloat(inSlider.value) + 1).toFixed(1);
|
||||
if (!video.paused) stopPlayback();
|
||||
updateTrimUI(false);
|
||||
});
|
||||
|
||||
@@ -2955,9 +3113,9 @@
|
||||
: (MediaRecorder.isTypeSupported('video/webm;codecs=vp9') ? 'video/webm;codecs=vp9' : 'video/webm');
|
||||
|
||||
function startRecording() {
|
||||
// Export at 540p regardless of source resolution — maximises duration at given bitrate
|
||||
canvas.width = 960;
|
||||
canvas.height = 540;
|
||||
// Export at 720p regardless of source resolution
|
||||
canvas.width = 1280;
|
||||
canvas.height = 720;
|
||||
|
||||
const stream = canvas.captureStream(30);
|
||||
|
||||
@@ -3057,7 +3215,7 @@
|
||||
// Switch back to thumbnail mode — destroy all live thumb streams, refresh canvases
|
||||
const cammanIdx = CAMERAS.findIndex(([,s]) => s === 'cameraman2-5');
|
||||
CAMERAS.forEach(([name, slug], i) => {
|
||||
if (i === DEFAULT_IDX || i === cammanIdx) return;
|
||||
if (i === DEFAULT_IDX) return;
|
||||
if (hlsInstances[i]) { hlsInstances[i].destroy(); delete hlsInstances[i]; }
|
||||
// Replace video with canvas if needed
|
||||
const cell = document.getElementById('cam-' + i);
|
||||
@@ -3078,7 +3236,7 @@
|
||||
if (window._thumbInterval) { clearInterval(window._thumbInterval); window._thumbInterval = null; }
|
||||
const cammanIdx = CAMERAS.findIndex(([,s]) => s === 'cameraman2-5');
|
||||
CAMERAS.forEach(([name, slug], i) => {
|
||||
if (i === DEFAULT_IDX || i === cammanIdx) return;
|
||||
if (i === DEFAULT_IDX) return;
|
||||
const cell = document.getElementById('cam-' + i);
|
||||
if (!cell) return;
|
||||
let canvas = cell.querySelector('canvas');
|
||||
@@ -3131,7 +3289,7 @@
|
||||
async function refreshAllThumbnails() {
|
||||
const cammanIdx = CAMERAS.findIndex(([,s]) => s === 'cameraman2-5');
|
||||
const tasks = CAMERAS.map(([name, slug], i) => {
|
||||
if (i === DEFAULT_IDX || i === cammanIdx) return null;
|
||||
if (i === DEFAULT_IDX) return null;
|
||||
if (slug === CAMERAS[featuredIdx][1]) return null;
|
||||
return captureThumb(slug, 'canvas-' + i);
|
||||
}).filter(Boolean);
|
||||
@@ -3198,10 +3356,11 @@
|
||||
// Build thumb grid — skip Director Mode and Cameraman
|
||||
const cammanIdx = CAMERAS.findIndex(([,s]) => s === "cameraman2-5");
|
||||
CAMERAS.forEach(([name, slug], i) => {
|
||||
if (i === DEFAULT_IDX || i === cammanIdx) return;
|
||||
if (i === DEFAULT_IDX) return;
|
||||
const cell = document.createElement('div');
|
||||
cell.className = 'cam-cell';
|
||||
cell.id = 'cam-' + i;
|
||||
cell.dataset.slug = slug;
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.style.cssText = 'width:100%;height:100%;display:block;object-fit:cover;';
|
||||
canvas.id = 'canvas-' + i;
|
||||
@@ -3349,7 +3508,6 @@
|
||||
setApiStatus('none');
|
||||
}
|
||||
initCameras();
|
||||
initCamerman();
|
||||
// Apply saved thumbnail interval from dropdown
|
||||
const iv = document.getElementById('intervalSelect');
|
||||
if (iv && parseInt(iv.value) !== 30000) changeInterval(iv.value);
|
||||
@@ -3364,67 +3522,6 @@
|
||||
}, 500);
|
||||
});
|
||||
|
||||
function initCamerman() {
|
||||
const video = document.getElementById('cammanVideo');
|
||||
if (!video) return;
|
||||
const idx = CAMERAS.findIndex(([,s]) => s === 'cameraman2-5');
|
||||
if (typeof Hls !== 'undefined' && Hls.isSupported()) {
|
||||
if (hlsInstances['camman']) hlsInstances['camman'].destroy();
|
||||
const hls = new Hls({ lowLatencyMode: true, maxBufferLength: 8 });
|
||||
hls.loadSource('http://localhost:3000/cam/cameraman2-5/index.m3u8');
|
||||
hls.attachMedia(video);
|
||||
hls.on(Hls.Events.MANIFEST_PARSED, () => video.play().catch(() => {}));
|
||||
hls.on(Hls.Events.ERROR, (e, d) => {
|
||||
if (d.fatal) {
|
||||
const label = document.getElementById('cammanLabel');
|
||||
if (label) label.textContent = 'CAMERAMAN · RECONNECTING...';
|
||||
hls.destroy();
|
||||
setTimeout(() => {
|
||||
if (video.isConnected) initCamerman();
|
||||
}, 3000);
|
||||
}
|
||||
});
|
||||
hlsInstances['camman'] = hls;
|
||||
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
|
||||
video.src = 'http://localhost:3000/cam/cameraman2-5/index.m3u8';
|
||||
video.play().catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// Update cameraman panel label when it gets swapped with featured
|
||||
const _origSetFeatured = setFeatured;
|
||||
setFeatured = function(i) {
|
||||
_origSetFeatured(i);
|
||||
// If cameraman is now in featured, show director in camman panel
|
||||
// If cameraman is what's being swapped back, restore its own stream
|
||||
const cammanIdx = CAMERAS.findIndex(([,s]) => s === 'cameraman2-5');
|
||||
const cammanVideo = document.getElementById('cammanVideo');
|
||||
const cammanLabel = document.getElementById('cammanLabel');
|
||||
if (!cammanVideo) return;
|
||||
|
||||
if (i === cammanIdx) {
|
||||
// Cameraman just went to featured - show director in camman panel
|
||||
const dirSlug = CAMERAS[DEFAULT_IDX][1];
|
||||
if (hlsInstances['camman']) hlsInstances['camman'].destroy();
|
||||
const hls = new Hls({ lowLatencyMode: true, maxBufferLength: 8 });
|
||||
hls.loadSource('http://localhost:3000/cam/' + dirSlug + '/index.m3u8');
|
||||
hls.attachMedia(cammanVideo);
|
||||
hls.on(Hls.Events.MANIFEST_PARSED, () => { cammanVideo.muted = true; cammanVideo.play().catch(() => {}); });
|
||||
hlsInstances['camman'] = hls;
|
||||
cammanLabel.textContent = 'DIRECTOR MODE';
|
||||
} else if (directorCell === cammanIdx) {
|
||||
// Director is in camman panel cell — keep it
|
||||
} else {
|
||||
// Restore camman panel to cameraman stream
|
||||
if (hlsInstances['camman']) hlsInstances['camman'].destroy();
|
||||
const hls = new Hls({ lowLatencyMode: true, maxBufferLength: 8 });
|
||||
hls.loadSource('http://localhost:3000/cam/cameraman2-5/index.m3u8');
|
||||
hls.attachMedia(cammanVideo);
|
||||
hls.on(Hls.Events.MANIFEST_PARSED, () => { cammanVideo.muted = true; cammanVideo.play().catch(() => {}); });
|
||||
hlsInstances['camman'] = hls;
|
||||
cammanLabel.textContent = 'CAMERAMAN';
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -3459,6 +3556,7 @@
|
||||
|
||||
if (msg._ft === 'ws_status') {
|
||||
setWsStatus(msg.status);
|
||||
inlineChatSetStatus(msg.status === 'connected' ? 'connected' : 'disconnected');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3475,6 +3573,10 @@
|
||||
window._lastPresenceData = msg.data;
|
||||
autoDiscoverCameras(msg.data);
|
||||
updateViewerCounts(msg.data);
|
||||
const totalEl = document.getElementById('inlineChatViewers');
|
||||
if (totalEl && msg.data.total !== undefined) {
|
||||
totalEl.textContent = msg.data.total.toLocaleString() + ' in chat';
|
||||
}
|
||||
}
|
||||
|
||||
if (msg.event === 'feature-toggles:update') {
|
||||
@@ -3486,7 +3588,7 @@
|
||||
// data is [{value, score}, ...] — update scores in cached poll
|
||||
if (Array.isArray(msg.data)) applyPollVote(msg.data);
|
||||
}
|
||||
if (msg.event === 'tts:update') {
|
||||
if (msg.event === 'tts:update' || msg.event === 'tts:insert') {
|
||||
// Full TTS object — add to history if not seen
|
||||
const m = msg.data;
|
||||
if (m && m.id && !seenTtsIds.has(m.id)) {
|
||||
@@ -3500,7 +3602,7 @@
|
||||
if (idx > -1) { ttsHistory[idx] = m; lastCombinedRenderCount = 0; renderCombined(); }
|
||||
}
|
||||
}
|
||||
if (msg.event === 'sfx:insert') {
|
||||
if (msg.event === 'sfx:insert' || msg.event === 'sfx:update') {
|
||||
const m = msg.data;
|
||||
if (!m || !m.id) return;
|
||||
if (!seenSfxIds.has(m.id)) {
|
||||
@@ -3513,6 +3615,16 @@
|
||||
lastCombinedRenderCount = 0;
|
||||
renderCombined();
|
||||
}
|
||||
if (msg.event === 'chat:message') {
|
||||
const data = msg.data;
|
||||
const messages = Array.isArray(data) ? data : [data];
|
||||
messages.forEach(m => {
|
||||
if (isRealChatMessage(m)) {
|
||||
addMessage(m);
|
||||
inlineChatAddMessage(m);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (msg._ft === 'raw') {
|
||||
@@ -3598,6 +3710,113 @@
|
||||
};
|
||||
// ── End grid row recalculation ────────────────────────────────
|
||||
|
||||
// ── Chat message helpers (shared by inline panel and popout forwarding) ────
|
||||
function isRealChatMessage(msg) {
|
||||
if (msg.user && msg.user.id === 'tts') return false;
|
||||
if (msg.metadata && msg.metadata.type === 'item') return false;
|
||||
if (!msg.message || typeof msg.message !== 'string') return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// addMessage forwards to the chat popout window if open
|
||||
function addMessage(msg) {
|
||||
if (window._chatPopout && !window._chatPopout.closed) {
|
||||
try { window._chatPopout.postMessage({ _ft: 'chat', msg }, '*'); } catch(e) {}
|
||||
}
|
||||
}
|
||||
// ── End chat helpers ─────────────────────────────────────────
|
||||
|
||||
// ── Inline chat ──────────────────────────────────────────────
|
||||
let inlineChatCount = 0;
|
||||
let inlineChatAutoScroll = true;
|
||||
let chatCollapsed = false;
|
||||
|
||||
const inlineFeed = document.getElementById('inlineChatFeed');
|
||||
const inlineChatScrollBtn = document.getElementById('inlineChatScrollBtn');
|
||||
|
||||
inlineFeed.addEventListener('scroll', () => {
|
||||
const atBottom = inlineFeed.scrollHeight - inlineFeed.scrollTop - inlineFeed.clientHeight < 60;
|
||||
inlineChatAutoScroll = atBottom;
|
||||
inlineChatScrollBtn.style.display = atBottom ? 'none' : 'block';
|
||||
});
|
||||
|
||||
window.inlineChatScrollToBottom = function inlineChatScrollToBottom() {
|
||||
inlineFeed.scrollTop = inlineFeed.scrollHeight;
|
||||
inlineChatAutoScroll = true;
|
||||
inlineChatScrollBtn.style.display = 'none';
|
||||
};
|
||||
|
||||
function inlineChatSetStatus(state) {
|
||||
const dot = document.getElementById('inlineChatDot');
|
||||
const label = document.getElementById('inlineChatStatus');
|
||||
if (!dot || !label) return;
|
||||
if (state === 'connected') { dot.className = 'dot live'; label.textContent = 'LIVE'; label.style.color = 'var(--green)'; }
|
||||
else if (state === 'disconnected') { dot.className = 'dot error'; label.textContent = 'OFFLINE'; label.style.color = 'var(--accent2)'; }
|
||||
else { dot.className = 'dot'; label.textContent = 'CONNECTING'; label.style.color = 'var(--muted)'; }
|
||||
}
|
||||
|
||||
function inlineChatAddMessage(msg) {
|
||||
const empty = inlineFeed.querySelector('.empty');
|
||||
if (empty) empty.remove();
|
||||
|
||||
const user = msg.user || {};
|
||||
const meta = msg.metadata || {};
|
||||
const isAdmin = meta.isAdmin || false;
|
||||
const isMod = meta.isMod || false;
|
||||
const isFish = meta.isFish || false;
|
||||
const isGM = meta.isGrandMarshall || false;
|
||||
|
||||
const cls = ['msg', isAdmin?'admin':'', isMod?'mod':'', isFish?'fish':'', isGM?'grand-marshall':''].filter(Boolean).join(' ');
|
||||
|
||||
const nameStyle = user.customUsernameColor ? `style="color:${user.customUsernameColor}"` : '';
|
||||
const endorsement = user.endorsement
|
||||
? `<span class="msg-endorsement" style="color:${user.endorsementColor||'#888'}">${user.endorsement}</span>` : '';
|
||||
const badges = [
|
||||
isAdmin ? '<span class="badge admin">ADMIN</span>' : '',
|
||||
isMod ? '<span class="badge mod">MOD</span>' : '',
|
||||
isFish ? '<span class="badge fish">FISH</span>' : '',
|
||||
isGM ? '<span class="badge gm">GM</span>' : '',
|
||||
].filter(Boolean).join('');
|
||||
const ts = msg.timestamp ? new Date(msg.timestamp).toLocaleTimeString([],{hour:'2-digit',minute:'2-digit',second:'2-digit'}) : '';
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.className = cls;
|
||||
div.innerHTML = `
|
||||
<div class="msg-header">
|
||||
<span class="msg-user" ${nameStyle}>${user.displayName||'unknown'}</span>
|
||||
${user.clan?`<span style="font-size:9px;color:var(--muted)">[${user.clan}]</span>`:''}
|
||||
${endorsement}${badges}
|
||||
${ts?`<span class="msg-time">${ts}</span>`:''}
|
||||
</div>
|
||||
<div class="msg-text">${msg.message.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')}</div>`;
|
||||
|
||||
inlineFeed.appendChild(div);
|
||||
// Keep max 500 messages to avoid memory growth
|
||||
while (inlineFeed.children.length > 500) inlineFeed.removeChild(inlineFeed.firstChild);
|
||||
inlineChatCount++;
|
||||
const countEl = document.getElementById('inlineChatCount');
|
||||
if (countEl) countEl.textContent = inlineChatCount.toLocaleString() + ' messages';
|
||||
if (inlineChatAutoScroll) inlineFeed.scrollTop = inlineFeed.scrollHeight;
|
||||
}
|
||||
|
||||
window.toggleChat = function() {
|
||||
chatCollapsed = !chatCollapsed;
|
||||
const main = document.querySelector('.main');
|
||||
const btn = document.getElementById('chatCollapseBtn');
|
||||
if (chatCollapsed) {
|
||||
main.classList.add('chat-collapsed');
|
||||
btn.textContent = '💬 CHAT';
|
||||
btn.title = 'Show chat panel';
|
||||
} else {
|
||||
main.classList.remove('chat-collapsed');
|
||||
btn.textContent = '💬 CHAT';
|
||||
btn.title = 'Hide chat panel';
|
||||
inlineFeed.scrollTop = inlineFeed.scrollHeight;
|
||||
}
|
||||
setTimeout(() => window.recalcGridRows && window.recalcGridRows(), 50);
|
||||
};
|
||||
// ── End inline chat ───────────────────────────────────────────
|
||||
|
||||
// ── Stocks panel collapse ────────────────────────────────────
|
||||
let stocksCollapsed = false;
|
||||
|
||||
@@ -3645,7 +3864,7 @@
|
||||
|
||||
function autoDiscoverCameras(presenceData) {
|
||||
// presence keys are camera slugs (plus "total")
|
||||
const knownSpecial = new Set(['total', 'cameraman2-5']);
|
||||
const knownSpecial = new Set(['total']);
|
||||
let added = false;
|
||||
|
||||
Object.keys(presenceData).forEach(slug => {
|
||||
@@ -3656,8 +3875,7 @@
|
||||
console.log('[CAM] Auto-discovered new camera:', slug);
|
||||
KNOWN_CAMERA_SLUGS.add(slug);
|
||||
const label = slugToLabel(slug);
|
||||
const cammanIdx = CAMERAS.findIndex(([, s]) => s === 'cameraman2-5');
|
||||
CAMERAS.splice(cammanIdx, 0, [label, slug]);
|
||||
CAMERAS.push([label, slug]);
|
||||
added = true;
|
||||
});
|
||||
|
||||
@@ -3667,11 +3885,12 @@
|
||||
const grid = document.getElementById('cameraGrid');
|
||||
const cammanIdx = CAMERAS.findIndex(([, s]) => s === 'cameraman2-5');
|
||||
CAMERAS.forEach(([name, slug], i) => {
|
||||
if (i === DEFAULT_IDX || i === cammanIdx) return;
|
||||
if (i === DEFAULT_IDX) return;
|
||||
if (document.getElementById('cam-' + i)) return; // already exists
|
||||
const cell = document.createElement('div');
|
||||
cell.className = 'cam-cell';
|
||||
cell.id = 'cam-' + i;
|
||||
cell.dataset.slug = slug;
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.style.cssText = 'width:100%;height:100%;display:block;object-fit:cover;';
|
||||
canvas.id = 'canvas-' + i;
|
||||
@@ -3693,22 +3912,31 @@
|
||||
// ── Viewer counts ────────────────────────────────────────────
|
||||
window.updateViewerCounts = function(data) {
|
||||
const cammanIdx = typeof CAMERAS !== 'undefined' ? CAMERAS.findIndex(([,s]) => s === 'cameraman2-5') : -1;
|
||||
if (typeof CAMERAS !== 'undefined') {
|
||||
CAMERAS.forEach(([name, slug], i) => {
|
||||
if (i === (typeof DEFAULT_IDX !== 'undefined' ? DEFAULT_IDX : 1)) return;
|
||||
if (i === cammanIdx) return;
|
||||
const cell = document.getElementById('cam-' + i);
|
||||
if (cell) setViewerBadge(cell, data[slug]);
|
||||
// Use data-slug from each cell — it may differ from CAMERAS[i] if director has swapped in
|
||||
const grid = document.getElementById('cameraGrid');
|
||||
if (grid) {
|
||||
grid.querySelectorAll('.cam-cell').forEach(cell => {
|
||||
const slug = cell.dataset.slug;
|
||||
if (slug) setViewerBadge(cell, data[slug]);
|
||||
});
|
||||
}
|
||||
|
||||
// Sort grid cells by viewer count descending
|
||||
if (grid && typeof CAMERAS !== 'undefined') {
|
||||
const cells = Array.from(grid.querySelectorAll('.cam-cell'));
|
||||
cells.sort((a, b) => {
|
||||
const aSlug = a.dataset.slug;
|
||||
const bSlug = b.dataset.slug;
|
||||
return (data[bSlug] || 0) - (data[aSlug] || 0);
|
||||
});
|
||||
cells.forEach(cell => grid.appendChild(cell));
|
||||
}
|
||||
|
||||
const featWrap = document.getElementById('camFeaturedWrap');
|
||||
if (featWrap && typeof CAMERAS !== 'undefined' && typeof featuredIdx !== 'undefined') {
|
||||
const slug = CAMERAS[featuredIdx] ? CAMERAS[featuredIdx][1] : null;
|
||||
const featCell = featWrap.querySelector('.cam-cell') || featWrap;
|
||||
if (slug) {
|
||||
const dirSlug = CAMERAS[typeof DEFAULT_IDX !== 'undefined' ? DEFAULT_IDX : 1][1];
|
||||
setViewerBadge(featCell, data[slug]);
|
||||
}
|
||||
if (slug) setViewerBadge(featCell, data[slug]);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user