@supasayan/ytm 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +44 -0
- package/client/index.html +13 -0
- package/client/package-lock.json +1003 -0
- package/client/package.json +22 -0
- package/client/public/favicon.svg +1 -0
- package/client/public/icons.svg +24 -0
- package/client/src/App.jsx +210 -0
- package/client/src/assets/hero.png +0 -0
- package/client/src/assets/typescript.svg +1 -0
- package/client/src/assets/vite.svg +1 -0
- package/client/src/components/Download/DownloadItem.css +246 -0
- package/client/src/components/Download/DownloadItem.jsx +108 -0
- package/client/src/components/Download/FlyingThumbnail.css +33 -0
- package/client/src/components/Download/FlyingThumbnail.jsx +48 -0
- package/client/src/components/Layout/Header.css +91 -0
- package/client/src/components/Layout/Header.jsx +49 -0
- package/client/src/components/Layout/SystemCheckScreen.css +250 -0
- package/client/src/components/Layout/SystemCheckScreen.jsx +110 -0
- package/client/src/components/Player/AudioPlayer.css +280 -0
- package/client/src/components/Player/AudioPlayer.jsx +154 -0
- package/client/src/components/Playlist/PlaylistInput.css +71 -0
- package/client/src/components/Playlist/PlaylistInput.jsx +44 -0
- package/client/src/components/Playlist/PlaylistView.css +150 -0
- package/client/src/components/Playlist/PlaylistView.jsx +104 -0
- package/client/src/components/Search/SearchBar.css +163 -0
- package/client/src/components/Search/SearchBar.jsx +127 -0
- package/client/src/components/Search/SearchResults.css +42 -0
- package/client/src/components/Search/SearchResults.jsx +64 -0
- package/client/src/components/Search/SongCard.css +296 -0
- package/client/src/components/Search/SongCard.jsx +204 -0
- package/client/src/components/Settings/SettingsPanel.css +345 -0
- package/client/src/components/Settings/SettingsPanel.jsx +174 -0
- package/client/src/hooks/useDownload.js +107 -0
- package/client/src/hooks/usePlayer.js +188 -0
- package/client/src/hooks/usePlaylist.js +52 -0
- package/client/src/hooks/useSearch.js +34 -0
- package/client/src/index.css +245 -0
- package/client/src/main.jsx +10 -0
- package/client/src/pages/DownloadsPage.css +37 -0
- package/client/src/pages/DownloadsPage.jsx +80 -0
- package/client/src/pages/PlaylistPage.jsx +46 -0
- package/client/src/pages/SearchPage.jsx +63 -0
- package/client/tsconfig.json +24 -0
- package/client/vite.config.js +12 -0
- package/main.js +79 -0
- package/package.json +48 -0
- package/server/index.js +54 -0
- package/server/package-lock.json +1428 -0
- package/server/package.json +14 -0
- package/server/routes/browse.js +23 -0
- package/server/routes/download.js +339 -0
- package/server/routes/playLocal.js +50 -0
- package/server/routes/playlist.js +28 -0
- package/server/routes/search.js +21 -0
- package/server/routes/stream.js +53 -0
- package/server/services/downloadService.js +143 -0
- package/server/services/ytmusicService.js +62 -0
- package/server/utils/dependencyChecker.js +174 -0
- package/server/utils/paths.js +11 -0
- package/server/utils/sanitize.js +9 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { Music, CheckCircle2, AlertCircle, Loader2, Clock } from 'lucide-react';
|
|
2
|
+
import './DownloadItem.css';
|
|
3
|
+
|
|
4
|
+
export default function DownloadItem({ download, onPlay, isCurrentlyPlaying, onSearchAlternative }) {
|
|
5
|
+
const { title, artist, album, thumbnail, status, percent, speed, eta, error } = download;
|
|
6
|
+
|
|
7
|
+
const isClickable = status === 'completed';
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<div
|
|
11
|
+
className={`download-item liquid-glass ${status} ${isClickable ? 'clickable' : ''} ${isCurrentlyPlaying ? 'playing' : ''}`}
|
|
12
|
+
onClick={(e) => {
|
|
13
|
+
if (isClickable && onPlay) {
|
|
14
|
+
const rect = e.currentTarget.getBoundingClientRect();
|
|
15
|
+
// Adjust starting rect to represent a smaller thumbnail-like size
|
|
16
|
+
const startRect = {
|
|
17
|
+
top: rect.top + 8,
|
|
18
|
+
left: rect.left + 8,
|
|
19
|
+
width: 56,
|
|
20
|
+
height: 56
|
|
21
|
+
};
|
|
22
|
+
onPlay(download, startRect);
|
|
23
|
+
}
|
|
24
|
+
}}
|
|
25
|
+
>
|
|
26
|
+
<div className="dl-thumbnail">
|
|
27
|
+
{thumbnail ? (
|
|
28
|
+
<>
|
|
29
|
+
<img src={thumbnail} alt={title} />
|
|
30
|
+
{isCurrentlyPlaying && (
|
|
31
|
+
<div className="equalizer-overlay">
|
|
32
|
+
<div className="bar bar1"></div>
|
|
33
|
+
<div className="bar bar2"></div>
|
|
34
|
+
<div className="bar bar3"></div>
|
|
35
|
+
</div>
|
|
36
|
+
)}
|
|
37
|
+
</>
|
|
38
|
+
) : (
|
|
39
|
+
<div className="placeholder-thumb">
|
|
40
|
+
<Music size={20} className="placeholder-icon" />
|
|
41
|
+
</div>
|
|
42
|
+
)}
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<div className="dl-info">
|
|
46
|
+
<div className="dl-title-row">
|
|
47
|
+
<span className="dl-title" title={title}>
|
|
48
|
+
<span className="dl-title-text">{title}</span>
|
|
49
|
+
{isCurrentlyPlaying && (
|
|
50
|
+
<span className="equalizer-inline">
|
|
51
|
+
<span className="bar bar1"></span>
|
|
52
|
+
<span className="bar bar2"></span>
|
|
53
|
+
<span className="bar bar3"></span>
|
|
54
|
+
</span>
|
|
55
|
+
)}
|
|
56
|
+
</span>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<span className="dl-meta">
|
|
60
|
+
{artist || 'Unknown Artist'} {album && `• ${album}`}
|
|
61
|
+
</span>
|
|
62
|
+
|
|
63
|
+
{(status === 'downloading' || status === 'queued') && (
|
|
64
|
+
<div className="dl-progress-container">
|
|
65
|
+
<div className="dl-progress-wrapper">
|
|
66
|
+
<div
|
|
67
|
+
className="dl-progress-bar"
|
|
68
|
+
style={{ width: percent || '0%' }}
|
|
69
|
+
/>
|
|
70
|
+
</div>
|
|
71
|
+
{status === 'downloading' && (
|
|
72
|
+
<span className="dl-stats">
|
|
73
|
+
{percent} • {speed} • ETA: {eta}
|
|
74
|
+
</span>
|
|
75
|
+
)}
|
|
76
|
+
{status === 'queued' && (
|
|
77
|
+
<span className="dl-stats">Queued...</span>
|
|
78
|
+
)}
|
|
79
|
+
</div>
|
|
80
|
+
)}
|
|
81
|
+
|
|
82
|
+
{status === 'error' && (
|
|
83
|
+
<div className="dl-error-container">
|
|
84
|
+
<span className="dl-error" title={error}>{error || 'Failed to download'}</span>
|
|
85
|
+
<button
|
|
86
|
+
className="dl-search-alternative-btn"
|
|
87
|
+
onClick={(e) => {
|
|
88
|
+
e.stopPropagation();
|
|
89
|
+
if (onSearchAlternative) {
|
|
90
|
+
onSearchAlternative(`${title} ${artist}`);
|
|
91
|
+
}
|
|
92
|
+
}}
|
|
93
|
+
>
|
|
94
|
+
Search Alternative
|
|
95
|
+
</button>
|
|
96
|
+
</div>
|
|
97
|
+
)}
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
<div className="dl-status-icon-wrapper">
|
|
101
|
+
{status === 'completed' && <CheckCircle2 size={20} className="status-icon completed" />}
|
|
102
|
+
{status === 'error' && <AlertCircle size={20} className="status-icon error" />}
|
|
103
|
+
{status === 'downloading' && <Loader2 size={20} className="status-icon spinner" />}
|
|
104
|
+
{status === 'queued' && <Clock size={20} className="status-icon queued" />}
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
.flying-thumbnail {
|
|
2
|
+
position: fixed;
|
|
3
|
+
z-index: 9999;
|
|
4
|
+
border-radius: var(--radius-sm);
|
|
5
|
+
overflow: hidden;
|
|
6
|
+
background: var(--bg-tertiary);
|
|
7
|
+
pointer-events: none;
|
|
8
|
+
box-shadow: var(--shadow-glow);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.flying-thumbnail img {
|
|
12
|
+
width: 100%;
|
|
13
|
+
height: 100%;
|
|
14
|
+
object-fit: cover;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.flying-thumbnail.animate {
|
|
18
|
+
animation: flyToTarget 0.6s cubic-bezier(0.4, 0, 0.2, 1) forwards;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@keyframes flyToTarget {
|
|
22
|
+
0% {
|
|
23
|
+
transform: translate(0, 0) scale(1);
|
|
24
|
+
opacity: 1;
|
|
25
|
+
}
|
|
26
|
+
70% {
|
|
27
|
+
opacity: 0.8;
|
|
28
|
+
}
|
|
29
|
+
100% {
|
|
30
|
+
transform: translate(var(--end-x), var(--end-y)) scale(0.4);
|
|
31
|
+
opacity: 0;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { createPortal } from 'react-dom';
|
|
3
|
+
import './FlyingThumbnail.css';
|
|
4
|
+
|
|
5
|
+
export default function FlyingThumbnail({ id, thumbnailSrc, startRect, endRect, onComplete }) {
|
|
6
|
+
const [isAnimating, setIsAnimating] = useState(false);
|
|
7
|
+
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
// Small delay to ensure DOM is ready and CSS transitions can catch the change
|
|
10
|
+
const raf = requestAnimationFrame(() => {
|
|
11
|
+
setIsAnimating(true);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
// Animation duration is 600ms
|
|
15
|
+
const timer = setTimeout(() => {
|
|
16
|
+
onComplete(id);
|
|
17
|
+
}, 600);
|
|
18
|
+
|
|
19
|
+
return () => {
|
|
20
|
+
cancelAnimationFrame(raf);
|
|
21
|
+
clearTimeout(timer);
|
|
22
|
+
};
|
|
23
|
+
}, [id, onComplete]);
|
|
24
|
+
|
|
25
|
+
// Initial position
|
|
26
|
+
const style = {
|
|
27
|
+
top: startRect.top,
|
|
28
|
+
left: startRect.left,
|
|
29
|
+
width: startRect.width || 40,
|
|
30
|
+
height: startRect.height || 40,
|
|
31
|
+
'--end-x': `${endRect.left - startRect.left}px`,
|
|
32
|
+
'--end-y': `${endRect.top - startRect.top}px`,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
return createPortal(
|
|
36
|
+
<div
|
|
37
|
+
className={`flying-thumbnail ${isAnimating ? 'animate' : ''}`}
|
|
38
|
+
style={style}
|
|
39
|
+
>
|
|
40
|
+
{thumbnailSrc ? (
|
|
41
|
+
<img src={thumbnailSrc} alt="fly" />
|
|
42
|
+
) : (
|
|
43
|
+
<div className="placeholder-thumb">🎵</div>
|
|
44
|
+
)}
|
|
45
|
+
</div>,
|
|
46
|
+
document.body
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
.app-header {
|
|
2
|
+
position: sticky;
|
|
3
|
+
top: 0;
|
|
4
|
+
z-index: 100;
|
|
5
|
+
background-color: var(--bg-primary);
|
|
6
|
+
border-bottom: 1px solid var(--glass-border);
|
|
7
|
+
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.3);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.header-content {
|
|
11
|
+
max-width: 1200px;
|
|
12
|
+
margin: 0 auto;
|
|
13
|
+
padding: 0 2rem;
|
|
14
|
+
height: 70px;
|
|
15
|
+
display: flex;
|
|
16
|
+
align-items: center;
|
|
17
|
+
justify-content: space-between;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.logo h2 {
|
|
21
|
+
font-size: 1.5rem;
|
|
22
|
+
font-weight: 700;
|
|
23
|
+
color: var(--text-primary);
|
|
24
|
+
margin: 0;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.logo span {
|
|
28
|
+
background: var(--accent-gradient);
|
|
29
|
+
-webkit-background-clip: text;
|
|
30
|
+
-webkit-text-fill-color: transparent;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.tab-nav {
|
|
34
|
+
display: flex;
|
|
35
|
+
gap: 1.5rem;
|
|
36
|
+
height: 100%;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.tab-btn {
|
|
40
|
+
display: flex;
|
|
41
|
+
align-items: center;
|
|
42
|
+
gap: 0.5rem;
|
|
43
|
+
height: 100%;
|
|
44
|
+
padding: 0 0.5rem;
|
|
45
|
+
font-size: 0.95rem;
|
|
46
|
+
font-weight: 500;
|
|
47
|
+
color: var(--text-secondary);
|
|
48
|
+
position: relative;
|
|
49
|
+
transition: color var(--transition-fast);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.tab-btn:hover {
|
|
53
|
+
color: var(--text-primary);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.tab-btn.active {
|
|
57
|
+
color: var(--accent-primary);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.tab-btn.active::after {
|
|
61
|
+
content: '';
|
|
62
|
+
position: absolute;
|
|
63
|
+
bottom: 0;
|
|
64
|
+
left: 0;
|
|
65
|
+
width: 100%;
|
|
66
|
+
height: 3px;
|
|
67
|
+
background: var(--accent-primary);
|
|
68
|
+
box-shadow: 0 -2px 10px var(--accent-primary);
|
|
69
|
+
border-radius: 3px 3px 0 0;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.tab-icon {
|
|
73
|
+
flex-shrink: 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.tab-badge {
|
|
77
|
+
position: absolute;
|
|
78
|
+
top: 15px;
|
|
79
|
+
right: -12px;
|
|
80
|
+
background: var(--accent-primary);
|
|
81
|
+
color: white;
|
|
82
|
+
min-width: 18px;
|
|
83
|
+
height: 18px;
|
|
84
|
+
border-radius: 50%;
|
|
85
|
+
font-size: 11px;
|
|
86
|
+
font-weight: 700;
|
|
87
|
+
display: flex;
|
|
88
|
+
align-items: center;
|
|
89
|
+
justify-content: center;
|
|
90
|
+
padding: 0 4px;
|
|
91
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Search, Music, Download, Settings } from 'lucide-react';
|
|
2
|
+
import './Header.css';
|
|
3
|
+
|
|
4
|
+
export default function Header({ activeTab, onTabChange, activeDownloadCount = 0, downloadsTabRef }) {
|
|
5
|
+
return (
|
|
6
|
+
<header className="app-header">
|
|
7
|
+
<div className="header-content">
|
|
8
|
+
<div className="logo">
|
|
9
|
+
<h2>YTM<span>Downloader</span></h2>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<nav className="tab-nav">
|
|
13
|
+
<button
|
|
14
|
+
className={`tab-btn ${activeTab === 'search' ? 'active' : ''}`}
|
|
15
|
+
onClick={() => onTabChange('search')}
|
|
16
|
+
>
|
|
17
|
+
<Search size={18} className="tab-icon" />
|
|
18
|
+
<span>Search</span>
|
|
19
|
+
</button>
|
|
20
|
+
<button
|
|
21
|
+
className={`tab-btn ${activeTab === 'playlist' ? 'active' : ''}`}
|
|
22
|
+
onClick={() => onTabChange('playlist')}
|
|
23
|
+
>
|
|
24
|
+
<Music size={18} className="tab-icon" />
|
|
25
|
+
<span>Playlist</span>
|
|
26
|
+
</button>
|
|
27
|
+
<button
|
|
28
|
+
ref={downloadsTabRef}
|
|
29
|
+
className={`tab-btn ${activeTab === 'downloads' ? 'active' : ''}`}
|
|
30
|
+
onClick={() => onTabChange('downloads')}
|
|
31
|
+
>
|
|
32
|
+
<Download size={18} className="tab-icon" />
|
|
33
|
+
<span>Downloads</span>
|
|
34
|
+
{activeDownloadCount > 0 && (
|
|
35
|
+
<span className="tab-badge">{activeDownloadCount}</span>
|
|
36
|
+
)}
|
|
37
|
+
</button>
|
|
38
|
+
<button
|
|
39
|
+
className={`tab-btn ${activeTab === 'settings' ? 'active' : ''}`}
|
|
40
|
+
onClick={() => onTabChange('settings')}
|
|
41
|
+
>
|
|
42
|
+
<Settings size={18} className="tab-icon" />
|
|
43
|
+
<span>Settings</span>
|
|
44
|
+
</button>
|
|
45
|
+
</nav>
|
|
46
|
+
</div>
|
|
47
|
+
</header>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
.system-check-container {
|
|
2
|
+
position: fixed;
|
|
3
|
+
top: 0;
|
|
4
|
+
left: 0;
|
|
5
|
+
right: 0;
|
|
6
|
+
bottom: 0;
|
|
7
|
+
background: radial-gradient(circle at top right, rgba(99, 102, 241, 0.15) 0%, rgba(15, 23, 42, 0.98) 80%);
|
|
8
|
+
display: flex;
|
|
9
|
+
align-items: center;
|
|
10
|
+
justify-content: center;
|
|
11
|
+
z-index: 9999;
|
|
12
|
+
padding: 2rem;
|
|
13
|
+
backdrop-filter: blur(10px);
|
|
14
|
+
-webkit-backdrop-filter: blur(10px);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.system-check-card {
|
|
18
|
+
width: 100%;
|
|
19
|
+
max-width: 540px;
|
|
20
|
+
padding: 3rem 2.5rem;
|
|
21
|
+
border-radius: var(--radius-lg);
|
|
22
|
+
text-align: center;
|
|
23
|
+
box-shadow: var(--glass-shadow);
|
|
24
|
+
border: 1px solid var(--glass-border);
|
|
25
|
+
animation: scaleUp 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.status-checking, .status-failed {
|
|
29
|
+
display: flex;
|
|
30
|
+
flex-direction: column;
|
|
31
|
+
align-items: center;
|
|
32
|
+
gap: 1.5rem;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.checking-spinner {
|
|
36
|
+
color: var(--accent-primary);
|
|
37
|
+
animation: spin 1.2s linear infinite;
|
|
38
|
+
filter: drop-shadow(0 0 10px rgba(99, 102, 241, 0.4));
|
|
39
|
+
margin-bottom: 0.5rem;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.error-icon {
|
|
43
|
+
color: #f59e0b;
|
|
44
|
+
filter: drop-shadow(0 0 15px rgba(245, 158, 11, 0.3));
|
|
45
|
+
margin-bottom: 0.5rem;
|
|
46
|
+
animation: pulseWarning 2s infinite ease-in-out;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.system-check-card h2 {
|
|
50
|
+
font-size: 1.5rem;
|
|
51
|
+
font-weight: 700;
|
|
52
|
+
letter-spacing: -0.02em;
|
|
53
|
+
color: var(--text-primary);
|
|
54
|
+
margin: 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.status-text {
|
|
58
|
+
font-size: 1.05rem;
|
|
59
|
+
color: var(--text-secondary);
|
|
60
|
+
line-height: 1.6;
|
|
61
|
+
margin: 0;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.sub-status-text {
|
|
65
|
+
font-size: 0.85rem;
|
|
66
|
+
color: var(--text-muted);
|
|
67
|
+
margin: 0;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.error-summary {
|
|
71
|
+
font-size: 1rem;
|
|
72
|
+
color: var(--text-secondary);
|
|
73
|
+
margin: 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.detailed-error-box {
|
|
77
|
+
width: 100%;
|
|
78
|
+
padding: 0.75rem 1rem;
|
|
79
|
+
background: rgba(239, 68, 68, 0.08);
|
|
80
|
+
border: 1px solid rgba(239, 68, 68, 0.2);
|
|
81
|
+
border-radius: var(--radius-sm);
|
|
82
|
+
font-family: monospace;
|
|
83
|
+
font-size: 0.75rem;
|
|
84
|
+
color: #ef4444;
|
|
85
|
+
text-align: left;
|
|
86
|
+
max-height: 80px;
|
|
87
|
+
overflow-y: auto;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.setup-tabs {
|
|
91
|
+
display: flex;
|
|
92
|
+
width: 100%;
|
|
93
|
+
background: rgba(255, 255, 255, 0.03);
|
|
94
|
+
border: 1px solid var(--glass-border);
|
|
95
|
+
border-radius: var(--radius-sm);
|
|
96
|
+
overflow: hidden;
|
|
97
|
+
margin-top: 0.5rem;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.system-check-card .tab-btn {
|
|
101
|
+
flex: 1;
|
|
102
|
+
padding: 0.65rem;
|
|
103
|
+
font-size: 0.9rem;
|
|
104
|
+
font-weight: 600;
|
|
105
|
+
color: var(--text-secondary);
|
|
106
|
+
background: transparent;
|
|
107
|
+
transition: all 0.2s ease;
|
|
108
|
+
border-radius: 0;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.system-check-card .tab-btn:hover {
|
|
112
|
+
color: var(--text-primary);
|
|
113
|
+
background: rgba(255, 255, 255, 0.02);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.system-check-card .tab-btn.active {
|
|
117
|
+
color: white;
|
|
118
|
+
background: var(--accent-gradient);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.setup-guide-content {
|
|
122
|
+
width: 100%;
|
|
123
|
+
text-align: left;
|
|
124
|
+
min-height: 140px;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.guide-tab-panel {
|
|
128
|
+
display: flex;
|
|
129
|
+
flex-direction: column;
|
|
130
|
+
gap: 0.75rem;
|
|
131
|
+
animation: fadeInTab 0.25s ease-out;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.guide-tab-panel p {
|
|
135
|
+
font-size: 0.9rem;
|
|
136
|
+
color: var(--text-secondary);
|
|
137
|
+
margin: 0;
|
|
138
|
+
line-height: 1.5;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.code-block-container {
|
|
142
|
+
display: flex;
|
|
143
|
+
align-items: center;
|
|
144
|
+
gap: 0.5rem;
|
|
145
|
+
padding: 0.75rem 1rem;
|
|
146
|
+
background: #090d16;
|
|
147
|
+
border: 1px solid var(--border);
|
|
148
|
+
border-radius: var(--radius-sm);
|
|
149
|
+
color: #38bdf8;
|
|
150
|
+
font-family: monospace;
|
|
151
|
+
font-size: 0.85rem;
|
|
152
|
+
position: relative;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.code-icon {
|
|
156
|
+
color: var(--text-muted);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.code-block-container code {
|
|
160
|
+
flex: 1;
|
|
161
|
+
word-break: break-all;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.guide-footer-text {
|
|
165
|
+
font-size: 0.75rem !important;
|
|
166
|
+
color: var(--text-muted) !important;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.guide-footer-text a {
|
|
170
|
+
color: var(--accent-primary);
|
|
171
|
+
text-decoration: underline;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.ffmpeg-download-link {
|
|
175
|
+
display: inline-flex;
|
|
176
|
+
align-items: center;
|
|
177
|
+
justify-content: center;
|
|
178
|
+
width: 100%;
|
|
179
|
+
padding: 0.75rem;
|
|
180
|
+
background: rgba(99, 102, 241, 0.1);
|
|
181
|
+
border: 1px solid rgba(99, 102, 241, 0.3);
|
|
182
|
+
border-radius: var(--radius-sm);
|
|
183
|
+
color: var(--text-primary);
|
|
184
|
+
font-weight: 600;
|
|
185
|
+
font-size: 0.9rem;
|
|
186
|
+
text-align: center;
|
|
187
|
+
transition: all 0.2s ease;
|
|
188
|
+
margin-top: 0.25rem;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.ffmpeg-download-link:hover {
|
|
192
|
+
background: var(--accent-gradient);
|
|
193
|
+
border-color: transparent;
|
|
194
|
+
color: white;
|
|
195
|
+
box-shadow: var(--shadow-glow);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.action-row {
|
|
199
|
+
margin-top: 1rem;
|
|
200
|
+
width: 100%;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.btn-retry {
|
|
204
|
+
display: inline-flex;
|
|
205
|
+
align-items: center;
|
|
206
|
+
justify-content: center;
|
|
207
|
+
gap: 0.5rem;
|
|
208
|
+
width: 100%;
|
|
209
|
+
padding: 0.75rem;
|
|
210
|
+
background: var(--glass-bg);
|
|
211
|
+
border: 1px solid var(--glass-border);
|
|
212
|
+
border-radius: var(--radius-sm);
|
|
213
|
+
color: var(--text-primary);
|
|
214
|
+
font-weight: 600;
|
|
215
|
+
font-size: 0.95rem;
|
|
216
|
+
transition: all 0.2s ease;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.btn-retry:hover {
|
|
220
|
+
background: rgba(255, 255, 255, 0.08);
|
|
221
|
+
border-color: var(--text-secondary);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.btn-retry svg {
|
|
225
|
+
transition: transform 0.3s ease;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.btn-retry:hover svg {
|
|
229
|
+
transform: rotate(180deg);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
@keyframes spin {
|
|
233
|
+
100% { transform: rotate(360deg); }
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
@keyframes scaleUp {
|
|
237
|
+
from { transform: scale(0.95); opacity: 0; }
|
|
238
|
+
to { transform: scale(1); opacity: 1; }
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
@keyframes fadeInTab {
|
|
242
|
+
from { opacity: 0; transform: translateY(5px); }
|
|
243
|
+
to { opacity: 1; transform: translateY(0); }
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
@keyframes pulseWarning {
|
|
247
|
+
0% { transform: scale(1); }
|
|
248
|
+
50% { transform: scale(1.05); }
|
|
249
|
+
100% { transform: scale(1); }
|
|
250
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { Loader2, Terminal, AlertTriangle, RefreshCw, HelpCircle } from 'lucide-react';
|
|
3
|
+
import './SystemCheckScreen.css';
|
|
4
|
+
|
|
5
|
+
export default function SystemCheckScreen({ status, error, onRetry }) {
|
|
6
|
+
const [activeTab, setActiveTab] = useState(() => {
|
|
7
|
+
const platform = window.navigator.platform.toLowerCase();
|
|
8
|
+
if (platform.includes('mac')) return 'mac';
|
|
9
|
+
if (platform.includes('win')) return 'windows';
|
|
10
|
+
return 'linux';
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const isDownloading = status === 'downloading' || status === 'checking';
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<div className="system-check-container">
|
|
17
|
+
<div className="system-check-card liquid-glass">
|
|
18
|
+
{isDownloading ? (
|
|
19
|
+
<div className="status-checking">
|
|
20
|
+
<Loader2 className="spinner checking-spinner" size={60} />
|
|
21
|
+
<h2>Setting Up Dependencies</h2>
|
|
22
|
+
<p className="status-text">
|
|
23
|
+
We are automatically configuring <strong>FFmpeg</strong> and downloading <strong>yt-dlp</strong> from GitHub...
|
|
24
|
+
</p>
|
|
25
|
+
<p className="sub-status-text">This will only happen once on first startup. Please wait.</p>
|
|
26
|
+
</div>
|
|
27
|
+
) : (
|
|
28
|
+
<div className="status-failed">
|
|
29
|
+
<AlertTriangle className="error-icon" size={60} />
|
|
30
|
+
<h2>System Dependencies Missing</h2>
|
|
31
|
+
<p className="error-summary">
|
|
32
|
+
The automated setup failed to configure the required tools on your machine.
|
|
33
|
+
</p>
|
|
34
|
+
{error && <div className="detailed-error-box">Error details: {error}</div>}
|
|
35
|
+
|
|
36
|
+
<div className="setup-tabs">
|
|
37
|
+
<button
|
|
38
|
+
className={`tab-btn ${activeTab === 'mac' ? 'active' : ''}`}
|
|
39
|
+
onClick={() => setActiveTab('mac')}
|
|
40
|
+
>
|
|
41
|
+
macOS
|
|
42
|
+
</button>
|
|
43
|
+
<button
|
|
44
|
+
className={`tab-btn ${activeTab === 'windows' ? 'active' : ''}`}
|
|
45
|
+
onClick={() => setActiveTab('windows')}
|
|
46
|
+
>
|
|
47
|
+
Windows
|
|
48
|
+
</button>
|
|
49
|
+
<button
|
|
50
|
+
className={`tab-btn ${activeTab === 'linux' ? 'active' : ''}`}
|
|
51
|
+
onClick={() => setActiveTab('linux')}
|
|
52
|
+
>
|
|
53
|
+
Linux
|
|
54
|
+
</button>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<div className="setup-guide-content">
|
|
58
|
+
{activeTab === 'mac' && (
|
|
59
|
+
<div className="guide-tab-panel">
|
|
60
|
+
<p>Open your terminal and paste this command to install the required packages via Homebrew:</p>
|
|
61
|
+
<div className="code-block-container">
|
|
62
|
+
<Terminal size={14} className="code-icon" />
|
|
63
|
+
<code>brew install yt-dlp ffmpeg</code>
|
|
64
|
+
</div>
|
|
65
|
+
<p className="guide-footer-text">Don't have Homebrew? Install it from <a href="https://brew.sh" target="_blank" rel="noreferrer">brew.sh</a> first.</p>
|
|
66
|
+
</div>
|
|
67
|
+
)}
|
|
68
|
+
|
|
69
|
+
{activeTab === 'windows' && (
|
|
70
|
+
<div className="guide-tab-panel">
|
|
71
|
+
<p>1. Open Command Prompt/PowerShell and install <strong>yt-dlp</strong>:</p>
|
|
72
|
+
<div className="code-block-container">
|
|
73
|
+
<Terminal size={14} className="code-icon" />
|
|
74
|
+
<code>winget install yt-dlp</code>
|
|
75
|
+
</div>
|
|
76
|
+
<p>2. Download <strong>FFmpeg</strong> from the official website and add it to your System PATH:</p>
|
|
77
|
+
<a
|
|
78
|
+
href="https://github.com/BtbN/FFmpeg-Builds/releases"
|
|
79
|
+
target="_blank"
|
|
80
|
+
rel="noreferrer"
|
|
81
|
+
className="ffmpeg-download-link"
|
|
82
|
+
>
|
|
83
|
+
Get FFmpeg Builds for Windows
|
|
84
|
+
</a>
|
|
85
|
+
</div>
|
|
86
|
+
)}
|
|
87
|
+
|
|
88
|
+
{activeTab === 'linux' && (
|
|
89
|
+
<div className="guide-tab-panel">
|
|
90
|
+
<p>Run the package manager command to install both tools:</p>
|
|
91
|
+
<div className="code-block-container">
|
|
92
|
+
<Terminal size={14} className="code-icon" />
|
|
93
|
+
<code>sudo apt update && sudo apt install -y ffmpeg yt-dlp</code>
|
|
94
|
+
</div>
|
|
95
|
+
<p className="guide-footer-text">For Fedora/RedHat: <code>sudo dnf install ffmpeg yt-dlp</code></p>
|
|
96
|
+
</div>
|
|
97
|
+
)}
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
<div className="action-row">
|
|
101
|
+
<button className="btn-retry" onClick={onRetry}>
|
|
102
|
+
<RefreshCw size={16} /> Check Again
|
|
103
|
+
</button>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
)}
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
}
|