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

View File

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

View File

@@ -1,15 +1,20 @@
<script> <script>
import { createEventDispatcher } from "svelte"; import { createEventDispatcher, onMount } from "svelte";
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
let search = ""; let search = "";
export let placeholder = "Search files..."; export let placeholder = "Search files...";
const onToggle = () => dispatch("toggleMenu"); const onToggle = () => dispatch("toggleMenu");
</script> </script>
<div class="topbar"> <div class="topbar">
<!-- Mobilde görünen hamburger --> <!-- 🔹 Hamburger butonu sadece küçük ekranlarda gösterilir -->
<button class="menu-toggle" on:click={onToggle} aria-label="Toggle menu"> <button
class="menu-toggle"
on:click={onToggle}
aria-label="Toggle menu"
>
<i class="fa-solid fa-bars"></i> <i class="fa-solid fa-bars"></i>
</button> </button>
@@ -18,3 +23,51 @@
<input placeholder={placeholder} bind:value={search} /> <input placeholder={placeholder} bind:value={search} />
</div> </div>
</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> <script>
import { onMount } from "svelte"; import { onMount, tick } from "svelte";
import { API, apiFetch } from "../utils/api.js"; import { API, apiFetch } from "../utils/api.js";
import { cleanFileName } from "../utils/filename.js"; import { cleanFileName } from "../utils/filename.js";
@@ -16,6 +16,10 @@
let currentTime = 0; let currentTime = 0;
let duration = 0; let duration = 0;
let volume = 1; let volume = 1;
let currentIndex;
let showImageModal = false;
let selectedImage = null;
// ✅ REACTIVE: selectedVideo güvenli kullanımlar // ✅ REACTIVE: selectedVideo güvenli kullanımlar
$: selectedName = selectedVideo?.name ?? ""; $: selectedName = selectedVideo?.name ?? "";
@@ -42,23 +46,81 @@
return (bytes / 1e9).toFixed(2) + " GB"; return (bytes / 1e9).toFixed(2) + " GB";
} }
function openModal(f) { async function openModal(f) {
selectedVideo = f; stopCurrentVideo();
showModal = true; 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() { function closeModal() {
stopCurrentVideo(); // 🔴 video tamamen durur
showModal = false; showModal = false;
selectedVideo = null; selectedVideo = null;
subtitleURL = null; subtitleURL = null;
isPlaying = false;
} }
// 🎞️ Video kontrolleri // 🎞️ Video kontrolleri
function togglePlay() { async function togglePlay() {
if (!videoEl) return; if (!videoEl) return;
if (isPlaying) videoEl.pause(); if (videoEl.paused) {
else videoEl.play(); try {
isPlaying = !isPlaying; await videoEl.play();
isPlaying = true;
} catch (err) {
console.warn("Play rejected:", err?.message || err);
isPlaying = false;
}
} else {
videoEl.pause();
isPlaying = false;
}
} }
function updateProgress() { function updateProgress() {
@@ -126,19 +188,23 @@
reader.readAsArrayBuffer(file); reader.readAsArrayBuffer(file);
} }
function onEsc(e) {
if (e.key === "Escape" && showModal) closeModal();
}
onMount(() => { onMount(() => {
loadFiles(); loadFiles();
const slider = document.querySelector(".volume-slider"); // ✅ Tek event handler içinde hem Esc hem ok tuşlarını kontrol et
if (slider) { function handleKey(e) {
slider.value = volume; if (e.key === "Escape") {
slider.style.setProperty("--fill", slider.value * 100); 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> </script>
@@ -153,9 +219,15 @@
{:else} {:else}
<div class="gallery"> <div class="gallery">
{#each files as f} {#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)}> <div class="media-card" on:click={() => openModal(f)}>
{#if f.thumbnail} {#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} {:else}
<div class="thumb placeholder"> <div class="thumb placeholder">
<i class="fa-regular fa-image"></i> <i class="fa-regular fa-image"></i>
@@ -165,6 +237,13 @@
<div class="name">{cleanFileName(f.name)}</div> <div class="name">{cleanFileName(f.name)}</div>
<div class="size">{formatSize(f.size)}</div> <div class="size">{formatSize(f.size)}</div>
</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> </div>
{/each} {/each}
</div> </div>
@@ -172,39 +251,69 @@
</section> </section>
{#if showModal && selectedVideo} {#if showModal && selectedVideo}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="modal-overlay" on:click={closeModal}> <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-content" on:click|stopPropagation>
<div class="modal-header"> <div class="modal-header">
<div class="video-title">{selectedName}</div> <div class="video-title">{cleanFileName(selectedName)}</div>
<button class="close-btn" on:click={closeModal}>✕</button>
</div> </div>
<div class="custom-player"> <div class="custom-player">
<!-- ✅ selectedVideo yokken boş src --> <!-- ✅ selectedVideo yokken boş src -->
<video <!-- svelte-ignore a11y-media-has-caption -->
bind:this={videoEl} {#key encName}
src={getVideoURL()} <!-- svelte-ignore a11y-media-has-caption -->
class="video-element" <video
on:timeupdate={updateProgress} bind:this={videoEl}
on:loadedmetadata={() => { src={getVideoURL()}
updateDuration(); class="video-element"
const slider = document.querySelector(".volume-slider"); playsinline
if (slider) { on:timeupdate={updateProgress}
slider.value = volume; on:loadedmetadata={async () => {
slider.style.setProperty("--fill", slider.value * 100); // her yeni videoda statei sıfırla
} isPlaying = false;
}} currentTime = 0;
> updateDuration();
{#if subtitleURL}
<track const slider = document.querySelector(".volume-slider");
kind="subtitles" if (slider) {
src={subtitleURL} slider.value = volume;
srclang={subtitleLang} slider.style.setProperty("--fill", slider.value * 100);
label={subtitleLabel} }
default
/> // 🎬 Otomatik oynatma (tarayıcı izin verirse)
{/if} try {
</video> 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="controls">
<div class="top-controls"> <div class="top-controls">
@@ -272,6 +381,31 @@
</div> </div>
</div> </div>
{/if} {/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> <style>
/* === GALERİ === */ /* === GALERİ === */
@@ -330,246 +464,67 @@
color: #666; color: #666;
} }
/* === MODAL & PLAYER (Transfers.svelte ile birebir) === */ .nav-btn {
.modal-overlay { position: absolute;
position: fixed; top: 50%;
inset: 0; transform: translateY(-50%);
backdrop-filter: blur(10px); background: rgba(0, 0, 0, 0.5);
background: rgba(0, 0, 0, 0.8); border: none;
color: white;
font-size: 28px;
cursor: pointer;
z-index: 2100;
width: 50px;
height: 60px;
border-radius: 8px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
z-index: 999; transition:
background 0.2s ease,
transform 0.2s ease;
} }
.modal-content { .nav-btn:hover {
width: 70%; background: rgba(255, 255, 255, 0.2);
height: 70%; transform: translateY(-50%) scale(1.05);
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 { .nav-btn.left {
display: flex; left: 15px;
justify-content: space-between;
align-items: center;
background: #2a2a2a;
padding: 10px 16px;
color: #fff;
font-size: 16px;
font-weight: 500;
} }
.video-title { .nav-btn.right {
flex: 1; right: 15px;
text-align: center;
font-weight: 600;
} }
.close-btn { .media-card {
background: transparent; position: relative; /* ikonun pozisyonlanması için gerekli */
border: none;
color: #fff;
font-size: 24px;
cursor: pointer;
} }
.custom-player { .media-type-icon {
flex: 1; position: absolute;
display: flex; bottom: 6px;
flex-direction: column; right: 8px;
justify-content: space-between; color: rgba(0, 0, 0, 0.45); /* sönük gri ton */
background: #000; font-size: 14px;
pointer-events: none; /* tıklamayı engelle */
} }
.video-element { .media-type-icon i {
flex: 1; filter: drop-shadow(0 1px 1px rgba(255, 255, 255, 0.3));
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%;
}
} }
/* === RESPONSIVE === */
@media (max-width: 768px) { @media (max-width: 768px) {
.gallery { .gallery {
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); 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) { @media (max-width: 480px) {
.modal-content { .gallery {
width: 98%; grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
height: 75%;
}
.volume-slider {
width: 50px;
}
.bottom-controls {
flex-direction: column;
align-items: stretch;
gap: 6px;
} }
} }
</style> </style>

View File

@@ -428,7 +428,13 @@
{/if} {/if}
<style> <style>
/* --- Torrent liste & satırları (eski App.svelte ile bire bir) --- */ /* --- Torrent Listeleme --- */
.torrent-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.torrent { .torrent {
display: grid; display: grid;
grid-template-columns: 100px 1fr; grid-template-columns: 100px 1fr;
@@ -441,11 +447,7 @@
box-sizing: border-box; box-sizing: border-box;
cursor: pointer; cursor: pointer;
} }
.torrent-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.thumb { .thumb {
width: 100px; width: 100px;
height: 60px; height: 60px;
@@ -454,6 +456,7 @@
background: #ddd; background: #ddd;
flex-shrink: 0; flex-shrink: 0;
} }
.placeholder { .placeholder {
width: 100px; width: 100px;
height: 60px; height: 60px;
@@ -464,21 +467,25 @@
border-radius: 6px; border-radius: 6px;
font-size: 24px; font-size: 24px;
} }
.torrent-info { .torrent-info {
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 4px; gap: 4px;
} }
.torrent-header { .torrent-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: flex-start; align-items: flex-start;
font-weight: 700; font-weight: 700;
} }
.torrent-name { .torrent-name {
word-break: break-word; word-break: break-word;
} }
.remove-btn { .remove-btn {
background: transparent; background: transparent;
border: none; border: none;
@@ -486,9 +493,11 @@
cursor: pointer; cursor: pointer;
transition: transform 0.15s; transition: transform 0.15s;
} }
.remove-btn:hover { .remove-btn:hover {
transform: scale(1.2); transform: scale(1.2);
} }
.torrent-hash { .torrent-hash {
font-size: 12px; font-size: 12px;
color: #777; color: #777;
@@ -500,12 +509,14 @@
flex-direction: column; flex-direction: column;
gap: 2px; gap: 2px;
} }
.file-row { .file-row {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 6px; gap: 6px;
font-size: 13px; font-size: 13px;
} }
.file-row button { .file-row button {
background: #eee; background: #eee;
border: none; border: none;
@@ -514,17 +525,21 @@
cursor: pointer; cursor: pointer;
font-size: 12px; font-size: 12px;
} }
.file-row button:hover { .file-row button:hover {
background: #ddd; background: #ddd;
} }
.filename { .filename {
flex: 1; flex: 1;
} }
.filesize { .filesize {
color: #666; color: #666;
font-size: 12px; font-size: 12px;
} }
/* --- İlerleme Çubuğu --- */
.progress-bar { .progress-bar {
width: 100%; width: 100%;
height: 6px; height: 6px;
@@ -532,11 +547,13 @@
border-radius: 3px; border-radius: 3px;
overflow: hidden; overflow: hidden;
} }
.progress { .progress {
height: 100%; height: 100%;
background: linear-gradient(90deg, #27ae60, #2ecc71); background: linear-gradient(90deg, #27ae60, #2ecc71);
transition: width 0.3s; transition: width 0.3s;
} }
.progress-text { .progress-text {
font-size: 12px; font-size: 12px;
color: #444; color: #444;
@@ -544,209 +561,7 @@
padding: 3px 0 8px 0; padding: 3px 0 8px 0;
} }
/* --- Modal & Player (eski ile bire bir) --- */ /* --- Responsive Düzenlemeler --- */
.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) */
@media (max-width: 1024px) { @media (max-width: 1024px) {
.torrent { .torrent {
grid-template-columns: 80px 1fr; grid-template-columns: 80px 1fr;
@@ -767,25 +582,9 @@
.torrent-files .file-row { .torrent-files .file-row {
font-size: 12px; font-size: 12px;
} }
.btn-primary {
font-size: 12px;
padding: 6px 10px;
height: 32px;
}
.modal-content {
width: 90%;
height: 75%;
}
} }
@media (max-width: 768px) { @media (max-width: 768px) {
.files {
margin: 0 8px 12px 8px;
padding-top: 10px;
}
.torrent { .torrent {
grid-template-columns: 1fr; grid-template-columns: 1fr;
gap: 8px; gap: 8px;
@@ -815,51 +614,12 @@
font-size: 11px; font-size: 11px;
} }
.btn-primary {
flex: 1;
justify-content: center;
}
.torrent-list { .torrent-list {
gap: 10px; 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) { @media (max-width: 480px) {
.btn-primary {
font-size: 11px;
padding: 6px 8px;
}
.torrent-header { .torrent-header {
font-size: 13px; font-size: 13px;
} }
@@ -867,20 +627,5 @@
.torrent-hash { .torrent-hash {
font-size: 10px; font-size: 10px;
} }
.modal-content {
width: 98%;
height: 75%;
}
.volume-slider {
width: 50px;
}
.bottom-controls {
flex-direction: column;
align-items: stretch;
gap: 6px;
}
} }
</style> </style>

View File

@@ -1,3 +1,6 @@
/* =======================================================
🎨 RENK DEĞİŞKENLERİ VE TEMEL STİLLER
======================================================= */
:root { :root {
--yellow: #f5b333; --yellow: #f5b333;
--yellow-dark: #e2a62f; --yellow-dark: #e2a62f;
@@ -6,9 +9,11 @@
--muted: #666; --muted: #666;
--green: #4caf50; --green: #4caf50;
} }
* { * {
box-sizing: border-box; box-sizing: border-box;
} }
html, html,
body, body,
#app { #app {
@@ -19,27 +24,43 @@ body,
color: #222; color: #222;
background: #fff; background: #fff;
} }
/* =======================================================
📐 GENEL YERLEŞİM
======================================================= */
.app { .app {
display: grid; display: grid;
grid-template-columns: 220px 1fr; grid-template-columns: 220px 1fr;
height: 100%; height: 100%;
} }
/* Sidebar */
.content {
display: flex;
flex-direction: column;
height: 100%;
}
/* =======================================================
🧭 SIDEBAR
======================================================= */
.sidebar { .sidebar {
background: var(--sidebar); background: var(--sidebar);
border-right: 1px solid var(--border); border-right: 1px solid var(--border);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.sidebar .logo { .sidebar .logo {
padding: 12px 16px; padding: 12px 16px;
font-weight: 900; font-weight: 900;
font-size: 28px; font-size: 28px;
letter-spacing: 0.5px; letter-spacing: 0.5px;
} }
.sidebar .menu { .sidebar .menu {
padding-top: 6px; padding-top: 6px;
} }
.sidebar .menu .item { .sidebar .menu .item {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -48,22 +69,37 @@ body,
color: #222; color: #222;
cursor: pointer; cursor: pointer;
text-decoration: none; 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 { .sidebar .menu .item .icon {
width: 18px; width: 18px;
text-align: center; text-align: center;
color: #333; color: #333;
} }
/* Content */
.content { /* Hover efekti */
display: flex; .sidebar .menu .item:hover {
flex-direction: column; background: #f0f0f0;
height: 100%; 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 { .topbar {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -71,6 +107,7 @@ body,
padding: 12px 16px; padding: 12px 16px;
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border);
} }
.search { .search {
flex: 1; flex: 1;
display: flex; display: flex;
@@ -81,12 +118,17 @@ body,
border-radius: 6px; border-radius: 6px;
padding: 8px 12px; padding: 8px 12px;
} }
.search input { .search input {
border: none; border: none;
outline: none; outline: none;
background: transparent; background: transparent;
flex: 1; flex: 1;
} }
/* =======================================================
🟨 BUTONLAR
======================================================= */
.btn-primary { .btn-primary {
background: var(--yellow); background: var(--yellow);
border: 1px solid var(--yellow-dark); border: 1px solid var(--yellow-dark);
@@ -95,20 +137,37 @@ body,
padding: 10px 14px; padding: 10px 14px;
border-radius: 6px; border-radius: 6px;
cursor: pointer; 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 { .btn-primary:active {
transform: translateY(1px); transform: translateY(1px);
} }
/* Files */
/* =======================================================
📂 FILES SAYFASI
======================================================= */
.files { .files {
margin: 0 16px 16px 16px; margin: 0 16px 16px 16px;
flex: 1; flex: 1;
border-top: 2px solid #f0c24d; border-top: 2px solid #f0c24d;
padding-top: 14px; padding-top: 14px;
} }
.files h2 { .files h2 {
margin: 0 0 10px 0; margin: 0 0 10px 0;
} }
.empty { .empty {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -119,6 +178,7 @@ body,
border: 2px dashed var(--border); border: 2px dashed var(--border);
border-radius: 8px; border-radius: 8px;
} }
.create-folder { .create-folder {
background: var(--yellow); background: var(--yellow);
border: 1px solid var(--yellow-dark); border: 1px solid var(--yellow-dark);
@@ -127,7 +187,10 @@ body,
font-weight: 700; font-weight: 700;
cursor: pointer; cursor: pointer;
} }
/* Transfers Page */
/* =======================================================
📦 TRANSFERS SAYFASI
======================================================= */
.torrent { .torrent {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -135,9 +198,11 @@ body,
padding: 10px 12px; padding: 10px 12px;
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border);
} }
.torrent:last-child { .torrent:last-child {
border-bottom: none; border-bottom: none;
} }
.progress { .progress {
height: 8px; height: 8px;
background: #eee; background: #eee;
@@ -145,28 +210,155 @@ body,
overflow: hidden; overflow: hidden;
flex: 1; flex: 1;
} }
.progress > div { .progress > div {
height: 100%; height: 100%;
background: var(--green); background: var(--green);
transition: width 0.3s; transition: width 0.3s;
} }
.small { .small {
color: var(--muted); color: var(--muted);
font-size: 12px; font-size: 12px;
} }
/* ====== Responsive & Off-Canvas Sidebar (EKLENDİ) ====== */
/* Hamburger butonunu varsayılan gizle; mobilde göstereceğiz */ /* =======================================================
.menu-toggle { 🎞️ MODAL & PLAYER (ORTAK)
display: none; ======================================================= */
background: none; .modal-overlay {
border: none; position: fixed;
font-size: 20px; inset: 0;
color: #333; backdrop-filter: blur(10px);
cursor: pointer; 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 { .backdrop {
position: fixed; position: fixed;
inset: 0; inset: 0;
@@ -174,14 +366,95 @@ body,
opacity: 0; opacity: 0;
pointer-events: none; pointer-events: none;
transition: opacity 0.2s ease; transition: opacity 0.2s ease;
z-index: 999; /* sidebarın üstünde */ z-index: 999;
} }
.backdrop.show { .backdrop.show {
opacity: 1; opacity: 1;
pointer-events: auto; 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) { @media (max-width: 768px) {
.app { .app {
grid-template-columns: 1fr; grid-template-columns: 1fr;
@@ -193,6 +466,11 @@ body,
justify-content: center; justify-content: center;
width: 36px; width: 36px;
height: 36px; height: 36px;
background: none;
border: none;
font-size: 20px;
color: #333;
cursor: pointer;
} }
.sidebar { .sidebar {
@@ -206,48 +484,16 @@ body,
transition: left 0.25s ease; transition: left 0.25s ease;
z-index: 1000; z-index: 1000;
} }
.sidebar.open { .sidebar.open {
left: 0; left: 0;
} }
/* Genel içerik kenar boşluklarını sıkılaştır */
.files { .files {
margin: 0 10px 14px 10px; margin: 0 10px 14px 10px;
padding-top: 12px; 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 { .btn-primary {
flex: 1 1 auto; flex: 1 1 auto;
justify-content: center; justify-content: center;
@@ -256,64 +502,48 @@ body,
font-size: 13px; font-size: 13px;
} }
/* Modal video oynatıcı mobil uyum */
.modal-content { .modal-content {
width: 95% !important; width: 95% !important;
height: 72% !important; height: 72% !important;
border-radius: 10px; border-radius: 10px;
} }
.controls { .controls {
padding: 8px 12px; padding: 8px 12px;
gap: 8px; gap: 8px;
} }
.volume-slider { .volume-slider {
width: 70px; width: 70px;
} }
.time { .time {
font-size: 12px; font-size: 12px;
min-width: 78px; min-width: 78px;
} }
} }
/* === Sidebar Hover & Active Effects === */ /* Küçük telefonlar */
/* 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 */
@media (max-width: 480px) { @media (max-width: 480px) {
.btn-primary { .btn-primary {
font-size: 12px; font-size: 12px;
padding: 8px 10px; padding: 8px 10px;
height: 34px; height: 34px;
} }
.torrent-hash { .torrent-hash {
font-size: 11px; font-size: 11px;
} }
.modal-content { .modal-content {
width: 98% !important; width: 98% !important;
height: 76% !important; height: 76% !important;
} }
.volume-slider { .volume-slider {
width: 56px; width: 56px;
} }
.bottom-controls { .bottom-controls {
flex-direction: column; flex-direction: column;
align-items: stretch; align-items: stretch;

View File

@@ -1,10 +1,10 @@
// utils/filename.js
/** /**
* Dosya adını temizler ve sadeleştirir. * Dosya adını temizler ve sadeleştirir.
* Örnek: * Örnek:
* The.Astronaut.2025.1080p.WEBRip.x265-KONTRAST * The.Astronaut.2025.1080p.WEBRip.x265-KONTRAST
* → "The Astronaut (2025)" * → "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) { export function cleanFileName(fullPath) {
if (!fullPath) return ""; if (!fullPath) return "";
@@ -15,8 +15,8 @@ export function cleanFileName(fullPath) {
// 2⃣ Uzantıyı kaldır // 2⃣ Uzantıyı kaldır
name = name.replace(/\.[^.]+$/, ""); name = name.replace(/\.[^.]+$/, "");
// 3⃣ Noktaları boşluğa çevir // 3⃣ Noktaları ve alt tireleri boşluğa çevir
name = name.replace(/\./g, " "); name = name.replace(/[._]+/g, " ");
// 4⃣ Gereksiz etiketleri kaldır // 4⃣ Gereksiz etiketleri kaldır
const trashWords = [ const trashWords = [
@@ -25,7 +25,7 @@ export function cleanFileName(fullPath) {
"2160p", "2160p",
"4k", "4k",
"bluray", "bluray",
"web-dl", "web[- ]?dl",
"webrip", "webrip",
"hdrip", "hdrip",
"x264", "x264",
@@ -34,6 +34,7 @@ export function cleanFileName(fullPath) {
"aac", "aac",
"h264", "h264",
"h265", "h265",
"ddp5",
"dvdrip", "dvdrip",
"brrip", "brrip",
"remux", "remux",
@@ -41,6 +42,8 @@ export function cleanFileName(fullPath) {
"sub", "sub",
"subs", "subs",
"turkce", "turkce",
"ita",
"eng",
"dublado", "dublado",
"dubbed", "dubbed",
"extended", "extended",
@@ -54,57 +57,42 @@ export function cleanFileName(fullPath) {
"hdtv", "hdtv",
"amzn", "amzn",
"nf", "nf",
"netflix" "netflix",
"mem",
"gp"
]; ];
const trashRegex = new RegExp(`\\b(${trashWords.join("|")})\\b`, "gi"); const trashRegex = new RegExp(`\\b(${trashWords.join("|")})\\b`, "gi");
name = name.replace(trashRegex, " "); name = name.replace(trashRegex, " ");
// 5Köşeli parantez içindekileri kaldır // 5Parantez veya köşeli parantez içindekileri kaldır
name = name.replace(/\[[^\]]*\]/g, ""); name = name.replace(/[\[\(].*?[\]\)]/g, " ");
// 6Parantez içindeki tarihleri kaldır // 6Fazla tireleri ve sayıları temizle
name = name name = name
.replace(/\(\d{2}\.\d{2}\.\d{2,4}\)/g, "") .replace(/[-]+/g, " ")
.replace(/\(\d{4}(-\d{2})?(-\d{2})?\)/g, ""); .replace(/\b\d{3,4}\b/g, " ") // tek başına 1080, 2025 gibi
.replace(/\s{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, " (") // " - (" → " ("
.trim(); .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 name = name
.split(" ") .split(" ")
.map( .filter((w) => w.length > 0)
(w) => .map((w) => {
w.length > 1 if (["di", "da", "de", "of", "and", "the"].includes(w.toLowerCase()))
? w[0].toUpperCase() + w.slice(1).toLowerCase() return w.toLowerCase();
: w.toUpperCase() return w[0].toUpperCase() + w.slice(1).toLowerCase();
) })
.join(" ") .join(" ")
.trim(); .trim();
// 12⃣ Yıl varsa sonuna ekle return name;
if (year) name += ` (${year})`;
return name.trim();
} }

View File

@@ -29,7 +29,6 @@ app.use(express.json());
app.use(express.urlencoded({ extended: true })); app.use(express.urlencoded({ extended: true }));
app.use("/downloads", express.static(DOWNLOAD_DIR)); app.use("/downloads", express.static(DOWNLOAD_DIR));
// --- En uygun video dosyasını seç --- // --- En uygun video dosyasını seç ---
function pickBestVideoFile(torrent) { function pickBestVideoFile(torrent) {
const videoExts = [".mp4", ".webm", ".mkv", ".mov", ".m4v"]; const videoExts = [".mp4", ".webm", ".mkv", ".mov", ".m4v"];
@@ -70,8 +69,8 @@ function snapshot() {
} }
// --- Basit kimlik doğrulama sistemi --- // --- Basit kimlik doğrulama sistemi ---
const USERNAME = process.env.USERNAME const USERNAME = process.env.USERNAME;
const PASSWORD = process.env.PASSWORD const PASSWORD = process.env.PASSWORD;
let activeTokens = new Set(); let activeTokens = new Set();
app.post("/api/login", (req, res) => { app.post("/api/login", (req, res) => {
@@ -85,14 +84,12 @@ app.post("/api/login", (req, res) => {
}); });
function requireAuth(req, res, next) { function requireAuth(req, res, next) {
const token = const token = req.headers.authorization?.split(" ")[1] || req.query.token;
req.headers.authorization?.split(" ")[1] || req.query.token;
if (!token || !activeTokens.has(token)) if (!token || !activeTokens.has(token))
return res.status(401).json({ error: "Unauthorized" }); return res.status(401).json({ error: "Unauthorized" });
next(); next();
} }
// --- Torrent veya magnet ekleme --- // --- Torrent veya magnet ekleme ---
app.post("/api/transfer", requireAuth, upload.single("torrent"), (req, res) => { app.post("/api/transfer", requireAuth, upload.single("torrent"), (req, res) => {
try { try {
@@ -131,8 +128,8 @@ app.post("/api/transfer", requireAuth, upload.single("torrent"), (req, res) => {
infoHash: torrent.infoHash, infoHash: torrent.infoHash,
name: torrent.name, name: torrent.name,
selectedIndex, selectedIndex,
tracker: torrent.announce?.[0] || null, // 🆕 tracker: torrent.announce?.[0] || null,
added, // 🆕 added,
files: torrent.files.map((f, i) => ({ files: torrent.files.map((f, i) => ({
index: i, index: i,
name: f.name, 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) => { 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"); if (!fs.existsSync(fullPath)) return res.status(404).send("File not found");
const stat = fs.statSync(fullPath); const stat = fs.statSync(fullPath);
const fileSize = stat.size; const fileSize = stat.size;
const type = mime.lookup(fullPath) || "application/octet-stream";
const isVideo = String(type).startsWith("video/");
const range = req.headers.range; const range = req.headers.range;
if (range) { if (isVideo && range) {
const [startStr, endStr] = range.replace(/bytes=/, "").split("-"); const [startStr, endStr] = range.replace(/bytes=/, "").split("-");
const start = parseInt(startStr, 10); const start = parseInt(startStr, 10);
const end = endStr ? parseInt(endStr, 10) : fileSize - 1; 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}`, "Content-Range": `bytes ${start}-${end}/${fileSize}`,
"Accept-Ranges": "bytes", "Accept-Ranges": "bytes",
"Content-Length": chunkSize, "Content-Length": chunkSize,
"Content-Type": "video/mp4" "Content-Type": type
}; };
res.writeHead(206, head); res.writeHead(206, head);
file.pipe(res); file.pipe(res);
} else { } else {
const head = { const head = {
"Content-Length": fileSize, "Content-Length": fileSize,
"Content-Type": "video/mp4" "Content-Type": type,
"Accept-Ranges": isVideo ? "bytes" : "none"
}; };
res.writeHead(200, head); res.writeHead(200, head);
fs.createReadStream(fullPath).pipe(res); 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) => { app.get("/api/files", requireAuth, (req, res) => {
const walk = (dir) => { const walk = (dir) => {
let result = []; let result = [];
@@ -251,21 +253,35 @@ app.get("/api/files", requireAuth, (req, res) => {
if (entry.isDirectory()) { if (entry.isDirectory()) {
result = result.concat(walk(full)); result = result.concat(walk(full));
} else { } else {
// thumbnail.jpg dosyasını listeleme
if (entry.name.toLowerCase() === "thumbnail.jpg") continue; 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 size = fs.statSync(full).size;
const thumb = fs.existsSync(thumbPath) 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` ? `/downloads/${rootHash}/thumbnail.jpg`
: null; : null;
result.push({ result.push({
name: rel, name: rel,
size, size,
type, // 🆕 "image/jpeg", "video/mp4", vs.
url, // 🆕 doğrudan görüntüleme/oynatma için
thumbnail: thumb 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) => { app.get("/stream/:hash", requireAuth, (req, res) => {
const entry = torrents.get(req.params.hash); const entry = torrents.get(req.params.hash);
if (!entry) return res.status(404).end(); if (!entry) return res.status(404).end();
@@ -331,8 +347,6 @@ const server = app.listen(PORT, () =>
const publicDir = path.join(__dirname, "public"); const publicDir = path.join(__dirname, "public");
if (fs.existsSync(publicDir)) { if (fs.existsSync(publicDir)) {
app.use(express.static(publicDir)); app.use(express.static(publicDir));
// Frontend route'larını index.html'e yönlendir
app.get("*", (req, res, next) => { app.get("*", (req, res, next) => {
if (req.path.startsWith("/api")) return next(); if (req.path.startsWith("/api")) return next();
res.sendFile(path.join(publicDir, "index.html")); res.sendFile(path.join(publicDir, "index.html"));