Skip to content

Commit b467471

Browse files
feat: add copy stream URL menu and desktop sidebar collapse (closes #43, closes #45)
1 parent 196b952 commit b467471

File tree

7 files changed

+327
-5
lines changed

7 files changed

+327
-5
lines changed

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "nodecast-tv",
3-
"version": "1.4.4",
3+
"version": "1.5.0",
44
"description": "Modern IPTV Player",
55
"license": "GPL-3.0-only",
66
"main": "server/index.js",

public/css/main.css

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,93 @@ h4 {
360360
border-bottom: 1px solid var(--color-border);
361361
}
362362

363+
.sidebar-header-row {
364+
display: flex;
365+
align-items: center;
366+
gap: var(--space-sm);
367+
}
368+
369+
.sidebar-header-row .search-wrapper {
370+
flex: 1;
371+
}
372+
373+
.sidebar-collapse-btn {
374+
display: none;
375+
/* Hidden on mobile */
376+
flex-shrink: 0;
377+
padding: var(--space-xs);
378+
}
379+
380+
.sidebar-collapse-btn .icon {
381+
transition: transform 0.2s ease;
382+
}
383+
384+
/* Desktop collapse button visibility */
385+
@media (min-width: 769px) {
386+
.sidebar-collapse-btn {
387+
display: flex;
388+
}
389+
}
390+
391+
/* Collapsed sidebar state (desktop only) */
392+
.channel-sidebar.collapsed {
393+
width: 0;
394+
min-width: 0;
395+
overflow: hidden;
396+
border-right: none;
397+
}
398+
399+
.channel-sidebar.collapsed .sidebar-header,
400+
.channel-sidebar.collapsed .channel-list {
401+
opacity: 0;
402+
visibility: hidden;
403+
}
404+
405+
/* Expand button when sidebar is collapsed */
406+
.sidebar-expand-btn {
407+
display: none;
408+
position: absolute;
409+
left: 0;
410+
top: calc(var(--navbar-height) + var(--space-md));
411+
z-index: 50;
412+
background: var(--glass-bg);
413+
backdrop-filter: blur(12px);
414+
border: 1px solid var(--glass-border);
415+
border-left: none;
416+
border-radius: 0 var(--radius-md) var(--radius-md) 0;
417+
padding: var(--space-sm);
418+
cursor: pointer;
419+
color: var(--color-text-primary);
420+
transition: opacity 0.3s ease, background var(--transition-fast);
421+
opacity: 0;
422+
pointer-events: none;
423+
}
424+
425+
.sidebar-expand-btn:hover {
426+
background: var(--color-bg-hover);
427+
}
428+
429+
.sidebar-expand-btn .icon {
430+
width: 20px;
431+
height: 20px;
432+
}
433+
434+
/* Show expand button when sidebar is collapsed */
435+
.home-layout.sidebar-collapsed .sidebar-expand-btn {
436+
display: flex;
437+
}
438+
439+
/* Fade in expand button with player controls */
440+
.home-layout.sidebar-collapsed .sidebar-expand-btn.visible {
441+
opacity: 1;
442+
pointer-events: auto;
443+
}
444+
445+
/* Smooth transitions */
446+
.channel-sidebar {
447+
transition: width 0.2s ease, min-width 0.2s ease;
448+
}
449+
363450
.search-input {
364451
width: 100%;
365452
padding: var(--space-sm) var(--space-md);
@@ -3888,4 +3975,47 @@ kbd {
38883975
.next-info h4 {
38893976
max-width: none;
38903977
}
3978+
}
3979+
3980+
/* Player Overflow Menu */
3981+
.player-overflow-wrapper {
3982+
position: relative;
3983+
}
3984+
3985+
.player-overflow-menu {
3986+
position: absolute;
3987+
bottom: 100%;
3988+
right: 0;
3989+
margin-bottom: var(--space-sm);
3990+
background: var(--glass-bg);
3991+
backdrop-filter: blur(12px);
3992+
border: 1px solid var(--glass-border);
3993+
border-radius: var(--radius-md);
3994+
min-width: 180px;
3995+
overflow: hidden;
3996+
z-index: 100;
3997+
}
3998+
3999+
.overflow-menu-item {
4000+
display: flex;
4001+
align-items: center;
4002+
gap: var(--space-sm);
4003+
width: 100%;
4004+
padding: var(--space-sm) var(--space-md);
4005+
background: transparent;
4006+
border: none;
4007+
color: var(--color-text-primary);
4008+
font-size: 0.875rem;
4009+
cursor: pointer;
4010+
transition: background var(--transition-fast);
4011+
}
4012+
4013+
.overflow-menu-item:hover {
4014+
background: var(--color-bg-hover);
4015+
}
4016+
4017+
.overflow-menu-item .icon {
4018+
width: 18px;
4019+
height: 18px;
4020+
flex-shrink: 0;
38914021
}

public/index.html

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,12 +97,27 @@
9797
<!-- Overlay for drawer -->
9898
<div class="channel-sidebar-overlay" id="channel-sidebar-overlay"></div>
9999

100+
<!-- Expand button (visible when sidebar collapsed on desktop) -->
101+
<button class="sidebar-expand-btn" id="sidebar-expand-btn" title="Expand sidebar">
102+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="icon">
103+
<path d="M8.59 16.59L10 18l6-6-6-6-1.41 1.41L13.17 12z" />
104+
</svg>
105+
</button>
106+
100107
<!-- Channel Sidebar -->
101108
<aside class="channel-sidebar" id="channel-sidebar">
102109
<div class="sidebar-header">
103-
<div class="search-wrapper">
104-
<input type="text" id="channel-search" placeholder="Search channels..." class="search-input">
105-
<button type="button" class="search-clear" title="Clear search">&times;</button>
110+
<div class="sidebar-header-row">
111+
<div class="search-wrapper">
112+
<input type="text" id="channel-search" placeholder="Search channels..." class="search-input">
113+
<button type="button" class="search-clear" title="Clear search">&times;</button>
114+
</div>
115+
<button id="sidebar-collapse-btn" class="btn btn-sm btn-ghost sidebar-collapse-btn"
116+
title="Collapse sidebar">
117+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="icon">
118+
<path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z" />
119+
</svg>
120+
</button>
106121
</div>
107122
<div class="sidebar-controls">
108123
<select id="source-select" class="source-select">
@@ -201,6 +216,24 @@ <h2 class="watch-title-overlay" id="player-channel-name"></h2>
201216
<path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" />
202217
</svg>
203218
</button>
219+
<!-- Overflow Menu -->
220+
<div class="player-overflow-wrapper">
221+
<button class="watch-btn" id="btn-overflow" title="More options">
222+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="icon">
223+
<path
224+
d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z" />
225+
</svg>
226+
</button>
227+
<div class="player-overflow-menu hidden" id="player-overflow-menu">
228+
<button class="overflow-menu-item" id="btn-copy-url">
229+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="icon">
230+
<path
231+
d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z" />
232+
</svg>
233+
Copy Stream URL
234+
</button>
235+
</div>
236+
</div>
204237
</div>
205238
</div>
206239
</div>
@@ -707,6 +740,24 @@ <h3 class="watch-title" id="watch-title"></h3>
707740
<path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" />
708741
</svg>
709742
</button>
743+
<!-- Overflow Menu -->
744+
<div class="player-overflow-wrapper">
745+
<button class="watch-btn" id="watch-overflow" title="More options">
746+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="icon">
747+
<path
748+
d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z" />
749+
</svg>
750+
</button>
751+
<div class="player-overflow-menu hidden" id="watch-overflow-menu">
752+
<button class="overflow-menu-item" id="watch-copy-url">
753+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="icon">
754+
<path
755+
d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z" />
756+
</svg>
757+
Copy Stream URL
758+
</button>
759+
</div>
760+
</div>
710761
</div>
711762
</div>
712763

public/js/app.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,29 @@ class App {
8282
});
8383
}
8484

