arama özelliği eklendi

This commit is contained in:
2025-11-01 15:39:01 +03:00
parent ebe00fb6f7
commit 1eef5d232f
6 changed files with 225 additions and 30 deletions

View File

@@ -1,11 +1,21 @@
<script> <script>
import { createEventDispatcher, onMount } from "svelte"; import { createEventDispatcher } from "svelte";
const dispatch = createEventDispatcher(); import {
activePlaceholder,
activeSearchTerm,
updateSearchTerm
} from "../stores/searchStore.js";
let search = ""; const dispatch = createEventDispatcher();
export let placeholder = "Search files..."; export let placeholder = "";
const onToggle = () => dispatch("toggleMenu"); const onToggle = () => dispatch("toggleMenu");
function handleInput(event) {
updateSearchTerm(event.target.value);
}
$: resolvedPlaceholder = placeholder || $activePlaceholder;
</script> </script>
<div class="topbar"> <div class="topbar">
@@ -20,7 +30,12 @@
<div class="search"> <div class="search">
<i class="fa-solid fa-magnifying-glass"></i> <i class="fa-solid fa-magnifying-glass"></i>
<input placeholder={placeholder} bind:value={search} /> <input
type="search"
placeholder={resolvedPlaceholder}
value={$activeSearchTerm}
on:input={handleInput}
/>
</div> </div>
</div> </div>

View File

