Profile avatar resize crop eklendi. Hatalar fixlendi.

This commit is contained in:
2025-12-04 21:29:20 +03:00
parent 3d67900557
commit bbc245ced1
9 changed files with 880 additions and 11 deletions

View File

@@ -15,6 +15,10 @@ Add torrents, monitor downloads, and instantly stream videos through a clean web
- Upload `.torrent` files (via form)
- Add magnet links (via prompt)
- 👤 **Profil & Avatar**
- Profil bilgilerini görüntüle
- Avatarı güvenli şekilde yükle/kırp (PNG, 3MB sınır)
- 📥 **Download Management**
- View active torrents
- See progress, speed, and remaining time

View File

@@ -17,6 +17,7 @@
import { refreshTvShowCount } from "./stores/tvStore.js";
import { refreshMusicCount } from "./stores/musicStore.js";
import { fetchTrashItems } from "./stores/trashStore.js";
import { setAvatarUrl } from "./stores/avatarStore.js";
const token = getAccessToken();
@@ -41,6 +42,34 @@
}, 400);
};
const loadUserProfile = async () => {
try {
const response = await fetch(`${API}/api/profile`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${getAccessToken()}`,
'Content-Type': 'application/json'
}
});
if (response.ok) {
const profileData = await response.json();
if (profileData?.avatarExists) {
const token = getAccessToken();
if (token) {
const avatarUrl = `${API}/api/profile/avatar?token=${token}&v=${Date.now()}`;
setAvatarUrl(avatarUrl);
}
} else {
setAvatarUrl(null);
}
}
} catch (error) {
console.warn('Profil bilgileri yüklenemedi:', error);
setAvatarUrl(null);
}
};
// Menü aç/kapat (hamburger butonuyla)
const toggleMenu = () => {
menuOpen = !menuOpen;
@@ -57,6 +86,7 @@
refreshTvShowCount();
refreshMusicCount();
fetchTrashItems();
loadUserProfile();
const authToken = getAccessToken();
if (authToken) {
const wsUrl = `${API.replace("http", "ws")}?token=${authToken}`;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -6,6 +6,7 @@
activeSearchTerm,
updateSearchTerm
} from "../stores/searchStore.js";
import { avatarUrlStore } from "../stores/avatarStore.js";
import { clearTokens } from "../utils/api.js";
@@ -15,6 +16,7 @@
export let avatarUrl = null;
let showAvatarMenu = false;
let avatarWrap;
$: resolvedAvatar = avatarUrl || $avatarUrlStore;
const onToggle = () => dispatch("toggleMenu");
@@ -82,8 +84,8 @@
<div class="avatar-wrap" bind:this={avatarWrap}>
<button class="avatar" type="button" aria-label="Profil" on:click={toggleAvatarMenu}>
{#if avatarUrl}
<img src={avatarUrl} alt="Avatar" loading="lazy" />
{#if resolvedAvatar}
<img src={resolvedAvatar} alt="Avatar" loading="lazy" on:error={() => (resolvedAvatar = null)} />
{:else}
<div class="placeholder">
<i class="fa-regular fa-user"></i>

View File

@@ -27,11 +27,13 @@
persistTokens({ accessToken, refreshToken });
if (accessToken) localStorage.setItem("token", accessToken); // Geçiş dönemi uyumluluğu
if (user) localStorage.setItem("user", JSON.stringify(user));
// Router state beklemeden anında yönlendir
navigate("/", { replace: true });
window.dispatchEvent(new Event("token-changed"));
// Tam yenileme ile tüm store ve websocket'ler temiz açılır
window.location.replace("/");
} else {
error = "Kullanıcı adı veya şifre hatalı.";
clearTokens();
window.dispatchEvent(new Event("token-changed"));
}
}
</script>

View File

@@ -1,5 +1,349 @@
<script>
// Tasarım, diğer sayfalardaki yapıyı korur; şimdilik boş içerik.
import { onMount } from "svelte";
import { API, fetchProfile, uploadAvatar, getAccessToken } from "../utils/api.js";
import { setAvatarUrl } from "../stores/avatarStore.js";
const MAX_SIZE = 3 * 1024 * 1024;
const ALLOWED_TYPES = ["image/png", "image/jpeg", "image/jpg"];
let profile = { username: "", role: "" };
let loading = true;
let error = null;
let avatarSrc = null;
let successMessage = "";
let uploadError = "";
let fileInput;
let previewUrl = null;
let previewImage = null;
let imageSize = { width: 0, height: 0 };
let croppedPreview = null;
let saving = false;
let showCropper = false;
const DISPLAY_SIZE = 320;
const STAGE_SIZE = 320;
let stageEl;
// Dikdörtgen crop kutusu (görüntü alanı koordinatlarıyla)
let cropBox = { x: 0, y: 0, width: 0, height: 0 };
let dragState = null;
onMount(() => {
loadProfile();
});
async function loadProfile() {
loading = true;
error = null;
try {
const res = await fetchProfile();
profile = {
username: res?.username || "User",
role: res?.role || "user"
};
const hasAvatar = !!res?.avatarExists;
avatarSrc = hasAvatar ? buildAvatarUrl() : null;
setAvatarUrl(avatarSrc);
} catch (err) {
error = "Profil yüklenemedi.";
avatarSrc = null;
setAvatarUrl(null);
} finally {
loading = false;
}
}
function resetSelection() {
if (previewUrl) URL.revokeObjectURL(previewUrl);
previewUrl = null;
previewImage = null;
imageSize = { width: 0, height: 0 };
cropBox = { x: 0, y: 0, width: 0, height: 0 };
croppedPreview = null;
}
function sniffSignature(bytes) {
if (!bytes || bytes.length < 4) return false;
const png = bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4e && bytes[3] === 0x47;
const jpeg = bytes[0] === 0xff && bytes[1] === 0xd8;
return png || jpeg;
}
async function handleFileChange(event) {
uploadError = "";
successMessage = "";
const file = event.target.files?.[0];
resetSelection();
if (!file) return;
if (!ALLOWED_TYPES.includes((file.type || "").toLowerCase())) {
uploadError = "Sadece jpg, jpeg veya png yükleyebilirsiniz.";
return;
}
if (file.size > MAX_SIZE) {
uploadError = "Dosya boyutu 3MB'ı aşmamalı.";
return;
}
const buffer = await file.arrayBuffer();
const header = new Uint8Array(buffer.slice(0, 4));
if (!sniffSignature(header)) {
uploadError = "Geçerli bir görsel dosyası değil.";
return;
}
const blob = new Blob([buffer], { type: file.type || "image/png" });
const url = URL.createObjectURL(blob);
previewUrl = url;
const img = new Image();
img.onload = () => {
previewImage = img;
imageSize = { width: img.width, height: img.height };
initializeCropBox();
buildPreview();
showCropper = true;
};
img.onerror = () => {
uploadError = "Görsel okunamadı.";
resetSelection();
};
img.src = url;
}
function clamp(val, min, max) {
return Math.min(Math.max(val, min), max);
}
function getRenderMetrics() {
const scale = Math.min(DISPLAY_SIZE / imageSize.width, DISPLAY_SIZE / imageSize.height);
const renderedW = imageSize.width * scale;
const renderedH = imageSize.height * scale;
const offsetX = (DISPLAY_SIZE - renderedW) / 2;
const offsetY = (DISPLAY_SIZE - renderedH) / 2;
return { scale, renderedW, renderedH, offsetX, offsetY };
}
function initializeCropBox() {
const { renderedW, renderedH, offsetX, offsetY } = getRenderMetrics();
const maxSize = Math.min(renderedW, renderedH);
// Çerçeve maksimum boyutun %80'inde başlasın, minimum 100px
const size = Math.max(100, Math.min(maxSize * 0.8, 200));
cropBox = {
x: offsetX + (renderedW - size) / 2,
y: offsetY + (renderedH - size) / 2,
width: size,
height: size
};
}
function clampCropBox(box) {
const { renderedW, renderedH, offsetX, offsetY } = getRenderMetrics();
const minSize = 40;
let width = Math.max(minSize, Math.min(box.width, renderedW));
let height = Math.max(minSize, Math.min(box.height, renderedH));
let x = box.x;
let y = box.y;
// Konteyner sınırları içinde tut
if (x < offsetX) x = offsetX;
if (y < offsetY) y = offsetY;
if (x + width > offsetX + renderedW) x = offsetX + renderedW - width;
if (y + height > offsetY + renderedH) y = offsetY + renderedH - height;
return { x, y, width, height };
}
function buildPreview() {
if (!previewImage || !cropBox.width || !cropBox.height) {
croppedPreview = null;
return;
}
const { scale, offsetX, offsetY } = getRenderMetrics();
const srcWidth = cropBox.width / scale;
const srcHeight = cropBox.height / scale;
const srcX = (cropBox.x - offsetX) / scale;
const srcY = (cropBox.y - offsetY) / scale;
const canvas = document.createElement("canvas");
const size = 256;
canvas.width = size;
canvas.height = size;
const ctx = canvas.getContext("2d");
ctx.clearRect(0, 0, size, size);
ctx.drawImage(
previewImage,
srcX,
srcY,
srcWidth,
srcHeight,
0,
0,
size,
size
);
croppedPreview = canvas.toDataURL("image/png");
}
function startDrag(handle, event) {
if (!previewImage) return;
event.preventDefault();
console.log('startDrag called with handle:', handle); // Debug
const { x, y } = toStageCoords(event);
dragState = {
handle,
startX: x,
startY: y,
box: { ...cropBox }
};
window.addEventListener("pointermove", onDrag);
window.addEventListener("pointerup", endDrag);
}
function onDrag(event) {
if (!dragState) return;
const { x, y } = toStageCoords(event);
const dx = x - dragState.startX;
const dy = y - dragState.startY;
const { handle, box } = dragState;
console.log('onDrag with handle:', handle, 'dx:', dx, 'dy:', dy, 'current box:', { x: box.x, y: box.y, width: box.width, height: box.height }); // Debug
let next = { ...box };
const minSize = 40;
const tl = { x: box.x, y: box.y };
const br = { x: box.x + box.width, y: box.y + box.height };
const tr = { x: box.x + box.width, y: box.y };
const bl = { x: box.x, y: box.y + box.height };
const pos = { x, y };
switch (handle) {
case "move":
next.x = box.x + dx;
next.y = box.y + dy;
break;
case "topleft": {
const newWidth = Math.max(minSize, box.width - dx);
const newHeight = Math.max(minSize, box.height - dy);
next.x = box.x + (box.width - newWidth);
next.y = box.y + (box.height - newHeight);
next.width = newWidth;
next.height = newHeight;
break;
}
case "topright": {
const newWidth = Math.max(minSize, box.width + dx);
const newHeight = Math.max(minSize, box.height - dy);
next.y = box.y + (box.height - newHeight);
next.width = newWidth;
next.height = newHeight;
break;
}
case "bottomleft": {
const newWidth = Math.max(minSize, box.width - dx);
const newHeight = Math.max(minSize, box.height + dy);
next.x = box.x + (box.width - newWidth);
next.width = newWidth;
next.height = newHeight;
break;
}
case "bottomright":
default: {
const newWidth = Math.max(minSize, box.width + dx);
const newHeight = Math.max(minSize, box.height + dy);
next.width = newWidth;
next.height = newHeight;
break;
}
// Kenar handle'ları - tek yönlü boyutlandırma
case "top": {
const newHeight = Math.max(minSize, box.height - dy);
next.y = box.y + (box.height - newHeight);
next.height = newHeight;
break;
}
case "right": {
const newWidth = Math.max(minSize, box.width + dx);
next.width = newWidth;
break;
}
case "bottom": {
const newHeight = Math.max(minSize, box.height + dy);
next.height = newHeight;
break;
}
case "left": {
const newWidth = Math.max(minSize, box.width - dx);
next.x = box.x + (box.width - newWidth);
next.width = newWidth;
break;
}
}
cropBox = clampCropBox(next);
buildPreview();
}
function endDrag() {
window.removeEventListener("pointermove", onDrag);
window.removeEventListener("pointerup", endDrag);
dragState = null;
}
function toStageCoords(event) {
const rect = stageEl?.getBoundingClientRect();
if (!rect) return { x: 0, y: 0 };
return {
x: event.clientX - rect.left,
y: event.clientY - rect.top
};
}
async function saveAvatar() {
uploadError = "";
successMessage = "";
if (!previewImage || !croppedPreview) {
uploadError = "Önce bir görsel seçin ve kırpın.";
return;
}
saving = true;
try {
const blob = await new Promise((resolve, reject) =>
fetch(croppedPreview)
.then((r) => r.blob())
.then(resolve)
.catch(reject)
);
if (blob.size > MAX_SIZE) {
uploadError = "Kırpılan görsel 3MB sınırınııyor.";
saving = false;
return;
}
const form = new FormData();
form.append("avatar", blob, "avatar.png");
const res = await uploadAvatar(form);
if (!res?.success) {
throw new Error(res?.error || "Avatar kaydedilemedi");
}
const nextUrl = buildAvatarUrl();
avatarSrc = nextUrl;
setAvatarUrl(nextUrl);
successMessage = "Avatar güncellendi.";
resetSelection();
if (fileInput) fileInput.value = "";
showCropper = false;
} catch (err) {
uploadError = err?.message || "Avatar yüklenirken hata oluştu.";
} finally {
saving = false;
}
}
function buildAvatarUrl() {
const token = getAccessToken();
if (!token) return null;
return `${API}/api/profile/avatar?token=${token}&v=${Date.now()}`;
}
</script>
<section class="files">
@@ -8,8 +352,106 @@
<h2>Profile</h2>
</div>
</div>
<div class="empty">
Profil içeriği yakında.
<div class="card-grid">
<div class="card">
<div class="card-title">Kullanıcı</div>
{#if loading}
<p class="muted">Yükleniyor...</p>
{:else if error}
<p class="error">{error}</p>
{:else}
<div class="info-row">
<span class="label">Kullanıcı Adı</span>
<span class="value">{profile.username}</span>
</div>
<div class="info-row">
<span class="label">Rol</span>
<span class="value role">{profile.role}</span>
</div>
<div class="info-row">
<span class="label">Avatar</span>
<div class="avatar-preview">
{#if avatarSrc}
<img src={avatarSrc} alt="Avatar" on:error={() => (avatarSrc = null)} />
{:else}
<div class="avatar-placeholder">
<i class="fa-regular fa-user"></i>
</div>
{/if}
</div>
</div>
{/if}
</div>
<div class="card">
<div class="card-title">Avatar Güncelle</div>
<p class="muted small">
Sadece jpg, jpeg veya png; maksimum 3MB. Kırpma sonrası PNG olarak kaydedilir.
</p>
<div class="upload-row">
<input
type="file"
accept="image/png,image/jpeg"
on:change={handleFileChange}
bind:this={fileInput}
/>
<button class="ghost" type="button" on:click={() => fileInput && (fileInput.value = "")}>
Temizle
</button>
</div>
{#if showCropper}
<div class="crop-wrapper">
<div class="crop-stage" bind:this={stageEl}>
{#if previewImage}
<img
src={previewUrl}
alt="Önizleme"
draggable="false"
style={`width:${DISPLAY_SIZE}px;height:${DISPLAY_SIZE}px;`}
/>
<div
class="crop-box"
style={`width:${cropBox.width}px;height:${cropBox.height}px;transform:translate(${cropBox.x}px, ${cropBox.y}px);`}
on:pointerdown={(e) => startDrag("move", e)}
>
<!-- Köşe handle'ları -->
<span class="handle tl" on:pointerdown={(e) => { e.stopPropagation(); startDrag("topleft", e); }}></span>
<span class="handle tr" on:pointerdown={(e) => { e.stopPropagation(); startDrag("topright", e); }}></span>
<span class="handle bl" on:pointerdown={(e) => { e.stopPropagation(); startDrag("bottomleft", e); }}></span>
<span class="handle br" on:pointerdown={(e) => { e.stopPropagation(); startDrag("bottomright", e); }}></span>
<!-- Kenar handle'ları -->
<span class="handle top" on:pointerdown={(e) => { e.stopPropagation(); startDrag("top", e); }}></span>
<span class="handle right" on:pointerdown={(e) => { e.stopPropagation(); startDrag("right", e); }}></span>
<span class="handle bottom" on:pointerdown={(e) => { e.stopPropagation(); startDrag("bottom", e); }}></span>
<span class="handle left" on:pointerdown={(e) => { e.stopPropagation(); startDrag("left", e); }}></span>
</div>
{:else}
<div class="avatar-placeholder large">
<i class="fa-regular fa-user"></i>
</div>
{/if}
</div>
<div class="actions">
<button class="ghost" type="button" on:click={() => { resetSelection(); showCropper = false; if (fileInput) fileInput.value = ""; }}>
Vazgeç
</button>
<button class="primary" type="button" on:click={saveAvatar} disabled={saving}>
{saving ? "Kaydediliyor..." : "Kırp ve Kaydet"}
</button>
</div>
</div>
{/if}
{#if uploadError}
<p class="error">{uploadError}</p>
{/if}
{#if successMessage}
<p class="success">{successMessage}</p>
{/if}
</div>
</div>
</section>
@@ -33,10 +475,278 @@
gap: 8px;
}
.empty {
padding: 24px;
border: 1px dashed var(--border, #dcdcdc);
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 12px;
}
.card {
background: #fff;
border: 1px solid var(--border, #dcdcdc);
border-radius: 10px;
color: #666;
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
}
.card-title {
font-weight: 600;
color: #22304e;
}
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 12px;
border: 1px solid var(--border, #e4e4e4);
border-radius: 8px;
background: #f8f9fb;
}
.label {
color: #5c5f6a;
font-size: 14px;
}
.value {
font-weight: 600;
color: #1f2937;
}
.role {
text-transform: uppercase;
font-size: 12px;
letter-spacing: 0.5px;
color: #0e7490;
}
.avatar-preview img,
.avatar-placeholder {
width: 72px;
height: 72px;
border-radius: 10px;
object-fit: cover;
border: 1px solid var(--border, #dcdcdc);
}
.avatar-placeholder {
display: inline-flex;
align-items: center;
justify-content: center;
background: #eef1f6;
color: #2c3e50;
font-size: 22px;
}
.muted {
color: #6b7280;
}
.muted.small {
font-size: 13px;
}
.upload-row {
display: flex;
gap: 10px;
align-items: center;
}
.upload-row input[type="file"] {
flex: 1;
}
.ghost {
background: #f1f5f9;
border: 1px solid var(--border, #dcdcdc);
border-radius: 6px;
padding: 8px 12px;
cursor: pointer;
}
.crop-panel {
display: grid;
grid-template-columns: 1fr;
gap: 12px;
align-items: center;
}
.crop-controls {
display: flex;
flex-direction: column;
gap: 10px;
}
.crop-controls label {
display: flex;
flex-direction: column;
gap: 6px;
font-size: 13px;
color: #4b5563;
}
.crop-wrapper {
display: flex;
flex-direction: column;
gap: 12px;
}
.crop-stage {
position: relative;
width: 320px;
height: 320px;
background: #2f3035;
border-radius: 8px;
overflow: hidden;
border: 1px solid var(--border, #dcdcdc);
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto;
}
.crop-stage img {
width: 100%;
height: 100%;
object-fit: contain;
border-radius: 4px;
user-select: none;
pointer-events: none;
}
.crop-box {
position: absolute;
top: 0;
left: 0;
border: 1px solid #000;
box-sizing: border-box;
cursor: move;
box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.5);
z-index: 1;
}
.crop-box .handle {
position: absolute;
width: 14px;
height: 14px;
background: #fff;
border: 2px solid #2f3035;
border-radius: 3px;
z-index: 2;
pointer-events: auto;
cursor: pointer;
}
.handle.tl { top: -8px; left: -8px; cursor: nwse-resize !important; }
.handle.tr { top: -8px; right: -8px; cursor: nesw-resize !important; }
.handle.bl { bottom: -8px; left: -8px; cursor: nesw-resize !important; }
.handle.br { bottom: -8px; right: -8px; cursor: nwse-resize !important; }
/* Kenar handle'ları */
.handle.top {
top: -3px;
left: 8px;
right: 8px;
height: 6px;
width: auto;
cursor: ns-resize !important;
background: #fff;
border: 1px solid #000;
}
.handle.right {
right: -3px;
top: 8px;
bottom: 8px;
width: 6px;
height: auto;
cursor: ew-resize !important;
background: #fff;
border: 1px solid #000;
}
.handle.bottom {
bottom: -3px;
left: 8px;
right: 8px;
height: 6px;
width: auto;
cursor: ns-resize !important;
background: #fff;
border: 1px solid #000;
}
.handle.left {
left: -3px;
top: 8px;
bottom: 8px;
width: 6px;
height: auto;
cursor: ew-resize !important;
background: #fff;
border: 1px solid #000;
}
.actions {
display: flex;
gap: 10px;
justify-content: flex-end;
}
.avatar-placeholder.large {
width: 280px;
height: 280px;
font-size: 28px;
border-radius: 6px;
}
.crop-controls input[type="range"] {
width: 100%;
}
.actions {
display: flex;
justify-content: flex-end;
}
.primary {
background: #22304e;
color: #fff;
border: none;
border-radius: 8px;
padding: 10px 14px;
cursor: pointer;
}
.primary[disabled] {
opacity: 0.6;
cursor: not-allowed;
}
.error {
color: #b91c1c;
font-size: 14px;
}
.success {
color: #0f766e;
font-size: 14px;
}
@media (max-width: 640px) {
.crop-panel {
grid-template-columns: 1fr;
}
.upload-row {
flex-direction: column;
align-items: stretch;
}
.ghost {
width: 100%;
text-align: center;
}
}
</style>

View File

@@ -0,0 +1,7 @@
import { writable } from "svelte/store";
export const avatarUrlStore = writable(null);
export function setAvatarUrl(url) {
avatarUrlStore.set(url || null);
}

View File

@@ -97,6 +97,20 @@ export async function deleteFromTrash(trashName) {
return res.json();
}
// 👤 Profil
export async function fetchProfile() {
const res = await apiFetch("/api/profile");
return res.json();
}
export async function uploadAvatar(formData) {
const res = await apiFetch("/api/profile/avatar", {
method: "POST",
body: formData
});
return res.json();
}
export async function moveEntry(sourcePath, targetDirectory) {
const res = await apiFetch("/api/file/move", {
method: "POST",

View File

@@ -90,6 +90,7 @@ const FFPROBE_MAX_BUFFER =
Number(process.env.FFPROBE_MAX_BUFFER) > 0
? Number(process.env.FFPROBE_MAX_BUFFER)
: 10 * 1024 * 1024;
const AVATAR_PATH = path.join(__dirname, "..", "client", "src", "assets", "avatar.png");
app.use(cors());
app.use(express.json());
@@ -180,6 +181,19 @@ const { router: authRouter, requireAuth, requireRole, issueMediaToken, verifyTok
app.use(authRouter);
const avatarUpload = multer({
storage: multer.memoryStorage(),
limits: { fileSize: 3 * 1024 * 1024 },
fileFilter: (req, file, cb) => {
const type = (file.mimetype || "").toLowerCase();
const allowed = ["image/png", "image/jpeg", "image/jpg"];
if (!allowed.includes(type)) {
return cb(new Error("INVALID_FILE_TYPE"));
}
cb(null, true);
}
});
buildHealthReport({
ffmpegPath: "ffmpeg",
ffprobePath: FFPROBE_PATH,
@@ -201,6 +215,73 @@ buildHealthReport({
app.get("/api/health", requireAuth, healthRouter(() => healthSnapshot));
// --- Profil bilgisi ---
app.get("/api/profile", requireAuth, (req, res) => {
const username = req.user?.sub || req.user?.username || "user";
const role = req.user?.role || "user";
const avatarExists = fs.existsSync(AVATAR_PATH);
res.json({
username,
role,
avatarExists,
avatarUrl: avatarExists ? "/api/profile/avatar" : null
});
});
app.get("/api/profile/avatar", requireAuth, (req, res) => {
if (!fs.existsSync(AVATAR_PATH)) {
return res.status(404).json({ error: "Avatar bulunamadı" });
}
res.setHeader("Content-Type", "image/png");
res.setHeader("Cache-Control", "no-store");
fs.createReadStream(AVATAR_PATH).pipe(res);
});
app.post(
"/api/profile/avatar",
requireAuth,
(req, res, next) => {
avatarUpload.single("avatar")(req, res, (err) => {
if (err) {
const isSize = err.code === "LIMIT_FILE_SIZE";
const message = isSize
? "Dosya boyutu 3MB'ı aşmamalı."
: "Geçersiz dosya tipi. Sadece jpg, jpeg veya png.";
return res.status(400).json({ error: message });
}
next();
});
},
(req, res) => {
try {
if (!req.file?.buffer) {
return res.status(400).json({ error: "Dosya yüklenemedi" });
}
const buffer = req.file.buffer;
if (!isAllowedImage(buffer)) {
return res.status(400).json({ error: "Sadece jpeg/jpg/png kabul edilir" });
}
if (!isPng(buffer)) {
return res
.status(400)
.json({ error: "Lütfen kırptıktan sonra PNG olarak yükleyin." });
}
ensureDirForFile(AVATAR_PATH);
fs.writeFileSync(AVATAR_PATH, buffer);
res.json({
success: true,
avatarUrl: "/api/profile/avatar"
});
} catch (err) {
console.error("Avatar yükleme hatası:", err);
res.status(500).json({ error: "Avatar kaydedilemedi" });
}
}
);
function tvdbImageUrl(pathSegment) {
if (!pathSegment) return null;
if (pathSegment.startsWith("http")) return pathSegment;
@@ -505,6 +586,25 @@ function sanitizeRelative(relPath) {
return relPath.replace(/^[\\/]+/, "");
}
function isPng(buffer) {
return (
buffer &&
buffer.length >= 8 &&
buffer[0] === 0x89 &&
buffer[1] === 0x50 &&
buffer[2] === 0x4e &&
buffer[3] === 0x47
);
}
function isJpeg(buffer) {
return buffer && buffer.length >= 2 && buffer[0] === 0xff && buffer[1] === 0xd8;
}
function isAllowedImage(buffer) {
return isPng(buffer) || isJpeg(buffer);
}
function determineMediaType({
tracker,
movieMatch,