mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 03:14:00 +00:00
feat: add Nepal flood incident scene
This commit is contained in:
1 parent
76c20be277
commit
60768ccc0b
48 files changed
+15431
-161
No files matched your search
@@ -103,6 +103,7 @@ export const WORLD_OVERLAY_OCCLUDER_SELECTORS = Object.freeze([
|
||||
'#space-mission-panel',
|
||||
'#space-mission-panel-host',
|
||||
'#military-awareness-panel',
|
||||
'#bhote-koshi-event-panel',
|
||||
// Cockpit: solid backdrop-filled windows only (both bounded to
|
||||
// `min(340px, 28vw)` wide and `min(42vh, 410px)` tall, both `hidden` until
|
||||
// toggled). Every other cockpit selector was removed — see the block comment.
|
||||
@@ -460,7 +461,20 @@ export function normalizeOverlayEntry(sourceId, entry) {
|
||||
edgeFade: entry.edgeFade === false || entry.edgeFade === 'none' ? 'none' : 'keyhole',
|
||||
horizonCull: entry.horizonCull !== false,
|
||||
terrainOcclusion: entry.terrainOcclusion === true,
|
||||
sourceAlpha: clamp01(entry.sourceAlpha ?? entry.alpha, 1),
|
||||
sourceAlpha: typeof (entry.sourceAlpha ?? entry.alpha) === 'function'
|
||||
? (entry.sourceAlpha ?? entry.alpha)
|
||||
: clamp01(entry.sourceAlpha ?? entry.alpha, 1),
|
||||
presentationScale: typeof entry.presentationScale === 'function'
|
||||
? entry.presentationScale
|
||||
: Math.max(0, Number(entry.presentationScale) || 1),
|
||||
leaderProgress: typeof entry.leaderProgress === 'function'
|
||||
? entry.leaderProgress
|
||||
: clamp01(entry.leaderProgress, 1),
|
||||
contentAlpha: typeof entry.contentAlpha === 'function'
|
||||
? entry.contentAlpha
|
||||
: clamp01(entry.contentAlpha, 1),
|
||||
anchorDot: entry.anchorDot === true,
|
||||
leaderStyle: entry.leaderStyle === 'elbow' ? 'elbow' : 'straight',
|
||||
temporalAlpha: clamp01(entry.temporalAlpha, 1),
|
||||
gapPx: Number.isFinite(Number(entry.gapPx)) ? Math.max(0, Number(entry.gapPx)) : 12,
|
||||
leaderOffsetPx: Number.isFinite(Number(entry.leaderOffsetPx))
|
||||
@@ -1513,6 +1527,8 @@ function getProjectionRecord(entry) {
|
||||
candidate: null,
|
||||
distanceAlpha: 1,
|
||||
paintScale: 1,
|
||||
leaderProgress: 1,
|
||||
contentAlpha: 1,
|
||||
altitudeAlpha: 1,
|
||||
sourceAlpha: 1,
|
||||
protectedPlacement: null,
|
||||
@@ -1575,6 +1591,30 @@ function resetFrameDomains() {
|
||||
}
|
||||
}
|
||||
|
||||
// Evaluate optional scene presentation outside the shared projection hot path.
|
||||
// Passing records (rather than intermediate doubles) also avoids boxing the
|
||||
// ordinary moving-source workload at a non-inlined call boundary.
|
||||
function applyPresentation(entry, source, record) {
|
||||
try {
|
||||
const scale = Number(typeof entry.presentationScale === 'function'
|
||||
? entry.presentationScale() : entry.presentationScale);
|
||||
record.paintScale *= Number.isFinite(scale) ? Math.max(0, scale) : 1;
|
||||
const alpha = Number(typeof entry.sourceAlpha === 'function'
|
||||
? entry.sourceAlpha() : entry.sourceAlpha);
|
||||
record.sourceAlpha = source.options.alpha
|
||||
* (Number.isFinite(alpha) ? Math.max(0, Math.min(1, alpha)) : 1);
|
||||
const leader = Number(typeof entry.leaderProgress === 'function'
|
||||
? entry.leaderProgress() : entry.leaderProgress);
|
||||
record.leaderProgress = Number.isFinite(leader) ? Math.max(0, Math.min(1, leader)) : 1;
|
||||
const content = Number(typeof entry.contentAlpha === 'function'
|
||||
? entry.contentAlpha() : entry.contentAlpha);
|
||||
record.contentAlpha = Number.isFinite(content) ? Math.max(0, Math.min(1, content)) : 1;
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function snapshotAndProject(entry, source, viewProjection, keyhole) {
|
||||
const record = getProjectionRecord(entry);
|
||||
let position;
|
||||
@@ -1651,7 +1691,14 @@ function snapshotAndProject(entry, source, viewProjection, keyhole) {
|
||||
record.altitudeAlpha = entry.altitudeFadeEnd === Number.POSITIVE_INFINITY
|
||||
? (Number.isFinite(cameraAltitude) && cameraAltitude < entry.minAltitude ? 0 : 1)
|
||||
: altitudeFade(cameraAltitude, record.altitudeOptions);
|
||||
record.sourceAlpha = source.options.alpha * entry.sourceAlpha;
|
||||
record.leaderProgress = 1;
|
||||
record.contentAlpha = 1;
|
||||
if (entry.presentationScale !== 1 || entry.leaderProgress !== 1
|
||||
|| entry.contentAlpha !== 1 || typeof entry.sourceAlpha === 'function') {
|
||||
if (!applyPresentation(entry, source, record)) return null;
|
||||
} else {
|
||||
record.sourceAlpha = source.options.alpha * entry.sourceAlpha;
|
||||
}
|
||||
if (record.distanceAlpha <= 0 || record.paintScale <= 0
|
||||
|| record.altitudeAlpha <= 0 || record.sourceAlpha <= 0) return null;
|
||||
|
||||
@@ -2075,7 +2122,14 @@ function paintEntryItem(item, keyhole) {
|
||||
* record.distanceAlpha * record.altitudeAlpha * keyholeAlpha;
|
||||
if (finalAlpha <= 0.001) return;
|
||||
if (record.paintScale === 1) {
|
||||
paintOverlayEntry(_ctx, entry, placement, finalAlpha);
|
||||
paintOverlayEntry(
|
||||
_ctx,
|
||||
entry,
|
||||
placement,
|
||||
finalAlpha,
|
||||
record.leaderProgress,
|
||||
record.contentAlpha,
|
||||
);
|
||||
} else {
|
||||
const scaled = localizeScaledPlacement(
|
||||
placement,
|
||||
@@ -2085,7 +2139,14 @@ function paintEntryItem(item, keyhole) {
|
||||
_ctx.save();
|
||||
_ctx.translate(placement.rect.x, placement.rect.y);
|
||||
_ctx.scale(record.paintScale, record.paintScale);
|
||||
paintOverlayEntry(_ctx, entry, scaled, finalAlpha);
|
||||
paintOverlayEntry(
|
||||
_ctx,
|
||||
entry,
|
||||
scaled,
|
||||
finalAlpha,
|
||||
record.leaderProgress,
|
||||
record.contentAlpha,
|
||||
);
|
||||
_ctx.restore();
|
||||
}
|
||||
publishPaintRect(item);
|
||||
|
||||
@@ -1821,6 +1821,85 @@ test('CCTV host binding preserves the smoothstep scale and 0.45/0.35 fade curve'
|
||||
}
|
||||
});
|
||||
|
||||
test('scene presentation samples once per frame and recovers after a callback failure', () => {
|
||||
const env = installMockEnvironment({ width: 500, height: 400, dpr: 1 });
|
||||
const alphas = [];
|
||||
Object.defineProperty(env.ctx, 'globalAlpha', {
|
||||
configurable: true,
|
||||
get() { return alphas.at(-1) ?? 1; },
|
||||
set(value) { alphas.push(value); },
|
||||
});
|
||||
initWorldOverlay(env.viewer);
|
||||
const entry = createCctvThumbnailOverlayEntry({
|
||||
id: 'scene-presentation', position: position(), title: 'SCENE',
|
||||
frameSlot: { frame: { ready: true }, stamp: 1 }, active: true,
|
||||
});
|
||||
Object.assign(entry, {
|
||||
horizonCull: false, maxDistance: Number.POSITIVE_INFINITY, edgeFade: 'none',
|
||||
altitudeScale: null, altitudeFadeEnd: Number.POSITIVE_INFINITY,
|
||||
});
|
||||
let scale = 0.5;
|
||||
let alpha = 0.8;
|
||||
let leader = 0.5;
|
||||
let content = 0.25;
|
||||
let fail = false;
|
||||
const sampled = { scale: 0, alpha: 0, leader: 0, content: 0 };
|
||||
entry.presentationScale = () => { sampled.scale++; return scale; };
|
||||
entry.sourceAlpha = () => {
|
||||
sampled.alpha++;
|
||||
if (fail) throw new Error('presentation unavailable');
|
||||
return alpha;
|
||||
};
|
||||
entry.leaderProgress = () => { sampled.leader++; return leader; };
|
||||
entry.contentAlpha = () => { sampled.content++; return content; };
|
||||
const publish = () => setOverlayEntries('scene-presentation', [entry], {
|
||||
cohortLimit: 1, collisionCapacity: 0,
|
||||
});
|
||||
const frame = () => {
|
||||
env.ctx.calls.length = 0;
|
||||
alphas.length = 0;
|
||||
env.postRender.raise();
|
||||
};
|
||||
try {
|
||||
publish();
|
||||
frame();
|
||||
assert.deepEqual(sampled, { scale: 1, alpha: 1, leader: 1, content: 1 });
|
||||
assert.equal(env.ctx.calls.find(([name]) => name === 'scale')?.[1], 0.5);
|
||||
assert.ok(Math.abs(alphas.at(-1) - 0.2) < 1e-12);
|
||||
assert.ok(env.ctx.calls.some(([name]) => name === 'drawImage'));
|
||||
|
||||
fail = true;
|
||||
assert.doesNotThrow(frame);
|
||||
assert.equal(env.ctx.calls.some(([name]) => name === 'drawImage'), false);
|
||||
assert.deepEqual(sampled, { scale: 2, alpha: 2, leader: 1, content: 1 });
|
||||
|
||||
fail = false;
|
||||
scale = Number.NaN;
|
||||
alpha = 2;
|
||||
leader = Number.NaN;
|
||||
content = 2;
|
||||
frame();
|
||||
assert.equal(env.ctx.calls.some(([name]) => name === 'scale'), false);
|
||||
assert.equal(alphas.at(-1), 1);
|
||||
assert.ok(env.ctx.calls.some(([name]) => name === 'drawImage'));
|
||||
|
||||
scale = 0;
|
||||
frame();
|
||||
assert.equal(env.ctx.calls.some(([name]) => name === 'drawImage'), false);
|
||||
|
||||
// Reusing the record for an ordinary card must not retain animated values.
|
||||
Object.assign(entry, {
|
||||
presentationScale: 1, sourceAlpha: 1, leaderProgress: 1, contentAlpha: 1,
|
||||
});
|
||||
publish();
|
||||
frame();
|
||||
assert.equal(alphas.at(-1), 1);
|
||||
assert.ok(env.ctx.calls.some(([name]) => name === 'drawImage'));
|
||||
} finally {
|
||||
env.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test('CCTV leader remains one CSS pixel at the 0.35 altitude scale point', () => {
|
||||
const env = installMockEnvironment({ width: 500, height: 400, dpr: 1 });
|
||||
env.viewer.camera.positionCartographic.height = 9500;
|
||||
|
||||
@@ -311,7 +311,10 @@ export function measureOverlayEntry(ctx, entry, out = {}) {
|
||||
out.padY = Math.max(0, Number(entry?.thumbnailPadTop) || 0);
|
||||
out.padBottom = Math.max(0, Number(entry?.thumbnailPadBottom) || 0);
|
||||
out.titleGap = Math.max(0, Number(entry?.thumbnailTitleGap) || 0);
|
||||
out.titleH = Math.max(0, Number(entry?.thumbnailTitleHeight) || out.titleH);
|
||||
const thumbnailTitleHeight = Number(entry?.thumbnailTitleHeight);
|
||||
out.titleH = Number.isFinite(thumbnailTitleHeight)
|
||||
? Math.max(0, thumbnailTitleHeight)
|
||||
: out.titleH;
|
||||
out.thumbW = Math.max(1, Number(entry?.thumbnailWidth) || 96);
|
||||
out.thumbH = Math.max(1, Number(entry?.thumbnailHeight) || 54);
|
||||
out.w = out.thumbW + out.padX * 2;
|
||||
@@ -444,28 +447,82 @@ export function placementVariants({
|
||||
return out;
|
||||
}
|
||||
|
||||
function drawLeader(ctx, placement, accent) {
|
||||
function drawLeader(
|
||||
ctx,
|
||||
placement,
|
||||
accent,
|
||||
progress = 1,
|
||||
width = 1,
|
||||
style = 'straight',
|
||||
) {
|
||||
const reveal = Math.max(0, Math.min(1, Number(progress) || 0));
|
||||
if (reveal <= 0) return;
|
||||
ctx.strokeStyle = accent || WORLD_OVERLAY_STYLE.leader;
|
||||
// Leaders are screen-space strokes. Card content may be painted inside an
|
||||
// altitude/distance scale transform, so counter-scale the canvas width to
|
||||
// retain the shipped one-CSS-pixel leader at every card size.
|
||||
ctx.lineWidth = 1 / (placement.paintScale || 1);
|
||||
ctx.beginPath();
|
||||
if (placement.leaderOffset === 0) {
|
||||
ctx.moveTo(placement.leadFromX, placement.leadFromY);
|
||||
} else if (placement.corner === 'above' || placement.corner === 'below') {
|
||||
ctx.moveTo(placement.leadFromX, placement.leadFromY + placement.leaderOffset);
|
||||
} else {
|
||||
ctx.moveTo(placement.leadFromX + placement.leaderOffset, placement.leadFromY);
|
||||
ctx.lineWidth = width / (placement.paintScale || 1);
|
||||
ctx.lineCap = 'round';
|
||||
ctx.lineJoin = 'round';
|
||||
let startX = placement.leadFromX;
|
||||
let startY = placement.leadFromY;
|
||||
if (placement.leaderOffset !== 0) {
|
||||
if (placement.corner === 'above' || placement.corner === 'below') {
|
||||
startY += placement.leaderOffset;
|
||||
} else {
|
||||
startX += placement.leaderOffset;
|
||||
}
|
||||
}
|
||||
ctx.lineTo(placement.leadToX, placement.leadToY);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(startX, startY);
|
||||
if (
|
||||
style === 'elbow'
|
||||
&& (placement.corner === 'left' || placement.corner === 'right')
|
||||
) {
|
||||
const elbowY = placement.rect.y + Math.min(
|
||||
Math.max(12, placement.rect.h * 0.12),
|
||||
26,
|
||||
);
|
||||
const verticalLength = Math.abs(elbowY - startY);
|
||||
const horizontalLength = Math.abs(placement.leadToX - startX);
|
||||
const totalLength = verticalLength + horizontalLength;
|
||||
const visibleLength = totalLength * reveal;
|
||||
if (visibleLength <= verticalLength && verticalLength > 0) {
|
||||
const direction = Math.sign(elbowY - startY);
|
||||
ctx.lineTo(startX, startY + visibleLength * direction);
|
||||
} else {
|
||||
ctx.lineTo(startX, elbowY);
|
||||
const remaining = Math.max(0, visibleLength - verticalLength);
|
||||
const direction = Math.sign(placement.leadToX - startX);
|
||||
ctx.lineTo(startX + Math.min(horizontalLength, remaining) * direction, elbowY);
|
||||
}
|
||||
ctx.stroke();
|
||||
return;
|
||||
}
|
||||
ctx.lineTo(
|
||||
startX + (placement.leadToX - startX) * reveal,
|
||||
startY + (placement.leadToY - startY) * reveal,
|
||||
);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
function drawCardChrome(ctx, entry, placement, selected = false) {
|
||||
function drawAnchorDot(ctx, entry, placement) {
|
||||
if (!entry.anchorDot) return;
|
||||
const paintScale = placement.paintScale || 1;
|
||||
const radius = WORLD_OVERLAY_STYLE.anchorDotRadius / paintScale;
|
||||
ctx.beginPath();
|
||||
ctx.arc(placement.anchorX, placement.anchorY, radius, 0, Math.PI * 2);
|
||||
ctx.fillStyle = entry.accent || WORLD_OVERLAY_STYLE.accent;
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = WORLD_OVERLAY_STYLE.anchorDotStroke;
|
||||
ctx.lineWidth = WORLD_OVERLAY_STYLE.anchorDotStrokeWidth / paintScale;
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
function drawCardChrome(ctx, entry, placement, selected = false, drawLeaderLine = true) {
|
||||
const { x, y, w, h } = placement.rect;
|
||||
const accent = entry.accent || WORLD_OVERLAY_STYLE.accent;
|
||||
drawLeader(ctx, placement, accent);
|
||||
if (drawLeaderLine) drawLeader(ctx, placement, accent, 1, 1, entry.leaderStyle);
|
||||
ctx.beginPath();
|
||||
roundedRectPath(ctx, x, y, w, h);
|
||||
ctx.fillStyle = selected ? WORLD_OVERLAY_STYLE.selectedBackground : WORLD_OVERLAY_STYLE.background;
|
||||
@@ -667,18 +724,46 @@ export function paintTrack(ctx, entry, placement, alpha = 1) {
|
||||
return placement.rect;
|
||||
}
|
||||
|
||||
/** Paint a standard detail card. */
|
||||
export function paintCard(ctx, entry, placement, alpha = 1) {
|
||||
/** Paint a standard detail card, with an opt-in staged anchor/leader reveal. */
|
||||
export function paintCard(
|
||||
ctx,
|
||||
entry,
|
||||
placement,
|
||||
alpha = 1,
|
||||
leaderProgress = 1,
|
||||
contentAlpha = 1,
|
||||
) {
|
||||
ctx.save();
|
||||
ctx.globalAlpha = alpha;
|
||||
drawCardChrome(ctx, entry, placement, false);
|
||||
if (entry.anchorDot) {
|
||||
drawLeader(
|
||||
ctx,
|
||||
placement,
|
||||
entry.accent || WORLD_OVERLAY_STYLE.accent,
|
||||
leaderProgress,
|
||||
WORLD_OVERLAY_STYLE.leaderWidth,
|
||||
entry.leaderStyle,
|
||||
);
|
||||
drawAnchorDot(ctx, entry, placement);
|
||||
ctx.globalAlpha = alpha * Math.max(0, Math.min(1, Number(contentAlpha) || 0));
|
||||
drawCardChrome(ctx, entry, placement, false, false);
|
||||
} else {
|
||||
drawCardChrome(ctx, entry, placement, false);
|
||||
}
|
||||
drawCardText(ctx, entry, placement, false);
|
||||
ctx.restore();
|
||||
return placement.rect;
|
||||
}
|
||||
|
||||
/** Paint a thumbnail card. The image slot remains source-owned. */
|
||||
export function paintThumbnail(ctx, entry, placement, alpha = 1) {
|
||||
export function paintThumbnail(
|
||||
ctx,
|
||||
entry,
|
||||
placement,
|
||||
alpha = 1,
|
||||
leaderProgress = 1,
|
||||
contentAlpha = 1,
|
||||
) {
|
||||
const layout = entry._overlayLayout || {};
|
||||
const { x, y, w, h } = placement.rect;
|
||||
const padX = Number.isFinite(layout.padX) ? layout.padX : 4;
|
||||
@@ -690,7 +775,16 @@ export function paintThumbnail(ctx, entry, placement, alpha = 1) {
|
||||
ctx.save();
|
||||
ctx.globalAlpha = alpha;
|
||||
|
||||
drawLeader(ctx, placement, entry.thumbnailLeaderColor || WORLD_OVERLAY_STYLE.leader);
|
||||
drawLeader(
|
||||
ctx,
|
||||
placement,
|
||||
entry.thumbnailLeaderColor || WORLD_OVERLAY_STYLE.leader,
|
||||
leaderProgress,
|
||||
entry.anchorDot ? WORLD_OVERLAY_STYLE.leaderWidth : 1,
|
||||
entry.leaderStyle,
|
||||
);
|
||||
drawAnchorDot(ctx, entry, placement);
|
||||
ctx.globalAlpha = alpha * Math.max(0, Math.min(1, Number(contentAlpha) || 0));
|
||||
ctx.beginPath();
|
||||
roundedRectPath(ctx, x, y, w, h, Number(entry.thumbnailRadius) || 4);
|
||||
ctx.fillStyle = entry.thumbnailBackground || WORLD_OVERLAY_STYLE.background;
|
||||
@@ -722,11 +816,13 @@ export function paintThumbnail(ctx, entry, placement, alpha = 1) {
|
||||
ctx.textBaseline = 'alphabetic';
|
||||
ctx.fillStyle = entry.thumbnailTitleColor || WORLD_OVERLAY_STYLE.title;
|
||||
ctx.font = entry.thumbnailTitleFont || WORLD_OVERLAY_STYLE.fontTitle;
|
||||
ctx.fillText(
|
||||
titleChars > 0 ? title.slice(0, titleChars) : title,
|
||||
imageX,
|
||||
imageY + thumbH + titleH - 3,
|
||||
);
|
||||
if (title && titleH > 0) {
|
||||
ctx.fillText(
|
||||
titleChars > 0 ? title.slice(0, titleChars) : title,
|
||||
imageX,
|
||||
imageY + thumbH + titleH - 3,
|
||||
);
|
||||
}
|
||||
ctx.restore();
|
||||
return placement.rect;
|
||||
}
|
||||
@@ -749,7 +845,7 @@ export function paintTracked(ctx, entry, placement, alpha = 1) {
|
||||
const accent = entry.accent || WORLD_OVERLAY_STYLE.accent;
|
||||
ctx.save();
|
||||
ctx.globalAlpha = alpha;
|
||||
drawLeader(ctx, placement, accent);
|
||||
drawLeader(ctx, placement, accent, 1, 1, entry.leaderStyle);
|
||||
ctx.beginPath();
|
||||
roundedRectPath(ctx, x, y, w, h, 5);
|
||||
ctx.fillStyle = WORLD_OVERLAY_STYLE.background;
|
||||
@@ -851,12 +947,23 @@ export function paintDetectionCallout(ctx, callout, alpha = 1) {
|
||||
}
|
||||
|
||||
/** Dispatch a normalized entry to its pure variant painter. */
|
||||
export function paintOverlayEntry(ctx, entry, placement, alpha = 1) {
|
||||
export function paintOverlayEntry(
|
||||
ctx,
|
||||
entry,
|
||||
placement,
|
||||
alpha = 1,
|
||||
leaderProgress = 1,
|
||||
contentAlpha = 1,
|
||||
) {
|
||||
if (entry.cardStyle === 'tactical') return paintTacticalCard(ctx, entry, placement, alpha);
|
||||
if (entry.variant === 'tracked') return paintTracked(ctx, entry, placement, alpha);
|
||||
if (entry.selected || entry.variant === 'selected') return paintSelected(ctx, entry, placement, alpha);
|
||||
if (entry.variant === 'thumbnail') return paintThumbnail(ctx, entry, placement, alpha);
|
||||
if (entry.variant === 'card') return paintCard(ctx, entry, placement, alpha);
|
||||
if (entry.variant === 'thumbnail') {
|
||||
return paintThumbnail(ctx, entry, placement, alpha, leaderProgress, contentAlpha);
|
||||
}
|
||||
if (entry.variant === 'card') {
|
||||
return paintCard(ctx, entry, placement, alpha, leaderProgress, contentAlpha);
|
||||
}
|
||||
if (entry.variant === 'track') return paintTrack(ctx, entry, placement, alpha);
|
||||
return paintLabel(ctx, entry, placement, alpha);
|
||||
}
|
||||
@@ -24,6 +24,10 @@ export const WORLD_OVERLAY_STYLE = Object.freeze({
|
||||
fontTrackedTitle: '600 13px "JetBrains Mono", monospace',
|
||||
fontTrackedDetail: '500 11px "JetBrains Mono", monospace',
|
||||
radius: 4,
|
||||
anchorDotRadius: 3.2,
|
||||
anchorDotStrokeWidth: 1,
|
||||
anchorDotStroke: 'rgba(4, 12, 16, 0.96)',
|
||||
leaderWidth: 1.35,
|
||||
});
|
||||
|
||||
/** CCTV's field-tested thumbnail-card overrides on top of shared card chrome. */
|
||||
|
||||
Reference in new issue
Block a user