feat: Add hidden channels functionality
This commit is contained in:
parent
a12d3f6fb1
commit
fb12e3ce26
5 changed files with 295 additions and 45 deletions
|
|
@ -5,6 +5,7 @@
|
|||
import VideoTable from "./VideoTable.svelte";
|
||||
import DashboardControls from "./DashboardControls.svelte";
|
||||
import RecoveryModal from "./RecoveryModal.svelte";
|
||||
import HiddenManager from "./HiddenManager.svelte";
|
||||
|
||||
let stats = {
|
||||
total_videos: 0,
|
||||
|
|
@ -16,6 +17,7 @@
|
|||
let loading = true;
|
||||
let error: string | null = null;
|
||||
let showRecovery = false;
|
||||
let showHiddenManager = false;
|
||||
|
||||
let interval: ReturnType<typeof setInterval>;
|
||||
|
||||
|
|
@ -102,6 +104,7 @@
|
|||
<DashboardControls
|
||||
on:scan={handleScanTriggered}
|
||||
on:openRecovery={() => (showRecovery = true)}
|
||||
on:openHidden={() => (showHiddenManager = true)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -116,4 +119,8 @@
|
|||
{#if showRecovery}
|
||||
<RecoveryModal on:close={() => (showRecovery = false)} />
|
||||
{/if}
|
||||
|
||||
{#if showHiddenManager}
|
||||
<HiddenManager on:close={() => (showHiddenManager = false)} />
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -75,6 +75,13 @@
|
|||
>
|
||||
<i class="bi bi-bandaid"></i> Recovery Mode
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="btn-cyber-secondary py-3 text-sm font-semibold border border-purple-500/30 text-purple-400 hover:bg-purple-500/10 transition-colors rounded-lg flex items-center justify-center gap-2 col-span-2"
|
||||
on:click={() => dispatch("openHidden")}
|
||||
>
|
||||
<i class="bi bi-eye-slash"></i> Hidden Channels
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if orphanResult}
|
||||
|
|
|
|||
164
ui/src/components/HiddenManager.svelte
Normal file
164
ui/src/components/HiddenManager.svelte
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
<script lang="ts">
|
||||
import { createEventDispatcher, onMount } from "svelte";
|
||||
|
||||
export let show = false;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
let hiddenChannels: string[] = [];
|
||||
let newChannel = "";
|
||||
let loading = true;
|
||||
let error: string | null = null;
|
||||
|
||||
async function fetchHidden() {
|
||||
loading = true;
|
||||
try {
|
||||
const res = await fetch("/api/hidden");
|
||||
if (!res.ok) throw new Error("Failed to fetch hidden list");
|
||||
const data = await res.json();
|
||||
hiddenChannels = data.channels || [];
|
||||
error = null;
|
||||
} catch (e: any) {
|
||||
error = e.message;
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function addChannel() {
|
||||
if (!newChannel.trim()) return;
|
||||
try {
|
||||
const res = await fetch("/api/hidden", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ channel: newChannel.trim() }),
|
||||
});
|
||||
if (!res.ok) throw new Error("Failed to add channel");
|
||||
newChannel = "";
|
||||
fetchHidden();
|
||||
} catch (e: any) {
|
||||
error = e.message;
|
||||
}
|
||||
}
|
||||
|
||||
async function removeChannel(channel: string) {
|
||||
if (
|
||||
!confirm(
|
||||
`Unhide channel "${channel}"? It will be moved to public target on next scan.`,
|
||||
)
|
||||
)
|
||||
return;
|
||||
try {
|
||||
const res = await fetch("/api/hidden", {
|
||||
method: "DELETE",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ channel }),
|
||||
});
|
||||
if (!res.ok) throw new Error("Failed to remove channel");
|
||||
fetchHidden();
|
||||
} catch (e: any) {
|
||||
error = e.message;
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
fetchHidden();
|
||||
});
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="fixed inset-0 bg-black/80 backdrop-blur-sm z-50 flex items-center justify-center p-4"
|
||||
>
|
||||
<div
|
||||
class="bg-gray-900 border border-gray-700 rounded-xl w-full max-w-lg shadow-2xl overflow-hidden"
|
||||
>
|
||||
<div
|
||||
class="p-6 border-b border-gray-800 flex justifying-between items-center"
|
||||
>
|
||||
<h2 class="text-xl font-bold text-white flex items-center">
|
||||
<i class="bi bi-eye-slash text-purple-400 mr-2"></i> Hidden Channels
|
||||
</h2>
|
||||
<button
|
||||
class="text-gray-400 hover:text-white transition-colors"
|
||||
on:click={() => dispatch("close")}
|
||||
>
|
||||
<i class="bi bi-x-lg text-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="p-6 space-y-6">
|
||||
<div
|
||||
class="text-sm text-gray-400 bg-gray-800/50 p-3 rounded border border-gray-700"
|
||||
>
|
||||
<i class="bi bi-info-circle mr-1"></i>
|
||||
Channels in this list will be synchronized to the
|
||||
<code>/hidden</code>
|
||||
folder instead of the public <code>/target</code> folder.
|
||||
</div>
|
||||
|
||||
<!-- Add New -->
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
bind:value={newChannel}
|
||||
placeholder="Enter Exact Channel Name..."
|
||||
class="flex-1 bg-gray-800 border border-gray-700 rounded px-4 py-2 text-white focus:outline-none focus:border-purple-500 transition-colors"
|
||||
on:keydown={(e) => e.key === "Enter" && addChannel()}
|
||||
/>
|
||||
<button
|
||||
class="bg-purple-600 hover:bg-purple-500 text-white px-4 py-2 rounded font-medium transition-colors"
|
||||
on:click={addChannel}
|
||||
>
|
||||
<i class="bi bi-plus-lg mr-1"></i> Hide
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- List -->
|
||||
<div
|
||||
class="max-h-60 overflow-y-auto space-y-2 pr-1 custom-scrollbar"
|
||||
>
|
||||
{#if loading}
|
||||
<div class="text-center py-4 text-gray-500">Loading...</div>
|
||||
{:else if hiddenChannels.length === 0}
|
||||
<div class="text-center py-4 text-gray-500 italic">
|
||||
No hidden channels
|
||||
</div>
|
||||
{:else}
|
||||
{#each hiddenChannels as channel}
|
||||
<div
|
||||
class="flex items-center justify-between bg-gray-800 p-3 rounded group hover:bg-gray-750 transition-colors border border-transparent hover:border-gray-700"
|
||||
>
|
||||
<span class="text-gray-200 font-medium"
|
||||
>{channel}</span
|
||||
>
|
||||
<button
|
||||
class="text-gray-500 hover:text-red-400 transition-colors opacity-0 group-hover:opacity-100"
|
||||
title="Unhide"
|
||||
on:click={() => removeChannel(channel)}
|
||||
>
|
||||
<i class="bi bi-eye text-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<div
|
||||
class="text-red-400 text-sm bg-red-900/20 p-2 rounded border border-red-900/50"
|
||||
>
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="p-4 bg-gray-950 border-t border-gray-800 flex justify-end">
|
||||
<button
|
||||
class="px-4 py-2 rounded text-gray-300 hover:bg-gray-800 transition-colors"
|
||||
on:click={() => dispatch("close")}
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Loading…
Add table
Add a link
Reference in a new issue