Skip to content

Commit 5a03a85

Browse files
authored
Merge pull request #13 from OlevO1/main
Enhance player UX: loading overlay, improved volume controls
2 parents 9837cd4 + 19c9b65 commit 5a03a85

3 files changed

Lines changed: 267 additions & 14 deletions

File tree

extension/manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"manifest_version": 3,
33
"name": "KickNoSub",
4-
"version": "1.1",
4+
"version": "1.1.1",
55
"description": "Unlock subscriber-only videos on Kick by replacing the overlay with an HLS player.",
66
"icons": {
77
"16": "icon.png",

extension/src/scripts/content.js

Lines changed: 130 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@ const ICONS = {
1515
update: `<svg width="98" height="96" viewBox="0 0 98 96" fill="none" xmlns="http://www.w3.org/2000/svg" style="width:18px;height:18px;"><g clip-path="url(#clip0_730_27136)"><path d="M41.4395 69.3848C28.8066 67.8535 19.9062 58.7617 19.9062 46.9902C19.9062 42.2051 21.6289 37.0371 24.5 33.5918C23.2559 30.4336 23.4473 23.7344 24.8828 20.959C28.7109 20.4805 33.8789 22.4902 36.9414 25.2656C40.5781 24.1172 44.4062 23.543 49.0957 23.543C53.7852 23.543 57.6133 24.1172 61.0586 25.1699C64.0254 22.4902 69.2891 20.4805 73.1172 20.959C74.457 23.543 74.6484 30.2422 73.4043 33.4961C76.4668 37.1328 78.0937 42.0137 78.0937 46.9902C78.0937 58.7617 69.1934 67.6621 56.3691 69.2891C59.623 71.3945 61.8242 75.9883 61.8242 81.252L61.8242 91.2051C61.8242 94.0762 64.2168 95.7031 67.0879 94.5547C84.4102 87.9512 98 70.6289 98 49.1914C98 22.1074 75.9883 6.69539e-07 48.9043 4.309e-07C21.8203 1.92261e-07 -1.9479e-07 22.1074 -4.3343e-07 49.1914C-6.20631e-07 70.4375 13.4941 88.0469 31.6777 94.6504C34.2617 95.6074 36.75 93.8848 36.75 91.3008L36.75 83.6445C35.4102 84.2188 33.6875 84.6016 32.1562 84.6016C25.8398 84.6016 22.1074 81.1563 19.4277 74.7441C18.375 72.1602 17.2266 70.6289 15.0254 70.3418C13.877 70.2461 13.4941 69.7676 13.4941 69.1934C13.4941 68.0449 15.4082 67.1836 17.3223 67.1836C20.0977 67.1836 22.4902 68.9063 24.9785 72.4473C26.8926 75.2227 28.9023 76.4668 31.2949 76.4668C33.6875 76.4668 35.2187 75.6055 37.4199 73.4043C39.0469 71.7773 40.291 70.3418 41.4395 69.3848Z" fill="white"/></g><defs><clipPath id="clip0_730_27136"><rect width="98" height="96" fill="white"/></clipPath></defs></svg>`,
1616
bigPlay: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="opacity:1;width:40px;height:40px;"><path fill="none" d="M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z"/></svg>`,
1717
backward: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="opacity:1;"><path d="M12 6a2 2 0 0 0-3.414-1.414l-6 6a2 2 0 0 0 0 2.828l6 6A2 2 0 0 0 12 18z"/><path d="M22 6a2 2 0 0 0-3.414-1.414l-6 6a2 2 0 0 0 0 2.828l6 6A2 2 0 0 0 22 18z"/></svg>`,
18-
forward: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="opacity:1;"><path d="M12 6a2 2 0 0 1 3.414-1.414l6 6a2 2 0 0 1 0 2.828l-6 6A2 2 0 0 1 12 18z"/><path d="M2 6a2 2 0 0 1 3.414-1.414l6 6a2 2 0 0 1 0 2.828l-6 6A2 2 0 0 1 2 18z"/></svg>`
18+
forward: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="opacity:1;"><path d="M12 6a2 2 0 0 1 3.414-1.414l6 6a2 2 0 0 1 0 2.828l-6 6A2 2 0 0 1 12 18z"/><path d="M2 6a2 2 0 0 1 3.414-1.414l6 6a2 2 0 0 1 0 2.828l-6 6A2 2 0 0 1 2 18z"/></svg>`,
19+
volumeHigh: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="opacity:1;"><path fill="none" d="M11 4.702a.705.705 0 0 0-1.203-.498L6.413 7.587A1.4 1.4 0 0 1 5.416 8H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2.416a1.4 1.4 0 0 1 .997.413l3.383 3.384A.705.705 0 0 0 11 19.298zM16 9a5 5 0 0 1 0 6m3.364 3.364a9 9 0 0 0 0-12.728"/></svg>`,
20+
volumeMedium: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="opacity:1;"><path fill="none" d="M11 4.702a.705.705 0 0 0-1.203-.498L6.413 7.587A1.4 1.4 0 0 1 5.416 8H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2.416a1.4 1.4 0 0 1 .997.413l3.383 3.384A.705.705 0 0 0 11 19.298zM16 9a5 5 0 0 1 0 6"/></svg>`,
21+
volumeLow: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="opacity:1;"><path fill="none" d="M11 4.702a.705.705 0 0 0-1.203-.498L6.413 7.587A1.4 1.4 0 0 1 5.416 8H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2.416a1.4 1.4 0 0 1 .997.413l3.383 3.384A.705.705 0 0 0 11 19.298z"/></svg>`,
22+
volumeMute: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="opacity:1;"><path fill="none" d="M11 4.702a.705.705 0 0 0-1.203-.498L6.413 7.587A1.4 1.4 0 0 1 5.416 8H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2.416a1.4 1.4 0 0 1 .997.413l3.383 3.384A.705.705 0 0 0 11 19.298zM22 9l-6 6m0-6l6 6"/></svg>`
1923
};
2024

2125
function getResumeKey(channelSlug, videoSlug) {
@@ -308,11 +312,34 @@ function formatTime(seconds) {
308312
}
309313
}
310314

315+
function getVolumeIcon(volume) {
316+
if (volume <= 0) return ICONS.volumeMute;
317+
if (volume < 0.2) return ICONS.volumeLow;
318+
if (volume < 0.5) return ICONS.volumeMedium;
319+
return ICONS.volumeHigh;
320+
}
321+
311322
async function unlockVideo(triggerElement) {
312323
if (isUnlocking) return;
313324
const container = triggerElement.closest('.relative.flex.flex-col');
314325
if (!container || container.dataset.kickUnlockerProcessing) return;
315326

327+
const containerRect = container.getBoundingClientRect();
328+
const fallbackMinHeight = Math.max(
329+
Math.round(containerRect.height || 0),
330+
Math.round((containerRect.width || 0) * 9 / 16),
331+
360
332+
);
333+
334+
container.style.width = '100%';
335+
container.style.minHeight = `${fallbackMinHeight}px`;
336+
337+
if (containerRect.width > 0 && containerRect.height > 0) {
338+
container.style.aspectRatio = `${containerRect.width} / ${containerRect.height}`;
339+
} else {
340+
container.style.aspectRatio = '16 / 9';
341+
}
342+
316343
if (activeHls) { activeHls.destroy(); activeHls = null; }
317344

318345
try {
@@ -321,7 +348,7 @@ async function unlockVideo(triggerElement) {
321348
// --- STEP 1: FULL WIPE & SPLASH ---
322349
// Immediately hide everything (including the underlying placeholder) and show our splash screen
323350
container.innerHTML = `
324-
<div style="width:100%;height:100%;background:#000;display:flex;flex-direction:column;justify-content:center;align-items:center;font-family:Inter,sans-serif;">
351+
<div style="width:100%;height:100%;min-height:${fallbackMinHeight}px;background:#000;display:flex;flex-direction:column;justify-content:center;align-items:center;font-family:Inter,sans-serif;">
325352
<div style="font-size:24px;font-weight:bold;color:#53fc18;margin-bottom:8px;">🔓 Kick Unlocker</div>
326353
<div style="font-size:14px;color:rgba(255,255,255,0.7);">Searching stream...</div>
327354
</div>
@@ -375,16 +402,25 @@ async function unlockVideo(triggerElement) {
375402
const playerHTML = `
376403
<div id="k-player" style="width:100%;height:100%;position:relative;background:black;overflow:hidden;font-family:Inter,sans-serif;">
377404
<video id="k-video" playsinline style="width:100%;height:100%;object-fit:contain;"></video>
405+
<div id="k-loading" class="visible" aria-hidden="true">
406+
<div class="k-loading-spinner"></div>
407+
</div>
378408
<div id="k-controls" style="position:absolute;bottom:0;left:0;width:100%;padding:20px 15px 10px 15px;background:linear-gradient(to top, rgba(0,0,0,0.9), transparent);display:flex;flex-direction:column;opacity:0;transition:opacity 0.2s;">
379409
<div id="k-track" style="width:100%;height:5px;padding:8px 0;background:rgba(255,255,255,0.3);background-clip:content-box;box-sizing:content-box;cursor:pointer;position:relative;margin-bottom:4px;border-radius:2px;">
380-
<div id="k-track-tooltip"></div>
410+
<div id="k-track-tooltip">
411+
<div id="k-track-tooltip-time">0:00</div>
412+
</div>
381413
<div id="k-progress" style="width:0%;height:100%;background:#53fc18;position:relative;border-radius:2px;"></div>
382414
</div>
383415
<div style="display:flex;align-items:center;justify-content:space-between;">
384416
<div style="display:flex;align-items:center;gap:15px;">
385417
<button id="k-play" style="background:none;border:none;cursor:pointer;opacity:0.9;">${ICONS.play}</button>
386418
<span id="k-time" style="font-size:13px;color:#ddd;font-variant-numeric:tabular-nums;">0:00 / 0:00</span>
387-
<input id="k-volume" type="range" min="0" max="1" step="0.01" value="1">
419+
<div id="k-volume-wrap">
420+
<button id="k-volume-btn" type="button" aria-label="Mute volume">${ICONS.volumeHigh}</button>
421+
<input id="k-volume" type="range" min="0" max="1" step="0.01" value="1">
422+
<span id="k-volume-value">100%</span>
423+
</div>
388424
</div>
389425
<div style="display:flex;align-items:center;gap:15px;">
390426
<button id="k-update-btn" type="button" title="Open latest update" style="display:none;">${ICONS.update}</button>
@@ -414,15 +450,66 @@ async function unlockVideo(triggerElement) {
414450
const progressBar = videoParent.querySelector('#k-progress');
415451
const track = videoParent.querySelector('#k-track');
416452
const trackTooltip = videoParent.querySelector('#k-track-tooltip');
453+
const trackTooltipTime = videoParent.querySelector('#k-track-tooltip-time');
417454
const timeDisplay = videoParent.querySelector('#k-time');
418455
const qualWrap = videoParent.querySelector('#k-quality-wrap');
419456
const qualBtn = videoParent.querySelector('#k-quality-btn');
420457
const qualMenu = videoParent.querySelector('#k-quality-menu');
421458
const seekIndicator = videoParent.querySelector('#k-seek-indicator');
459+
const loadingOverlay = videoParent.querySelector('#k-loading');
422460

461+
const volumeWrap = videoParent.querySelector('#k-volume-wrap');
462+
const volumeButton = videoParent.querySelector('#k-volume-btn');
423463
const volumeSlider = videoParent.querySelector('#k-volume');
464+
const volumeValue = videoParent.querySelector('#k-volume-value');
424465
const initialVolume = Number.isFinite(savedPlayerSettings.volume) ? savedPlayerSettings.volume : 1;
425466
let seekIndicatorTimeout = null;
467+
let hasStartedPlayback = false;
468+
let loadingStateTimeout = null;
469+
let previousVolumeBeforeMute = initialVolume > 0 ? initialVolume : 1;
470+
471+
const updateVolumeSliderVisual = (volume) => {
472+
const percent = Math.max(0, Math.min(100, Math.round(volume * 100)));
473+
volumeSlider.style.setProperty('--k-volume-percent', `${percent}%`);
474+
volumeValue.textContent = `${percent}%`;
475+
volumeButton.innerHTML = getVolumeIcon(volume);
476+
volumeButton.setAttribute('aria-label', percent === 0 ? 'Unmute volume' : 'Mute volume');
477+
volumeWrap.dataset.muted = percent === 0 ? 'true' : 'false';
478+
};
479+
480+
const applyVolume = (volume, { persist = true } = {}) => {
481+
const normalizedVolume = Math.max(0, Math.min(1, Number(volume) || 0));
482+
if (normalizedVolume > 0) previousVolumeBeforeMute = normalizedVolume;
483+
484+
vid.muted = normalizedVolume === 0;
485+
vid.volume = normalizedVolume;
486+
volumeSlider.value = String(normalizedVolume);
487+
updateVolumeSliderVisual(normalizedVolume);
488+
489+
if (persist) {
490+
savePlayerSettings(playerSettingsKey, { volume: normalizedVolume });
491+
}
492+
};
493+
494+
const setLoadingState = (isLoading, { immediate = false } = {}) => {
495+
clearTimeout(loadingStateTimeout);
496+
497+
if (!isLoading) {
498+
loadingOverlay.classList.remove('visible');
499+
return;
500+
}
501+
502+
if (immediate) {
503+
loadingOverlay.classList.add('visible');
504+
return;
505+
}
506+
507+
loadingStateTimeout = setTimeout(() => {
508+
if (!vid.paused && !vid.ended) {
509+
loadingOverlay.classList.add('visible');
510+
}
511+
}, 250);
512+
};
426513

427514
const showSeekIndicator = (direction) => {
428515
seekIndicator.innerHTML = direction === 'forward' ? ICONS.forward : ICONS.backward;
@@ -443,8 +530,7 @@ async function unlockVideo(triggerElement) {
443530
};
444531
bindGlobalPlayerListeners();
445532

446-
volumeSlider.value = String(initialVolume);
447-
vid.volume = initialVolume;
533+
applyVolume(initialVolume, { persist: false });
448534

449535
getLatestReleaseInfo().then((release) => {
450536
if (!release || !btnUpdate?.isConnected) return;
@@ -462,9 +548,17 @@ async function unlockVideo(triggerElement) {
462548
});
463549

464550
volumeSlider.addEventListener('input', () => {
465-
const volume = parseFloat(volumeSlider.value);
466-
vid.volume = volume;
467-
savePlayerSettings(playerSettingsKey, { volume });
551+
applyVolume(parseFloat(volumeSlider.value));
552+
});
553+
554+
volumeButton.addEventListener('click', () => {
555+
if (vid.volume <= 0 || vid.muted) {
556+
applyVolume(previousVolumeBeforeMute > 0 ? previousVolumeBeforeMute : 1);
557+
return;
558+
}
559+
560+
previousVolumeBeforeMute = vid.volume > 0 ? vid.volume : previousVolumeBeforeMute;
561+
applyVolume(0);
468562
});
469563

470564
chatController.connectVideo(vid);
@@ -481,6 +575,29 @@ async function unlockVideo(triggerElement) {
481575
btnPlay.innerHTML = ICONS.play;
482576
btnBig.classList.add('visible');
483577
});
578+
vid.addEventListener('playing', () => {
579+
hasStartedPlayback = true;
580+
setLoadingState(false);
581+
});
582+
vid.addEventListener('waiting', () => {
583+
if (!vid.paused) setLoadingState(true);
584+
});
585+
vid.addEventListener('seeking', () => {
586+
if (hasStartedPlayback) setLoadingState(true);
587+
});
588+
vid.addEventListener('seeked', () => {
589+
if (!vid.paused && vid.readyState >= 3) setLoadingState(false);
590+
});
591+
vid.addEventListener('canplay', () => {
592+
if (!vid.paused && hasStartedPlayback) setLoadingState(false);
593+
});
594+
vid.addEventListener('stalled', () => {
595+
if (!vid.paused) setLoadingState(true);
596+
});
597+
vid.addEventListener('loadeddata', () => {
598+
if (vid.paused && !hasStartedPlayback) setLoadingState(false);
599+
});
600+
vid.addEventListener('ended', () => setLoadingState(false));
484601
let lastSave = 0;
485602

486603
vid.addEventListener('timeupdate', () => {
@@ -508,7 +625,7 @@ async function unlockVideo(triggerElement) {
508625
const ratio = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
509626
const hoverTime = ratio * vid.duration;
510627

511-
trackTooltip.textContent = formatTime(hoverTime);
628+
trackTooltipTime.textContent = formatTime(hoverTime);
512629
trackTooltip.style.left = `${ratio * rect.width}px`;
513630
trackTooltip.classList.add('visible');
514631
});
@@ -532,7 +649,9 @@ async function unlockVideo(triggerElement) {
532649
activeHls = hls;
533650
hls.loadSource(finalUrl);
534651
hls.attachMedia(vid);
652+
hls.on(Hls.Events.MANIFEST_LOADING, () => setLoadingState(true, { immediate: true }));
535653
hls.on(Hls.Events.MANIFEST_PARSED, () => {
654+
setLoadingState(false);
536655
const setQuality = (level, label, optionRef = null, qualitySettings = null) => {
537656
hls.currentLevel = level;
538657
qualBtn.textContent = `${label} ▴`;
@@ -603,6 +722,7 @@ async function unlockVideo(triggerElement) {
603722
}
604723

605724
hls.on(Hls.Events.ERROR, (e, data) => {
725+
if (data.fatal) setLoadingState(false);
606726
if (data.fatal) {
607727
if (data.type === Hls.ErrorTypes.NETWORK_ERROR) hls.startLoad();
608728
else if (data.type === Hls.ErrorTypes.MEDIA_ERROR) hls.recoverMediaError();

0 commit comments

Comments
 (0)