@@ -4,6 +4,11 @@
import { cleanFileName, extractTitleAndYear } from "../utils/filename.js"; import { cleanFileName, extractTitleAndYear } from "../utils/filename.js";
import { refreshMovieCount } from "../stores/movieStore.js"; import { refreshMovieCount } from "../stores/movieStore.js";
import { refreshTvShowCount } from "../stores/tvStore.js"; import { refreshTvShowCount } from "../stores/tvStore.js";
import {
activeSearchTerm,
setSearchScope,
clearSearch
} from "../stores/searchStore.js";
const HIDDEN_ROOT_REGEX = /^\d{10,}$/; const HIDDEN_ROOT_REGEX = /^\d{10,}$/;
const FOLDER_ICON_PATH = "/folder.svg"; const FOLDER_ICON_PATH = "/folder.svg";
const MAX_TRAIL_SEGMENTS = 3; const MAX_TRAIL_SEGMENTS = 3;
@@ -13,6 +18,7 @@
let visibleFolders = []; let visibleFolders = [];
let visibleFiles = []; let visibleFiles = [];
let visibleEntries = []; let visibleEntries = [];
let renderedEntries = [];
let allDirectories = []; let allDirectories = [];
let breadcrumbs = []; let breadcrumbs = [];
let currentFileScope = []; let currentFileScope = [];
@@ -21,6 +27,8 @@
let draggingItem = null; let draggingItem = null;
let dragOverItem = null; let dragOverItem = null;
let lastDragPath = ""; let lastDragPath = "";
let searchTerm = "";
let hasSearch = false;
const normalizePath = (value) => { const normalizePath = (value) => {
if (!value) return ""; if (!value) return "";
@@ -33,6 +41,22 @@
/safari/i.test(navigator.userAgent || "") && /safari/i.test(navigator.userAgent || "") &&
!/chrome|crios|android/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) => const shouldHideRootSegment = (segment) =>
segment && (HIDDEN_ROOT_REGEX.test(segment) || segment === "downloads"); segment && (HIDDEN_ROOT_REGEX.test(segment) || segment === "downloads");
@@ -157,6 +181,10 @@
customOrder.set(key, filteredOrder); customOrder.set(key, filteredOrder);
} }
$: searchTerm = $activeSearchTerm;
$: hasSearch = searchTerm.trim().length > 0;
$: renderedEntries = filterEntriesBySearch(visibleEntries, searchTerm);
function computeBreadcrumbs(path) { function computeBreadcrumbs(path) {
const segments = path ? path.split("/").filter(Boolean) : []; const segments = path ? path.split("/").filter(Boolean) : [];
const crumbs = [{ label: "Home", path: "" }]; const crumbs = [{ label: "Home", path: "" }];
@@ -320,11 +348,10 @@
} }
let selectedItems = new Set(); let selectedItems = new Set();
let allSelected = false; 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)); allSelected = keys.length > 0 && keys.every((key) => selectedItems.has(key));
} }
$: syncSelectionState();
let pendingPlayTarget = null; let pendingPlayTarget = null;
let activeMenu = null; // Aktif menü öğesi let activeMenu = null; // Aktif menü öğesi
let menuPosition = { top: 0, left: 0 }; // Menü pozisyonu let menuPosition = { top: 0, left: 0 }; // Menü pozisyonu
@@ -485,7 +512,7 @@
if (allSelected) { if (allSelected) {
selectedItems = new Set(); selectedItems = new Set();
} else { } else {
const keys = visibleEntries.map((entry) => entry.name).filter(Boolean); const keys = renderedEntries.map((entry) => entry.name).filter(Boolean);
selectedItems = new Set(keys); selectedItems = new Set(keys);
} }
} }
@@ -624,12 +651,19 @@
) { ) {
return; return;
} }
const hadSearch = searchTerm.trim().length > 0;
const nextSearchTerm = hadSearch ? "" : searchTerm;
currentPath = normalized; currentPath = normalized;
currentOriginalPath = normalizedOriginal; currentOriginalPath = normalizedOriginal;
if (hadSearch) {
clearSearch("files");
}
selectedItems = new Set(); selectedItems = new Set();
activeMenu = null; activeMenu = null;
if (isCreatingFolder) cancelCreateFolder(); if (isCreatingFolder) cancelCreateFolder();
updateUrlPath(normalized, normalizedOriginal, { replace }); updateUrlPath(normalized, normalizedOriginal, { replace });
updateVisibleState(files, normalized);
renderedEntries = filterEntriesBySearch(visibleEntries, nextSearchTerm);
} }
function handleEntryClick(entry) { function handleEntryClick(entry) {
@@ -1288,6 +1322,7 @@
} }
onMount(async () => { onMount(async () => {
setSearchScope("files");
await loadFiles(); // önce dosyaları getir await loadFiles(); // önce dosyaları getir
const token = localStorage.getItem("token"); const token = localStorage.getItem("token");
const wsUrl = `${API.replace("http", "ws")}?token=${token}`; const wsUrl = `${API.replace("http", "ws")}?token=${token}`;
@@ -1565,10 +1600,10 @@
</div> </div>
</div> </div>
<div class="header-actions"> <div class="header-actions">
{#if visibleEntries.length > 0 && selectedItems.size > 0} {#if renderedEntries.length > 0 && selectedItems.size > 0}
<span class="selection-count">{selectedItems.size} öğe seçildi</span> <span class="selection-count">{selectedItems.size} öğe seçildi</span>
{/if} {/if}
{#if visibleEntries.length > 0 && selectedItems.size > 0} {#if renderedEntries.length > 0 && selectedItems.size > 0}
<button <button
class="select-all-btn" class="select-all-btn"
type="button" type="button"
@@ -1603,10 +1638,16 @@
</button> </button>
</div> </div>
</div> </div>
{#if visibleEntries.length === 0 && !isCreatingFolder} {#if renderedEntries.length === 0 && !isCreatingFolder}
<div class="empty"> <div class="empty">
<div style="font-size:42px"><i class="fa-solid fa-folder-open"></i></div> <div style="font-size:42px"><i class="fa-solid fa-folder-open"></i></div>
<div style="font-weight:700">No media found</div> <div style="font-weight:700">
{#if hasSearch}
Aramanla eşleşen öğe bulunamadı
{:else}
No media found
{/if}
</div>
</div> </div>
{:else} {:else}
<div <div
@@ -1633,7 +1674,7 @@
</div> </div>
</div> </div>
{/if} {/if}
{#each visibleEntries as entry (entry.name)} {#each renderedEntries as entry (entry.name)}
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions --> <!-- svelte-ignore a11y-no-static-element-interactions -->
<div <div

View File

@@ -3,6 +3,10 @@
import { API, apiFetch } from "../utils/api.js"; import { API, apiFetch } from "../utils/api.js";
import { cleanFileName } from "../utils/filename.js"; import { cleanFileName } from "../utils/filename.js";
import { movieCount } from "../stores/movieStore.js"; import { movieCount } from "../stores/movieStore.js";
import {
activeSearchTerm,
setSearchScope
} from "../stores/searchStore.js";
let movies = []; let movies = [];
let loading = true; let loading = true;
@@ -35,6 +39,9 @@ let subtitleURL = null;
let subtitleLabel = "Custom Subtitles"; let subtitleLabel = "Custom Subtitles";
$: selectedName = selectedVideo?.name ?? ""; $: selectedName = selectedVideo?.name ?? "";
$: encName = selectedName ? encodeURIComponent(selectedName) : ""; $: encName = selectedName ? encodeURIComponent(selectedName) : "";
let searchTerm = "";
let hasSearch = false;
let filteredMovies = [];
function runtimeToText(runtime) { function runtimeToText(runtime) {
if (!runtime || Number.isNaN(runtime)) return null; if (!runtime || Number.isNaN(runtime)) return null;
@@ -119,6 +126,25 @@ $: if (showPlayerModal && selectedVideo) {
} }
} }
$: searchTerm = $activeSearchTerm;
$: hasSearch = searchTerm.trim().length > 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) { async function handlePlay(movie) {
if (!movie) return; if (!movie) return;
closeMovie(); closeMovie();
@@ -335,6 +361,7 @@ async function loadMovies() {
} }
onMount(() => { onMount(() => {
setSearchScope("movies");
loadMovies(); loadMovies();
const handleKey = (event) => { const handleKey = (event) => {
if (!showPlayerModal) return; if (!showPlayerModal) return;
@@ -381,9 +408,11 @@ async function loadMovies() {
<div class="state-placeholder error">{error}</div> <div class="state-placeholder error">{error}</div>
{:else if movies.length === 0} {:else if movies.length === 0}
<div class="state-placeholder">No movie metadata found yet.</div> <div class="state-placeholder">No movie metadata found yet.</div>
{:else if hasSearch && filteredMovies.length === 0}
<div class="state-placeholder">Aramanıza uyan film bulunamadı.</div>
{:else} {:else}
<div class="movies-grid"> <div class="movies-grid">
{#each movies as movie} {#each filteredMovies as movie}
<div class="movie-card" on:click={() => openMovie(movie)}> <div class="movie-card" on:click={() => openMovie(movie)}>
{#if movie.poster} {#if movie.poster}
<div class="poster-wrapper"> <div class="poster-wrapper">

View File

@@ -3,6 +3,10 @@
import { API, apiFetch } from "../utils/api.js"; import { API, apiFetch } from "../utils/api.js";
import { cleanFileName } from "../utils/filename.js"; import { cleanFileName } from "../utils/filename.js";
import { tvShowCount } from "../stores/tvStore.js"; import { tvShowCount } from "../stores/tvStore.js";
import {
activeSearchTerm,
setSearchScope
} from "../stores/searchStore.js";
let shows = []; let shows = [];
let loading = true; let loading = true;
@@ -28,6 +32,9 @@ let seasonPlaylist = [];
let seasonPlaylistIndex = -1; let seasonPlaylistIndex = -1;
let canPlayPrev = false; let canPlayPrev = false;
let canPlayNext = false; let canPlayNext = false;
let searchTerm = "";
let hasSearch = false;
let filteredShows = [];
function runtimeToText(runtime) { function runtimeToText(runtime) {
if (!runtime || Number.isNaN(runtime)) return null; 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() { async function refreshShows() {
try { try {
refreshing = true; refreshing = true;
@@ -542,6 +568,7 @@ async function openVideoAtIndex(index) {
} }
onMount(() => { onMount(() => {
setSearchScope("tv");
loadShows(); loadShows();
const handleKey = (event) => { const handleKey = (event) => {
if (!showPlayerModal) return; if (!showPlayerModal) return;
@@ -588,9 +615,11 @@ async function openVideoAtIndex(index) {
<div class="state-placeholder error">{error}</div> <div class="state-placeholder error">{error}</div>
{:else if shows.length === 0} {:else if shows.length === 0}
<div class="state-placeholder">No TV metadata found yet.</div> <div class="state-placeholder">No TV metadata found yet.</div>
{:else if hasSearch && filteredShows.length === 0}
<div class="state-placeholder">Aramanıza uyan dizi bulunamadı.</div>
{:else} {:else}
<div class="tv-grid"> <div class="tv-grid">
{#each shows as show} {#each filteredShows as show}
<div class="tv-card" on:click={() => openShow(show)}> <div class="tv-card" on:click={() => openShow(show)}>
{#if show.poster} {#if show.poster}
<div class="poster-wrapper"> <div class="poster-wrapper">

View File

@@ -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]: ""
}
};
});
}

View File

@@ -2811,23 +2811,25 @@ app.delete("/api/file", requireAuth, (req, res) => {
if (!name) return false; if (!name) return false;
if (name === INFO_FILENAME) return false; if (name === INFO_FILENAME) return false;
if (name.startsWith(".")) 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; return true;
}); });
if (meaningful.length === 0 || stats?.isDirectory?.()) { pruneInfoEntry(folderId, relWithinRoot);
purgeRootFolder(folderId); 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 { } else {
pruneInfoEntry(folderId, relWithinRoot);
const infoAfter = readInfoForRoot(folderId); const infoAfter = readInfoForRoot(folderId);
const displayName = infoAfter?.name || folderId; const displayName = infoAfter?.name || folderId;
const primaryVideo = infoAfter?.primaryVideoPath || guessPrimaryVideo(folderId); const primaryVideo = infoAfter?.primaryVideoPath || guessPrimaryVideo(folderId);
@@ -2843,7 +2845,6 @@ app.delete("/api/file", requireAuth, (req, res) => {
) )
); );
} }
removeSeriesEpisode(folderId, relWithinRoot);
} }
} }