Files
dupe/client/src/routes/Settings.svelte

774 lines
21 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script>
import { onMount } from "svelte";
import { apiFetch } from "../utils/api.js";
import { showToast } from "../stores/toastStore.js";
const tabs = [
{ id: "general", label: "General", icon: "sliders" },
{ id: "youtube", label: "YouTube", icon: "youtube" },
{ id: "rclone", label: "Rclone", icon: "cloud" },
{ id: "advanced", label: "Advanced", icon: "gear" }
];
let activeTab = "youtube";
let youtubeCookies = "";
let cookiesUpdatedAt = null;
let loadingCookies = false;
let savingCookies = false;
let loadingYtSettings = false;
let savingYtSettings = false;
let selectedResolution = "1080p";
let onlyAudio = false;
const resolutionOptions = ["1080p", "720p", "480p", "360p", "240p", "144p"];
let error = null;
let rcloneStatus = null;
let rcloneLoading = false;
let rcloneSaving = false;
let rcloneAutoMove = false;
let rcloneAutoMount = false;
let rcloneCacheCleanMinutes = 0;
let rcloneConfText = "";
let rcloneConfVisible = false;
async function loadCookies() {
loadingCookies = true;
error = null;
try {
const resp = await apiFetch("/api/youtube/cookies");
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
const data = await resp.json();
youtubeCookies = data?.cookies || "";
cookiesUpdatedAt = data?.updatedAt || null;
} catch (err) {
error = err?.message || "Cookies alınamadı.";
} finally {
loadingCookies = false;
}
}
async function saveCookies() {
if (savingCookies) return;
error = null;
savingCookies = true;
try {
const payload = {
cookies: youtubeCookies
.split("\n")
.map((line) => line.trim())
.filter(Boolean)
.join("\n")
};
const resp = await apiFetch("/api/youtube/cookies", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
const data = await resp.json().catch(() => ({}));
if (!resp.ok || !data?.ok) {
throw new Error(data?.error || `HTTP ${resp.status}`);
}
cookiesUpdatedAt = data.updatedAt || Date.now();
showToast("Cookies kaydedildi.", "success");
} catch (err) {
error = err?.message || "Cookies kaydedilemedi.";
} finally {
savingCookies = false;
}
}
async function loadYoutubeSettings() {
loadingYtSettings = true;
error = null;
try {
const resp = await apiFetch("/api/youtube/settings");
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
const data = await resp.json();
if (data?.resolution) selectedResolution = data.resolution;
if (typeof data?.onlyAudio === "boolean") onlyAudio = data.onlyAudio;
} catch (err) {
error = err?.message || "YouTube ayarları alınamadı.";
} finally {
loadingYtSettings = false;
}
}
async function saveYoutubeSettings() {
if (savingYtSettings) return;
savingYtSettings = true;
error = null;
try {
const resp = await apiFetch("/api/youtube/settings", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
resolution: selectedResolution,
onlyAudio
})
});
const data = await resp.json().catch(() => ({}));
if (!resp.ok || !data?.ok) {
throw new Error(data?.error || `HTTP ${resp.status}`);
}
showToast("YouTube indirme ayarları kaydedildi.", "success");
} catch (err) {
error = err?.message || "YouTube ayarları kaydedilemedi.";
} finally {
savingYtSettings = false;
}
}
async function loadRcloneStatus() {
rcloneLoading = true;
error = null;
try {
const resp = await apiFetch("/api/rclone/status");
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
const data = await resp.json();
rcloneStatus = data;
rcloneAutoMove = Boolean(data?.autoMove);
rcloneAutoMount = Boolean(data?.autoMount);
rcloneCacheCleanMinutes = Number(data?.cacheCleanMinutes) || 0;
} catch (err) {
error = err?.message || "Rclone durumu alınamadı.";
} finally {
rcloneLoading = false;
}
}
async function loadRcloneConf() {
try {
const resp = await apiFetch("/api/rclone/conf");
const data = await resp.json().catch(() => ({}));
if (!resp.ok || !data?.ok) {
throw new Error(data?.error || `HTTP ${resp.status}`);
}
rcloneConfText = data.content || "";
} catch (err) {
error = err?.message || "rclone.conf okunamadı.";
}
}
async function saveRcloneSettings() {
if (rcloneSaving) return;
rcloneSaving = true;
error = null;
try {
const resp = await apiFetch("/api/rclone/settings", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
autoMove: rcloneAutoMove,
autoMount: rcloneAutoMount,
cacheCleanMinutes: rcloneCacheCleanMinutes
})
});
const data = await resp.json().catch(() => ({}));
if (!resp.ok || !data?.ok) {
throw new Error(data?.error || `HTTP ${resp.status}`);
}
const confResp = await apiFetch("/api/rclone/conf", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ content: rcloneConfText })
});
const confData = await confResp.json().catch(() => ({}));
if (!confResp.ok || !confData?.ok) {
throw new Error(confData?.error || `HTTP ${confResp.status}`);
}
showToast("Rclone ayarları kaydedildi.", "success");
await loadRcloneStatus();
} catch (err) {
error = err?.message || "Rclone ayarları kaydedilemedi.";
} finally {
rcloneSaving = false;
}
}
async function startRcloneMount() {
error = null;
try {
const resp = await apiFetch("/api/rclone/mount", { method: "POST" });
const data = await resp.json().catch(() => ({}));
if (!resp.ok || !data?.ok) {
throw new Error(data?.error || `HTTP ${resp.status}`);
}
// Mount başlatıldı, birkaç saniye bekleyip tekrar kontrol et
showToast("Rclone mount başlatılıyor...", "info");
// 2 saniye sonra status güncelle
setTimeout(async () => {
await loadRcloneStatus();
// Status yüklendikten sonra mesajı güncelle
if (rcloneStatus?.mounted) {
showToast("Rclone mount başarıyla başlatıldı.", "success");
} else if (rcloneStatus?.running) {
showToast("Rclone mount başlatıldı, mount tamamlanıyor...", "info");
} else {
error = "Rclone mount başlatılamadı.";
}
}, 2000);
// İlk status güncellemesi
await loadRcloneStatus();
} catch (err) {
error = err?.message || "Rclone mount başlatılamadı.";
}
}
async function stopRcloneMount() {
error = null;
try {
const resp = await apiFetch("/api/rclone/unmount", { method: "POST" });
const data = await resp.json().catch(() => ({}));
if (!resp.ok || !data?.ok) {
throw new Error(data?.error || `HTTP ${resp.status}`);
}
showToast("Rclone mount durduruldu.", "success");
await loadRcloneStatus();
} catch (err) {
error = err?.message || "Rclone mount durdurulamadı.";
}
}
async function cleanRcloneCache() {
error = null;
try {
const resp = await apiFetch("/api/rclone/cache/clean", { method: "POST" });
const data = await resp.json().catch(() => ({}));
if (!resp.ok || !data?.ok) {
throw new Error(data?.error || `HTTP ${resp.status}`);
}
showToast("Cache temizlendi.", "success");
await loadRcloneStatus();
} catch (err) {
error = err?.message || "Cache temizlenemedi.";
showToast(error, "error");
}
}
onMount(() => {
loadCookies();
loadYoutubeSettings();
loadRcloneStatus();
loadRcloneConf();
});
function formatDate(ts) {
if (!ts) return "—";
const d = new Date(Number(ts));
if (Number.isNaN(d.getTime())) return "—";
return d.toLocaleString();
}
</script>
<section class="files">
<div class="files-header">
<div class="header-title">
<h2>Settings</h2>
</div>
</div>
<div class="tabs">
<button
class="tab {activeTab === 'general' ? 'active' : ''}"
type="button"
on:click={() => activeTab = 'general'}
>
<i class="fa-solid fa-sliders"></i>
<span>General</span>
</button>
<button
class="tab {activeTab === 'youtube' ? 'active' : ''}"
type="button"
on:click={() => activeTab = 'youtube'}
>
<i class="fa-brands fa-youtube"></i>
<span>YouTube</span>
</button>
<button
class="tab {activeTab === 'rclone' ? 'active' : ''}"
type="button"
on:click={() => activeTab = 'rclone'}
>
<i class="fa-solid fa-cloud"></i>
<span>Rclone</span>
</button>
<button
class="tab {activeTab === 'advanced' ? 'active' : ''}"
type="button"
on:click={() => activeTab = 'advanced'}
>
<i class="fa-solid fa-gear"></i>
<span>Advanced</span>
</button>
</div>
{#if activeTab === "youtube"}
<div class="card">
<div class="card-header">
<div class="title">
<i class="fa-solid fa-circle-down"></i>
<span>YouTube Download</span>
</div>
</div>
<div class="field inline compact left-align">
<div class="inline-field">
<label for="resolution">Çözünürlük</label>
<select
id="resolution"
bind:value={selectedResolution}
disabled={loadingYtSettings || savingYtSettings}
>
{#each resolutionOptions as res}
<option value={res}>{res}</option>
{/each}
</select>
</div>
<label class="checkbox-row">
<input
type="checkbox"
bind:checked={onlyAudio}
disabled={loadingYtSettings || savingYtSettings}
/>
<span>Only Audio (sadece ses indir)</span>
</label>
</div>
<div class="actions left">
<button class="btn" on:click={loadYoutubeSettings} disabled={loadingYtSettings || savingYtSettings}>
<i class="fa-solid fa-rotate"></i> Yenile
</button>
<button class="btn primary" on:click={saveYoutubeSettings} disabled={loadingYtSettings || savingYtSettings}>
<i class="fa-solid fa-floppy-disk"></i> Kaydet
</button>
</div>
</div>
<div class="card">
<div class="card-header">
<div class="title">
<i class="fa-brands fa-youtube"></i>
<span>YouTube Cookies</span>
</div>
{#if cookiesUpdatedAt}
<div class="meta">Son güncelleme: {formatDate(cookiesUpdatedAt)}</div>
{/if}
</div>
<div class="field">
<label for="cookies">cookies.txt içeriği</label>
<textarea
id="cookies"
spellcheck="false"
placeholder="Netscape HTTP Cookie formatında (cookies.txt) içerik girin"
bind:value={youtubeCookies}
></textarea>
<small>
Zararlı komut çalıştırılamaz; yalnızca düz metin cookie satırları yazılır.
Maksimum 20KB. Engellenen karakterler otomatik reddedilir.
</small>
</div>
<div class="actions">
<button class="btn" on:click={loadCookies} disabled={loadingCookies || savingCookies}>
<i class="fa-solid fa-rotate"></i> Yenile
</button>
<button class="btn primary" on:click={saveCookies} disabled={loadingCookies || savingCookies}>
<i class="fa-solid fa-floppy-disk"></i> Kaydet
</button>
</div>
{#if error}
<div class="alert error">
<i class="fa-solid fa-circle-exclamation"></i>
{error}
</div>
{/if}
</div>
{:else if activeTab === "general"}
<div class="card muted">Genel ayarlar burada yer alacak.</div>
{:else if activeTab === "rclone"}
<div class="card">
<div class="card-header">
<div class="title">
<i class="fa-solid fa-cloud"></i>
<span>Google Drive (Rclone)</span>
</div>
</div>
<div class="field inline compact left-align">
<label class="checkbox-row">
<input
type="checkbox"
bind:checked={rcloneAutoMove}
disabled={rcloneLoading || rcloneSaving}
/>
<span>İndirince otomatik taşı</span>
</label>
<label class="checkbox-row">
<input
type="checkbox"
bind:checked={rcloneAutoMount}
disabled={rcloneLoading || rcloneSaving}
/>
<span>Başlangıçta otomatik mount</span>
</label>
</div>
<div class="field inline compact left-align">
<div class="inline-field">
<label for="rclone-cache-clean">Cache temizleme (dakika)</label>
<input
id="rclone-cache-clean"
type="number"
min="0"
step="1"
bind:value={rcloneCacheCleanMinutes}
disabled={rcloneLoading || rcloneSaving}
/>
</div>
<button class="btn primary" on:click={cleanRcloneCache}>
<i class="fa-solid fa-broom"></i> Cache Temizle
</button>
</div>
<div class="field">
<label>rclone.conf</label>
<div class="password-field">
<textarea
class="conf-textarea {rcloneConfVisible ? '' : 'masked'}"
bind:value={rcloneConfText}
placeholder="rclone.conf içeriğini yapıştırın"
></textarea>
<button
class="eye-btn"
type="button"
on:click={() => (rcloneConfVisible = !rcloneConfVisible)}
aria-label="Gizli/Görünür"
>
<i class={rcloneConfVisible ? "fa-solid fa-eye-slash" : "fa-solid fa-eye"}></i>
</button>
</div>
</div>
<div class="actions">
<button class="btn primary" on:click={saveRcloneSettings} disabled={rcloneLoading || rcloneSaving}>
<i class="fa-solid fa-floppy-disk"></i> Kaydet
</button>
</div>
{#if error}
<div class="alert error" style="margin-top:10px;">
<i class="fa-solid fa-circle-exclamation"></i>
{error}
</div>
{/if}
</div>
<!-- Mount Kontrol Kartı -->
<div class="card">
<div class="card-header">
<div class="title">
<i class="fa-solid fa-cloud"></i>
<span>Mount Kontrol</span>
</div>
</div>
<div class="actions left">
<button class="btn" on:click={startRcloneMount}>
<i class="fa-solid fa-play"></i> Mount Başlat
</button>
<button class="btn" on:click={stopRcloneMount}>
<i class="fa-solid fa-stop"></i> Mount Durdur
</button>
</div>
{#if rcloneStatus}
<div class="card muted" style="margin-top:12px;">
<div><strong>Durum:</strong></div>
<div>Enabled: {rcloneStatus.enabled ? "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>
{/if}
{#if rcloneStatus.diskUsage}
<div style="margin-top: 8px;">
<div style="font-size: 12px; color: #666;">Disk Kullanımı:</div>
<div style="font-size: 13px;">
Kullanım: %{rcloneStatus.diskUsage.usedPercent} |
Boş: {rcloneStatus.diskUsage.availableGB.toFixed(1)}GB /
{rcloneStatus.diskUsage.totalGB.toFixed(1)}GB
</div>
{#if rcloneStatus.cacheCleanThreshold}
<div style="font-size: 11px; color: #888;">
Temizleme eşiği: %{rcloneStatus.cacheCleanThreshold}
</div>
{/if}
</div>
{/if}
{#if rcloneStatus.lastError}
<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}
</div>
{:else if activeTab === "advanced"}
<div class="card muted">Gelişmiş ayarlar burada yer alacak.</div>
{/if}
</section>
<style>
.files {
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
}
.files-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.header-title {
display: flex;
align-items: center;
gap: 8px;
}
.tabs {
display: flex;
gap: 8px;
}
.tab {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 12px;
border: 1px solid var(--border, #dcdcdc);
border-radius: 8px;
background: #f7f7f7;
cursor: pointer;
color: #333;
}
.tab.active {
background: #222;
color: #fff;
border-color: #222;
}
.card {
border: 1px solid var(--border, #e0e0e0);
border-radius: 10px;
padding: 14px;
background: #fff;
display: flex;
flex-direction: column;
gap: 12px;
}
.card.muted {
color: #777;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 8px;
}
.card-header .title {
display: flex;
align-items: center;
gap: 8px;
font-weight: 700;
}
.card-header .meta {
color: #666;
font-size: 12px;
}
.field {
display: flex;
flex-direction: column;
gap: 6px;
}
.field label {
font-weight: 600;
}
.field.inline {
display: flex;
align-items: center;
gap: 16px;
flex-wrap: wrap;
}
.field.inline.compact {
align-items: flex-end;
}
.field.inline.left-align {
flex-direction: column;
align-items: flex-start;
justify-content: flex-start;
gap: 10px;
}
.inline-field {
display: flex;
flex-direction: column;
gap: 6px;
}
.field select {
min-width: 180px;
max-width: 240px;
padding: 8px 10px;
border: 1px solid var(--border, #dcdcdc);
border-radius: 8px;
background: #fff;
}
.checkbox-row {
display: inline-flex;
align-items: center;
gap: 8px;
font-weight: 600;
}
.field textarea {
min-height: 180px;
padding: 10px;
border: 1px solid var(--border, #dcdcdc);
border-radius: 8px;
font-family: monospace;
font-size: 13px;
resize: vertical;
}
.field small {
color: #666;
}
.actions {
display: flex;
gap: 8px;
justify-content: flex-end;
}
.actions.left {
justify-content: flex-start;
}
.divider {
height: 1px;
background: #eee;
margin: 6px 0;
}
.btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 12px;
border: 1px solid var(--border, #dcdcdc);
border-radius: 8px;
background: #f7f7f7;
cursor: pointer;
}
.btn.primary {
background: #222;
color: #fff;
border-color: #222;
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.alert {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 12px;
border-radius: 8px;
font-size: 14px;
}
.alert.error {
background: #ffe2e2;
color: #b30000;
}
.alert.success {
background: #e5ffe7;
color: #0f7a1f;
}
:global(code) {
background: #f0f0f0;
padding: 2px 6px;
border-radius: 4px;
font-family: monospace;
font-size: 12px;
}
.password-field {
position: relative;
}
.conf-textarea {
width: 100%;
min-height: 180px;
resize: vertical;
font-family: "Courier New", monospace;
letter-spacing: 0.2px;
}
.conf-textarea.masked {
-webkit-text-security: disc;
text-security: disc;
}
.eye-btn {
position: absolute;
right: 20px;
top: 8px;
background: #f5f5f5;
border: 1px solid #ddd;
border-radius: 6px;
padding: 6px 8px;
cursor: pointer;
}
</style>