feat: Refactored channel header into grouped control sections (Monitori…
- "src/frontend/src/pages/ChannelDetail.tsx" GSD-Task: S03/T02
This commit is contained in:
parent
49ac76c379
commit
cca396a7e8
1 changed files with 315 additions and 198 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import { useState, useEffect, useMemo, useCallback } from 'react';
|
||||
import { useState, useEffect, useMemo, useCallback, useRef } from 'react';
|
||||
import { useParams, useNavigate, Link } from 'react-router-dom';
|
||||
import {
|
||||
ArrowLeft,
|
||||
|
|
@ -7,6 +7,7 @@ import {
|
|||
CheckCircle,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
ChevronUp,
|
||||
Download,
|
||||
ExternalLink,
|
||||
Film,
|
||||
|
|
@ -17,7 +18,6 @@ import {
|
|||
Music,
|
||||
RefreshCw,
|
||||
Save,
|
||||
Search,
|
||||
Trash2,
|
||||
} from 'lucide-react';
|
||||
import { useChannel, useUpdateChannel, useDeleteChannel, useScanChannel, useSetMonitoringMode, useScanStatus } from '../api/hooks/useChannels';
|
||||
|
|
@ -151,6 +151,24 @@ export function ChannelDetail() {
|
|||
const [checkIntervalSaved, setCheckIntervalSaved] = useState(false);
|
||||
const { toast } = useToast();
|
||||
|
||||
// ── Collapsible header ──
|
||||
const headerRef = useRef<HTMLDivElement>(null);
|
||||
const [isHeaderCollapsed, setIsHeaderCollapsed] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const el = headerRef.current;
|
||||
if (!el) return;
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
// Collapse when the header is mostly out of view
|
||||
setIsHeaderCollapsed(!entry.isIntersecting);
|
||||
},
|
||||
{ threshold: 0, rootMargin: '-60px 0px 0px 0px' },
|
||||
);
|
||||
observer.observe(el);
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
// Sync local check interval from channel data
|
||||
useEffect(() => {
|
||||
if (channel?.checkInterval != null) {
|
||||
|
|
@ -774,248 +792,347 @@ export function ChannelDetail() {
|
|||
<ArrowLeft size={14} /> Back to Channels
|
||||
</Link>
|
||||
|
||||
{/* Channel header */}
|
||||
{/* Compact sticky bar — visible when full header scrolls out of view */}
|
||||
<div
|
||||
style={{
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
zIndex: 100,
|
||||
transform: isHeaderCollapsed ? 'translateY(0)' : 'translateY(-100%)',
|
||||
opacity: isHeaderCollapsed ? 1 : 0,
|
||||
pointerEvents: isHeaderCollapsed ? 'auto' : 'none',
|
||||
transition: 'transform 0.25s ease, opacity 0.2s ease',
|
||||
display: 'flex',
|
||||
gap: 'var(--space-5)',
|
||||
padding: 'var(--space-5)',
|
||||
alignItems: 'center',
|
||||
gap: 'var(--space-3)',
|
||||
padding: 'var(--space-2) var(--space-4)',
|
||||
backgroundColor: 'var(--bg-card)',
|
||||
borderRadius: 'var(--radius-xl)',
|
||||
border: '1px solid var(--border)',
|
||||
marginBottom: 'var(--space-6)',
|
||||
alignItems: 'flex-start',
|
||||
borderBottom: '1px solid var(--border)',
|
||||
boxShadow: isHeaderCollapsed ? '0 2px 8px rgba(0,0,0,0.15)' : 'none',
|
||||
flexWrap: 'wrap',
|
||||
}}
|
||||
aria-hidden={!isHeaderCollapsed}
|
||||
>
|
||||
{/* Avatar */}
|
||||
{/* Identity — compact */}
|
||||
<img
|
||||
src={
|
||||
channel.imageUrl ||
|
||||
`https://ui-avatars.com/api/?name=${encodeURIComponent(channel.name)}&background=242731&color=e1e2e6&size=80`
|
||||
`https://ui-avatars.com/api/?name=${encodeURIComponent(channel.name)}&background=242731&color=e1e2e6&size=32`
|
||||
}
|
||||
alt={`${channel.name} avatar`}
|
||||
alt=""
|
||||
style={{
|
||||
width: 80,
|
||||
height: 80,
|
||||
width: 28,
|
||||
height: 28,
|
||||
borderRadius: '50%',
|
||||
objectFit: 'cover',
|
||||
backgroundColor: 'var(--bg-hover)',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
<span style={{ fontWeight: 600, fontSize: 'var(--font-size-sm)', color: 'var(--text-primary)', whiteSpace: 'nowrap' }}>
|
||||
{channel.name}
|
||||
</span>
|
||||
<PlatformBadge platform={channel.platform} />
|
||||
|
||||
{/* Info */}
|
||||
<div style={{ flex: 1, minWidth: 200 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 'var(--space-3)', flexWrap: 'wrap' }}>
|
||||
<h1
|
||||
<div style={{ width: 1, height: 20, backgroundColor: 'var(--border)', flexShrink: 0 }} />
|
||||
|
||||
{/* Key actions — compact */}
|
||||
<select
|
||||
value={channel.monitoringMode}
|
||||
onChange={handleMonitoringModeChange}
|
||||
disabled={setMonitoringMode.isPending}
|
||||
aria-label="Monitoring mode"
|
||||
style={{ padding: '4px 8px', borderRadius: 'var(--radius-md)', fontSize: 'var(--font-size-xs)', minWidth: 110 }}
|
||||
>
|
||||
{MONITORING_MODE_OPTIONS.map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
<button
|
||||
onClick={handleScan}
|
||||
disabled={scanChannel.isPending || scanInProgress}
|
||||
title={scanInProgress ? 'Scan in progress…' : 'Refresh & Scan'}
|
||||
className="btn btn-ghost"
|
||||
style={{ padding: '4px 10px', fontSize: 'var(--font-size-xs)', opacity: (scanChannel.isPending || scanInProgress) ? 0.6 : 1 }}
|
||||
>
|
||||
{(scanChannel.isPending || scanInProgress) ? (
|
||||
<Loader size={12} style={{ animation: 'spin 1s linear infinite' }} />
|
||||
) : (
|
||||
<RefreshCw size={12} />
|
||||
)}
|
||||
{scanInProgress ? 'Scanning…' : 'Scan'}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleCollect}
|
||||
disabled={collectMonitored.isPending}
|
||||
title="Collect Monitored"
|
||||
className="btn btn-ghost"
|
||||
style={{ padding: '4px 10px', fontSize: 'var(--font-size-xs)', opacity: collectMonitored.isPending ? 0.6 : 1 }}
|
||||
>
|
||||
{collectMonitored.isPending ? (
|
||||
<Loader size={12} style={{ animation: 'spin 1s linear infinite' }} />
|
||||
) : (
|
||||
<Download size={12} />
|
||||
)}
|
||||
Collect
|
||||
</button>
|
||||
|
||||
<div style={{ flex: 1 }} />
|
||||
|
||||
{/* Scroll-to-top to reveal full header */}
|
||||
<button
|
||||
onClick={() => headerRef.current?.scrollIntoView({ behavior: 'smooth' })}
|
||||
title="Show full header"
|
||||
aria-label="Expand header"
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: 28,
|
||||
height: 28,
|
||||
borderRadius: 'var(--radius-sm)',
|
||||
color: 'var(--text-muted)',
|
||||
cursor: 'pointer',
|
||||
transition: 'color var(--transition-fast)',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<ChevronUp size={16} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Full channel header — observed for collapse trigger */}
|
||||
<div
|
||||
ref={headerRef}
|
||||
style={{
|
||||
padding: 'var(--space-5)',
|
||||
backgroundColor: 'var(--bg-card)',
|
||||
borderRadius: 'var(--radius-xl)',
|
||||
border: '1px solid var(--border)',
|
||||
marginBottom: 'var(--space-6)',
|
||||
}}
|
||||
>
|
||||
{/* Identity row */}
|
||||
<div style={{ display: 'flex', gap: 'var(--space-4)', alignItems: 'center', marginBottom: 'var(--space-5)' }}>
|
||||
<img
|
||||
src={
|
||||
channel.imageUrl ||
|
||||
`https://ui-avatars.com/api/?name=${encodeURIComponent(channel.name)}&background=242731&color=e1e2e6&size=80`
|
||||
}
|
||||
alt={`${channel.name} avatar`}
|
||||
style={{
|
||||
width: 64,
|
||||
height: 64,
|
||||
borderRadius: '50%',
|
||||
objectFit: 'cover',
|
||||
backgroundColor: 'var(--bg-hover)',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
<div style={{ minWidth: 0, flex: 1 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 'var(--space-2)', flexWrap: 'wrap' }}>
|
||||
<h1 style={{ fontSize: 'var(--font-size-xl)', fontWeight: 600, color: 'var(--text-primary)', margin: 0 }}>
|
||||
{channel.name}
|
||||
</h1>
|
||||
<PlatformBadge platform={channel.platform} />
|
||||
</div>
|
||||
<a
|
||||
href={channel.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{
|
||||
fontSize: 'var(--font-size-2xl)',
|
||||
fontWeight: 600,
|
||||
color: 'var(--text-primary)',
|
||||
margin: 0,
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: 'var(--space-1)',
|
||||
color: 'var(--text-secondary)',
|
||||
fontSize: 'var(--font-size-sm)',
|
||||
marginTop: 2,
|
||||
maxWidth: '100%',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
title={channel.url}
|
||||
>
|
||||
{channel.name}
|
||||
</h1>
|
||||
<PlatformBadge platform={channel.platform} />
|
||||
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{channel.url}</span>
|
||||
<ExternalLink size={12} style={{ flexShrink: 0 }} />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Control groups */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'flex-end',
|
||||
gap: 'var(--space-5)',
|
||||
flexWrap: 'wrap',
|
||||
paddingTop: 'var(--space-4)',
|
||||
borderTop: '1px solid var(--border)',
|
||||
}}
|
||||
>
|
||||
{/* Monitoring group */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-1)' }}>
|
||||
<span style={{ fontSize: 'var(--font-size-xs)', color: 'var(--text-muted)', fontWeight: 500, textTransform: 'uppercase', letterSpacing: '0.04em' }}>
|
||||
Monitoring
|
||||
</span>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 'var(--space-2)' }}>
|
||||
<select
|
||||
value={channel.monitoringMode}
|
||||
onChange={handleMonitoringModeChange}
|
||||
disabled={setMonitoringMode.isPending}
|
||||
aria-label="Monitoring mode"
|
||||
style={{ padding: 'var(--space-2) var(--space-3)', borderRadius: 'var(--radius-md)', fontSize: 'var(--font-size-sm)', minWidth: 130 }}
|
||||
>
|
||||
{MONITORING_MODE_OPTIONS.map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
||||
))}
|
||||
</select>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 'var(--space-1)' }}>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
value={localCheckInterval}
|
||||
onChange={(e) => setLocalCheckInterval(e.target.value === '' ? '' : Number(e.target.value))}
|
||||
aria-label="Check interval in minutes"
|
||||
title="Check interval (minutes)"
|
||||
style={{
|
||||
width: 56,
|
||||
padding: 'var(--space-2)',
|
||||
borderRadius: 'var(--radius-md)',
|
||||
border: '1px solid var(--border)',
|
||||
backgroundColor: 'var(--bg-main)',
|
||||
color: 'var(--text-primary)',
|
||||
fontSize: 'var(--font-size-sm)',
|
||||
}}
|
||||
/>
|
||||
<span style={{ color: 'var(--text-muted)', fontSize: 'var(--font-size-xs)', whiteSpace: 'nowrap' }}>min</span>
|
||||
<button
|
||||
onClick={handleCheckIntervalSave}
|
||||
disabled={
|
||||
localCheckInterval === '' ||
|
||||
Number(localCheckInterval) < 1 ||
|
||||
Number(localCheckInterval) === channel.checkInterval ||
|
||||
updateChannel.isPending
|
||||
}
|
||||
title={checkIntervalSaved ? 'Saved!' : 'Save check interval'}
|
||||
aria-label="Save check interval"
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: 28,
|
||||
height: 28,
|
||||
borderRadius: 'var(--radius-sm)',
|
||||
color: checkIntervalSaved
|
||||
? 'var(--success)'
|
||||
: localCheckInterval !== '' && Number(localCheckInterval) !== channel.checkInterval
|
||||
? 'var(--accent)'
|
||||
: 'var(--text-muted)',
|
||||
cursor:
|
||||
localCheckInterval === '' ||
|
||||
Number(localCheckInterval) === channel.checkInterval ||
|
||||
updateChannel.isPending
|
||||
? 'not-allowed'
|
||||
: 'pointer',
|
||||
opacity:
|
||||
localCheckInterval === '' ||
|
||||
Number(localCheckInterval) === channel.checkInterval
|
||||
? 0.4
|
||||
: 1,
|
||||
transition: 'color var(--transition-fast), opacity var(--transition-fast)',
|
||||
}}
|
||||
>
|
||||
{updateChannel.isPending ? (
|
||||
<Loader size={14} style={{ animation: 'spin 1s linear infinite' }} />
|
||||
) : checkIntervalSaved ? (
|
||||
<CheckCircle size={14} />
|
||||
) : (
|
||||
<Save size={14} />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a
|
||||
href={channel.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: 'var(--space-1)',
|
||||
color: 'var(--text-secondary)',
|
||||
fontSize: 'var(--font-size-sm)',
|
||||
marginTop: 'var(--space-1)',
|
||||
maxWidth: '100%',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
title={channel.url}
|
||||
>
|
||||
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
{channel.url}
|
||||
{/* Format group */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-1)' }}>
|
||||
<span style={{ fontSize: 'var(--font-size-xs)', color: 'var(--text-muted)', fontWeight: 500, textTransform: 'uppercase', letterSpacing: '0.04em' }}>
|
||||
Format
|
||||
</span>
|
||||
<ExternalLink size={12} style={{ flexShrink: 0 }} />
|
||||
</a>
|
||||
|
||||
{/* Actions row */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 'var(--space-3)',
|
||||
marginTop: 'var(--space-4)',
|
||||
flexWrap: 'wrap',
|
||||
}}
|
||||
>
|
||||
{/* Monitoring mode dropdown */}
|
||||
<select
|
||||
value={channel.monitoringMode}
|
||||
onChange={handleMonitoringModeChange}
|
||||
disabled={setMonitoringMode.isPending}
|
||||
aria-label="Monitoring mode"
|
||||
style={{
|
||||
padding: 'var(--space-2) var(--space-3)',
|
||||
borderRadius: 'var(--radius-md)',
|
||||
fontSize: 'var(--font-size-sm)',
|
||||
minWidth: 140,
|
||||
}}
|
||||
>
|
||||
{MONITORING_MODE_OPTIONS.map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
{/* Format profile selector */}
|
||||
<select
|
||||
value={channel.formatProfileId ?? ''}
|
||||
onChange={handleFormatProfileChange}
|
||||
aria-label="Format profile"
|
||||
style={{
|
||||
padding: 'var(--space-2) var(--space-3)',
|
||||
borderRadius: 'var(--radius-md)',
|
||||
fontSize: 'var(--font-size-sm)',
|
||||
minWidth: 140,
|
||||
}}
|
||||
style={{ padding: 'var(--space-2) var(--space-3)', borderRadius: 'var(--radius-md)', fontSize: 'var(--font-size-sm)', minWidth: 140 }}
|
||||
>
|
||||
<option value="">Default Profile</option>
|
||||
{formatProfiles?.map((fp) => (
|
||||
<option key={fp.id} value={fp.id}>
|
||||
{fp.name}
|
||||
</option>
|
||||
<option key={fp.id} value={fp.id}>{fp.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Per-channel check interval */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 'var(--space-1)' }}>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
value={localCheckInterval}
|
||||
onChange={(e) => setLocalCheckInterval(e.target.value === '' ? '' : Number(e.target.value))}
|
||||
aria-label="Check interval in minutes"
|
||||
title="Check interval (minutes)"
|
||||
style={{
|
||||
width: 64,
|
||||
padding: 'var(--space-2) var(--space-2)',
|
||||
borderRadius: 'var(--radius-md)',
|
||||
border: '1px solid var(--border)',
|
||||
backgroundColor: 'var(--bg-main)',
|
||||
color: 'var(--text-primary)',
|
||||
fontSize: 'var(--font-size-sm)',
|
||||
}}
|
||||
/>
|
||||
<span style={{ color: 'var(--text-muted)', fontSize: 'var(--font-size-xs)', whiteSpace: 'nowrap' }}>min</span>
|
||||
{/* Actions group */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-1)' }}>
|
||||
<span style={{ fontSize: 'var(--font-size-xs)', color: 'var(--text-muted)', fontWeight: 500, textTransform: 'uppercase', letterSpacing: '0.04em' }}>
|
||||
Actions
|
||||
</span>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 'var(--space-2)' }}>
|
||||
<button
|
||||
onClick={handleCheckIntervalSave}
|
||||
disabled={
|
||||
localCheckInterval === '' ||
|
||||
Number(localCheckInterval) < 1 ||
|
||||
Number(localCheckInterval) === channel.checkInterval ||
|
||||
updateChannel.isPending
|
||||
}
|
||||
title={checkIntervalSaved ? 'Saved!' : 'Save check interval'}
|
||||
aria-label="Save check interval"
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: 28,
|
||||
height: 28,
|
||||
borderRadius: 'var(--radius-sm)',
|
||||
color: checkIntervalSaved
|
||||
? 'var(--success)'
|
||||
: localCheckInterval !== '' && Number(localCheckInterval) !== channel.checkInterval
|
||||
? 'var(--accent)'
|
||||
: 'var(--text-muted)',
|
||||
cursor:
|
||||
localCheckInterval === '' ||
|
||||
Number(localCheckInterval) === channel.checkInterval ||
|
||||
updateChannel.isPending
|
||||
? 'not-allowed'
|
||||
: 'pointer',
|
||||
opacity:
|
||||
localCheckInterval === '' ||
|
||||
Number(localCheckInterval) === channel.checkInterval
|
||||
? 0.4
|
||||
: 1,
|
||||
transition: 'color var(--transition-fast), opacity var(--transition-fast)',
|
||||
}}
|
||||
>
|
||||
{updateChannel.isPending ? (
|
||||
<Loader size={14} style={{ animation: 'spin 1s linear infinite' }} />
|
||||
) : checkIntervalSaved ? (
|
||||
<CheckCircle size={14} />
|
||||
) : (
|
||||
<Save size={14} />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Refresh & Scan button */}
|
||||
<button
|
||||
onClick={handleScan}
|
||||
disabled={scanChannel.isPending || scanInProgress}
|
||||
title={scanInProgress ? 'Scan in progress…' : 'Refresh & Scan'}
|
||||
className="btn btn-ghost"
|
||||
style={{ opacity: (scanChannel.isPending || scanInProgress) ? 0.6 : 1 }}
|
||||
>
|
||||
{(scanChannel.isPending || scanInProgress) ? (
|
||||
<Loader size={14} style={{ animation: 'spin 1s linear infinite' }} />
|
||||
) : (
|
||||
<RefreshCw size={14} />
|
||||
)}
|
||||
{scanInProgress ? 'Scanning…' : scanChannel.isPending ? 'Scanning…' : 'Refresh & Scan'}
|
||||
</button>
|
||||
|
||||
{/* Collect Monitored button */}
|
||||
<button
|
||||
onClick={handleCollect}
|
||||
disabled={collectMonitored.isPending}
|
||||
title="Collect Monitored"
|
||||
className="btn btn-ghost"
|
||||
style={{ opacity: collectMonitored.isPending ? 0.6 : 1 }}
|
||||
>
|
||||
{collectMonitored.isPending ? (
|
||||
<Loader size={14} style={{ animation: 'spin 1s linear infinite' }} />
|
||||
) : (
|
||||
<Search size={14} />
|
||||
)}
|
||||
{collectMonitored.isPending ? 'Collecting...' : 'Collect Monitored'}
|
||||
</button>
|
||||
|
||||
{/* Refresh Playlists button (YouTube only) */}
|
||||
{isYouTube ? (
|
||||
<button
|
||||
onClick={handleRefreshPlaylists}
|
||||
disabled={refreshPlaylists.isPending}
|
||||
title="Refresh Playlists"
|
||||
onClick={handleScan}
|
||||
disabled={scanChannel.isPending || scanInProgress}
|
||||
title={scanInProgress ? 'Scan in progress…' : 'Refresh & Scan'}
|
||||
className="btn btn-ghost"
|
||||
style={{ opacity: refreshPlaylists.isPending ? 0.6 : 1 }}
|
||||
style={{ opacity: (scanChannel.isPending || scanInProgress) ? 0.6 : 1 }}
|
||||
>
|
||||
{refreshPlaylists.isPending ? (
|
||||
{(scanChannel.isPending || scanInProgress) ? (
|
||||
<Loader size={14} style={{ animation: 'spin 1s linear infinite' }} />
|
||||
) : (
|
||||
<ListMusic size={14} />
|
||||
<RefreshCw size={14} />
|
||||
)}
|
||||
{refreshPlaylists.isPending ? 'Refreshing...' : 'Playlists'}
|
||||
{scanInProgress ? 'Scanning…' : scanChannel.isPending ? 'Scanning…' : 'Scan'}
|
||||
</button>
|
||||
) : null}
|
||||
<button
|
||||
onClick={handleCollect}
|
||||
disabled={collectMonitored.isPending}
|
||||
title="Collect Monitored"
|
||||
className="btn btn-ghost"
|
||||
style={{ opacity: collectMonitored.isPending ? 0.6 : 1 }}
|
||||
>
|
||||
{collectMonitored.isPending ? (
|
||||
<Loader size={14} style={{ animation: 'spin 1s linear infinite' }} />
|
||||
) : (
|
||||
<Download size={14} />
|
||||
)}
|
||||
{collectMonitored.isPending ? 'Collecting…' : 'Collect'}
|
||||
</button>
|
||||
{isYouTube ? (
|
||||
<button
|
||||
onClick={handleRefreshPlaylists}
|
||||
disabled={refreshPlaylists.isPending}
|
||||
title="Refresh Playlists"
|
||||
className="btn btn-ghost"
|
||||
style={{ opacity: refreshPlaylists.isPending ? 0.6 : 1 }}
|
||||
>
|
||||
{refreshPlaylists.isPending ? (
|
||||
<Loader size={14} style={{ animation: 'spin 1s linear infinite' }} />
|
||||
) : (
|
||||
<ListMusic size={14} />
|
||||
)}
|
||||
{refreshPlaylists.isPending ? 'Refreshing…' : 'Playlists'}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Delete button */}
|
||||
{/* Spacer + Delete */}
|
||||
<div style={{ marginLeft: 'auto' }}>
|
||||
<button
|
||||
onClick={() => setShowDeleteConfirm(true)}
|
||||
className="btn btn-danger"
|
||||
style={{ marginLeft: 'auto' }}
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
Delete
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue