Added image modal

This commit is contained in:
2025-10-23 22:24:39 +03:00
parent 7a3a244c6f
commit 8fc749b735
8 changed files with 745 additions and 708 deletions

View File

@@ -11,30 +11,38 @@
const token = localStorage.getItem("token");
let menuOpen = false;
// Menü aç/kapat (hamburger butonuyla)
const toggleMenu = () => {
menuOpen = !menuOpen;
};
// 🔹 Sidebar'ı kapatma fonksiyonu
function closeSidebar() {
menuOpen = false;
}
</script>
{#if token}
<Router>
<div class="app">
<Sidebar {menuOpen} />
<!-- Sidebar -->
<Sidebar {menuOpen} on:closeMenu={closeSidebar} />
<!-- İçerik -->
<div class="content">
<Topbar on:toggleMenu={toggleMenu} />
<Route path="/" component={Files} />
<Route path="/files" component={Files} />
<Route path="/transfers" component={Transfers} />
<Route path="/sharing" component={Sharing} />
<Route path="/trash" component={Trash} />
</div>
<!-- Sidebar dışına tıklayınca kapanma -->
{#if menuOpen}
<div
class="backdrop show"
on:click={() => {
menuOpen = false;
}}
></div>
<div class="backdrop show" on:click={closeSidebar}></div>
{/if}
</div>
</Router>

View File

@@ -1,22 +1,66 @@
<script>
import { Link } from "svelte-routing";
import { createEventDispatcher } from "svelte";
export let menuOpen = false;
const dispatch = createEventDispatcher();
// Menü öğesine tıklanınca sidebar'ı kapat
function handleLinkClick() {
dispatch("closeMenu");
}
</script>
<div class="sidebar" class:open={menuOpen}>
<div class="logo">du.pe</div>
<div class="menu">
<Link to="/" class="item" getProps={({ isCurrent }) => ({ class: isCurrent ? "item active" : "item" })}>
<i class="fa-solid fa-folder icon"></i> Files
<Link
to="/"
class="item"
getProps={({ isCurrent }) => ({
class: isCurrent ? "item active" : "item",
})}
on:click={handleLinkClick}
>
<i class="fa-solid fa-folder icon"></i>
Files
</Link>
<Link to="/transfers" class="item" getProps={({ isCurrent }) => ({ class: isCurrent ? "item active" : "item" })}>
<i class="fa-solid fa-arrow-down icon"></i> Transfers
<Link
to="/transfers"
class="item"
getProps={({ isCurrent }) => ({
class: isCurrent ? "item active" : "item",
})}
on:click={handleLinkClick}
>
<i class="fa-solid fa-arrow-down icon"></i>
Transfers
</Link>
<Link to="/sharing" class="item" getProps={({ isCurrent }) => ({ class: isCurrent ? "item active" : "item" })}>
<i class="fa-solid fa-share-nodes icon"></i> Sharing
<Link
to="/sharing"
class="item"
getProps={({ isCurrent }) => ({
class: isCurrent ? "item active" : "item",
})}
on:click={handleLinkClick}
>
<i class="fa-solid fa-share-nodes icon"></i>
Sharing
</Link>
<Link to="/trash" class="item" getProps={({ isCurrent }) => ({ class: isCurrent ? "item active" : "item" })}>
<i class="fa-solid fa-trash icon"></i> Trash
<Link
to="/trash"
class="item"
getProps={({ isCurrent }) => ({
class: isCurrent ? "item active" : "item",
})}
on:click={handleLinkClick}
>
<i class="fa-solid fa-trash icon"></i>
Trash
</Link>
</div>
</div>

View File

@@ -1,15 +1,20 @@
<script>
import { createEventDispatcher } from "svelte";
import { createEventDispatcher, onMount } from "svelte";
const dispatch = createEventDispatcher();
let search = "";
export let placeholder = "Search files...";
const onToggle = () => dispatch("toggleMenu");
</script>
<div class="topbar">
<!-- Mobilde görünen hamburger -->
<button class="menu-toggle" on:click={onToggle} aria-label="Toggle menu">
<!-- 🔹 Hamburger butonu sadece küçük ekranlarda gösterilir -->
<button
class="menu-toggle"
on:click={onToggle}
aria-label="Toggle menu"
>
<i class="fa-solid fa-bars"></i>
</button>
@@ -18,3 +23,51 @@
<input placeholder={placeholder} bind:value={search} />
</div>
</div>
<style>
.topbar {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
border-bottom: 1px solid var(--border);
}
.search {
flex: 1;
display: flex;
align-items: center;
gap: 8px;
background: #f8f8f8;
border: 1px solid var(--border);
border-radius: 6px;
padding: 8px 12px;
}
.search input {
border: none;
outline: none;
background: transparent;
flex: 1;
}
/* 🟡 Hamburger sadece küçük ekranlarda görünsün */
.menu-toggle {
display: none;
background: none;
border: none;
font-size: 20px;
color: #333;
cursor: pointer;
}
@media (max-width: 768px) {
.menu-toggle {
display: inline-flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
}
}
</style>

View File

@@ -1,5 +1,5 @@
<script>
import { onMount } from "svelte";
import { onMount, tick } from "svelte";
import { API, apiFetch } from "../utils/api.js";
import { cleanFileName } from "../utils/filename.js";
@@ -16,6 +16,10 @@
let currentTime = 0;
let duration = 0;
let volume = 1;
let currentIndex;
let showImageModal = false;
let selectedImage = null;
// ✅ REACTIVE: selectedVideo güvenli kullanımlar
$: selectedName = selectedVideo?.name ?? "";
@@ -42,23 +46,81 @@
return (bytes / 1e9).toFixed(2) + " GB";
}
function openModal(f) {
selectedVideo = f;
showModal = true;
async function openModal(f) {
stopCurrentVideo();
videoEl = null;
isPlaying = false;
currentTime = 0;
duration = 0;
subtitleURL = null; // ← eklendi
const index = files.findIndex((file) => file.name === f.name);
currentIndex = index;
if (f.type?.startsWith("video/")) {
selectedImage = null;
showImageModal = false;
selectedVideo = f;
await tick(); // DOM güncellensin
showModal = true; // video {#key} ile yeniden mount edilecek
} else if (f.type?.startsWith("image/")) {
selectedVideo = null;
showModal = false;
selectedImage = f;
await tick();
showImageModal = true;
}
}
function stopCurrentVideo() {
if (videoEl) {
try {
videoEl.pause();
videoEl.src = "";
videoEl.load();
} catch (err) {
console.warn("Video stop error:", err.message);
}
}
}
async function showNext() {
if (files.length === 0) return;
stopCurrentVideo();
currentIndex = (currentIndex + 1) % files.length;
await openModal(files[currentIndex]); // ← await
}
async function showPrev() {
if (files.length === 0) return;
stopCurrentVideo();
currentIndex = (currentIndex - 1 + files.length) % files.length;
await openModal(files[currentIndex]); // ← await
}
function closeModal() {
stopCurrentVideo(); // 🔴 video tamamen durur
showModal = false;
selectedVideo = null;
subtitleURL = null;
isPlaying = false;
}
// 🎞️ Video kontrolleri
function togglePlay() {
async function togglePlay() {
if (!videoEl) return;
if (isPlaying) videoEl.pause();
else videoEl.play();
isPlaying = !isPlaying;
if (videoEl.paused) {
try {
await videoEl.play();
isPlaying = true;
} catch (err) {
console.warn("Play rejected:", err?.message || err);
isPlaying = false;
}
} else {
videoEl.pause();
isPlaying = false;
}
}
function updateProgress() {
@@ -126,19 +188,23 @@
reader.readAsArrayBuffer(file);
}
function onEsc(e) {
if (e.key === "Escape" && showModal) closeModal();
}
onMount(() => {
loadFiles();
const slider = document.querySelector(".volume-slider");
if (slider) {
slider.value = volume;
slider.style.setProperty("--fill", slider.value * 100);
// ✅ Tek event handler içinde hem Esc hem ok tuşlarını kontrol et
function handleKey(e) {
if (e.key === "Escape") {
if (showModal) closeModal();
if (showImageModal) showImageModal = false;
} else if (showModal || showImageModal) {
if (e.key === "ArrowRight") showNext();
if (e.key === "ArrowLeft") showPrev();
}
}
window.addEventListener("keydown", onEsc);
return () => window.removeEventListener("keydown", onEsc);
window.addEventListener("keydown", handleKey);
return () => {
window.removeEventListener("keydown", handleKey);
};
});
</script>
@@ -153,9 +219,15 @@
{:else}
<div class="gallery">
{#each files as f}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="media-card" on:click={() => openModal(f)}>
{#if f.thumbnail}
<img src={`${API}${f.thumbnail}`} alt={f.name} class="thumb" />
<img
src={`${API}${f.thumbnail}?token=${localStorage.getItem("token")}`}
alt={f.name}
class="thumb"
/>
{:else}
<div class="thumb placeholder">
<i class="fa-regular fa-image"></i>
@@ -165,6 +237,13 @@
<div class="name">{cleanFileName(f.name)}</div>
<div class="size">{formatSize(f.size)}</div>
</div>
<div class="media-type-icon">
{#if f.type?.startsWith("video/")}
<i class="fa-solid fa-film"></i>
{:else if f.type?.startsWith("image/")}
<i class="fa-solid fa-image"></i>
{/if}
</div>
</div>
{/each}
</div>
@@ -172,39 +251,69 @@
</section>
{#if showModal && selectedVideo}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="modal-overlay" on:click={closeModal}>
<button class="global-close-btn" on:click|stopPropagation={closeModal}
>✕</button
>
<button class="nav-btn left" on:click|stopPropagation={showPrev}>
<i class="fa-solid fa-chevron-left"></i>
</button>
<button class="nav-btn right" on:click|stopPropagation={showNext}>
<i class="fa-solid fa-chevron-right"></i>
</button>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="modal-content" on:click|stopPropagation>
<div class="modal-header">
<div class="video-title">{selectedName}</div>
<button class="close-btn" on:click={closeModal}>✕</button>
<div class="video-title">{cleanFileName(selectedName)}</div>
</div>
<div class="custom-player">
<!-- ✅ selectedVideo yokken boş src -->
<video
bind:this={videoEl}
src={getVideoURL()}
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>
<!-- svelte-ignore a11y-media-has-caption -->
{#key encName}
<!-- svelte-ignore a11y-media-has-caption -->
<video
bind:this={videoEl}
src={getVideoURL()}
class="video-element"
playsinline
on:timeupdate={updateProgress}
on:loadedmetadata={async () => {
// her yeni videoda statei sıfırla
isPlaying = false;
currentTime = 0;
updateDuration();
const slider = document.querySelector(".volume-slider");
if (slider) {
slider.value = volume;
slider.style.setProperty("--fill", slider.value * 100);
}
// 🎬 Otomatik oynatma (tarayıcı izin verirse)
try {
await videoEl.play();
isPlaying = true;
} catch (err) {
console.warn("Autoplay engellendi:", err?.message || err);
isPlaying = false;
}
}}
on:ended={() => (isPlaying = false)}
>
{#if subtitleURL}
<track
kind="subtitles"
src={subtitleURL}
srclang={subtitleLang}
label={subtitleLabel}
default
/>
{/if}
</video>
{/key}
<div class="controls">
<div class="top-controls">
@@ -272,6 +381,31 @@
</div>
</div>
{/if}
{#if showImageModal && selectedImage}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="image-modal-overlay" on:click={() => (showImageModal = false)}>
<button
class="image-close-btn"
on:click|stopPropagation={() => (showImageModal = false)}>✕</button
>
<button class="nav-btn left" on:click|stopPropagation={showPrev}>
<i class="fa-solid fa-chevron-left"></i>
</button>
<button class="nav-btn right" on:click|stopPropagation={showNext}>
<i class="fa-solid fa-chevron-right"></i>
</button>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="image-modal-content" on:click|stopPropagation>
<img
src={`${API}${selectedImage.url}?token=${localStorage.getItem("token")}`}
alt={selectedImage.name}
class="image-modal-img"
/>
</div>
</div>
{/if}
<style>
/* === GALERİ === */
@@ -330,246 +464,67 @@
color: #666;
}
/* === MODAL & PLAYER (Transfers.svelte ile birebir) === */
.modal-overlay {
position: fixed;
inset: 0;
backdrop-filter: blur(10px);
background: rgba(0, 0, 0, 0.8);
.nav-btn {
position: absolute;
top: 50%;
transform: translateY(-50%);
background: rgba(0, 0, 0, 0.5);
border: none;
color: white;
font-size: 28px;
cursor: pointer;
z-index: 2100;
width: 50px;
height: 60px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
z-index: 999;
transition:
background 0.2s ease,
transform 0.2s ease;
}
.modal-content {
width: 70%;
height: 70%;
background: #1a1a1a;
border-radius: 12px;
display: flex;
flex-direction: column;
overflow: hidden;
box-shadow: 0 0 30px rgba(0, 0, 0, 0.8);
.nav-btn:hover {
background: rgba(255, 255, 255, 0.2);
transform: translateY(-50%) scale(1.05);
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
background: #2a2a2a;
padding: 10px 16px;
color: #fff;
font-size: 16px;
font-weight: 500;
.nav-btn.left {
left: 15px;
}
.video-title {
flex: 1;
text-align: center;
font-weight: 600;
.nav-btn.right {
right: 15px;
}
.close-btn {
background: transparent;
border: none;
color: #fff;
font-size: 24px;
cursor: pointer;
.media-card {
position: relative; /* ikonun pozisyonlanması için gerekli */
}
.custom-player {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
background: #000;
.media-type-icon {
position: absolute;
bottom: 6px;
right: 8px;
color: rgba(0, 0, 0, 0.45); /* sönük gri ton */
font-size: 14px;
pointer-events: none; /* tıklamayı engelle */
}
.video-element {
flex: 1;
width: 100%;
height: 100%;
object-fit: contain;
background: #000;
border: none;
outline: none;
}
.video-element:focus {
outline: none !important;
box-shadow: none !important;
}
/* === Kontroller === */
.controls {
background: #1c1c1c;
padding: 10px 16px;
display: flex;
flex-direction: column;
gap: 8px;
border-top: 1px solid #333;
}
.top-controls {
display: flex;
justify-content: space-between;
align-items: center;
}
.control-btn {
background: none;
border: none;
color: #fff;
font-size: 18px;
cursor: pointer;
transition: opacity 0.2s;
}
.control-btn:hover {
opacity: 0.7;
}
.right-controls {
display: flex;
align-items: center;
gap: 10px;
}
/* === Ses Kaydırıcısı === */
.volume-slider {
-webkit-appearance: none;
width: 100px;
height: 4px;
border-radius: 2px;
background: linear-gradient(
to right,
#ff3b30 calc(var(--fill, 100%) * 1%),
rgba(255, 255, 255, 0.3) calc(var(--fill, 100%) * 1%)
);
outline: none;
cursor: pointer;
transition: background 0.2s ease;
}
.volume-slider::-webkit-slider-runnable-track {
height: 4px;
border-radius: 2px;
background: transparent;
}
.volume-slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 12px;
height: 12px;
border-radius: 50%;
background: #fff;
cursor: pointer;
margin-top: -4px;
transition: transform 0.2s ease;
}
.volume-slider::-webkit-slider-thumb:hover {
transform: scale(1.3);
}
.volume-slider::-moz-range-thumb {
width: 12px;
height: 12px;
border-radius: 50%;
background: #fff;
cursor: pointer;
transition: transform 0.2s ease;
}
.volume-slider::-moz-range-thumb:hover {
transform: scale(1.3);
}
.volume-slider::-moz-range-progress {
height: 4px;
background: #ff3b30;
border-radius: 2px;
}
/* === Alt Kontroller === */
.bottom-controls {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.progress-slider {
flex: 1;
cursor: pointer;
accent-color: #27ae60;
}
.time {
color: #ccc;
font-size: 13px;
min-width: 90px;
text-align: right;
white-space: nowrap;
}
/* === Responsive === */
@media (max-width: 1024px) {
.modal-content {
width: 90%;
height: 75%;
}
.media-type-icon i {
filter: drop-shadow(0 1px 1px rgba(255, 255, 255, 0.3));
}
/* === RESPONSIVE === */
@media (max-width: 768px) {
.gallery {
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
}
.modal-content {
width: 95%;
height: 70%;
border-radius: 8px;
}
.controls {
padding: 6px 10px;
gap: 6px;
}
.volume-slider {
width: 70px;
}
.time {
font-size: 11px;
min-width: 70px;
}
.video-title {
font-size: 14px;
}
.close-btn {
font-size: 20px;
}
}
@media (max-width: 480px) {
.modal-content {
width: 98%;
height: 75%;
}
.volume-slider {
width: 50px;
}
.bottom-controls {
flex-direction: column;
align-items: stretch;
gap: 6px;
.gallery {
grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
}
}
</style>

View File

@@ -428,7 +428,13 @@
{/if}
<style>
/* --- Torrent liste & satırları (eski App.svelte ile bire bir) --- */
/* --- Torrent Listeleme --- */
.torrent-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.torrent {
display: grid;
grid-template-columns: 100px 1fr;
@@ -441,11 +447,7 @@
box-sizing: border-box;
cursor: pointer;
}
.torrent-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.thumb {
width: 100px;
height: 60px;
@@ -454,6 +456,7 @@
background: #ddd;
flex-shrink: 0;
}
.placeholder {
width: 100px;
height: 60px;
@@ -464,21 +467,25 @@
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-name {
word-break: break-word;
}
.remove-btn {
background: transparent;
border: none;
@@ -486,9 +493,11 @@
cursor: pointer;
transition: transform 0.15s;
}
.remove-btn:hover {
transform: scale(1.2);
}
.torrent-hash {
font-size: 12px;
color: #777;
@@ -500,12 +509,14 @@
flex-direction: column;
gap: 2px;
}
.file-row {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
}
.file-row button {
background: #eee;
border: none;
@@ -514,17 +525,21 @@
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;
@@ -532,11 +547,13 @@
border-radius: 3px;
overflow: hidden;
}
.progress {
height: 100%;
background: linear-gradient(90deg, #27ae60, #2ecc71);
transition: width 0.3s;
}
.progress-text {
font-size: 12px;
color: #444;
@@ -544,209 +561,7 @@
padding: 3px 0 8px 0;
}
/* --- Modal & Player (eski ile bire bir) --- */
.modal-overlay {
position: fixed;
inset: 0;
backdrop-filter: blur(10px);
background: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 999;
}
.modal-content {
width: 70%;
height: 70%;
background: #1a1a1a;
border-radius: 12px;
display: flex;
flex-direction: column;
overflow: hidden;
box-shadow: 0 0 30px rgba(0, 0, 0, 0.8);
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
background: #2a2a2a;
padding: 10px 16px;
color: #fff;
font-size: 16px;
font-weight: 500;
}
.video-title {
flex: 1;
text-align: center;
font-weight: 600;
}
.close-btn {
background: transparent;
border: none;
color: #fff;
font-size: 24px;
cursor: pointer;
}
.custom-player {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
background: #000;
}
.video-element {
flex: 1;
width: 100%;
height: 100%;
object-fit: contain;
background: #000;
border: none;
outline: none;
}
.video-element:focus {
outline: none !important;
box-shadow: none !important;
}
.controls {
background: #1c1c1c;
padding: 10px 16px;
display: flex;
flex-direction: column;
gap: 8px;
border-top: 1px solid #333;
}
.top-controls {
display: flex;
justify-content: space-between;
align-items: center;
}
.control-btn {
background: none;
border: none;
color: #fff;
font-size: 18px;
cursor: pointer;
transition: opacity 0.2s;
}
.control-btn:hover {
opacity: 0.7;
}
.right-controls {
display: flex;
align-items: center;
gap: 10px;
}
/* Volume slider — kırmızı dolum, beyaz knob */
.volume-slider {
-webkit-appearance: none;
width: 100px;
height: 4px;
border-radius: 2px;
background: linear-gradient(
to right,
#ff3b30 calc(var(--fill, 100%) * 1%),
rgba(255, 255, 255, 0.3) calc(var(--fill, 100%) * 1%)
);
outline: none;
cursor: pointer;
transition: background 0.2s ease;
}
.volume-slider::-webkit-slider-runnable-track {
height: 4px;
border-radius: 2px;
background: transparent;
}
.volume-slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 12px;
height: 12px;
border-radius: 50%;
background: #fff;
cursor: pointer;
margin-top: -4px;
transition: transform 0.2s ease;
}
.volume-slider::-webkit-slider-thumb:hover {
transform: scale(1.3);
}
.volume-slider::-moz-range-track {
height: 4px;
background: rgba(255, 255, 255, 0.3);
border-radius: 2px;
}
.volume-slider::-moz-range-progress {
height: 4px;
background: #ff3b30;
border-radius: 2px;
}
.volume-slider::-moz-range-thumb {
width: 12px;
height: 12px;
border-radius: 50%;
background: #fff;
cursor: pointer;
transition: transform 0.2s ease;
}
.volume-slider::-moz-range-thumb:hover {
transform: scale(1.3);
}
.subtitle-icon {
position: relative;
}
.bottom-controls {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.progress-slider {
flex: 1;
cursor: pointer;
accent-color: #27ae60;
}
.time {
color: #ccc;
font-size: 13px;
min-width: 90px;
text-align: right;
white-space: nowrap;
}
/* NEW TRANSFER / Magnet düğmeleri */
.btn-primary {
display: inline-flex;
align-items: center;
gap: 6px;
background: #fdce45;
border: none;
color: #000;
font-weight: 600;
text-transform: uppercase;
border-radius: 6px;
padding: 8px 14px;
cursor: pointer;
font-size: 13px;
transition: background 0.2s;
height: 36px;
line-height: 1;
}
.btn-primary:hover {
background: #fdce45;
}
/* Responsive */
@media (max-width: 768px) {
.modal-content {
width: 95%;
height: 75%;
}
}
/* 🔹 Responsive Düzenlemeler (hiçbir mevcut stili bozmadan eklenmiştir) */
/* --- Responsive Düzenlemeler --- */
@media (max-width: 1024px) {
.torrent {
grid-template-columns: 80px 1fr;
@@ -767,25 +582,9 @@
.torrent-files .file-row {
font-size: 12px;
}
.btn-primary {
font-size: 12px;
padding: 6px 10px;
height: 32px;
}
.modal-content {
width: 90%;
height: 75%;
}
}
@media (max-width: 768px) {
.files {
margin: 0 8px 12px 8px;
padding-top: 10px;
}
.torrent {
grid-template-columns: 1fr;
gap: 8px;
@@ -815,51 +614,12 @@
font-size: 11px;
}
.btn-primary {
flex: 1;
justify-content: center;
}
.torrent-list {
gap: 10px;
}
/* 🎬 Modal video oynatıcı mobil optimizasyonu */
.modal-content {
width: 95%;
height: 70%;
border-radius: 8px;
}
.controls {
padding: 6px 10px;
gap: 6px;
}
.volume-slider {
width: 70px;
}
.time {
font-size: 11px;
min-width: 70px;
}
.video-title {
font-size: 14px;
}
.close-btn {
font-size: 20px;
}
}
@media (max-width: 480px) {
.btn-primary {
font-size: 11px;
padding: 6px 8px;
}
.torrent-header {
font-size: 13px;
}
@@ -867,20 +627,5 @@
.torrent-hash {
font-size: 10px;
}
.modal-content {
width: 98%;
height: 75%;
}
.volume-slider {
width: 50px;
}
.bottom-controls {
flex-direction: column;
align-items: stretch;
gap: 6px;
}
}
</style>

View File

@@ -1,3 +1,6 @@
/* =======================================================
🎨 RENK DEĞİŞKENLERİ VE TEMEL STİLLER
======================================================= */
:root {
--yellow: #f5b333;
--yellow-dark: #e2a62f;
@@ -6,9 +9,11 @@
--muted: #666;
--green: #4caf50;
}
* {
box-sizing: border-box;
}
html,
body,
#app {
@@ -19,27 +24,43 @@ body,
color: #222;
background: #fff;
}
/* =======================================================
📐 GENEL YERLEŞİM
======================================================= */
.app {
display: grid;
grid-template-columns: 220px 1fr;
height: 100%;
}
/* Sidebar */
.content {
display: flex;
flex-direction: column;
height: 100%;
}
/* =======================================================
🧭 SIDEBAR
======================================================= */
.sidebar {
background: var(--sidebar);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
}
.sidebar .logo {
padding: 12px 16px;
font-weight: 900;
font-size: 28px;
letter-spacing: 0.5px;
}
.sidebar .menu {
padding-top: 6px;
}
.sidebar .menu .item {
display: flex;
align-items: center;
@@ -48,22 +69,37 @@ body,
color: #222;
cursor: pointer;
text-decoration: none;
transition: background 0.2s ease, color 0.2s ease;
}
.sidebar .menu .item.active {
background: #fff;
border-left: 3px solid var(--yellow);
}
.sidebar .menu .item .icon {
width: 18px;
text-align: center;
color: #333;
}
/* Content */
.content {
display: flex;
flex-direction: column;
height: 100%;
/* Hover efekti */
.sidebar .menu .item:hover {
background: #f0f0f0;
color: #000;
}
/* Aktif menü öğesi */
.sidebar .menu .item.active {
background: #e5e5e5;
border-left: 3px solid var(--yellow);
font-weight: 600;
color: #000;
}
.sidebar .menu .item:hover .icon,
.sidebar .menu .item.active .icon {
color: #000;
}
/* =======================================================
🔍 TOPBAR VE ARAMA
======================================================= */
.topbar {
display: flex;
align-items: center;
@@ -71,6 +107,7 @@ body,
padding: 12px 16px;
border-bottom: 1px solid var(--border);
}
.search {
flex: 1;
display: flex;
@@ -81,12 +118,17 @@ body,
border-radius: 6px;
padding: 8px 12px;
}
.search input {
border: none;
outline: none;
background: transparent;
flex: 1;
}
/* =======================================================
🟨 BUTONLAR
======================================================= */
.btn-primary {
background: var(--yellow);
border: 1px solid var(--yellow-dark);
@@ -95,20 +137,37 @@ body,
padding: 10px 14px;
border-radius: 6px;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
text-transform: uppercase;
gap: 6px;
height: 36px;
transition: background 0.2s;
}
.btn-primary:hover {
background: var(--yellow-dark);
}
.btn-primary:active {
transform: translateY(1px);
}
/* Files */
/* =======================================================
📂 FILES SAYFASI
======================================================= */
.files {
margin: 0 16px 16px 16px;
flex: 1;
border-top: 2px solid #f0c24d;
padding-top: 14px;
}
.files h2 {
margin: 0 0 10px 0;
}
.empty {
display: flex;
flex-direction: column;
@@ -119,6 +178,7 @@ body,
border: 2px dashed var(--border);
border-radius: 8px;
}
.create-folder {
background: var(--yellow);
border: 1px solid var(--yellow-dark);
@@ -127,7 +187,10 @@ body,
font-weight: 700;
cursor: pointer;
}
/* Transfers Page */
/* =======================================================
📦 TRANSFERS SAYFASI
======================================================= */
.torrent {
display: flex;
align-items: center;
@@ -135,9 +198,11 @@ body,
padding: 10px 12px;
border-bottom: 1px solid var(--border);
}
.torrent:last-child {
border-bottom: none;
}
.progress {
height: 8px;
background: #eee;
@@ -145,28 +210,155 @@ body,
overflow: hidden;
flex: 1;
}
.progress > div {
height: 100%;
background: var(--green);
transition: width 0.3s;
}
.small {
color: var(--muted);
font-size: 12px;
}
/* ====== Responsive & Off-Canvas Sidebar (EKLENDİ) ====== */
/* Hamburger butonunu varsayılan gizle; mobilde göstereceğiz */
.menu-toggle {
display: none;
background: none;
border: none;
font-size: 20px;
color: #333;
cursor: pointer;
/* =======================================================
🎞️ MODAL & PLAYER (ORTAK)
======================================================= */
.modal-overlay {
position: fixed;
inset: 0;
backdrop-filter: blur(10px);
background: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 999;
}
.modal-content {
width: 70%;
height: 70%;
background: #1a1a1a;
border-radius: 12px;
display: flex;
flex-direction: column;
overflow: hidden;
box-shadow: 0 0 30px rgba(0, 0, 0, 0.8);
}
/* === Video Player === */
.custom-player {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
background: #000;
}
.video-element {
flex: 1;
width: 100%;
height: 100%;
object-fit: contain;
background: #000;
border: none;
outline: none;
}
.video-element:focus {
outline: none !important;
box-shadow: none !important;
}
/* === Kontroller === */
.controls {
background: #1c1c1c;
padding: 10px 16px;
display: flex;
flex-direction: column;
gap: 8px;
border-top: 1px solid #333;
}
.top-controls {
display: flex;
justify-content: space-between;
align-items: center;
}
.control-btn {
background: none;
border: none;
color: #fff;
font-size: 18px;
cursor: pointer;
transition: opacity 0.2s;
}
.control-btn:hover {
opacity: 0.7;
}
.right-controls {
display: flex;
align-items: center;
gap: 10px;
}
/* === Ses Kaydırıcısı === */
.volume-slider {
-webkit-appearance: none;
width: 100px;
height: 4px;
border-radius: 2px;
background: linear-gradient(
to right,
#ff3b30 calc(var(--fill, 100%) * 1%),
rgba(255, 255, 255, 0.3) calc(var(--fill, 100%) * 1%)
);
outline: none;
cursor: pointer;
transition: background 0.2s ease;
}
.volume-slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 12px;
height: 12px;
border-radius: 50%;
background: #fff;
cursor: pointer;
margin-top: -4px;
transition: transform 0.2s ease;
}
.volume-slider::-webkit-slider-thumb:hover {
transform: scale(1.3);
}
/* === Alt Kontroller === */
.bottom-controls {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.progress-slider {
flex: 1;
cursor: pointer;
accent-color: #27ae60;
}
.time {
color: #ccc;
font-size: 13px;
min-width: 90px;
text-align: right;
white-space: nowrap;
}
/* Sidebar arkası için tıklanabilir backdrop (mobilde sidebar açıkken) */
.backdrop {
position: fixed;
inset: 0;
@@ -174,14 +366,95 @@ body,
opacity: 0;
pointer-events: none;
transition: opacity 0.2s ease;
z-index: 999; /* sidebarın üstünde */
z-index: 999;
}
.backdrop.show {
opacity: 1;
pointer-events: auto;
}
/* Tablet ve aşağısında grid tek sütun; sidebar off-canvas olur */
/* === Global Close Button (Resim + Video) === */
.global-close-btn,
.image-close-btn {
position: fixed;
top: 20px;
right: 30px;
background: rgba(0, 0, 0, 0.6);
border: none;
color: #fff;
font-size: 36px;
cursor: pointer;
z-index: 2100;
border-radius: 50%;
width: 46px;
height: 46px;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.2s ease, transform 0.2s ease;
}
.global-close-btn:hover,
.image-close-btn:hover {
background: rgba(255, 255, 255, 0.15);
transform: scale(1.05);
}
/* =======================================================
🖼️ RESİM MODALI
======================================================= */
.image-modal-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.8);
backdrop-filter: blur(10px);
display: flex;
justify-content: center;
align-items: center;
z-index: 2000;
}
.image-modal-content {
position: relative;
max-width: 90%;
max-height: 90%;
display: flex;
justify-content: center;
align-items: center;
}
.image-modal-img {
max-width: 75vw;
max-height: 75vh;
object-fit: contain;
border-radius: 8px;
box-shadow: 0 0 25px rgba(0, 0, 0, 0.6);
}
/* === Modal Başlığı (Ortak: Files + Transfers) === */
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
background: #2a2a2a;
padding: 10px 16px;
color: #fff;
font-size: 16px;
font-weight: 500;
}
.video-title {
flex: 1;
text-align: center;
font-weight: 600;
}
/* =======================================================
📱 RESPONSIVE
======================================================= */
/* Tablet ve aşağısı */
@media (max-width: 768px) {
.app {
grid-template-columns: 1fr;
@@ -193,6 +466,11 @@ body,
justify-content: center;
width: 36px;
height: 36px;
background: none;
border: none;
font-size: 20px;
color: #333;
cursor: pointer;
}
.sidebar {
@@ -206,48 +484,16 @@ body,
transition: left 0.25s ease;
z-index: 1000;
}
.sidebar.open {
left: 0;
}
/* Genel içerik kenar boşluklarını sıkılaştır */
.files {
margin: 0 10px 14px 10px;
padding-top: 12px;
}
/* Transfers ve diğer sayfalar için liste öğelerini dikeyleştir */
.torrent {
/* Transfersteki kutular */
grid-template-columns: 1fr !important;
gap: 10px;
}
.thumb {
width: 100% !important;
height: 180px !important;
}
.torrent-header {
flex-direction: column;
align-items: flex-start;
gap: 4px;
}
.torrent-hash {
word-break: break-word;
white-space: normal;
font-size: 12px;
line-height: 1.3;
}
.file-row {
flex-direction: column;
align-items: flex-start;
gap: 4px;
}
.progress-text {
text-align: left;
font-size: 12px;
}
/* Butonlar eş görünsün ve kolay dokunulsun */
.btn-primary {
flex: 1 1 auto;
justify-content: center;
@@ -256,64 +502,48 @@ body,
font-size: 13px;
}
/* Modal video oynatıcı mobil uyum */
.modal-content {
width: 95% !important;
height: 72% !important;
border-radius: 10px;
}
.controls {
padding: 8px 12px;
gap: 8px;
}
.volume-slider {
width: 70px;
}
.time {
font-size: 12px;
min-width: 78px;
}
}
/* === Sidebar Hover & Active Effects === */
/* Hover efekti: hafif gri arka plan */
.sidebar .menu .item:hover {
background: #f0f0f0;
color: #000;
transition: background 0.2s ease, color 0.2s ease;
}
/* Aktif item: Daha koyu gri arka plan */
.sidebar .menu .item.active {
background: #e5e5e5; /* aktif olan menü item */
font-weight: 600;
color: #000;
}
/* Hover ve aktif durumlarda ikon da koyulaşsın */
.sidebar .menu .item:hover .icon,
.sidebar .menu .item.active .icon {
color: #000;
}
/* Daha küçük telefonlar */
/* Küçük telefonlar */
@media (max-width: 480px) {
.btn-primary {
font-size: 12px;
padding: 8px 10px;
height: 34px;
}
.torrent-hash {
font-size: 11px;
}
.modal-content {
width: 98% !important;
height: 76% !important;
}
.volume-slider {
width: 56px;
}
.bottom-controls {
flex-direction: column;
align-items: stretch;

View File

@@ -1,10 +1,10 @@
// utils/filename.js
/**
* Dosya adını temizler ve sadeleştirir.
* Örnek:
* The.Astronaut.2025.1080p.WEBRip.x265-KONTRAST
* → "The Astronaut (2025)"
* 1761244874124/Gen.V.S02E08.Cavallo.di.Troia.ITA.ENG.1080p.AMZN.WEB-DL.DDP5.1.H.264-MeM.GP.mkv
* → "Gen V S02E08 Cavallo Di Troia"
*/
export function cleanFileName(fullPath) {
if (!fullPath) return "";
@@ -15,8 +15,8 @@ export function cleanFileName(fullPath) {
// 2⃣ Uzantıyı kaldır
name = name.replace(/\.[^.]+$/, "");
// 3⃣ Noktaları boşluğa çevir
name = name.replace(/\./g, " ");
// 3⃣ Noktaları ve alt tireleri boşluğa çevir
name = name.replace(/[._]+/g, " ");
// 4⃣ Gereksiz etiketleri kaldır
const trashWords = [
@@ -25,7 +25,7 @@ export function cleanFileName(fullPath) {
"2160p",
"4k",
"bluray",
"web-dl",
"web[- ]?dl",
"webrip",
"hdrip",
"x264",
@@ -34,6 +34,7 @@ export function cleanFileName(fullPath) {
"aac",
"h264",
"h265",
"ddp5",
"dvdrip",
"brrip",
"remux",
@@ -41,6 +42,8 @@ export function cleanFileName(fullPath) {
"sub",
"subs",
"turkce",
"ita",
"eng",
"dublado",
"dubbed",
"extended",
@@ -54,57 +57,42 @@ export function cleanFileName(fullPath) {
"hdtv",
"amzn",
"nf",
"netflix"
"netflix",
"mem",
"gp"
];
const trashRegex = new RegExp(`\\b(${trashWords.join("|")})\\b`, "gi");
name = name.replace(trashRegex, " ");
// 5Köşeli parantez içindekileri kaldır
name = name.replace(/\[[^\]]*\]/g, "");
// 5Parantez veya köşeli parantez içindekileri kaldır
name = name.replace(/[\[\(].*?[\]\)]/g, " ");
// 6Parantez içindeki tarihleri kaldır
// 6Fazla tireleri ve sayıları temizle
name = name
.replace(/\(\d{2}\.\d{2}\.\d{2,4}\)/g, "")
.replace(/\(\d{4}(-\d{2})?(-\d{2})?\)/g, "");
// 7⃣ Fazla boşlukları temizle
name = name.replace(/\s{2,}/g, " ").trim();
// 8⃣ Yılı tespit et (ör. 2024, 1999)
const yearMatch = name.match(/\b(19|20)\d{2}\b/);
let year = "";
if (yearMatch) {
year = yearMatch[0];
name = name.replace(year, "").trim();
}
// 9⃣ Dizi formatı (S03E01) varsa koru
const match = name.match(/(.+?)\s*-\s*(S\d{2}E\d{2})/i);
if (match) {
const formatted = `${match[1].trim()} - ${match[2].toUpperCase()}`;
return year ? `${formatted} (${year})` : formatted;
}
// 🔟 Fazla tireleri ve tire + parantez boşluklarını düzelt
name = name
.replace(/[-_]+/g, " ") // birden fazla tireyi temizle
.replace(/\s-\s*\(/g, " (") // " - (" → " ("
.replace(/[-]+/g, " ")
.replace(/\b\d{3,4}\b/g, " ") // tek başına 1080, 2025 gibi
.replace(/\s{2,}/g, " ")
.trim();
// 11️⃣ Baş harfleri büyüt
// 7️⃣ Dizi formatını (S02E08) koru
const episodeMatch = name.match(/(S\d{1,2}E\d{1,2})/i);
if (episodeMatch) {
const epTag = episodeMatch[0].toUpperCase();
// örnek: Gen V S02E08 Cavallo di Troia
name = name.replace(episodeMatch[0], epTag);
}
// 8⃣ Baş harfleri büyüt (küçük kelimeleri koruyarak)
name = name
.split(" ")
.map(
(w) =>
w.length > 1
? w[0].toUpperCase() + w.slice(1).toLowerCase()
: w.toUpperCase()
)
.filter((w) => w.length > 0)
.map((w) => {
if (["di", "da", "de", "of", "and", "the"].includes(w.toLowerCase()))
return w.toLowerCase();
return w[0].toUpperCase() + w.slice(1).toLowerCase();
})
.join(" ")
.trim();
// 12⃣ Yıl varsa sonuna ekle
if (year) name += ` (${year})`;
return name.trim();
return name;
}

View File

@@ -29,7 +29,6 @@ app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use("/downloads", express.static(DOWNLOAD_DIR));
// --- En uygun video dosyasını seç ---
function pickBestVideoFile(torrent) {
const videoExts = [".mp4", ".webm", ".mkv", ".mov", ".m4v"];
@@ -70,8 +69,8 @@ function snapshot() {
}
// --- Basit kimlik doğrulama sistemi ---
const USERNAME = process.env.USERNAME
const PASSWORD = process.env.PASSWORD
const USERNAME = process.env.USERNAME;
const PASSWORD = process.env.PASSWORD;
let activeTokens = new Set();
app.post("/api/login", (req, res) => {
@@ -85,14 +84,12 @@ app.post("/api/login", (req, res) => {
});
function requireAuth(req, res, next) {
const token =
req.headers.authorization?.split(" ")[1] || req.query.token;
const token = req.headers.authorization?.split(" ")[1] || req.query.token;
if (!token || !activeTokens.has(token))
return res.status(401).json({ error: "Unauthorized" });
next();
}
// --- Torrent veya magnet ekleme ---
app.post("/api/transfer", requireAuth, upload.single("torrent"), (req, res) => {
try {
@@ -131,8 +128,8 @@ app.post("/api/transfer", requireAuth, upload.single("torrent"), (req, res) => {
infoHash: torrent.infoHash,
name: torrent.name,
selectedIndex,
tracker: torrent.announce?.[0] || null, // 🆕
added, // 🆕
tracker: torrent.announce?.[0] || null,
added,
files: torrent.files.map((f, i) => ({
index: i,
name: f.name,
@@ -206,15 +203,19 @@ app.delete("/api/torrents/:hash", requireAuth, (req, res) => {
});
});
// --- GENEL MEDYA SUNUMU (🆕 resimler + videolar) ---
app.get("/media/:path(*)", requireAuth, (req, res) => {
const fullPath = path.join(DOWNLOAD_DIR, req.params.path);
const relPath = req.params.path;
const fullPath = path.join(DOWNLOAD_DIR, relPath);
if (!fs.existsSync(fullPath)) return res.status(404).send("File not found");
const stat = fs.statSync(fullPath);
const fileSize = stat.size;
const type = mime.lookup(fullPath) || "application/octet-stream";
const isVideo = String(type).startsWith("video/");
const range = req.headers.range;
if (range) {
if (isVideo && range) {
const [startStr, endStr] = range.replace(/bytes=/, "").split("-");
const start = parseInt(startStr, 10);
const end = endStr ? parseInt(endStr, 10) : fileSize - 1;
@@ -224,21 +225,22 @@ app.get("/media/:path(*)", requireAuth, (req, res) => {
"Content-Range": `bytes ${start}-${end}/${fileSize}`,
"Accept-Ranges": "bytes",
"Content-Length": chunkSize,
"Content-Type": "video/mp4"
"Content-Type": type
};
res.writeHead(206, head);
file.pipe(res);
} else {
const head = {
"Content-Length": fileSize,
"Content-Type": "video/mp4"
"Content-Type": type,
"Accept-Ranges": isVideo ? "bytes" : "none"
};
res.writeHead(200, head);
fs.createReadStream(fullPath).pipe(res);
}
});
// --- 📁 Dosya gezgini: /downloads altındaki dosyaları listele ---
// --- 📁 Dosya gezgini (🆕 type ve url alanları eklendi; resim thumb'ı) ---
app.get("/api/files", requireAuth, (req, res) => {
const walk = (dir) => {
let result = [];
@@ -251,21 +253,35 @@ app.get("/api/files", requireAuth, (req, res) => {
if (entry.isDirectory()) {
result = result.concat(walk(full));
} else {
// thumbnail.jpg dosyasını listeleme
if (entry.name.toLowerCase() === "thumbnail.jpg") continue;
const size = fs.statSync(full).size;
const parts = rel.split(path.sep);
const rootHash = parts[0]; // ilk klasör adı
const thumbPath = path.join(DOWNLOAD_DIR, rootHash, "thumbnail.jpg");
// ✅ Thumbnail dosyası gerçekten varsa ekle
const thumb = fs.existsSync(thumbPath)
const size = fs.statSync(full).size;
const type = mime.lookup(full) || "application/octet-stream";
// kök klasör (thumbnail varsa video kartlarında kullanıyoruz)
const parts = rel.split(path.sep);
const rootHash = parts[0];
const videoThumbPath = path.join(DOWNLOAD_DIR, rootHash, "thumbnail.jpg");
const hasVideoThumb = fs.existsSync(videoThumbPath);
// URL (segment bazlı encode → / işaretlerini koru)
const urlPath = encodeURIComponent(rel).replace(/%2F/g, "/");
const url = `/media/${urlPath}`;
// Resimler için küçük önizleme: kendi dosyasını thumbnail yap
const isImage = String(type).startsWith("image/");
const isVideo = String(type).startsWith("video/");
const thumb = isImage
? url
: hasVideoThumb
? `/downloads/${rootHash}/thumbnail.jpg`
: null;
result.push({
name: rel,
size,
type, // 🆕 "image/jpeg", "video/mp4", vs.
url, // 🆕 doğrudan görüntüleme/oynatma için
thumbnail: thumb
});
}
@@ -283,7 +299,7 @@ app.get("/api/files", requireAuth, (req, res) => {
}
});
// --- Stream endpoint ---
// --- Stream endpoint (torrent içinden) ---
app.get("/stream/:hash", requireAuth, (req, res) => {
const entry = torrents.get(req.params.hash);
if (!entry) return res.status(404).end();
@@ -331,8 +347,6 @@ const server = app.listen(PORT, () =>
const publicDir = path.join(__dirname, "public");
if (fs.existsSync(publicDir)) {
app.use(express.static(publicDir));
// Frontend route'larını index.html'e yönlendir
app.get("*", (req, res, next) => {
if (req.path.startsWith("/api")) return next();
res.sendFile(path.join(publicDir, "index.html"));