diff --git a/client/src/components/Topbar.svelte b/client/src/components/Topbar.svelte index 387a14e..ad007d2 100644 --- a/client/src/components/Topbar.svelte +++ b/client/src/components/Topbar.svelte @@ -1,11 +1,21 @@
@@ -20,7 +30,12 @@
diff --git a/client/src/routes/Files.svelte b/client/src/routes/Files.svelte index 1e015f1..91993fa 100644 --- a/client/src/routes/Files.svelte +++ b/client/src/routes/Files.svelte @@ -4,6 +4,11 @@ import { cleanFileName, extractTitleAndYear } from "../utils/filename.js"; import { refreshMovieCount } from "../stores/movieStore.js"; import { refreshTvShowCount } from "../stores/tvStore.js"; + import { + activeSearchTerm, + setSearchScope, + clearSearch + } from "../stores/searchStore.js"; const HIDDEN_ROOT_REGEX = /^\d{10,}$/; const FOLDER_ICON_PATH = "/folder.svg"; const MAX_TRAIL_SEGMENTS = 3; @@ -13,6 +18,7 @@ let visibleFolders = []; let visibleFiles = []; let visibleEntries = []; + let renderedEntries = []; let allDirectories = []; let breadcrumbs = []; let currentFileScope = []; @@ -21,6 +27,8 @@ let draggingItem = null; let dragOverItem = null; let lastDragPath = ""; + let searchTerm = ""; + let hasSearch = false; const normalizePath = (value) => { if (!value) return ""; @@ -33,6 +41,22 @@ /safari/i.test(navigator.userAgent || "") && !/chrome|crios|android/i.test(navigator.userAgent || ""); + function filterEntriesBySearch(entries, term) { + const query = term.trim().toLowerCase(); + if (!query) return entries; + return entries.filter((entry) => { + const labels = [ + entry.displayName, + entry.displayPath, + entry.name + ] + .filter(Boolean) + .join(" ") + .toLowerCase(); + return labels.includes(query); + }); + } + const shouldHideRootSegment = (segment) => segment && (HIDDEN_ROOT_REGEX.test(segment) || segment === "downloads"); @@ -157,6 +181,10 @@ customOrder.set(key, filteredOrder); } + $: searchTerm = $activeSearchTerm; + $: hasSearch = searchTerm.trim().length > 0; + $: renderedEntries = filterEntriesBySearch(visibleEntries, searchTerm); + function computeBreadcrumbs(path) { const segments = path ? path.split("/").filter(Boolean) : []; const crumbs = [{ label: "Home", path: "" }]; @@ -320,11 +348,10 @@ } let selectedItems = new Set(); let allSelected = false; - function syncSelectionState() { - const keys = visibleEntries.map((entry) => entry.name).filter(Boolean); + $: { + const keys = renderedEntries.map((entry) => entry.name).filter(Boolean); allSelected = keys.length > 0 && keys.every((key) => selectedItems.has(key)); } - $: syncSelectionState(); let pendingPlayTarget = null; let activeMenu = null; // Aktif menü öğesi let menuPosition = { top: 0, left: 0 }; // Menü pozisyonu @@ -485,7 +512,7 @@ if (allSelected) { selectedItems = new Set(); } else { - const keys = visibleEntries.map((entry) => entry.name).filter(Boolean); + const keys = renderedEntries.map((entry) => entry.name).filter(Boolean); selectedItems = new Set(keys); } } @@ -624,12 +651,19 @@ ) { return; } + const hadSearch = searchTerm.trim().length > 0; + const nextSearchTerm = hadSearch ? "" : searchTerm; currentPath = normalized; currentOriginalPath = normalizedOriginal; + if (hadSearch) { + clearSearch("files"); + } selectedItems = new Set(); activeMenu = null; if (isCreatingFolder) cancelCreateFolder(); updateUrlPath(normalized, normalizedOriginal, { replace }); + updateVisibleState(files, normalized); + renderedEntries = filterEntriesBySearch(visibleEntries, nextSearchTerm); } function handleEntryClick(entry) { @@ -1288,6 +1322,7 @@ } onMount(async () => { + setSearchScope("files"); await loadFiles(); // önce dosyaları getir const token = localStorage.getItem("token"); const wsUrl = `${API.replace("http", "ws")}?token=${token}`; @@ -1565,10 +1600,10 @@
- {#if visibleEntries.length > 0 && selectedItems.size > 0} + {#if renderedEntries.length > 0 && selectedItems.size > 0} {selectedItems.size} öğe seçildi {/if} - {#if visibleEntries.length > 0 && selectedItems.size > 0} + {#if renderedEntries.length > 0 && selectedItems.size > 0}
- {#if visibleEntries.length === 0 && !isCreatingFolder} + {#if renderedEntries.length === 0 && !isCreatingFolder}
-
No media found
+
+ {#if hasSearch} + Aramanla eşleşen öğe bulunamadı + {:else} + No media found + {/if} +
{:else}
{/if} - {#each visibleEntries as entry (entry.name)} + {#each renderedEntries as entry (entry.name)}
0; +$: filteredMovies = (() => { + const query = searchTerm.trim().toLowerCase(); + if (!query) return movies; + return movies.filter((movie) => { + const fields = [ + movie.title, + movie.originalTitle, + movie.metadata?.matched_title, + movie.metadata?.original_title, + movie.folder + ]; + return fields + .filter(Boolean) + .some((value) => String(value).toLowerCase().includes(query)); + }); +})(); + async function handlePlay(movie) { if (!movie) return; closeMovie(); @@ -335,6 +361,7 @@ async function loadMovies() { } onMount(() => { + setSearchScope("movies"); loadMovies(); const handleKey = (event) => { if (!showPlayerModal) return; @@ -381,9 +408,11 @@ async function loadMovies() {
{error}
{:else if movies.length === 0}
No movie metadata found yet.
+ {:else if hasSearch && filteredMovies.length === 0} +
Aramanıza uyan film bulunamadı.
{:else}
- {#each movies as movie} + {#each filteredMovies as movie}
openMovie(movie)}> {#if movie.poster}
diff --git a/client/src/routes/TvShows.svelte b/client/src/routes/TvShows.svelte index 0d806f6..0044ef7 100644 --- a/client/src/routes/TvShows.svelte +++ b/client/src/routes/TvShows.svelte @@ -3,6 +3,10 @@ import { API, apiFetch } from "../utils/api.js"; import { cleanFileName } from "../utils/filename.js"; import { tvShowCount } from "../stores/tvStore.js"; + import { + activeSearchTerm, + setSearchScope + } from "../stores/searchStore.js"; let shows = []; let loading = true; @@ -28,6 +32,9 @@ let seasonPlaylist = []; let seasonPlaylistIndex = -1; let canPlayPrev = false; let canPlayNext = false; +let searchTerm = ""; +let hasSearch = false; +let filteredShows = []; function runtimeToText(runtime) { if (!runtime || Number.isNaN(runtime)) return null; @@ -190,6 +197,25 @@ let canPlayNext = false; } } + $: searchTerm = $activeSearchTerm; + $: hasSearch = searchTerm.trim().length > 0; + $: filteredShows = (() => { + const query = searchTerm.trim().toLowerCase(); + if (!query) return shows; + return shows.filter((show) => { + const fields = [ + show.title, + show.originalTitle, + show.metadata?.matched_title, + show.metadata?.original_name, + show.folder + ]; + return fields + .filter(Boolean) + .some((value) => String(value).toLowerCase().includes(query)); + }); + })(); + async function refreshShows() { try { refreshing = true; @@ -542,6 +568,7 @@ async function openVideoAtIndex(index) { } onMount(() => { + setSearchScope("tv"); loadShows(); const handleKey = (event) => { if (!showPlayerModal) return; @@ -588,9 +615,11 @@ async function openVideoAtIndex(index) {
{error}
{:else if shows.length === 0}
No TV metadata found yet.
+ {:else if hasSearch && filteredShows.length === 0} +
Aramanıza uyan dizi bulunamadı.
{:else}
- {#each shows as show} + {#each filteredShows as show}
openShow(show)}> {#if show.poster}
diff --git a/client/src/stores/searchStore.js b/client/src/stores/searchStore.js new file mode 100644 index 0000000..b6c57f4 --- /dev/null +++ b/client/src/stores/searchStore.js @@ -0,0 +1,80 @@ +import { derived, writable } from "svelte/store"; + +const KNOWN_SCOPES = new Set(["files", "movies", "tv"]); + +const initialState = { + scope: "files", + terms: { + files: "", + movies: "", + tv: "" + } +}; + +export const searchState = writable(initialState); + +export const activeScope = derived(searchState, ($state) => $state.scope); + +export const activeSearchTerm = derived( + searchState, + ($state) => $state.terms[$state.scope] || "" +); + +const PLACEHOLDERS = { + files: "Dosya ara...", + movies: "Film ara...", + tv: "Dizi ara..." +}; + +export const activePlaceholder = derived( + searchState, + ($state) => PLACEHOLDERS[$state.scope] || "Ara..." +); + +export function setSearchScope(scope) { + const normalized = KNOWN_SCOPES.has(scope) ? scope : "files"; + searchState.update((state) => { + const hasScope = Object.prototype.hasOwnProperty.call(state.terms, normalized); + const terms = hasScope ? state.terms : { ...state.terms, [normalized]: "" }; + if (state.scope === normalized && terms === state.terms) { + return state; + } + return { + scope: normalized, + terms + }; + }); +} + +export function updateSearchTerm(term) { + searchState.update((state) => { + const scope = state.scope; + const nextTerms = { + ...state.terms, + [scope]: term + }; + if (nextTerms[scope] === state.terms[scope]) { + return state; + } + return { + scope, + terms: nextTerms + }; + }); +} + +export function clearSearch(scope) { + searchState.update((state) => { + const targetScope = scope && KNOWN_SCOPES.has(scope) ? scope : state.scope; + if ((state.terms[targetScope] || "") === "") { + return state; + } + return { + scope: state.scope, + terms: { + ...state.terms, + [targetScope]: "" + } + }; + }); +} diff --git a/server/server.js b/server/server.js index b568800..70311da 100644 --- a/server/server.js +++ b/server/server.js @@ -2811,23 +2811,25 @@ app.delete("/api/file", requireAuth, (req, res) => { if (!name) return false; if (name === INFO_FILENAME) return false; if (name.startsWith(".")) return false; - const full = path.join(rootDir, name); - try { - const stat = fs.statSync(full); - if (stat.isDirectory()) { - const subItems = fs.readdirSync(full); - return subItems.some((entry) => !entry.startsWith(".")); - } - } catch (err) { - return false; - } return true; }); - if (meaningful.length === 0 || stats?.isDirectory?.()) { - purgeRootFolder(folderId); + pruneInfoEntry(folderId, relWithinRoot); + removeSeriesEpisode(folderId, relWithinRoot); + + if (meaningful.length === 0) { + removeAllThumbnailsForRoot(folderId); + removeMovieData(folderId); + removeSeriesData(folderId); + const infoPath = path.join(rootDir, INFO_FILENAME); + if (fs.existsSync(infoPath)) { + try { + fs.rmSync(infoPath, { force: true }); + } catch (err) { + console.warn(`⚠️ info.json kaldırılamadı (${infoPath}): ${err.message}`); + } + } } else { - pruneInfoEntry(folderId, relWithinRoot); const infoAfter = readInfoForRoot(folderId); const displayName = infoAfter?.name || folderId; const primaryVideo = infoAfter?.primaryVideoPath || guessPrimaryVideo(folderId); @@ -2843,7 +2845,6 @@ app.delete("/api/file", requireAuth, (req, res) => { ) ); } - removeSeriesEpisode(folderId, relWithinRoot); } }