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

@@ -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>
</style>