Files
dupe/client/src/routes/Transfers.svelte

1185 lines
29 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script>
import { onMount } from "svelte";
import { API, apiFetch, getAccessToken, withToken } from "../utils/api.js"; // ✅ apiFetch eklendi
let torrents = [];
let ws;
let isAllPaused = false;
let totalDownloaded = 0;
let totalDownloadSpeed = 0;
// Modal / player state
let showModal = false;
let selectedVideo = null;
let subtitleURL = null;
let subtitleLang = "en";
let subtitleLabel = "Custom Subtitles";
// Player kontrolleri
let videoEl;
let isPlaying = false;
let currentTime = 0;
let duration = 0;
let volume = 1;
// --- WebSocket & API ---
function wsConnect() {
const token = getAccessToken();
const url = `${API.replace("http", "ws")}?token=${token || ""}`;
ws = new WebSocket(url);
ws.onmessage = (e) => {
const d = JSON.parse(e.data);
if (d.type === "progress") {
torrents = d.torrents || [];
// Tüm torrentlerin pause durumunu kontrol et
updateAllPausedState();
// Toplam download miktarını ve hızını güncelle
updateTransferStats();
}
};
}
async function list() {
const r = await apiFetch("/api/torrents"); // ✅ fetch yerine apiFetch
if (!r.ok) return;
torrents = await r.json();
updateAllPausedState();
updateTransferStats();
}
async function upload(e) {
const f = e.target.files?.[0];
if (!f) return;
const fd = new FormData();
fd.append("torrent", f);
await apiFetch("/api/transfer", { method: "POST", body: fd }); // ✅
await list();
}
const YT_VIDEO_ID_RE = /^[A-Za-z0-9_-]{11}$/;
function isMagnetLink(value) {
if (!value || typeof value !== "string") return false;
const normalized = value.trim().toLowerCase();
return normalized.startsWith("magnet:?xt=");
}
function normalizeYoutubeUrl(value) {
if (!value || typeof value !== "string") return null;
try {
const url = new URL(value.trim());
if (url.protocol !== "https:") return null;
const host = url.hostname.toLowerCase();
if (host !== "youtube.com" && host !== "www.youtube.com") return null;
if (url.pathname !== "/watch") return null;
const videoId = url.searchParams.get("v");
if (!videoId || !YT_VIDEO_ID_RE.test(videoId)) return null;
return `https://www.youtube.com/watch?v=${videoId}`;
} catch {
return null;
}
}
async function handleUrlInput() {
const input = prompt("Magnet veya YouTube URL girin:");
if (!input) return;
if (isMagnetLink(input)) {
await apiFetch("/api/transfer", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ magnet: input })
});
await list();
return;
}
const normalizedYoutube = normalizeYoutubeUrl(input);
if (normalizedYoutube) {
const resp = await apiFetch("/api/youtube/download", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ url: normalizedYoutube })
});
if (!resp.ok) {
const data = await resp.json().catch(() => null);
alert(data?.error || "YouTube indirmesi başlatılamadı");
return;
}
await list();
return;
}
alert(
"Yalnızca magnet linkleri veya https://www.youtube.com/watch?v=... formatındaki YouTube URL'leri destekleniyor."
);
}
async function selectFile(hash, index) {
try {
await apiFetch(`/api/torrents/${hash}/select/${index}`, {
method: "POST"
});
} catch (err) {
console.error("Select file error:", err);
}
}
async function removeTorrent(hash) {
if (!confirm("Bu transferi silmek istediğine emin misin?")) return;
await apiFetch(`/api/torrents/${hash}`, { method: "DELETE" });
torrents = torrents.filter((t) => t.infoHash !== hash);
await list();
}
async function removeAllTorrents() {
if (!confirm("Tüm torrent listesini silmek istediğinizden emin misiniz?")) return;
// Tüm torrentleri API üzerinden sil
for (const torrent of torrents) {
await apiFetch(`/api/torrents/${torrent.infoHash}`, { method: "DELETE" });
}
// Listeyi güncelle
await list();
}
async function toggleAllTorrents() {
const action = isAllPaused ? "resume" : "pause";
try {
const r = await apiFetch("/api/torrents/toggle-all", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action })
});
if (!r.ok) return;
const result = await r.json();
console.log(`${action} işlemi: ${result.updatedCount}/${result.totalCount} torrent güncellendi`);
// Durumu güncelle
isAllPaused = !isAllPaused;
// Listeyi yenile
await list();
} catch (err) {
console.error("Toggle all torrents error:", err);
}
}
function updateAllPausedState() {
const torrentOnly = torrents.filter((t) => !t.type || t.type === "torrent");
if (torrentOnly.length === 0) {
isAllPaused = false;
return;
}
// Eğer tüm torrentler paused ise, global durumu paused yap
const allPaused = torrentOnly.every((t) => t.paused === true);
isAllPaused = allPaused;
}
function updateTransferStats() {
totalDownloaded = torrents.reduce((sum, t) => sum + (t.downloaded || 0), 0);
totalDownloadSpeed = torrents.reduce((sum, t) => sum + (t.downloadSpeed || 0), 0);
}
async function toggleSingleTorrent(hash) {
const torrent = torrents.find(t => t.infoHash === hash);
if (!torrent) return;
const action = torrent.paused ? "resume" : "pause";
try {
const r = await apiFetch(`/api/torrents/${hash}/toggle`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action })
});
if (!r.ok) return;
const result = await r.json();
console.log(`Single torrent ${action}:`, result);
// Listeyi güncelle
await list();
} catch (err) {
console.error("Toggle single torrent error:", err);
}
}
function streamURL(hash, index = 0) {
const base = `${API}/stream/${hash}?index=${index}`;
return withToken(base);
}
function formatSpeed(bytesPerSec) {
if (!bytesPerSec || bytesPerSec <= 0) return "0 MB/s";
return (bytesPerSec / 1e6).toFixed(2) + " MB/s";
}
function formatDate(value) {
if (!value) return "Unknown";
try {
return new Date(value).toLocaleString();
} catch {
return "Unknown";
}
}
function openModal(t) {
if (!t.files || !t.files.length) {
alert("Bu indirme için oynatılabilir video bulunamadı.");
return;
}
const selectedFile =
t.files.find((f) => f.index === t.selectedIndex) || t.files[0];
if (!selectedFile) {
alert("Bu indirmede oynatılabilir video dosyası bulunamadı!");
return;
}
selectedVideo = {
...t,
fileIndex: selectedFile.index,
fileName: selectedFile.name,
type: t.type || "torrent"
};
showModal = true;
}
function closeModal() {
showModal = false;
selectedVideo = null;
subtitleURL = null;
}
// --- Altyazı işlemleri (hiç değişmedi) ---
function detectSubtitleLang(text) {
const lower = (text || "").toLowerCase();
if (lower.includes("ş") || lower.includes("ğ") || lower.includes("ı"))
return { code: "tr", label: "Türkçe" };
if (lower.includes("é") || lower.includes("è") || lower.includes("à"))
return { code: "fr", label: "Français" };
if (lower.includes("¿") || lower.includes("¡") || lower.includes("ñ"))
return { code: "es", label: "Español" };
if (lower.includes("ß") || lower.includes("ä") || lower.includes("ü"))
return { code: "de", label: "Deutsch" };
return { code: "en", label: "English" };
}
function srtToVtt(srtText) {
const utf8BOM = "\uFEFF";
return (
utf8BOM +
"WEBVTT\n\n" +
srtText
.replace(/\r+/g, "")
.replace(/^\s+|\s+$/g, "")
.split("\n\n")
.map((block) => {
const lines = block.split("\n");
if (lines.length >= 2) {
const time = lines[1]
.replace(/,/g, ".")
.replace(/(\d{2}):(\d{2}):(\d{2})/g, "$1:$2:$3");
return lines.slice(1).join("\n").replace(lines[1], time);
}
return lines.join("\n");
})
.join("\n\n")
);
}
function handleSubtitleUpload(e) {
const file = e.target.files?.[0];
if (!file) return;
const ext = file.name.split(".").pop().toLowerCase();
const reader = new FileReader();
reader.onload = (ev) => {
const decoder = new TextDecoder("utf-8");
const content =
typeof ev.target.result === "string"
? ev.target.result
: decoder.decode(ev.target.result);
const detected = detectSubtitleLang(content);
subtitleLang = detected.code;
subtitleLabel = detected.label;
if (ext === "srt") {
const vttText = srtToVtt(content);
const blob = new Blob([vttText], {
type: "text/vtt;charset=utf-8"
});
subtitleURL = URL.createObjectURL(blob);
} else if (ext === "vtt") {
const blob = new Blob([content], {
type: "text/vtt;charset=utf-8"
});
subtitleURL = URL.createObjectURL(blob);
} else {
alert("Yalnızca .srt veya .vtt dosyaları destekleniyor.");
}
};
reader.readAsArrayBuffer(file);
}
// ESC ile kapatma
function onEsc(e) {
if (e.key === "Escape" && showModal) closeModal();
}
// Player kontrolleri
function togglePlay() {
if (!videoEl) return;
if (isPlaying) videoEl.pause();
else videoEl.play();
isPlaying = !isPlaying;
}
function updateProgress() {
currentTime = videoEl?.currentTime || 0;
}
function updateDuration() {
duration = videoEl?.duration || 0;
}
function seekVideo(e) {
if (!videoEl) return;
const newTime = parseFloat(e.target.value);
if (Math.abs(videoEl.currentTime - newTime) > 0.2) {
videoEl.currentTime = newTime;
}
}
function changeVolume(e) {
if (!videoEl) return;
const val = parseFloat(e.target.value);
videoEl.volume = val;
e.target.style.setProperty("--fill", (val || 0) * 100);
}
function toggleFullscreen() {
if (!videoEl) return;
if (document.fullscreenElement) document.exitFullscreen();
else videoEl.requestFullscreen();
}
function formatTime(seconds) {
const m = Math.floor(seconds / 60)
.toString()
.padStart(2, "0");
const s = Math.floor(seconds % 60)
.toString()
.padStart(2, "0");
return `${m}:${s}`;
}
let dragActive = false;
let pageDragOverlay = false;
let dragCounter = 0;
// sadece drop-zone alanına gelen olayları işleme
function handleDragOver(e) {
e.preventDefault();
e.stopPropagation();
if (torrents.length === 0) dragActive = true;
else pageDragOverlay = true;
}
function handleDragEnter(e) {
e.preventDefault();
e.stopPropagation();
dragCounter++;
if (torrents.length === 0) dragActive = true;
else pageDragOverlay = true;
}
function handleDragLeave(e) {
e.preventDefault();
e.stopPropagation();
dragCounter--;
if (dragCounter <= 0) {
dragActive = false;
pageDragOverlay = false;
}
}
async function handleDrop(e) {
e.preventDefault();
e.stopPropagation();
dragCounter = 0;
dragActive = false;
pageDragOverlay = false;
const files = Array.from(e.dataTransfer?.files || []);
const torrentsToUpload = files.filter((f) => f.name.endsWith(".torrent"));
if (!torrentsToUpload.length) return;
for (const file of torrentsToUpload) {
const fd = new FormData();
fd.append("torrent", file);
await apiFetch("/api/transfer", { method: "POST", body: fd });
}
await list();
}
// 🧩 Global dinleyiciler — sadece overlay için, drop'u engellemeden
function addGlobalDragListeners() {
window.addEventListener("dragenter", handleDragEnter);
window.addEventListener("dragleave", handleDragLeave);
window.addEventListener("dragover", (e) => e.preventDefault());
window.addEventListener("drop", (e) => {
e.preventDefault();
dragCounter = 0;
dragActive = false;
pageDragOverlay = false;
});
}
onMount(() => {
list(); // 🔒 token'lı liste çekimi
wsConnect(); // 🔒 token'lı WebSocket
addGlobalDragListeners();
const slider = document.querySelector(".volume-slider");
if (slider) {
slider.value = volume;
slider.style.setProperty("--fill", slider.value * 100);
}
window.addEventListener("keydown", onEsc);
return () => window.removeEventListener("keydown", onEsc);
});
</script>
<!-- 💡 HTML ve stil kısmı aynı kalıyor -->
<section class="files">
<h2>Transfers</h2>
<div style="display:flex; gap:10px; margin-bottom:10px; justify-content: space-between;">
<div style="display:flex; gap:10px;">
<label class="btn-primary" style="cursor:pointer;">
<i class="fa-solid fa-plus btn-icon"></i> NEW TRANSFER
<input
type="file"
accept=".torrent"
on:change={upload}
style="display:none;"
/>
</label>
<label class="btn-primary" on:click={handleUrlInput}>
<i class="fa-solid fa-magnet btn-icon"></i> ADD URL
</label>
</div>
<div style="display:flex; gap:10px;" title="Total Transfer Speed">
<div class="transfer-info-box">
<div class="transfer-speed">
<i class="fa-solid fa-down-long"></i>
<span>
{formatSpeed(totalDownloadSpeed)}
</span>
</div>
</div>
<button
class="btn-toggle-all"
on:click={toggleAllTorrents}
title={isAllPaused ? "Resume All Torrents" : "Pause All Torrents"}
>
{#if isAllPaused}
<i class="fa-solid fa-play"></i>
{:else}
<i class="fa-solid fa-pause"></i>
{/if}
</button>
<button
class="btn-remove-all"
on:click={removeAllTorrents}
title="Remove All Torrent List"
>
<i class="fa-solid fa-trash"></i>
</button>
</div>
</div>
{#if torrents.length === 0}
<div
class="empty drop-zone {dragActive ? 'active' : ''}"
on:dragenter={handleDragEnter}
on:dragover={handleDragOver}
on:dragleave={handleDragLeave}
on:drop={handleDrop}
>
<div class="drop-inner">
<i class="fa-solid fa-cloud-arrow-up"></i>
<div class="title">Drop your .torrent file here</div>
<div class="subtitle">or use the buttons above</div>
</div>
</div>
{:else}
<div
class="torrent-list"
on:dragenter={handleDragEnter}
on:dragover={handleDragOver}
on:dragleave={handleDragLeave}
on:drop={handleDrop}
>
{#each torrents as t (t.infoHash)}
<div class="torrent" on:click={() => openModal(t)}>
{#if t.thumbnail}
<img
src={withToken(`${API}${t.thumbnail}`)}
alt="thumb"
class="thumb"
on:load={(e) => e.target.classList.add("loaded")}
/>
{:else}
<div class="thumb placeholder">
<i class="fa-regular fa-image"></i>
</div>
{/if}
<div class="torrent-info">
<div class="torrent-header">
<div class="torrent-title">
<div class="torrent-name">{t.name}</div>
{#if t.type === "youtube"}
<div class="torrent-subtitle">
Added: {formatDate(t.added)}
</div>
{/if}
</div>
<div style="display:flex; gap:5px;">
{#if t.type !== "youtube"}
<button
class="toggle-btn"
on:click|stopPropagation={() => toggleSingleTorrent(t.infoHash)}
title={t.paused ? "Devam Ettir" : "Durdur"}
>
{#if t.paused}
<i class="fa-solid fa-play"></i>
{:else}
<i class="fa-solid fa-pause"></i>
{/if}
</button>
{/if}
<button
class="remove-btn"
on:click|stopPropagation={() => removeTorrent(t.infoHash)}
title="Sil"></button
>
</div>
</div>
<div class="torrent-hash">
{#if t.type === "youtube"}
Source: YouTube | Added:
{t.added ? formatDate(t.added) : "Unknown"}
{:else}
Hash: {t.infoHash} | Tracker: {t.tracker ?? "Unknown"} | Added:
{t.added ? formatDate(t.added) : "Unknown"}
{/if}
</div>
{#if t.files && t.files.length}
<div class="torrent-files">
{#each t.files as f}
<div class="file-row">
<button
on:click|stopPropagation={() =>
selectFile(t.infoHash, f.index)}
>
{f.index === t.selectedIndex ? "Selected" : "Select"}
</button>
<div class="filename">{f.name}</div>
<div class="filesize">
{(f.length / 1e6).toFixed(1)} MB
</div>
</div>
{/each}
</div>
{/if}
<div class="progress-bar">
<div
class="progress"
style="width:{(t.progress || 0) * 100}%"
></div>
</div>
<div class="progress-text">
{#if (t.progress || 0) < 1}
{(t.progress * 100).toFixed(1)}% •
{t.downloaded ? (t.downloaded / 1e6).toFixed(1) : 0} MB •
{formatSpeed(t.downloadSpeed)}
{#if t.type !== "youtube"}
{t.numPeers ?? 0} peers
{/if}
{:else}
100.0% • {(t.downloaded / 1e6).toFixed(1)} MB
{/if}
</div>
{#if t.status === "error"}
<div class="torrent-error">Download failed</div>
{/if}
</div>
</div>
{/each}
{#if pageDragOverlay}
<div class="page-drop-overlay">
<div class="page-drop-text">
<i class="fa-solid fa-cloud-arrow-up"></i>
<span>Drop to add torrent</span>
</div>
</div>
{/if}
</div>
{/if}
</section>
{#if showModal && selectedVideo}
<div class="modal-overlay" on:click={closeModal}>
<!-- 🟢 Global Close Button (Files.svelte ile aynı) -->
<button class="global-close-btn" on:click|stopPropagation={closeModal}
>✕</button
>
<div class="modal-content" on:click|stopPropagation>
<div class="modal-header">
<div class="video-title">{selectedVideo.name}</div>
</div>
<div class="custom-player">
<video
bind:this={videoEl}
src={streamURL(selectedVideo.infoHash, selectedVideo.fileIndex)}
class="video-element"
on:timeupdate={updateProgress}
on:loadedmetadata={() => {
updateDuration();
const slider = document.querySelector(".volume-slider");
if (slider) {
slider.value = volume;
slider.style.setProperty("--fill", slider.value * 100);
}
}}
>
{#if subtitleURL}
<track
kind="subtitles"
src={subtitleURL}
srclang={subtitleLang}
label={subtitleLabel}
default
/>
{/if}
</video>
<div class="controls">
<div class="top-controls">
<button class="control-btn" on:click={togglePlay}>
{#if isPlaying}<i class="fa-solid fa-pause"></i>{:else}<i
class="fa-solid fa-play"
></i>{/if}
</button>
<div class="right-controls">
<input
type="range"
min="0"
max="1"
step="0.01"
bind:value={volume}
on:input={changeVolume}
class="volume-slider"
/>
<button class="control-btn" on:click={toggleFullscreen}>
<i class="fa-solid fa-expand"></i>
</button>
<a
href={streamURL(
selectedVideo.infoHash,
selectedVideo.fileIndex
)}
download={selectedVideo.name}
class="control-btn"
title="Download"
>
<i class="fa-solid fa-download"></i>
</a>
<label class="control-btn subtitle-icon" title="Add subtitles">
<i class="fa-solid fa-closed-captioning"></i>
<input
type="file"
accept=".srt,.vtt"
on:change={handleSubtitleUpload}
style="display: none"
/>
</label>
</div>
</div>
<div class="bottom-controls">
<span class="time">
{formatTime(currentTime)} / {formatTime(duration)}
</span>
<input
type="range"
min="0"
max={duration}
step="0.1"
bind:value={currentTime}
on:input={seekVideo}
class="progress-slider"
/>
</div>
</div>
</div>
</div>
</div>
{/if}
<style>
/* --- Torrent Listeleme --- */
.torrent-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.torrent {
display: grid;
grid-template-columns: 100px 1fr;
align-items: flex-start;
gap: 12px;
border: 1px solid #ccc;
background: #f6f6f6;
border-radius: 8px;
padding: 10px 12px 0 12px;
box-sizing: border-box;
cursor: pointer;
}
.thumb {
width: 100px;
height: 60px;
border-radius: 6px;
object-fit: cover;
background: #ddd;
flex-shrink: 0;
}
.placeholder {
width: 100px;
height: 60px;
display: flex;
align-items: center;
justify-content: center;
background: #ddd;
border-radius: 6px;
font-size: 24px;
}
.torrent-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
}
.torrent-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
font-weight: 700;
}
.torrent-title {
display: flex;
flex-direction: column;
gap: 2px;
}
.torrent-name {
word-break: break-word;
}
.torrent-subtitle {
font-size: 12px;
font-weight: 400;
color: #666;
}
.toggle-btn {
background: transparent;
border: none;
font-size: 18px;
cursor: pointer;
transition: transform 0.15s;
color: #4caf50;
padding: 2px;
border-radius: 4px;
}
.toggle-btn:hover {
transform: scale(1.2);
background: rgba(76, 175, 80, 0.1);
}
.remove-btn {
background: transparent;
border: none;
font-size: 18px;
cursor: pointer;
transition: transform 0.15s;
}
.remove-btn:hover {
transform: scale(1.2);
}
.torrent-hash {
font-size: 12px;
color: #777;
font-family: monospace;
}
.torrent-files {
display: flex;
flex-direction: column;
gap: 2px;
}
.file-row {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
}
.file-row button {
background: #eee;
border: none;
padding: 2px 8px;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
}
.file-row button:hover {
background: #ddd;
}
.filename {
flex: 1;
}
.filesize {
color: #666;
font-size: 12px;
}
/* --- İlerleme Çubuğu --- */
.progress-bar {
width: 100%;
height: 6px;
background: #ddd;
border-radius: 3px;
overflow: hidden;
}
.progress {
height: 100%;
background: linear-gradient(90deg, #27ae60, #2ecc71);
transition: width 0.3s;
}
.torrent-error {
color: #e74c3c;
font-size: 12px;
margin-top: 4px;
}
.progress-text {
font-size: 12px;
color: #444;
text-align: right;
padding: 3px 0 8px 0;
}
/* 🪄 Drop Zone */
/* === 🧊 Drop Zone (boş sayfa görünümü) === */
.drop-zone {
border: 2px dashed rgba(160, 160, 160, 0.4);
border-radius: 12px;
padding: 60px 20px;
text-align: center;
background: rgba(245, 245, 245, 0.5);
transition: background 0.3s ease;
}
.drop-zone.active {
backdrop-filter: blur(10px) brightness(0.9);
background: rgba(150, 150, 150, 0.35);
border-color: rgba(100, 100, 100, 0.6);
transition: all 0.3s ease;
}
.drop-inner {
color: #777;
}
.drop-inner i {
font-size: 42px;
color: #aaa;
}
.drop-inner .title {
font-weight: 600;
margin-top: 6px;
}
.drop-inner .subtitle {
font-size: 13px;
color: #999;
}
/* === Liste doluyken sayfa üstü blur === */
.page-drop-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
backdrop-filter: blur(8px);
background: rgba(200, 200, 200, 0.4);
border-radius: 12px;
z-index: 200;
display: flex;
align-items: center;
justify-content: center;
animation: fadeIn 0.2s ease;
}
.page-drop-text {
color: #666;
display: flex;
flex-direction: column;
align-items: center;
font-weight: 600;
font-size: 18px;
}
.page-drop-text i {
font-size: 42px;
margin-bottom: 8px;
color: #888;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
/* --- Responsive Düzenlemeler --- */
@media (max-width: 1024px) {
.torrent {
grid-template-columns: 80px 1fr;
gap: 10px;
}
.torrent-hash {
font-size: 11px;
line-height: 1.3;
}
.torrent-header {
flex-direction: column;
align-items: flex-start;
gap: 4px;
}
.torrent-files .file-row {
font-size: 12px;
}
}
@media (max-width: 768px) {
.torrent {
grid-template-columns: 1fr;
gap: 8px;
}
.thumb {
width: 100%;
height: 180px;
}
.torrent-hash {
word-break: break-word;
white-space: normal;
}
.torrent-files {
gap: 4px;
}
.file-row {
flex-direction: column;
align-items: flex-start;
}
.progress-text {
text-align: left;
font-size: 11px;
}
.torrent-list {
gap: 10px;
}
}
@media (max-width: 480px) {
.torrent-header {
font-size: 13px;
}
.torrent-hash {
font-size: 10px;
}
}
/* --- Toggle All Torrents Button --- */
.btn-toggle-all {
background: transparent;
border: 1px solid #ddd;
color: #666;
padding: 10px 14px;
border-radius: 6px;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
height: 36px;
width: 36px;
transition: all 0.2s ease;
font-size: 14px;
}
.btn-toggle-all:hover {
background: var(--yellow);
border-color: var(--yellow-dark);
color: #222;
transform: scale(1.05);
}
.btn-toggle-all:active {
transform: scale(0.95);
}
/* --- Remove All Torrent List Button --- */
.btn-remove-all {
background: transparent;
border: 1px solid #ddd;
color: #666;
padding: 10px 14px;
border-radius: 6px;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
height: 36px;
width: 36px;
transition: all 0.2s ease;
font-size: 14px;
}
.btn-remove-all:hover {
background: #ff4444;
border-color: #cc0000;
color: white;
transform: scale(1.05);
}
.btn-remove-all:active {
transform: scale(0.95);
}
/* Responsive adjustments for toggle and remove buttons */
@media (max-width: 768px) {
.btn-toggle-all,
.btn-remove-all {
height: 36px;
width: 36px;
padding: 8px;
}
}
@media (max-width: 480px) {
.btn-toggle-all,
.btn-remove-all {
height: 34px;
width: 34px;
font-size: 12px;
}
}
/* --- Transfer Info Box --- */
.transfer-info-box {
background: #f8f9fa;
border: 1px solid #ddd;
border-radius: 6px;
padding: 8px 8px;
display: flex;
align-items: center;
justify-content: center;
min-width: 100px;
height: 36px;
cursor: default;
}
.transfer-speed {
display: flex;
align-items: center;
justify-content: center;
gap: 5px;
font-size: 11px;
color: #666;
cursor: default;
}
.transfer-speed i {
color: #4caf50;
}
/* Responsive adjustments for transfer info box */
@media (max-width: 768px) {
.transfer-info-box {
min-width: 85px;
padding: 6px 6px;
}
.transfer-speed {
font-size: 10px;
}
}
@media (max-width: 480px) {
.transfer-info-box {
min-width: 75px;
padding: 5px 5px;
}
.transfer-speed {
font-size: 9px;
}
}
</style>