85+
// Desktop sidebar collapse toggle
86+
const sidebarCollapseBtn = document.getElementById('sidebar-collapse-btn');
87+
const sidebarExpandBtn = document.getElementById('sidebar-expand-btn');
88+
const homeLayout = document.querySelector('.home-layout');
89+
90+
const toggleSidebarCollapse = () => {
91+
channelSidebar?.classList.toggle('collapsed');
92+
homeLayout?.classList.toggle('sidebar-collapsed');
93+
94+
// Persist preference
95+
const isCollapsed = channelSidebar?.classList.contains('collapsed');
96+
localStorage.setItem('sidebarCollapsed', isCollapsed ? 'true' : 'false');
97+
};
98+
99+
sidebarCollapseBtn?.addEventListener('click', toggleSidebarCollapse);
100+
sidebarExpandBtn?.addEventListener('click', toggleSidebarCollapse);
101+
102+
// Restore sidebar state from localStorage
103+
if (localStorage.getItem('sidebarCollapsed') === 'true') {
104+
channelSidebar?.classList.add('collapsed');
105+
homeLayout?.classList.add('sidebar-collapsed');
106+
}
107+
85108
// Navigation handling
86109
document.querySelectorAll('.nav-link').forEach(link => {
87110
link.addEventListener('click', (e) => {

public/js/components/VideoPlayer.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,21 +275,49 @@ class VideoPlayer {
275275
this.togglePictureInPicture();
276276
});
277277

278+
// Overflow Menu
279+
const btnOverflow = document.getElementById('btn-overflow');
280+
const overflowMenu = document.getElementById('player-overflow-menu');
281+
282+
btnOverflow?.addEventListener('click', (e) => {
283+
e.stopPropagation();
284+
overflowMenu?.classList.toggle('hidden');
285+
});
286+
287+
// Copy Stream URL
288+
const btnCopyUrl = document.getElementById('btn-copy-url');
289+
btnCopyUrl?.addEventListener('click', (e) => {
290+
e.stopPropagation();
291+
this.copyStreamUrl();
292+
overflowMenu?.classList.add('hidden');
293+
});
294+
295+
// Close overflow menu when clicking outside
296+
document.addEventListener('click', (e) => {
297+
if (overflowMenu && !overflowMenu.classList.contains('hidden') &&
298+
!overflowMenu.contains(e.target) && e.target !== btnOverflow) {
299+
overflowMenu.classList.add('hidden');
300+
}
301+
});
302+
278303
this.container.addEventListener('dblclick', () => this.toggleFullscreen());
279304

280305
// Overlay Auto-hide Logic
281306
let overlayTimeout;
307+
const sidebarExpandBtn = document.getElementById('sidebar-expand-btn');
282308

283309
const showOverlay = () => {
284310
this.controlsOverlay.classList.remove('hidden');
285311
this.container.style.cursor = 'default';
312+
sidebarExpandBtn?.classList.add('visible');
286313
resetOverlayTimer();
287314
};
288315

289316
const hideOverlay = () => {
290317
if (!this.video.paused) {
291318
this.controlsOverlay.classList.add('hidden');
292319
this.container.style.cursor = 'none';
320+
sidebarExpandBtn?.classList.remove('visible');
293321
}
294322
};
295323

@@ -351,6 +379,37 @@ class VideoPlayer {
351379
}
352380
}
353381

382+
/**
383+
* Copy current stream URL to clipboard
384+
*/
385+
copyStreamUrl() {
386+
if (!this.currentUrl) {
387+
console.warn('[Player] No stream URL to copy');
388+
return;
389+
}
390+
391+
let streamUrl = this.currentUrl;
392+
393+
// If it's a relative URL, make it absolute
394+
if (streamUrl.startsWith('/')) {
395+
streamUrl = window.location.origin + streamUrl;
396+
}
397+
398+
navigator.clipboard.writeText(streamUrl).then(() => {
399+
// Show brief feedback
400+
const btn = document.getElementById('btn-copy-url');
401+
if (btn) {
402+
const originalText = btn.textContent;
403+
btn.textContent = '✓ Copied!';
404+
setTimeout(() => {
405+
btn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="icon"><path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg> Copy Stream URL`;
406+
}, 1500);
407+
}
408+
console.log('[Player] Stream URL copied:', streamUrl);
409+
}).catch(() => {
410+
prompt('Copy this URL:', streamUrl);
411+
});
412+
}
354413

355414

356415
/**

0 commit comments

Comments
 (0)