refactor(toolbar): modularize component
Break down the monolithic Toolbar component into smaller, highly cohesive modules to improve maintainability and separation of concerns. - Extract distinct UI regions into dedicated components (`ToolbarTitle`, `ToolbarSearch`, `SortControl`, `ZoomControl`, `ToolbarFilters`, and `FilterPill`). - Abstract complex search state, debouncing, event listeners, and autocomplete logic into a custom `useToolbarSearch` hook. - Relocate search value formatting and sort configurations to dedicated utility files (`searchValue.ts` and `sortOptions.ts`). - Simplify the main `Toolbar` component to act as a clean presentation orchestrator.
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import {
|
||||
ExploreTagEntry,
|
||||
parseSearchValue,
|
||||
SearchCommand,
|
||||
useGalleryStore,
|
||||
} from "../../store";
|
||||
import { useDismissable } from "../menu";
|
||||
import { commandPrefix, composeSearchValue } from "./searchValue";
|
||||
|
||||
export function useToolbarSearch() {
|
||||
const search = useGalleryStore((state) => state.search);
|
||||
const setSearch = useGalleryStore((state) => state.setSearch);
|
||||
const clearSearch = useGalleryStore((state) => state.clearSearch);
|
||||
const selectedFolderId = useGalleryStore((state) => state.selectedFolderId);
|
||||
|
||||
const [searchCommand, setSearchCommand] = useState<SearchCommand | null>(null);
|
||||
const [searchQuery, setSearchQuery] = useState(search);
|
||||
const [searchPanelOpen, setSearchPanelOpen] = useState(false);
|
||||
const [tagSuggestions, setTagSuggestions] = useState<ExploreTagEntry[]>([]);
|
||||
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const suggestDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||
const searchShellRef = useRef<HTMLDivElement>(null);
|
||||
const userHasTyped = useRef(false);
|
||||
|
||||
const hasActiveSearch = search.trim().length > 0;
|
||||
const parsedSearch = parseSearchValue(composeSearchValue(searchCommand, searchQuery));
|
||||
const showTagSuggestions = searchCommand === "tag" && searchPanelOpen;
|
||||
const showCommandHints = !searchCommand && searchPanelOpen;
|
||||
|
||||
useEffect(() => {
|
||||
if (!userHasTyped.current) return;
|
||||
if (debounceRef.current) clearTimeout(debounceRef.current);
|
||||
debounceRef.current = setTimeout(() => {
|
||||
setSearch(composeSearchValue(searchCommand, searchQuery));
|
||||
}, 200);
|
||||
return () => {
|
||||
if (debounceRef.current) clearTimeout(debounceRef.current);
|
||||
};
|
||||
}, [searchCommand, searchQuery, setSearch]);
|
||||
|
||||
useEffect(() => {
|
||||
const parsed = parseSearchValue(search);
|
||||
setSearchCommand(parsed.prefix && parsed.mode !== "filename" ? parsed.mode : null);
|
||||
setSearchQuery(parsed.prefix ? parsed.query : search);
|
||||
}, [search]);
|
||||
|
||||
useEffect(() => {
|
||||
if (searchCommand !== "tag") {
|
||||
setTagSuggestions([]);
|
||||
return;
|
||||
}
|
||||
if (suggestDebounceRef.current) clearTimeout(suggestDebounceRef.current);
|
||||
suggestDebounceRef.current = setTimeout(async () => {
|
||||
try {
|
||||
const results = await invoke<ExploreTagEntry[]>("search_tags_autocomplete", {
|
||||
params: { query: searchQuery.trim(), folder_id: selectedFolderId ?? null, limit: 10 },
|
||||
});
|
||||
setTagSuggestions(Array.isArray(results) ? results : []);
|
||||
} catch {
|
||||
setTagSuggestions([]);
|
||||
}
|
||||
}, 120);
|
||||
return () => {
|
||||
if (suggestDebounceRef.current) clearTimeout(suggestDebounceRef.current);
|
||||
};
|
||||
}, [searchCommand, searchQuery, selectedFolderId]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
const activeElement = document.activeElement;
|
||||
const searchFocused = activeElement === searchInputRef.current;
|
||||
if (event.key === "Escape" && (searchFocused || hasActiveSearch)) {
|
||||
event.preventDefault();
|
||||
setSearchCommand(null);
|
||||
setSearchQuery("");
|
||||
clearSearch();
|
||||
}
|
||||
};
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, [clearSearch, hasActiveSearch]);
|
||||
|
||||
useDismissable(searchShellRef, () => setSearchPanelOpen(false));
|
||||
|
||||
const handleSearchChange = (nextValue: string) => {
|
||||
userHasTyped.current = true;
|
||||
if (!searchCommand) {
|
||||
const parsed = parseSearchValue(nextValue);
|
||||
if (parsed.prefix) {
|
||||
setSearchCommand(parsed.mode === "filename" ? null : parsed.mode);
|
||||
setSearchQuery(parsed.query);
|
||||
return;
|
||||
}
|
||||
}
|
||||
setSearchQuery(nextValue);
|
||||
};
|
||||
|
||||
const handleSearchKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.key === "Backspace" && searchQuery.length === 0 && searchCommand !== null) {
|
||||
event.preventDefault();
|
||||
setSearchCommand(null);
|
||||
}
|
||||
};
|
||||
|
||||
const clearSearchInput = () => {
|
||||
setSearchCommand(null);
|
||||
setSearchQuery("");
|
||||
setTagSuggestions([]);
|
||||
clearSearch();
|
||||
};
|
||||
|
||||
const removeSearchCommand = () => {
|
||||
setSearchCommand(null);
|
||||
setTagSuggestions([]);
|
||||
};
|
||||
|
||||
const chooseTagSuggestion = (tag: string) => {
|
||||
userHasTyped.current = true;
|
||||
setSearchQuery(tag);
|
||||
setSearch(`/t ${tag}`);
|
||||
setSearchPanelOpen(false);
|
||||
searchInputRef.current?.blur();
|
||||
};
|
||||
|
||||
const chooseCommandHint = (command: SearchCommand) => {
|
||||
userHasTyped.current = true;
|
||||
setSearchCommand(command);
|
||||
searchInputRef.current?.focus();
|
||||
};
|
||||
|
||||
return {
|
||||
chooseCommandHint,
|
||||
chooseTagSuggestion,
|
||||
clearSearchInput,
|
||||
commandPrefix,
|
||||
handleSearchChange,
|
||||
handleSearchKeyDown,
|
||||
hasActiveSearch,
|
||||
parsedSearch,
|
||||
removeSearchCommand,
|
||||
searchCommand,
|
||||
searchInputRef,
|
||||
searchPanelOpen,
|
||||
searchQuery,
|
||||
searchShellRef,
|
||||
setSearchPanelOpen,
|
||||
showCommandHints,
|
||||
showTagSuggestions,
|
||||
tagSuggestions,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user