feat(rabbit): PH video indirme ve yönetim özelliği ekle
PH video indirme, yönetim ve oynatma özelliği eklendi. Yeni Rabbit sayfası ile indirilen videolar listelenebilir ve oynatılabilir. Kenar menüye Rabbit sekmesi eklendi, dinamik olarak göster/gizle. Transferler sayfasına PH URL desteği eklendi. WebSocket üzerinden Rabbit sayısı güncellemeleri sağlandı. Dosya görünümü Rabbit içeriklerini filtreleyecek şekilde güncellendi. Arka planda Rabbit metadata yönetimi ve dosya sistemi entegrasyonu.
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
import Files from "./routes/Files.svelte";
|
||||
import Transfers from "./routes/Transfers.svelte";
|
||||
import Trash from "./routes/Trash.svelte";
|
||||
import Rabbit from "./routes/Rabbit.svelte";
|
||||
import Movies from "./routes/Movies.svelte";
|
||||
import TvShows from "./routes/TvShows.svelte";
|
||||
import Music from "./routes/Music.svelte";
|
||||
@@ -150,6 +151,7 @@
|
||||
<Route path="/movies" component={Movies} />
|
||||
<Route path="/tv" component={TvShows} />
|
||||
<Route path="/music" component={Music} />
|
||||
<Route path="/rabbit" component={Rabbit} />
|
||||
<Route path="/profile" component={Profile} />
|
||||
<Route path="/settings" component={Settings} />
|
||||
<Route path="/transfers" component={Transfers} />
|
||||
|
||||
3
client/src/assets/rabbit.svg
Normal file
3
client/src/assets/rabbit.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="currentColor">
|
||||
<path d="M342.5 46.2c-9.9 2.3-18.8 7-28.9 14.4-8.5 6.3-24.5 22.1-30.7 30-4.7 6-5.9 9-5.3 12.3 1.1 6.2 7 12 12.5 12 6.6 0 17.1-7.3 28.3-20 12-13.8 16-22.5 16-35.3 0-8-.2-8.4-2-12.2-3.5-7.3-12-11.4-19.9-9.2zm-97 50.6c-6.4 4.2-9.2 11.8-7.2 19 2 7.3 6.8 10.4 18.6 12.2 11.6 1.8 17 4.4 17 8.4 0 2.2-5.4 10-18.1 26-9.9 12.5-20.9 28.4-24.4 35.4-2.3 4.7-2.7 6.7-2.3 11.5 1 11.2 7.7 20.4 17.9 24.6 3.7 1.5 6.6 1.8 14.3 1.6 10.3-.2 19.1-2 29.6-6 14.7-5.7 24.6-12.8 38.3-27.1 18.1-19 25.7-33.8 25.6-51.4-.1-12.6-2.6-20.4-9-28-9-10.7-23.5-17-45.3-19.5-7.4-.9-14.2-2.3-18.2-3.8-7.3-2.7-10.5-5.7-10.5-9.7 0-2.7 1.5-5.3 9-15.8 5-7 10.2-15.2 11.6-18.1 1.5-3 2.7-8.5 3-13.9.6-9.4-.4-12.9-5.4-19-7.4-8.9-21.7-11.4-32.3-5.6zM230 250.6c-1.3.7-5 4.2-8.2 7.7-10.2 11-14.5 20.1-14.5 31.4.1 9.4 2.5 15.6 11.2 27.9 6.4 9.1 7 10.2 7 14.9 0 4.4-.8 6.6-5.3 14-15.5 25.8-20.7 43.7-15.4 54.9 5.7 11.8 21.6 17.7 33.6 12.6 6.3-2.7 9.5-6.1 16.9-18.5 4.1-7 8.5-13.7 9.7-14.9 1.8-1.8 2.7-2 5.1-1.3 9.5 3 13.4 15.4 8.6 26.9-2.9 7.2-7.5 11.9-16 16.3-7.6 4-7.9 4.2-7.9 7.8 0 3.4.2 3.7 3.8 5.2 6.3 2.7 18.3 4.6 28.2 4.6 23.1 0 38.4-9.2 52-32.4 10.1-17.2 16.8-35.2 20.1-54.7 1.8-10.9 1.5-35.8-.5-47.3-2.5-14.3-6.2-26-12.6-38.6-10-19.8-22.8-32.9-42.5-43.2-13.3-6.9-17.6-7.8-33.5-7.8-13.7 0-15.3.2-16.9 1.8-.9 1-6.9 10.8-13.4 21.8-12.1 20.6-14.2 23.7-18.5 27.8-6.7 6.4-14.9 10.3-24.4 11.8-5.2.9-7.3.5-10.1-1-2-.9-3.6-1.6-3.7-1.4z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -1,11 +1,12 @@
|
||||
<script>
|
||||
import { Link } from "svelte-routing";
|
||||
import { createEventDispatcher, onDestroy, onMount, tick } from "svelte";
|
||||
import { Link } from "svelte-routing";
|
||||
import { createEventDispatcher, onDestroy, onMount, tick } from "svelte";
|
||||
import { movieCount } from "../stores/movieStore.js";
|
||||
import { tvShowCount } from "../stores/tvStore.js";
|
||||
import { musicCount } from "../stores/musicStore.js";
|
||||
import { trashCount } from "../stores/trashStore.js";
|
||||
import { apiFetch } from "../utils/api.js";
|
||||
import { rabbitCount } from "../stores/rabbitStore.js";
|
||||
import { trashCount } from "../stores/trashStore.js";
|
||||
import { apiFetch, getAccessToken } from "../utils/api.js";
|
||||
|
||||
export let menuOpen = false;
|
||||
const dispatch = createEventDispatcher();
|
||||
@@ -13,12 +14,13 @@ import { musicCount } from "../stores/musicStore.js";
|
||||
let hasShows = false;
|
||||
let hasTrash = false;
|
||||
let hasMusic = false;
|
||||
let hasRabbit = false;
|
||||
// Svelte store kullanarak reaktivite sağla
|
||||
import { writable } from 'svelte/store';
|
||||
const diskSpaceStore = writable({ totalGB: '0', usedGB: '0', usedPercent: 0 });
|
||||
let diskSpace;
|
||||
let hasMedia = false;
|
||||
$: hasMedia = hasMovies || hasShows || hasMusic;
|
||||
let hasMedia = false;
|
||||
$: hasMedia = hasMovies || hasShows || hasMusic || hasRabbit;
|
||||
|
||||
// Store subscription'ı temizlemek için
|
||||
let unsubscribeDiskSpace;
|
||||
@@ -50,12 +52,16 @@ const unsubscribeTrash = trashCount.subscribe((count) => {
|
||||
const unsubscribeMusic = musicCount.subscribe((count) => {
|
||||
hasMusic = (count ?? 0) > 0;
|
||||
});
|
||||
const unsubscribeRabbit = rabbitCount.subscribe((count) => {
|
||||
hasRabbit = (count ?? 0) > 0;
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
unsubscribeMovie();
|
||||
unsubscribeTv();
|
||||
unsubscribeTrash();
|
||||
unsubscribeMusic();
|
||||
unsubscribeRabbit();
|
||||
if (unsubscribeDiskSpace) {
|
||||
unsubscribeDiskSpace();
|
||||
}
|
||||
@@ -84,52 +90,64 @@ const unsubscribeMusic = musicCount.subscribe((count) => {
|
||||
|
||||
// Component yüklendiğinde disk space bilgilerini al
|
||||
onMount(() => {
|
||||
console.log('🔌 Sidebar component mounted');
|
||||
fetchDiskSpace();
|
||||
|
||||
// WebSocket bağlantısı kur
|
||||
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
// Server port'unu doğru almak için
|
||||
fetchRabbitCount();
|
||||
|
||||
const wsProtocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||
const currentHost = window.location.host;
|
||||
// Eğer client farklı portta çalışıyorsa, server port'unu manuel belirt
|
||||
const wsHost = currentHost.includes(':3000') ? currentHost.replace(':3000', ':3001') : currentHost;
|
||||
const wsUrl = `${wsProtocol}//${wsHost}`;
|
||||
console.log('🔌 Connecting to WebSocket at:', wsUrl);
|
||||
|
||||
// WebSocket bağlantısını global olarak saklayalım
|
||||
const wsHost = currentHost.includes(":3000")
|
||||
? currentHost.replace(":3000", ":3001")
|
||||
: currentHost;
|
||||
const token = getAccessToken() || "";
|
||||
const wsUrl = `${wsProtocol}//${wsHost}?token=${token}`;
|
||||
|
||||
window.diskSpaceWs = new WebSocket(wsUrl);
|
||||
|
||||
|
||||
window.diskSpaceWs.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
console.log('WebSocket message received:', data);
|
||||
if (data.type === 'diskSpace') {
|
||||
console.log('Disk space update received:', data.data);
|
||||
if (data.type === "diskSpace") {
|
||||
updateDiskSpace(data.data);
|
||||
} else if (data.type === "rabbitCount") {
|
||||
rabbitCount.set(data.count || 0);
|
||||
hasRabbit = (data.count || 0) > 0;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('WebSocket message parse error:', err);
|
||||
console.error("WebSocket message parse error:", err);
|
||||
}
|
||||
};
|
||||
|
||||
window.diskSpaceWs.onopen = () => {
|
||||
console.log('WebSocket connected for disk space updates');
|
||||
};
|
||||
|
||||
|
||||
window.diskSpaceWs.onopen = () => {};
|
||||
|
||||
window.diskSpaceWs.onerror = (error) => {
|
||||
console.error('WebSocket error:', error);
|
||||
console.error("WebSocket error:", error);
|
||||
};
|
||||
|
||||
window.diskSpaceWs.onclose = () => {
|
||||
console.log('WebSocket disconnected');
|
||||
};
|
||||
|
||||
|
||||
window.diskSpaceWs.onclose = () => {};
|
||||
|
||||
onDestroy(() => {
|
||||
if (window.diskSpaceWs && (window.diskSpaceWs.readyState === WebSocket.OPEN || window.diskSpaceWs.readyState === WebSocket.CONNECTING)) {
|
||||
if (
|
||||
window.diskSpaceWs &&
|
||||
(window.diskSpaceWs.readyState === WebSocket.OPEN ||
|
||||
window.diskSpaceWs.readyState === WebSocket.CONNECTING)
|
||||
) {
|
||||
window.diskSpaceWs.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
async function fetchRabbitCount() {
|
||||
try {
|
||||
const resp = await apiFetch("/api/rabbit");
|
||||
if (!resp.ok) return;
|
||||
const data = await resp.json().catch(() => null);
|
||||
const count = data?.count ?? data?.items?.length ?? 0;
|
||||
rabbitCount.set(count);
|
||||
hasRabbit = count > 0;
|
||||
} catch (err) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="sidebar" class:open={menuOpen}>
|
||||
@@ -224,6 +242,20 @@ const unsubscribeMusic = musicCount.subscribe((count) => {
|
||||
Music
|
||||
</Link>
|
||||
{/if}
|
||||
|
||||
{#if hasRabbit}
|
||||
<Link
|
||||
to="/rabbit"
|
||||
class="item"
|
||||
getProps={({ isCurrent }) => ({
|
||||
class: isCurrent ? "item active" : "item",
|
||||
})}
|
||||
on:click={handleLinkClick}
|
||||
>
|
||||
<img src="/rabbit.svg" alt="Rabbit" class="icon rabbit-icon" />
|
||||
Rabbit
|
||||
</Link>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -304,4 +336,10 @@ const unsubscribeMusic = musicCount.subscribe((count) => {
|
||||
border-radius: 9px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.rabbit-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
object-fit: contain;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -303,6 +303,20 @@
|
||||
}
|
||||
|
||||
function updateVisibleState(fileList, path = currentPath) {
|
||||
const rabbitCompletedRoots = new Set(
|
||||
fileList
|
||||
.filter(
|
||||
(f) =>
|
||||
!f.isDirectory &&
|
||||
(f.displayName === ".ph_complete" || f.name?.endsWith("/.ph_complete"))
|
||||
)
|
||||
.map((f) => {
|
||||
const segs = f.displaySegments || f.name?.split("/") || [];
|
||||
return segs.length ? segs[0] : null;
|
||||
})
|
||||
.filter(Boolean)
|
||||
);
|
||||
|
||||
const dirs = buildDirectoryEntries(fileList);
|
||||
const directoryMap = new Map();
|
||||
dirs.forEach((dir) => {
|
||||
@@ -334,7 +348,16 @@
|
||||
(file) =>
|
||||
!file.isDirectory &&
|
||||
normalizePath(file.displayParentPath) === normalizePath(path) &&
|
||||
file.displayName.toLowerCase() !== "info.js",
|
||||
file.displayName.toLowerCase() !== "info.js" &&
|
||||
!file.displayName.toLowerCase().includes(".part") &&
|
||||
file.displayName !== ".ph_complete" &&
|
||||
!(() => {
|
||||
const segs = file.displaySegments || [];
|
||||
const root = segs.length ? segs[0] : "";
|
||||
const isRabbit = root.startsWith("ph_");
|
||||
const isMp4 = file.displayName.toLowerCase().endsWith(".mp4");
|
||||
return isRabbit && isMp4 && !rabbitCompletedRoots.has(root);
|
||||
})()
|
||||
);
|
||||
applyOrdering(path);
|
||||
breadcrumbs = computeBreadcrumbs(path);
|
||||
|
||||
223
client/src/routes/Rabbit.svelte
Normal file
223
client/src/routes/Rabbit.svelte
Normal file
@@ -0,0 +1,223 @@
|
||||
<script>
|
||||
import { onMount } from "svelte";
|
||||
import { apiFetch, withToken, API } from "../utils/api.js";
|
||||
import { setRabbitCount } from "../stores/rabbitStore.js";
|
||||
|
||||
let items = [];
|
||||
let loading = true;
|
||||
let error = null;
|
||||
let selected = null;
|
||||
let showPlayer = false;
|
||||
|
||||
async function load() {
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const resp = await apiFetch("/api/rabbit");
|
||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
||||
const data = await resp.json();
|
||||
items = data?.items || [];
|
||||
setRabbitCount(items.length);
|
||||
} catch (err) {
|
||||
error = err?.message || "Rabbit listesi alınamadı";
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMount(load);
|
||||
|
||||
function thumbUrl(item) {
|
||||
if (!item.thumbnail) return null;
|
||||
const url = `${API}${item.thumbnail}`;
|
||||
return withToken(url);
|
||||
}
|
||||
|
||||
function videoUrl(item) {
|
||||
if (!item?.file || !item?.folderId) return null;
|
||||
const safeFolder = encodeURIComponent(item.folderId.trim());
|
||||
const segments = String(item.file)
|
||||
.split("/")
|
||||
.filter(Boolean)
|
||||
.map((seg) => encodeURIComponent(seg.trim()));
|
||||
const relPath = segments.join("/");
|
||||
const url = `${API}/downloads/${safeFolder}/${relPath}`;
|
||||
return withToken(url);
|
||||
}
|
||||
|
||||
function playItem(item) {
|
||||
selected = item;
|
||||
showPlayer = true;
|
||||
}
|
||||
</script>
|
||||
|
||||
<section class="rabbit-page">
|
||||
<div class="header">
|
||||
<h2>Rabbit</h2>
|
||||
<button class="btn" on:click={load} disabled={loading}>
|
||||
<i class="fa-solid fa-rotate"></i> Yenile
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if loading}
|
||||
<div class="state">Yükleniyor...</div>
|
||||
{:else if error}
|
||||
<div class="state error">{error}</div>
|
||||
{:else if items.length === 0}
|
||||
<div class="state">Henüz içerik yok.</div>
|
||||
{:else}
|
||||
<div class="grid">
|
||||
{#each items as item (item.id)}
|
||||
<div class="card" on:click={() => playItem(item)}>
|
||||
{#if thumbUrl(item)}
|
||||
<img class="thumb" src={thumbUrl(item)} alt={item.title} />
|
||||
{:else}
|
||||
<div class="thumb placeholder"><i class="fa-regular fa-image"></i></div>
|
||||
{/if}
|
||||
<div class="info">
|
||||
<div class="title" title={item.title}>{item.title}</div>
|
||||
{#if item.added}
|
||||
<div class="meta">{new Date(item.added).toLocaleString()}</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if showPlayer && selected}
|
||||
<div class="player" on:click={() => (showPlayer = false)}>
|
||||
<div class="player-content" on:click|stopPropagation>
|
||||
<button class="player-close" on:click={() => (showPlayer = false)}>Kapat</button>
|
||||
<div class="player-title">{selected.title}</div>
|
||||
{#if videoUrl(selected)}
|
||||
<video controls autoplay src={videoUrl(selected)}></video>
|
||||
{:else}
|
||||
<div class="state error">Video yolu bulunamadı</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.rabbit-page {
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #dcdcdc;
|
||||
border-radius: 8px;
|
||||
background: #f7f7f7;
|
||||
cursor: pointer;
|
||||
}
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
.card {
|
||||
position: relative;
|
||||
background: #f5f5f5;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
isolation: isolate;
|
||||
transition: box-shadow 0.18s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
.thumb {
|
||||
width: 100%;
|
||||
height: 180px;
|
||||
object-fit: cover;
|
||||
background: #f1f1f1;
|
||||
}
|
||||
.thumb.placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #999;
|
||||
font-size: 28px;
|
||||
}
|
||||
.info {
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
.title {
|
||||
font-weight: 700;
|
||||
font-size: 13px;
|
||||
line-height: 1.2;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.meta {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
.player {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0,0,0,0.65);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 999;
|
||||
}
|
||||
.player-content {
|
||||
background: #111;
|
||||
padding: 12px;
|
||||
border-radius: 10px;
|
||||
max-width: 900px;
|
||||
width: 90%;
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,0.35);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
.player video {
|
||||
width: 100%;
|
||||
max-height: 520px;
|
||||
border-radius: 8px;
|
||||
background: #000;
|
||||
}
|
||||
.player-title {
|
||||
font-weight: 700;
|
||||
font-size: 15px;
|
||||
color: #fff;
|
||||
}
|
||||
.player-close {
|
||||
align-self: flex-end;
|
||||
background: #fff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 6px 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.state {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: #666;
|
||||
}
|
||||
.state.error {
|
||||
color: #b00020;
|
||||
}
|
||||
</style>
|
||||
@@ -75,6 +75,23 @@
|
||||
}
|
||||
|
||||
const YT_VIDEO_ID_RE = /^[A-Za-z0-9_-]{11}$/;
|
||||
function normalizePornhubUrl(value) {
|
||||
if (!value || typeof value !== "string") return null;
|
||||
try {
|
||||
const url = new URL(value.trim());
|
||||
if (url.protocol !== "https:") return null;
|
||||
const host = url.hostname.toLowerCase();
|
||||
if (host !== "pornhub.com" && host !== "www.pornhub.com") return null;
|
||||
if (url.pathname !== "/view_video.php") return null;
|
||||
const viewkey = url.searchParams.get("viewkey");
|
||||
if (!viewkey) return null;
|
||||
return `https://www.pornhub.com/view_video.php?viewkey=${encodeURIComponent(
|
||||
viewkey
|
||||
)}`;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function isMagnetLink(value) {
|
||||
if (!value || typeof value !== "string") return false;
|
||||
@@ -99,7 +116,7 @@
|
||||
}
|
||||
|
||||
async function handleUrlInput() {
|
||||
const input = prompt("Magnet veya YouTube URL girin:");
|
||||
const input = prompt("Magnet, YouTube veya Pornhub URL girin:");
|
||||
if (!input) return;
|
||||
if (isMagnetLink(input)) {
|
||||
await apiFetch("/api/transfer", {
|
||||
@@ -125,8 +142,23 @@
|
||||
await list();
|
||||
return;
|
||||
}
|
||||
const normalizedPh = normalizePornhubUrl(input);
|
||||
if (normalizedPh) {
|
||||
const resp = await apiFetch("/api/pornhub/download", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ url: normalizedPh })
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const data = await resp.json().catch(() => null);
|
||||
alert(data?.error || "Pornhub indirmesi başlatılamadı");
|
||||
return;
|
||||
}
|
||||
await list();
|
||||
return;
|
||||
}
|
||||
alert(
|
||||
"Yalnızca magnet linkleri veya https://www.youtube.com/watch?v=... formatındaki YouTube URL'leri destekleniyor."
|
||||
"Yalnızca magnet linkleri, YouTube (https://www.youtube.com/watch?v=...) veya Pornhub (https://www.pornhub.com/view_video.php?viewkey=...) URL'leri destekleniyor."
|
||||
);
|
||||
}
|
||||
|
||||
@@ -556,7 +588,11 @@
|
||||
class="thumb"
|
||||
on:load={(e) => e.target.classList.add("loaded")}
|
||||
/>
|
||||
{:else if t.type === "youtube" && (!t.progress || t.progress <= 0)}
|
||||
{:else if (t.type === "pornhub" && (!t.progress || t.progress <= 0))}
|
||||
<div class="thumb placeholder loading">
|
||||
<div class="spinner"></div>
|
||||
</div>
|
||||
{:else if (t.type === "youtube" && (!t.progress || t.progress <= 0))}
|
||||
<div class="thumb placeholder loading">
|
||||
<div class="spinner"></div>
|
||||
</div>
|
||||
@@ -568,8 +604,8 @@
|
||||
|
||||
<div class="torrent-info">
|
||||
<div class="torrent-header">
|
||||
<div class="torrent-title">
|
||||
<div class="torrent-name">{t.name}</div>
|
||||
<div class="torrent-title">
|
||||
<div class="torrent-name">{t.name}</div>
|
||||
{#if t.type === "youtube"}
|
||||
<div class="torrent-subtitle">
|
||||
Source: YouTube
|
||||
@@ -577,8 +613,15 @@
|
||||
<div class="torrent-subtitle">
|
||||
Added: {formatDate(t.added)}
|
||||
</div>
|
||||
{:else if t.type === "pornhub"}
|
||||
<div class="torrent-subtitle">
|
||||
Source: Rabbit
|
||||
</div>
|
||||
<div class="torrent-subtitle">
|
||||
Added: {formatDate(t.added)}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex; gap:5px;">
|
||||
{#if t.type !== "youtube"}
|
||||
<button
|
||||
|
||||
8
client/src/stores/rabbitStore.js
Normal file
8
client/src/stores/rabbitStore.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
const countStore = writable(0);
|
||||
export const rabbitCount = countStore;
|
||||
|
||||
export function setRabbitCount(count) {
|
||||
countStore.set(Number(count) || 0);
|
||||
}
|
||||
Reference in New Issue
Block a user