Compare commits

...

3 Commits

Author SHA1 Message Date
20da34beb2 feat(ui): silme işlemini iki aşamalı onay sistemine dönüştür
Tarayıcı doğrulama penceresi yerine inline onay mekanizması eklendi.
Kullanıcı dosya silmek için "Sil" butonuna ilk tıkladığında buton kırmızıya
dönerek "Emin misiniz?" sorusunu gösterir ve ikinci tıklamada silme işlemini
gerçekleştirir. Bu yaklaşım kullanıcı deneyimini iyileştirir ve uygulama
tutarlılığını artırır.
2026-02-02 22:49:26 +03:00
2b5bb86b3e feat(rclone): GDrive dosya silme desteği ekle
Silme API'si artık dosyaların konumunu otomatik olarak tespit edebiliyor
(DOWNLOAD_DIR veya GDRIVE_ROOT). GDrive dosyaları için doğrudan silme
mantığı uygulanırken, Downloads dosyaları için mevcut trash sistemi
korunuyor.
2026-02-02 22:30:38 +03:00
1e4fb38cfb feat(rclone): mount başlatma durumu ve log sunumunu geliştir
Mount işlemi için "Başlatılıyor" ara durumu eklenerek kullanıcı geri bildirimi
iyileştirildi. Sunucu tarafında log seviyeleri ayrıştırılarak gerçek hatalar
bilgi mesajlarından ayırt edildi ve arayüze yansıtıldı.
2026-02-02 22:14:41 +03:00
3 changed files with 198 additions and 54 deletions

View File

