1347 lines
42 KiB
HTML
1347 lines
42 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>just-download</title>
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
|
<style>
|
|
* {
|
|
box-sizing: border-box;
|
|
margin: 0;
|
|
padding: 0;
|
|
border-radius: 0 !important;
|
|
}
|
|
|
|
:root {
|
|
--bg: #0b0b0b;
|
|
--panel: #111111;
|
|
--border: #1f1f1f;
|
|
--border-hover: #333333;
|
|
--text: #e5e5e5;
|
|
--text-dim: #666666;
|
|
--text-sub: #888888;
|
|
--accent: #4db8ff;
|
|
--yt: #ff4444;
|
|
--ig: #e1306c;
|
|
--success: #22c55e;
|
|
--error: #ef4444;
|
|
}
|
|
|
|
body {
|
|
background: var(--bg);
|
|
color: var(--text);
|
|
font-family: 'JetBrains Mono', monospace;
|
|
min-height: 100vh;
|
|
position: relative;
|
|
overflow-x: hidden;
|
|
-webkit-font-smoothing: antialiased;
|
|
}
|
|
|
|
#particles {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
pointer-events: none;
|
|
z-index: 0;
|
|
}
|
|
|
|
.wrap {
|
|
position: relative;
|
|
z-index: 1;
|
|
max-width: 720px;
|
|
margin: 0 auto;
|
|
padding: 48px 20px 80px;
|
|
}
|
|
|
|
/* Header */
|
|
.header {
|
|
margin-bottom: 30px;
|
|
text-align: center;
|
|
}
|
|
|
|
h1 {
|
|
font-size: 2.1rem;
|
|
font-weight: 600;
|
|
letter-spacing: -0.04em;
|
|
color: #fff;
|
|
margin-bottom: 6px;
|
|
}
|
|
|
|
.subtitle-row {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 10px;
|
|
font-size: 0.78rem;
|
|
letter-spacing: 0.03em;
|
|
}
|
|
|
|
.subtitle {
|
|
color: var(--text-dim);
|
|
}
|
|
|
|
.status-sep {
|
|
color: #262626;
|
|
}
|
|
|
|
.backend-status {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
cursor: pointer;
|
|
user-select: none;
|
|
padding: 2px 6px;
|
|
background: rgba(255, 255, 255, 0.02);
|
|
border: 1px solid #1a1a1a;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.backend-status:hover {
|
|
border-color: #333;
|
|
}
|
|
|
|
.status-dot {
|
|
width: 6px;
|
|
height: 6px;
|
|
background: #555;
|
|
transition: background 0.3s ease;
|
|
}
|
|
|
|
.backend-status.online .status-dot {
|
|
background: var(--success);
|
|
box-shadow: 0 0 6px rgba(34, 197, 94, 0.6);
|
|
}
|
|
|
|
.backend-status.offline .status-dot {
|
|
background: var(--error);
|
|
box-shadow: 0 0 6px rgba(239, 68, 68, 0.6);
|
|
}
|
|
|
|
.status-text {
|
|
font-size: 0.72rem;
|
|
color: var(--text-sub);
|
|
}
|
|
|
|
/* Input zone */
|
|
.input-zone {
|
|
background: var(--panel);
|
|
border: 1px solid var(--border);
|
|
padding: 16px;
|
|
margin-bottom: 18px;
|
|
transition: border-color 0.2s ease;
|
|
}
|
|
|
|
.input-zone.drag-over {
|
|
border-color: var(--accent);
|
|
background: #141414;
|
|
}
|
|
|
|
.input-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
|
|
.input-prefix {
|
|
color: var(--text-dim);
|
|
font-weight: 700;
|
|
user-select: none;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.url-input {
|
|
flex: 1;
|
|
background: #080808;
|
|
border: 1px solid #242424;
|
|
padding: 10px 14px;
|
|
color: #fff;
|
|
font-family: inherit;
|
|
font-size: 0.85rem;
|
|
outline: none;
|
|
transition: border-color 0.2s ease;
|
|
}
|
|
|
|
.url-input:focus {
|
|
border-color: var(--accent);
|
|
}
|
|
|
|
.url-input::placeholder {
|
|
color: #444;
|
|
}
|
|
|
|
.drag-hint {
|
|
font-size: 0.7rem;
|
|
color: var(--text-dim);
|
|
margin-top: 8px;
|
|
text-align: right;
|
|
}
|
|
|
|
/* Buttons */
|
|
.btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 10px 18px;
|
|
background: #141414;
|
|
color: #ccc;
|
|
border: 1px solid #2a2a2a;
|
|
font-family: inherit;
|
|
font-size: 0.8rem;
|
|
cursor: pointer;
|
|
user-select: none;
|
|
letter-spacing: 0.02em;
|
|
transition: all 0.15s ease;
|
|
}
|
|
|
|
.btn:hover:not(:disabled) {
|
|
background: #222;
|
|
border-color: var(--accent);
|
|
color: var(--accent);
|
|
}
|
|
|
|
.btn:active:not(:disabled) {
|
|
background: #fff;
|
|
color: #000;
|
|
border-color: #fff;
|
|
}
|
|
|
|
.btn:disabled {
|
|
opacity: 0.3;
|
|
cursor: not-allowed;
|
|
border-color: #1f1f1f;
|
|
}
|
|
|
|
.btn-primary {
|
|
background: #191919;
|
|
border-color: #383838;
|
|
color: #fff;
|
|
}
|
|
|
|
.btn-main {
|
|
width: 100%;
|
|
padding: 13px;
|
|
font-size: 0.88rem;
|
|
font-weight: 500;
|
|
background: #181818;
|
|
border: 1px solid #383838;
|
|
color: #fff;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.btn-main:hover:not(:disabled) {
|
|
background: #fff;
|
|
color: #000;
|
|
border-color: #fff;
|
|
}
|
|
|
|
.btn-secondary {
|
|
width: 100%;
|
|
padding: 10px;
|
|
font-size: 0.78rem;
|
|
background: #0f0f0f;
|
|
border: 1px solid #222;
|
|
color: var(--text-sub);
|
|
}
|
|
|
|
/* Platform Tabs */
|
|
.platform-tabs {
|
|
display: flex;
|
|
gap: 8px;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.tab-btn {
|
|
flex: 1;
|
|
background: #0e0e0e;
|
|
border: 1px solid var(--border);
|
|
padding: 10px;
|
|
font-family: inherit;
|
|
font-size: 0.8rem;
|
|
color: var(--text-sub);
|
|
cursor: pointer;
|
|
text-align: center;
|
|
transition: all 0.15s ease;
|
|
user-select: none;
|
|
}
|
|
|
|
.tab-btn:hover {
|
|
border-color: var(--border-hover);
|
|
color: #fff;
|
|
}
|
|
|
|
.tab-btn.active-yt {
|
|
background: #141414;
|
|
border-color: var(--yt);
|
|
color: var(--yt);
|
|
}
|
|
|
|
.tab-btn.active-ig {
|
|
background: #141414;
|
|
border-color: var(--ig);
|
|
color: var(--ig);
|
|
}
|
|
|
|
/* Preview Card */
|
|
.preview-card {
|
|
background: var(--panel);
|
|
border: 1px solid var(--border);
|
|
padding: 16px;
|
|
margin-bottom: 18px;
|
|
display: none;
|
|
gap: 16px;
|
|
}
|
|
|
|
.preview-card.show {
|
|
display: flex;
|
|
}
|
|
|
|
.preview-thumb {
|
|
width: 140px;
|
|
height: 80px;
|
|
background: #080808;
|
|
border: 1px solid #222;
|
|
flex-shrink: 0;
|
|
overflow: hidden;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.preview-thumb img {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
}
|
|
|
|
.preview-info {
|
|
flex: 1;
|
|
min-width: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
gap: 6px;
|
|
}
|
|
|
|
.preview-title {
|
|
font-size: 0.88rem;
|
|
font-weight: 500;
|
|
color: #fff;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.preview-meta {
|
|
display: flex;
|
|
gap: 8px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.meta-pill {
|
|
background: #080808;
|
|
border: 1px solid #222;
|
|
padding: 3px 8px;
|
|
font-size: 0.7rem;
|
|
color: var(--text-sub);
|
|
}
|
|
|
|
.preview-quality {
|
|
font-size: 0.7rem;
|
|
color: var(--text-dim);
|
|
}
|
|
|
|
.skeleton {
|
|
background: linear-gradient(90deg, #141414 25%, #222 50%, #141414 75%);
|
|
background-size: 200%;
|
|
animation: skel 1.4s infinite;
|
|
}
|
|
|
|
@keyframes skel {
|
|
0% { background-position: 200%; }
|
|
100% { background-position: -200%; }
|
|
}
|
|
|
|
/* Options Section */
|
|
.panel {
|
|
display: none;
|
|
}
|
|
|
|
.panel.active {
|
|
display: block;
|
|
}
|
|
|
|
.section-label {
|
|
font-size: 0.7rem;
|
|
color: var(--text-dim);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.08em;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.options-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
|
|
gap: 8px;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.opt-card {
|
|
background: #0e0e0e;
|
|
border: 1px solid var(--border);
|
|
padding: 12px;
|
|
cursor: pointer;
|
|
user-select: none;
|
|
transition: all 0.15s ease;
|
|
position: relative;
|
|
}
|
|
|
|
.opt-card:hover {
|
|
border-color: var(--border-hover);
|
|
background: #141414;
|
|
}
|
|
|
|
.opt-card.selected-yt {
|
|
border-color: var(--yt);
|
|
background: rgba(255, 68, 68, 0.06);
|
|
}
|
|
|
|
.opt-card.selected-ig {
|
|
border-color: var(--ig);
|
|
background: rgba(225, 48, 108, 0.06);
|
|
}
|
|
|
|
.opt-title {
|
|
font-size: 0.8rem;
|
|
font-weight: 500;
|
|
color: #fff;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.opt-desc {
|
|
font-size: 0.68rem;
|
|
color: var(--text-dim);
|
|
}
|
|
|
|
.sub-panel {
|
|
background: var(--panel);
|
|
border: 1px solid var(--border);
|
|
padding: 16px;
|
|
margin-bottom: 18px;
|
|
}
|
|
|
|
.form-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 14px;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.form-row:last-child {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.form-label {
|
|
font-size: 0.78rem;
|
|
color: var(--text-sub);
|
|
min-width: 90px;
|
|
}
|
|
|
|
.form-select {
|
|
background: #080808;
|
|
border: 1px solid #242424;
|
|
padding: 8px 12px;
|
|
color: #fff;
|
|
font-family: inherit;
|
|
font-size: 0.78rem;
|
|
outline: none;
|
|
cursor: pointer;
|
|
flex: 1;
|
|
}
|
|
|
|
.form-toggle {
|
|
display: flex;
|
|
gap: 4px;
|
|
}
|
|
|
|
.toggle-btn {
|
|
background: #080808;
|
|
border: 1px solid #242424;
|
|
padding: 6px 14px;
|
|
font-size: 0.74rem;
|
|
color: var(--text-sub);
|
|
cursor: pointer;
|
|
user-select: none;
|
|
transition: all 0.15s ease;
|
|
}
|
|
|
|
.toggle-btn:hover {
|
|
border-color: #444;
|
|
color: #fff;
|
|
}
|
|
|
|
.toggle-btn.on {
|
|
background: #202020;
|
|
border-color: var(--accent);
|
|
color: var(--accent);
|
|
}
|
|
|
|
.time-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
margin-top: 8px;
|
|
}
|
|
|
|
.time-input {
|
|
background: #080808;
|
|
border: 1px solid #242424;
|
|
padding: 6px 10px;
|
|
color: #fff;
|
|
font-family: inherit;
|
|
font-size: 0.76rem;
|
|
width: 80px;
|
|
outline: none;
|
|
text-align: center;
|
|
}
|
|
|
|
.time-input:focus {
|
|
border-color: var(--accent);
|
|
}
|
|
|
|
.time-sep {
|
|
color: var(--text-dim);
|
|
font-size: 0.74rem;
|
|
}
|
|
|
|
/* Progress wrap */
|
|
.progress-wrap {
|
|
background: var(--panel);
|
|
border: 1px solid var(--border);
|
|
padding: 16px;
|
|
margin-bottom: 18px;
|
|
display: none;
|
|
}
|
|
|
|
.progress-wrap.show {
|
|
display: block;
|
|
}
|
|
|
|
.progress-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.progress-label {
|
|
font-size: 0.78rem;
|
|
color: #fff;
|
|
}
|
|
|
|
.progress-pct {
|
|
font-size: 0.78rem;
|
|
color: var(--accent);
|
|
font-weight: 600;
|
|
}
|
|
|
|
.progress-bar-bg {
|
|
background: #080808;
|
|
border: 1px solid #1a1a1a;
|
|
height: 6px;
|
|
margin-bottom: 8px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.progress-bar {
|
|
height: 100%;
|
|
background: var(--accent);
|
|
width: 0%;
|
|
transition: width 0.3s ease;
|
|
}
|
|
|
|
.progress-status {
|
|
font-size: 0.7rem;
|
|
color: var(--text-dim);
|
|
}
|
|
|
|
/* History section */
|
|
.history-section {
|
|
margin-top: 32px;
|
|
background: var(--panel);
|
|
border: 1px solid var(--border);
|
|
padding: 16px;
|
|
}
|
|
|
|
.history-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
padding: 10px 0;
|
|
border-bottom: 1px solid #181818;
|
|
font-size: 0.76rem;
|
|
}
|
|
|
|
.history-item:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.history-tag {
|
|
padding: 2px 6px;
|
|
font-size: 0.68rem;
|
|
background: #080808;
|
|
border: 1px solid #222;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.history-info {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.history-title {
|
|
color: #ddd;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.history-meta {
|
|
color: var(--text-dim);
|
|
font-size: 0.68rem;
|
|
margin-top: 2px;
|
|
}
|
|
|
|
.history-status {
|
|
color: var(--success);
|
|
font-size: 0.7rem;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
/* Toast */
|
|
.toast {
|
|
position: fixed;
|
|
bottom: 24px;
|
|
right: 24px;
|
|
background: #111;
|
|
border: 1px solid #2a2a2a;
|
|
padding: 10px 16px;
|
|
font-size: 0.78rem;
|
|
color: #e5e5e5;
|
|
transform: translateY(60px);
|
|
opacity: 0;
|
|
transition: all 0.25s ease;
|
|
z-index: 1000;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.toast.show {
|
|
transform: translateY(0);
|
|
opacity: 1;
|
|
}
|
|
|
|
@media (max-width: 600px) {
|
|
.wrap {
|
|
padding: 30px 14px 60px;
|
|
}
|
|
.preview-card.show {
|
|
flex-direction: column;
|
|
}
|
|
.preview-thumb {
|
|
width: 100%;
|
|
height: 140px;
|
|
}
|
|
.form-row {
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<canvas id="particles"></canvas>
|
|
|
|
<main class="wrap">
|
|
<header class="header">
|
|
<h1>just-download</h1>
|
|
<div class="subtitle-row">
|
|
<span class="subtitle">// yeah</span>
|
|
<span class="status-sep">•</span>
|
|
<div id="backendStatus" class="backend-status" title="Click to refresh status">
|
|
<span class="status-dot"></span>
|
|
<span id="statusText" class="status-text">connecting...</span>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<div class="input-zone" id="inputZone">
|
|
<div class="input-row">
|
|
<span class="input-prefix">></span>
|
|
<input class="url-input" id="urlInput" type="text" placeholder="paste youtube or instagram link..." autocomplete="off" spellcheck="false"/>
|
|
<button class="btn btn-primary" id="fetchBtn" onclick="handleUrl()">[go]</button>
|
|
</div>
|
|
<div class="drag-hint">// or drag & drop url here</div>
|
|
</div>
|
|
|
|
<div class="platform-tabs">
|
|
<button class="tab-btn" id="tab-yt" onclick="switchPlatform('yt')">[youtube]</button>
|
|
<button class="tab-btn" id="tab-ig" onclick="switchPlatform('ig')">[instagram]</button>
|
|
</div>
|
|
|
|
<div id="previewCard" class="preview-card">
|
|
<div class="preview-thumb" id="previewThumb">
|
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#444" stroke-width="1.5"><rect x="2" y="2" width="20" height="20"/><path d="M10 8l6 4-6 4V8z"/></svg>
|
|
</div>
|
|
<div class="preview-info">
|
|
<div class="preview-title" id="previewTitle">—</div>
|
|
<div class="preview-meta" id="previewMeta"></div>
|
|
<div class="preview-quality" id="previewQuality"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- YouTube Options -->
|
|
<div id="panel-yt" class="panel">
|
|
<div class="section-label">// options</div>
|
|
<div class="options-grid">
|
|
<div class="opt-card selected-yt" id="yt-video" onclick="selectOpt('yt','yt-video')">
|
|
<div class="opt-title">[video]</div>
|
|
<div class="opt-desc">MP4 with audio</div>
|
|
</div>
|
|
<div class="opt-card" id="yt-audio" onclick="selectOpt('yt','yt-audio')">
|
|
<div class="opt-title">[audio]</div>
|
|
<div class="opt-desc">MP3 extract</div>
|
|
</div>
|
|
<div class="opt-card" id="yt-thumb" onclick="selectOpt('yt','yt-thumb')">
|
|
<div class="opt-title">[thumbnail]</div>
|
|
<div class="opt-desc">JPG/WebP</div>
|
|
</div>
|
|
<div class="opt-card" id="yt-subs" onclick="selectOpt('yt','yt-subs')">
|
|
<div class="opt-title">[subtitles]</div>
|
|
<div class="opt-desc">SRT/VTT</div>
|
|
</div>
|
|
<div class="opt-card" id="yt-channel" onclick="selectOpt('yt','yt-channel')">
|
|
<div class="opt-title">[channel art]</div>
|
|
<div class="opt-desc">Avatar + banner</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="sub-panel" id="yt-video-opts">
|
|
<div class="section-label">// settings</div>
|
|
<div class="form-row">
|
|
<span class="form-label">resolution</span>
|
|
<select class="form-select" id="yt-res">
|
|
<option value="best">Auto (best quality)</option>
|
|
<option value="2160">4K (2160p)</option>
|
|
<option value="1440">1440p</option>
|
|
<option value="1080">1080p</option>
|
|
<option value="720">720p</option>
|
|
<option value="480">480p</option>
|
|
<option value="360">360p</option>
|
|
<option value="240">240p</option>
|
|
<option value="144">144p</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-row">
|
|
<span class="form-label">format</span>
|
|
<div class="form-toggle" id="yt-fmt-toggle">
|
|
<div class="toggle-btn on" onclick="toggler(this,'yt-fmt-toggle')">MP4</div>
|
|
<div class="toggle-btn" onclick="toggler(this,'yt-fmt-toggle')">WebM</div>
|
|
<div class="toggle-btn" onclick="toggler(this,'yt-fmt-toggle')">MKV</div>
|
|
</div>
|
|
</div>
|
|
<div class="form-row">
|
|
<span class="form-label">trim</span>
|
|
<div>
|
|
<div class="form-toggle">
|
|
<div class="toggle-btn on" id="trim-full" onclick="setTrim('full')">Full</div>
|
|
<div class="toggle-btn" id="trim-custom" onclick="setTrim('custom')">Custom</div>
|
|
</div>
|
|
<div class="time-row" id="trim-inputs" style="display:none">
|
|
<input class="time-input" id="startTime" placeholder="00:00"/>
|
|
<span class="time-sep">→</span>
|
|
<input class="time-input" id="endTime" placeholder="10:24"/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Instagram Options -->
|
|
<div id="panel-ig" class="panel">
|
|
<div class="section-label">// options</div>
|
|
<div class="options-grid">
|
|
<div class="opt-card selected-ig" id="ig-post" onclick="selectOpt('ig','ig-post')">
|
|
<div class="opt-title">[post]</div>
|
|
<div class="opt-desc">Photo or video</div>
|
|
</div>
|
|
<div class="opt-card" id="ig-reel" onclick="selectOpt('ig','ig-reel')">
|
|
<div class="opt-title">[reel]</div>
|
|
<div class="opt-desc">Short clip</div>
|
|
</div>
|
|
<div class="opt-card" id="ig-profile" onclick="selectOpt('ig','ig-profile')">
|
|
<div class="opt-title">[profile pic]</div>
|
|
<div class="opt-desc">Full size avatar</div>
|
|
</div>
|
|
<div class="opt-card" id="ig-thumb" onclick="selectOpt('ig','ig-thumb')">
|
|
<div class="opt-title">[thumbnail]</div>
|
|
<div class="opt-desc">Cover frame</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="sub-panel">
|
|
<div class="section-label">// settings</div>
|
|
<div class="form-row">
|
|
<span class="form-label">format</span>
|
|
<div class="form-toggle" id="ig-fmt-toggle">
|
|
<div class="toggle-btn on" onclick="toggler(this,'ig-fmt-toggle')">MP4</div>
|
|
<div class="toggle-btn" onclick="toggler(this,'ig-fmt-toggle')">JPG</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Progress Wrap -->
|
|
<div class="progress-wrap" id="progressWrap">
|
|
<div class="progress-header">
|
|
<span class="progress-label" id="progressLabel">downloading...</span>
|
|
<span class="progress-pct" id="progressPct">0%</span>
|
|
</div>
|
|
<div class="progress-bar-bg"><div class="progress-bar" id="progressBar"></div></div>
|
|
<div class="progress-status" id="progressStatus">connecting...</div>
|
|
</div>
|
|
|
|
<!-- Actions -->
|
|
<button class="btn btn-main" id="dlBtn" onclick="startDownload()">[download file]</button>
|
|
<button class="btn btn-secondary" id="quickBtn" onclick="quickDownload()">[quick download — best quality]</button>
|
|
|
|
<!-- History -->
|
|
<div class="history-section" id="historySection" style="display:none">
|
|
<div class="section-label">// recent downloads</div>
|
|
<div id="historyList"></div>
|
|
</div>
|
|
</main>
|
|
|
|
<div id="toast" class="toast"></div>
|
|
|
|
<script>
|
|
// ── Particle Background ──────────────────────────────────────────────────
|
|
const canvas = document.getElementById('particles');
|
|
const ctx = canvas.getContext('2d');
|
|
let particles = [];
|
|
let mouse = { x: null, y: null, radius: 100 };
|
|
|
|
function resizeCanvas() {
|
|
canvas.width = window.innerWidth;
|
|
canvas.height = window.innerHeight;
|
|
}
|
|
resizeCanvas();
|
|
window.addEventListener('resize', resizeCanvas);
|
|
|
|
window.addEventListener('mousemove', e => {
|
|
mouse.x = e.x;
|
|
mouse.y = e.y;
|
|
});
|
|
window.addEventListener('mouseout', () => {
|
|
mouse.x = null;
|
|
mouse.y = null;
|
|
});
|
|
|
|
class Particle {
|
|
constructor() {
|
|
this.x = Math.random() * canvas.width;
|
|
this.y = Math.random() * canvas.height;
|
|
this.size = Math.random() * 1.5 + 0.5;
|
|
this.baseX = this.x;
|
|
this.baseY = this.y;
|
|
this.density = (Math.random() * 20) + 1;
|
|
this.vx = (Math.random() - 0.5) * 0.4;
|
|
this.vy = (Math.random() - 0.5) * 0.4;
|
|
}
|
|
draw() {
|
|
ctx.fillStyle = 'rgba(255, 255, 255, 0.25)';
|
|
ctx.beginPath();
|
|
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
}
|
|
update() {
|
|
this.x += this.vx;
|
|
this.y += this.vy;
|
|
if (this.x < 0 || this.x > canvas.width) this.vx = -this.vx;
|
|
if (this.y < 0 || this.y > canvas.height) this.vy = -this.vy;
|
|
|
|
if (mouse.x != null && mouse.y != null) {
|
|
const dx = mouse.x - this.x;
|
|
const dy = mouse.y - this.y;
|
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
if (distance < mouse.radius) {
|
|
const force = (mouse.radius - distance) / mouse.radius;
|
|
const dirX = (dx / distance) * force * this.density * 0.4;
|
|
const dirY = (dy / distance) * force * this.density * 0.4;
|
|
this.x -= dirX;
|
|
this.y -= dirY;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function initParticles() {
|
|
particles = [];
|
|
const count = Math.min(60, Math.floor((canvas.width * canvas.height) / 18000));
|
|
for (let i = 0; i < count; i++) {
|
|
particles.push(new Particle());
|
|
}
|
|
}
|
|
initParticles();
|
|
window.addEventListener('resize', initParticles);
|
|
|
|
function connectParticles() {
|
|
for (let a = 0; a < particles.length; a++) {
|
|
for (let b = a + 1; b < particles.length; b++) {
|
|
const dx = particles[a].x - particles[b].x;
|
|
const dy = particles[a].y - particles[b].y;
|
|
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
if (dist < 80) {
|
|
const alpha = 0.08 * (1 - dist / 80);
|
|
ctx.strokeStyle = `rgba(255, 255, 255, ${alpha})`;
|
|
ctx.lineWidth = 1;
|
|
ctx.beginPath();
|
|
ctx.moveTo(particles[a].x, particles[a].y);
|
|
ctx.lineTo(particles[b].x, particles[b].y);
|
|
ctx.stroke();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function animateParticles() {
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
for (let i = 0; i < particles.length; i++) {
|
|
particles[i].update();
|
|
particles[i].draw();
|
|
}
|
|
connectParticles();
|
|
requestAnimationFrame(animateParticles);
|
|
}
|
|
requestAnimationFrame(animateParticles);
|
|
|
|
// ── API Configuration ────────────────────────────────────────────────────
|
|
const DEFAULT_REMOTE_API = 'https://YOUR_BACKEND_URL.trycloudflare.com';
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
if (urlParams.has('api')) {
|
|
const customApi = urlParams.get('api').replace(/\/+$/, '');
|
|
localStorage.setItem('justdownload_api', customApi);
|
|
}
|
|
const isLocalOrigin = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1' || window.location.hostname.startsWith('192.168.');
|
|
const API = window.JUSTDOWNLOAD_API || localStorage.getItem('justdownload_api') || (isLocalOrigin ? 'http://localhost:8000' : DEFAULT_REMOTE_API);
|
|
|
|
let activePlatform = null;
|
|
let activeOpts = { yt: 'yt-video', ig: 'ig-post' };
|
|
let downloading = false;
|
|
let downloadHistory = [];
|
|
|
|
// ── Backend Health Check ─────────────────────────────────────────────────
|
|
const backendStatus = document.getElementById('backendStatus');
|
|
const statusText = document.getElementById('statusText');
|
|
|
|
async function checkBackendStatus() {
|
|
if (!API || API.includes('YOUR_BACKEND_URL')) {
|
|
backendStatus.className = 'backend-status offline';
|
|
statusText.textContent = 'set ?api=...';
|
|
return;
|
|
}
|
|
try {
|
|
const controller = new AbortController();
|
|
const timeoutId = setTimeout(() => controller.abort(), 4000);
|
|
const res = await fetch(`${API}/health`, { signal: controller.signal });
|
|
clearTimeout(timeoutId);
|
|
if (res.ok) {
|
|
backendStatus.className = 'backend-status online';
|
|
statusText.textContent = 'online';
|
|
} else {
|
|
backendStatus.className = 'backend-status offline';
|
|
statusText.textContent = 'offline';
|
|
}
|
|
} catch (err) {
|
|
backendStatus.className = 'backend-status offline';
|
|
statusText.textContent = 'offline';
|
|
}
|
|
}
|
|
checkBackendStatus();
|
|
setInterval(checkBackendStatus, 15000);
|
|
backendStatus.addEventListener('click', checkBackendStatus);
|
|
|
|
// ── Toast ────────────────────────────────────────────────────────────────
|
|
let toastTimeout;
|
|
function showToast(msg) {
|
|
clearTimeout(toastTimeout);
|
|
const t = document.getElementById('toast');
|
|
t.textContent = `// ${msg}`;
|
|
t.classList.add('show');
|
|
toastTimeout = setTimeout(() => t.classList.remove('show'), 2600);
|
|
}
|
|
|
|
// ── URL helpers ──────────────────────────────────────────────────────────
|
|
function cleanYtUrl(url) {
|
|
const m = url.match(/[?&]v=([A-Za-z0-9_-]{11})/);
|
|
if (m) return `https://www.youtube.com/watch?v=${m[1]}`;
|
|
const s = url.match(/youtu\.be\/([A-Za-z0-9_-]{11})/);
|
|
if (s) return `https://youtu.be/${s[1]}`;
|
|
return url;
|
|
}
|
|
|
|
function extractIgUsername(raw) {
|
|
if (raw.includes('instagram.com')) {
|
|
const path = raw.replace(/https?:\/\/(www\.)?instagram\.com\/?/, '').replace(/^@/, '');
|
|
return path.split('/')[0].split('?')[0].trim();
|
|
}
|
|
return raw.replace(/^@/, '').trim();
|
|
}
|
|
|
|
function detectPlatform(url) {
|
|
if (/youtube\.com|youtu\.be/.test(url)) return 'yt';
|
|
if (/instagram\.com|instagr\.am/.test(url)) return 'ig';
|
|
return null;
|
|
}
|
|
|
|
// ── Preview ──────────────────────────────────────────────────────────────
|
|
async function handleUrl() {
|
|
const url = document.getElementById('urlInput').value.trim();
|
|
if (!url) { showToast('paste a link first'); return; }
|
|
const platform = detectPlatform(url);
|
|
if (!platform) { showToast('only youtube and instagram supported'); return; }
|
|
switchPlatform(platform);
|
|
showSkeleton();
|
|
const btn = document.getElementById('fetchBtn');
|
|
btn.disabled = true; btn.textContent = '[...]';
|
|
try {
|
|
const res = await fetch(`${API}/api/info/preview`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ url })
|
|
});
|
|
if (!res.ok) throw new Error(await res.text());
|
|
const data = await res.json();
|
|
renderPreview(data, platform);
|
|
showToast('preview ready');
|
|
} catch (e) {
|
|
document.getElementById('previewCard').classList.remove('show');
|
|
showToast('fetch failed: ' + e.message.slice(0, 50));
|
|
} finally {
|
|
btn.disabled = false; btn.textContent = '[go]';
|
|
}
|
|
}
|
|
|
|
function showSkeleton() {
|
|
document.getElementById('previewCard').classList.add('show');
|
|
document.getElementById('previewTitle').innerHTML = '<div class="skeleton" style="height:14px;width:80%"></div>';
|
|
document.getElementById('previewMeta').innerHTML = '<div class="skeleton" style="height:18px;width:60px"></div><div class="skeleton" style="height:18px;width:50px"></div>';
|
|
document.getElementById('previewQuality').innerHTML = '';
|
|
document.getElementById('previewThumb').innerHTML = '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#333" stroke-width="1.5"><rect x="2" y="2" width="20" height="20"/><path d="M10 8l6 4-6 4V8z"/></svg>';
|
|
}
|
|
|
|
function renderPreview(data, platform) {
|
|
document.getElementById('previewCard').classList.add('show');
|
|
const thumbEl = document.getElementById('previewThumb');
|
|
if (data.thumbnail) thumbEl.innerHTML = `<img src="${data.thumbnail}" alt="" onerror="this.style.display='none'"/>`;
|
|
document.getElementById('previewTitle').textContent = data.title || data.owner || '—';
|
|
const color = platform === 'yt' ? 'var(--yt)' : 'var(--ig)';
|
|
const pills = [];
|
|
if (data.duration_str) pills.push(`<span class="meta-pill">${data.duration_str}</span>`);
|
|
if (data.view_count) pills.push(`<span class="meta-pill">${fmtNum(data.view_count)} views</span>`);
|
|
if (data.likes) pills.push(`<span class="meta-pill">${fmtNum(data.likes)} ${data.type==='profile'?'followers':'likes'}</span>`);
|
|
if (data.uploader) pills.push(`<span class="meta-pill" style="color:${color}">${data.uploader}</span>`);
|
|
if (data.owner) pills.push(`<span class="meta-pill" style="color:${color}">@${data.owner}</span>`);
|
|
document.getElementById('previewMeta').innerHTML = pills.join('');
|
|
const res = data.available_resolutions || [];
|
|
document.getElementById('previewQuality').textContent = res.length ? `// max: ${res[0]}p available` : '';
|
|
}
|
|
|
|
function fmtNum(n) {
|
|
if (n >= 1e6) return (n/1e6).toFixed(1)+'M';
|
|
if (n >= 1e3) return (n/1e3).toFixed(1)+'K';
|
|
return String(n);
|
|
}
|
|
|
|
// ── Platform & Options ───────────────────────────────────────────────────
|
|
function switchPlatform(p) {
|
|
activePlatform = p;
|
|
['yt','ig'].forEach(id => {
|
|
document.getElementById('tab-'+id).className = 'tab-btn' + (id===p ? ' active-'+p : '');
|
|
document.getElementById('panel-'+id).className = 'panel' + (id===p ? ' active' : '');
|
|
});
|
|
}
|
|
|
|
function selectOpt(platform, id) {
|
|
document.querySelectorAll('#panel-'+platform+' .opt-card').forEach(c => c.className = 'opt-card');
|
|
document.getElementById(id).className = 'opt-card selected-'+platform;
|
|
activeOpts[platform] = id;
|
|
if (platform === 'yt') document.getElementById('yt-video-opts').style.display = id==='yt-video' ? 'block' : 'none';
|
|
}
|
|
|
|
function toggler(el, groupId) {
|
|
document.getElementById(groupId).querySelectorAll('.toggle-btn').forEach(b => b.classList.remove('on'));
|
|
el.classList.add('on');
|
|
}
|
|
|
|
function setTrim(mode) {
|
|
document.getElementById('trim-full').classList.toggle('on', mode==='full');
|
|
document.getElementById('trim-custom').classList.toggle('on', mode==='custom');
|
|
document.getElementById('trim-inputs').style.display = mode==='custom' ? 'flex' : 'none';
|
|
}
|
|
|
|
// ── Download Execution ───────────────────────────────────────────────────
|
|
function startDownload() {
|
|
if (downloading) return;
|
|
if (!activePlatform) { showToast('select youtube or instagram first'); return; }
|
|
runDownload(false);
|
|
}
|
|
|
|
function quickDownload() {
|
|
if (downloading) return;
|
|
if (!activePlatform) { showToast('paste a link first'); return; }
|
|
runDownload(true);
|
|
}
|
|
|
|
async function runDownload(quick) {
|
|
const raw = document.getElementById('urlInput').value.trim();
|
|
if (!raw) { showToast('paste a link first'); return; }
|
|
downloading = true;
|
|
setButtons(false);
|
|
|
|
const wrap = document.getElementById('progressWrap');
|
|
const bar = document.getElementById('progressBar');
|
|
const pct = document.getElementById('progressPct');
|
|
const stat = document.getElementById('progressStatus');
|
|
const lbl = document.getElementById('progressLabel');
|
|
|
|
wrap.className = 'progress-wrap show';
|
|
bar.style.width = '0%'; pct.textContent = '0%';
|
|
lbl.textContent = 'sending request...';
|
|
stat.textContent = 'connecting to backend...';
|
|
|
|
let fakeP = 0;
|
|
const fakeInterval = setInterval(() => {
|
|
if (fakeP < 85) {
|
|
fakeP += Math.random()*4 + 1;
|
|
bar.style.width = fakeP + '%';
|
|
pct.textContent = Math.round(fakeP) + '%';
|
|
}
|
|
}, 400);
|
|
const statuses = ['fetching via yt-dlp...','downloading stream...','muxing media...','finalizing...'];
|
|
let si = 0;
|
|
const statInterval = setInterval(() => {
|
|
if (si < statuses.length) stat.textContent = statuses[si++];
|
|
}, 2800);
|
|
|
|
try {
|
|
const opt = activeOpts[activePlatform];
|
|
|
|
// Channel art: returns JSON with list of images
|
|
if (activePlatform === 'yt' && opt === 'yt-channel') {
|
|
lbl.textContent = 'fetching channel art...';
|
|
const res = await fetch(`${API}/api/youtube/channel-art`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ channel_url: raw })
|
|
});
|
|
clearInterval(fakeInterval); clearInterval(statInterval);
|
|
if (!res.ok) throw new Error(await res.text());
|
|
const data = await res.json();
|
|
bar.style.width = '100%'; pct.textContent = '100%'; stat.textContent = 'opening files...';
|
|
for (const asset of data.assets) {
|
|
const a = document.createElement('a');
|
|
a.href = `${API}${asset.url}`;
|
|
a.download = asset.filename;
|
|
document.body.appendChild(a); a.click(); document.body.removeChild(a);
|
|
await new Promise(r => setTimeout(r, 600));
|
|
}
|
|
showToast(`saved ${data.assets.length} file(s)`);
|
|
addHistory(`channel art (${data.assets.length} files)`);
|
|
setTimeout(() => { wrap.className='progress-wrap'; bar.style.width='0%'; }, 1500);
|
|
return;
|
|
}
|
|
|
|
// Instagram profile avatar
|
|
if (activePlatform === 'ig' && opt === 'ig-profile') {
|
|
lbl.textContent = 'fetching profile picture...';
|
|
const username = extractIgUsername(raw);
|
|
const res = await fetch(`${API}/api/instagram/profile`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ username })
|
|
});
|
|
clearInterval(fakeInterval); clearInterval(statInterval);
|
|
if (!res.ok) throw new Error(await res.text());
|
|
bar.style.width = '95%'; pct.textContent = '95%'; stat.textContent = 'saving file...';
|
|
await triggerBlobDownload(res, `${username}_profile.jpg`);
|
|
bar.style.width = '100%'; pct.textContent = '100%'; stat.textContent = 'done';
|
|
showToast('download complete');
|
|
addHistory(`${username}_profile.jpg`);
|
|
setTimeout(() => { wrap.className='progress-wrap'; bar.style.width='0%'; }, 1500);
|
|
return;
|
|
}
|
|
|
|
// Standard single-file download
|
|
const endpoint = getEndpoint(quick);
|
|
const body = buildBody(raw, quick);
|
|
lbl.textContent = 'downloading — ' + (activePlatform==='yt' ? 'YouTube' : 'Instagram');
|
|
|
|
const res = await fetch(`${API}${endpoint}`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(body)
|
|
});
|
|
clearInterval(fakeInterval); clearInterval(statInterval);
|
|
if (!res.ok) throw new Error(await res.text());
|
|
|
|
bar.style.width = '95%'; pct.textContent = '95%'; stat.textContent = 'saving file...';
|
|
const filename = await triggerBlobDownload(res, guessFilename());
|
|
bar.style.width = '100%'; pct.textContent = '100%'; stat.textContent = 'done';
|
|
showToast('download complete');
|
|
addHistory(filename);
|
|
setTimeout(() => { wrap.className='progress-wrap'; bar.style.width='0%'; }, 2000);
|
|
|
|
} catch (e) {
|
|
clearInterval(fakeInterval); clearInterval(statInterval);
|
|
wrap.className = 'progress-wrap'; bar.style.width = '0%';
|
|
showToast('error: ' + e.message.slice(0, 60));
|
|
} finally {
|
|
downloading = false;
|
|
setButtons(true);
|
|
}
|
|
}
|
|
|
|
async function triggerBlobDownload(res, fallbackName) {
|
|
const disposition = res.headers.get('content-disposition') || '';
|
|
const filename = disposition.split('filename=')[1]?.replace(/"/g,'') || fallbackName;
|
|
const blob = await res.blob();
|
|
const a = document.createElement('a');
|
|
a.href = URL.createObjectURL(blob);
|
|
a.download = filename;
|
|
document.body.appendChild(a); a.click(); document.body.removeChild(a);
|
|
setTimeout(() => URL.revokeObjectURL(a.href), 5000);
|
|
return filename;
|
|
}
|
|
|
|
function getEndpoint(quick) {
|
|
if (activePlatform === 'yt') {
|
|
if (quick) return '/api/youtube/video';
|
|
const opt = activeOpts.yt;
|
|
if (opt==='yt-video') return '/api/youtube/video';
|
|
if (opt==='yt-audio') return '/api/youtube/audio';
|
|
if (opt==='yt-thumb') return '/api/youtube/thumbnail';
|
|
if (opt==='yt-subs') return '/api/youtube/subtitles';
|
|
}
|
|
if (activePlatform === 'ig') {
|
|
const opt = activeOpts.ig;
|
|
if (opt==='ig-post') return '/api/instagram/post';
|
|
if (opt==='ig-reel') return '/api/instagram/reel';
|
|
if (opt==='ig-thumb') return '/api/instagram/thumbnail';
|
|
}
|
|
return '/api/youtube/video';
|
|
}
|
|
|
|
function buildBody(raw, quick) {
|
|
if (activePlatform === 'yt') {
|
|
const url = cleanYtUrl(raw);
|
|
if (quick) return { url, resolution: 'best', format: 'mp4' };
|
|
const opt = activeOpts.yt;
|
|
if (opt==='yt-video') {
|
|
const res = document.getElementById('yt-res').value;
|
|
const fmt = document.querySelector('#yt-fmt-toggle .on')?.textContent.toLowerCase() || 'mp4';
|
|
const start = document.getElementById('startTime').value || null;
|
|
const end = document.getElementById('endTime').value || null;
|
|
return { url, resolution: res, format: fmt, start_time: start, end_time: end };
|
|
}
|
|
if (opt==='yt-audio') return { url, quality: '192', format: 'mp3' };
|
|
if (opt==='yt-subs') return { url, lang: 'en' };
|
|
return { url };
|
|
}
|
|
if (activePlatform === 'ig') {
|
|
const url = raw.split('?')[0].replace(/\/$/, '');
|
|
const fmt = document.querySelector('#ig-fmt-toggle .on')?.textContent.toLowerCase() || 'mp4';
|
|
if (activeOpts.ig==='ig-post' || activeOpts.ig==='ig-reel') return { url, format: fmt };
|
|
return { url };
|
|
}
|
|
return { url: raw };
|
|
}
|
|
|
|
function guessFilename() {
|
|
const opt = activeOpts[activePlatform] || '';
|
|
if (opt.includes('audio')) return 'audio.mp3';
|
|
if (opt.includes('thumb')) return 'thumbnail.jpg';
|
|
if (opt.includes('subs')) return 'subtitles.srt';
|
|
return activePlatform==='ig' ? 'post.mp4' : 'video.mp4';
|
|
}
|
|
|
|
function setButtons(enabled) {
|
|
['dlBtn','quickBtn','fetchBtn'].forEach(id => {
|
|
const b = document.getElementById(id);
|
|
if (b) b.disabled = !enabled;
|
|
});
|
|
}
|
|
|
|
function addHistory(filename) {
|
|
downloadHistory.unshift({
|
|
title: filename,
|
|
platform: activePlatform,
|
|
time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
|
});
|
|
renderHistory();
|
|
}
|
|
|
|
function renderHistory() {
|
|
const sec = document.getElementById('historySection');
|
|
const list = document.getElementById('historyList');
|
|
sec.style.display = downloadHistory.length ? 'block' : 'none';
|
|
list.innerHTML = downloadHistory.slice(0, 8).map(item => `
|
|
<div class="history-item">
|
|
<span class="history-tag" style="color:var(--${item.platform==='yt'?'yt':'ig'})">[${item.platform.toUpperCase()}]</span>
|
|
<div class="history-info">
|
|
<div class="history-title">${escapeHtml(item.title)}</div>
|
|
<div class="history-meta">// ${item.time}</div>
|
|
</div>
|
|
<div class="history-status">✓ done</div>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
function escapeHtml(str) {
|
|
return String(str).replace(/[&<>'"]/g, tag => ({
|
|
'&': '&',
|
|
'<': '<',
|
|
'>': '>',
|
|
"'": ''',
|
|
'"': '"'
|
|
}[tag] || tag));
|
|
}
|
|
|
|
// ── Event Listeners ──────────────────────────────────────────────────────
|
|
document.getElementById('urlInput').addEventListener('paste', () => setTimeout(handleUrl, 60));
|
|
document.getElementById('urlInput').addEventListener('keydown', e => {
|
|
if (e.key === 'Enter') handleUrl();
|
|
});
|
|
|
|
const zone = document.getElementById('inputZone');
|
|
zone.addEventListener('dragover', e => { e.preventDefault(); zone.classList.add('drag-over'); });
|
|
zone.addEventListener('dragleave', () => zone.classList.remove('drag-over'));
|
|
zone.addEventListener('drop', e => {
|
|
e.preventDefault();
|
|
zone.classList.remove('drag-over');
|
|
const url = e.dataTransfer.getData('text');
|
|
if (url) {
|
|
document.getElementById('urlInput').value = url;
|
|
handleUrl();
|
|
}
|
|
});
|
|
|
|
switchPlatform('yt');
|
|
document.getElementById('yt-video-opts').style.display = 'block';
|
|
</script>
|
|
</body>
|
|
</html>
|