@@ -468,6 +468,7 @@
let pendingPlayTarget = null;
let activeMenu = null; // Aktif menü öğesi
let menuPosition = { top: 0, left: 0 }; // Menü pozisyonu
let deleteConfirmPending = false; // Silme onayı beklemede mi
let showMatchModal = false;
let matchingFile = null;
let matchTitle = "";
@@ -1444,48 +1445,53 @@
async function deleteFile(item) {
if (!item) return;
const target = resolveDeletionTargets(item);
if (!target) {
// Eğer onay beklemedeyse, silme işlemini gerçekleştir
if (deleteConfirmPending) {
const target = resolveDeletionTargets(item);
if (!target) {
closeMenu();
return;
}
const result = await performDeletion(target);
deleteConfirmPending = false; // Reset flag
if (!result.ok) {
alert("Silme hatası: " + (result.error || "Bilinmeyen hata"));
closeMenu();
return;
}
if (item.isDirectory) {
const displayKey = normalizePath(
item.displayPath ||
(item.name?.startsWith("dir:") ? item.name.slice(4) : ""),
);
if (displayKey || displayKey === "") {
pendingFolders.delete(displayKey);
}
}
await loadFiles();
await Promise.all([refreshMovieCount(), refreshTvShowCount(), fetchTrashItems()]);
selectedItems = new Set(
[...selectedItems].filter((name) => name !== item.name),
);
closeMenu();
return;
}
const label =
target.type === "directory"
? target.label || item.displayName || "Klasör"
: target.label || cleanFileName(item.name);
const message =
target.type === "directory"
? `"${label}" klasörünü silmek istediğine emin misin?`
: `"${label}" dosyasını silmek istediğinizden emin misiniz?`;
if (!confirm(message)) {
closeMenu();
return;
}
const result = await performDeletion(target);
if (!result.ok) {
alert("Silme hatası: " + (result.error || "Bilinmeyen hata"));
closeMenu();
return;
}
if (item.isDirectory) {
const displayKey = normalizePath(
item.displayPath ||
(item.name?.startsWith("dir:") ? item.name.slice(4) : ""),
);
if (displayKey || displayKey === "") {
pendingFolders.delete(displayKey);
}
// İlk tıklama - onay moduna geç
deleteConfirmPending = true;
}
await loadFiles();
await Promise.all([refreshMovieCount(), refreshTvShowCount(), fetchTrashItems()]);
selectedItems = new Set(
[...selectedItems].filter((name) => name !== item.name),
);
closeMenu();
// Menü kapandığında onay durumunu resetle
function closeMenu() {
activeMenu = null;
deleteConfirmPending = false;
showMatchModal = false;
matchingFile = null;
}
// Klasör oluşturma fonksiyonları
@@ -2419,11 +2425,11 @@
</button>
<div class="menu-divider"></div>
<button
class="menu-item delete"
class="menu-item delete {deleteConfirmPending ? 'confirming' : ''}"
on:click|stopPropagation={() => deleteFile(activeMenu)}
>
<i class="fa-solid fa-trash"></i>
<span>Sil</span>
<span>{deleteConfirmPending ? 'Emin misiniz?' : 'Sil'}</span>
</button>
</div>
{/if}
@@ -3979,7 +3985,17 @@
.menu-item.delete {
color: #e53935;
}
.menu-item.delete.confirming {
color: #fff;
background-color: #e53935;
font-weight: 600;
}
.menu-item.delete.confirming:hover {
background-color: #c62828;
}
.menu-item.delete:hover {
background-color: #ffebee;
}

View File

@@ -198,7 +198,24 @@
if (!resp.ok || !data?.ok) {
throw new Error(data?.error || `HTTP ${resp.status}`);
}
success = "Rclone mount başlatıldı.";
// Mount başlatıldı, birkaç saniye bekleyip tekrar kontrol et
success = "Rclone mount başlatılıyor...";
// 2 saniye sonra status güncelle
setTimeout(async () => {
await loadRcloneStatus();
// Status yüklendikten sonra mesajı güncelle
if (rcloneStatus?.mounted) {
success = "Rclone mount başarıyla başlatıldı.";
} else if (rcloneStatus?.running) {
success = "Rclone mount başlatıldı, mount tamamlanıyor...";
} else {
error = "Rclone mount başlatılamadı.";
}
}, 2000);
// İlk status güncellemesi
await loadRcloneStatus();
} catch (err) {
error = err?.message || "Rclone mount başlatılamadı.";
@@ -468,7 +485,17 @@
<div class="card muted" style="margin-top:10px;">
<div><strong>Durum:</strong></div>
<div>Enabled: {rcloneStatus.enabled ? "Evet" : "Hayır"}</div>
<div>Mounted: {rcloneStatus.mounted ? "Evet" : "Hayır"}</div>
<div>
Mounted:
{#if rcloneStatus.mountStatus === "starting"}
<span style="color: #f57c00;">Başlatılıyor...</span>
{:else if rcloneStatus.mounted}
<span style="color: #388e3c;">Evet</span>
{:else}
Hayır
{/if}
</div>
<div>Running: {rcloneStatus.running ? "Evet" : "Hayır"}</div>
<div>Remote: {rcloneStatus.remoteConfigured ? "Hazır" : "Eksik"}</div>
{#if rcloneStatus.vfsCacheMode}
<div>VFS Cache Mode: <code>{rcloneStatus.vfsCacheMode}</code></div>
@@ -489,7 +516,14 @@
</div>
{/if}
{#if rcloneStatus.lastError}
<div style="margin-top: 8px; color: #d32f2f;">Son hata: {rcloneStatus.lastError}</div>
<div style="margin-top: 8px; color: #d32f2f;">
<i class="fa-solid fa-circle-exclamation"></i> Son hata: {rcloneStatus.lastError}
</div>
{/if}
{#if rcloneStatus.lastLog && !rcloneStatus.lastError}
<div style="margin-top: 8px; font-size: 11px; color: #666;">
<i class="fa-solid fa-circle-info"></i> Son log: {rcloneStatus.lastLog}
</div>
{/if}
</div>
{/if}

View File

@@ -780,6 +780,7 @@ function resolveRootDir(rootFolder) {
let rcloneProcess = null;
let rcloneLastError = null;
let rcloneLastLogMessage = null; // Tüm log mesajları için (NOTICE dahil)
const rcloneAuthSessions = new Map();
let rcloneCacheCleanTimer = null;
// Auto-restart sayaçları
@@ -1276,12 +1277,34 @@ function startRcloneMount(settings) {
rcloneProcess.stdout.on("data", (data) => {
const msg = data.toString().trim();
if (msg) console.log(`🌀 rclone: ${msg}`);
if (msg) {
rcloneLastLogMessage = msg;
// NOTICE mesajları için farklı ikon, diğerleri için normal
if (msg.toUpperCase().includes("NOTICE")) {
console.log(`📡 rclone: ${msg}`);
} else {
console.log(`🌀 rclone: ${msg}`);
}
}
});
rcloneProcess.stderr.on("data", (data) => {
const msg = data.toString().trim();
if (msg) {
rcloneLastError = msg;
rcloneLastLogMessage = msg;
// NOTICE ve INFO seviyesindeki loglar hata değil
// Sadece ERROR, FATAL, CRITICAL seviyesindekileri "son hata" olarak işaretle
const upperMsg = msg.toUpperCase();
if (upperMsg.includes("ERROR") ||
upperMsg.includes("FATAL") ||
upperMsg.includes("CRITICAL") ||
upperMsg.includes("FAILED") ||
upperMsg.includes("COULDN'T") ||
upperMsg.includes("CANNOT") ||
upperMsg.includes("REFUSED") ||
upperMsg.includes("TIMEOUT") ||
upperMsg.includes("CONNECTION")) {
rcloneLastError = msg;
}
console.warn(`⚠️ rclone: ${msg}`);
}
});
@@ -7744,17 +7767,58 @@ app.delete("/api/file", requireAuth, (req, res) => {
if (!filePath) return res.status(400).json({ error: "path gerekli" });
const safePath = sanitizeRelative(filePath);
const fullPath = path.join(DOWNLOAD_DIR, safePath);
let folderId = (safePath.split(/[\/]/)[0] || "").trim();
let rootDir = folderId ? path.join(DOWNLOAD_DIR, folderId) : null;
let folderIsDirectory = false;
if (rootDir && fs.existsSync(rootDir)) {
try {
folderIsDirectory = fs.statSync(rootDir).isDirectory();
} catch (err) {
folderIsDirectory = false;
// Dosyanın nerede olduğunu bul (DOWNLOAD_DIR veya GDRIVE_ROOT)
let fullPath = null;
let storageBase = null; // Dosyanın bulunduğu storage base (DOWNLOAD_DIR veya GDRIVE_ROOT)
for (const baseDir of getStorageRoots()) {
const testPath = path.join(baseDir, safePath);
if (fs.existsSync(testPath)) {
fullPath = testPath;
storageBase = baseDir;
break;
}
}
// Dosya bulunamadı
if (!fullPath) {
return res.status(404).json({ error: "Dosya bulunamadı" });
}
let folderId = (safePath.split(/[\/]/)[0] || "").trim();
let rootDir = null;
let folderIsDirectory = false;
// GDrive'da ise special handling - GDrive'ın kendisi root olarak kabul edilir
const isGDriveFile = storageBase === GDRIVE_ROOT;
if (isGDriveFile) {
// GDrive'da klasör yapısı farklıdır
// folderId varsa ve GDRIVE_ROOT/folderId bir klasörse
const testRootDir = path.join(GDRIVE_ROOT, folderId);
if (folderId && fs.existsSync(testRootDir)) {
try {
folderIsDirectory = fs.statSync(testRootDir).isDirectory();
if (folderIsDirectory) {
rootDir = GDRIVE_ROOT; // GDrive root'u
}
} catch (err) {
folderIsDirectory = false;
}
}
} else {
// Downloads klasörü için normal mantık
rootDir = folderId ? path.join(DOWNLOAD_DIR, folderId) : null;
if (rootDir && fs.existsSync(rootDir)) {
try {
folderIsDirectory = fs.statSync(rootDir).isDirectory();
} catch (err) {
folderIsDirectory = false;
}
}
}
// Kök dosyalarda ilk segment dosya adıdır; klasör değilse root davranışı uygula
if (folderId && !folderIsDirectory) {
folderId = "";
@@ -7783,6 +7847,31 @@ app.delete("/api/file", requireAuth, (req, res) => {
const isDirectory = stats.isDirectory();
const relWithinRoot = safePath.split(/[\\/]/).slice(1).join("/");
let trashEntry = null;
// GDrive dosyaları için özel handling - doğrudan sil
const isGDriveFile = storageBase === GDRIVE_ROOT;
if (isGDriveFile) {
// GDrive dosyaları için doğrudan silme (trash sistemi yok)
try {
if (isDirectory) {
fs.rmSync(fullPath, { recursive: true, force: true });
console.log(`🗑️ GDrive klasör silindi: ${safePath}`);
} else {
fs.unlinkSync(fullPath);
console.log(`🗑️ GDrive dosya silindi: ${safePath}`);
}
removeThumbnailsForPath(safePath);
broadcastFileUpdate("gdrive");
broadcastDiskSpace();
return res.json({ ok: true, filesRemoved: true, deletedFrom: "gdrive" });
} catch (deleteErr) {
console.error(`❌ GDrive dosya silme hatası: ${deleteErr.message}`);
return res.status(500).json({ error: `Silme hatası: ${deleteErr.message}` });
}
}
// Downloads klasörü için normal trash sistemi
if (folderId && folderIsDirectory && rootDir) {
const infoBeforeDelete = readInfoForRoot(folderId);
mediaFlags = detectMediaFlagsForPath(
@@ -9210,6 +9299,10 @@ app.get("/api/rclone/status", requireAuth, async (req, res) => {
enabled: RCLONE_ENABLED,
mounted,
running: Boolean(rcloneProcess),
// Mount durumu hakkında daha fazla bilgi
mountStatus: !rcloneProcess ? "stopped" :
mounted ? "mounted" :
"starting", // Process çalışıyor ama mount henüz tamamlanmadı
mountDir: settings.mountDir,
remoteName: settings.remoteName,
remotePath: settings.remotePath,
@@ -9219,7 +9312,8 @@ app.get("/api/rclone/status", requireAuth, async (req, res) => {
cacheCleanMinutes: settings.cacheCleanMinutes || 0,
configExists: fs.existsSync(settings.configPath),
remoteConfigured: rcloneConfigHasRemote(settings.remoteName),
lastError: rcloneLastError || null,
lastError: rcloneLastError || null, // Sadece gerçek hatalar
lastLog: rcloneLastLogMessage || null, // Son log mesajı (NOTICE dahil)
// Performans ayarları
vfsCacheMode: RCLONE_VFS_CACHE_MODE,
bufferSize: RCLONE_BUFFER_SIZE,