diff --git a/app/src/DashboardPage.js b/app/src/DashboardPage.js
index 012080d4..10b0ea93 100644
--- a/app/src/DashboardPage.js
+++ b/app/src/DashboardPage.js
@@ -33,8 +33,7 @@ import {SearchView, useSearch, useSearchSuggestions} from "./components/Search";
import {OutdatedZimsMessage} from "./components/Zim";
import {useSearchFilter, useSearchRecentFiles, useWROLMode} from "./hooks/customHooks";
import {ExtensionInstallSuggestion} from "./components/admin/ExtensionInstallSuggestion";
-import {FileCards, FileSearchFilterButton} from "./components/Files";
-import {DateSelectorButton} from "./components/DatesSelector";
+import {fileMimetypeFilterOptions, FileCards, SearchFilterButton} from "./components/Files";
import {useCalculators} from "./components/Calculators";
import {classifyAndEvaluate} from "./components/calculators/mathConfig";
@@ -394,7 +393,7 @@ export function DashboardPage() {
loading,
setSearchStr: setSuggestionSearchStr,
setSearchTags,
- months, dateRange, setDates, clearDate,
+ clearDate,
} = useSearchSuggestions(searchStr, activeTags, anyTag);
React.useEffect(() => {
@@ -444,7 +443,7 @@ export function DashboardPage() {
}
const getSearchResultsInput = (props) => {
- return ;
};
+ // The search input grows to fill the row; the Filter button sits immediately after it. Flexbox adapts
+ // to the client width, so only the control size changes between mobile and larger screens.
+ const searchRow = (big) => {
+ const inputProps = {style: {flexGrow: 1, minWidth: 0, marginBottom: 0}};
+ if (big) {
+ inputProps.size = 'big';
+ }
+ return
+ {getSearchResultsInput(inputProps)}
+
+
;
+ };
+
return
-
-
-
-
- {getSearchResultsInput()}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {getSearchResultsInput({size: 'big'})}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {getSearchResultsInput({size: 'big'})}
-
-
-
-
+ {searchRow(false)}
+ {searchRow(true)}
{!searchStr && }
{!searchStr && }
{body}
diff --git a/app/src/components/Archive.js b/app/src/components/Archive.js
index 2a83b443..67b59837 100644
--- a/app/src/components/Archive.js
+++ b/app/src/components/Archive.js
@@ -38,7 +38,6 @@ import {
PreviewPath,
resolveDataPath,
SearchInput,
- SortButton,
TabLinks,
textEllipsis,
useTitle
@@ -78,10 +77,10 @@ import {
useSearchDomain,
useSearchOrder
} from "../hooks/customHooks";
-import {FileCards, FileRowTagIcon, FilesView} from "./Files";
+import {FileCards, FileRowTagIcon, FilesView, SearchControlBar} from "./Files";
import Grid from "semantic-ui-react/dist/commonjs/collections/Grid";
import _ from "lodash";
-import {Media, ThemeContext} from "../contexts/contexts";
+import {ThemeContext} from "../contexts/contexts";
import {Button, Card, darkTheme, Header, Loader, Placeholder, Popup, Segment, Statistic, Tab} from "./Theme";
import {BulkTagModal} from "./BulkTagModal";
import {taggedImageLabel, TagsSelector} from "../Tags";
@@ -1216,7 +1215,7 @@ function ArchivesPage() {
/>
;
- const {body, paginator, viewButton, limitDropdown, tagQuerySelector} = FilesView(
+ const {body, paginator, viewButton} = FilesView(
{
files: archives,
activePage: activePage,
@@ -1230,16 +1229,6 @@ function ArchivesPage() {
);
- const [localSearchStr, setLocalSearchStr] = React.useState(searchStr || '');
- const searchInput = setLocalSearchStr(null)}
- searchStr={localSearchStr}
- onSubmit={setSearchStr}
- placeholder='Search Archives...'
- inputRef={searchInputRef}
- />;
-
// Domain header with edit link when filtering by domain
let header;
if (domainObj && domainObj.domain) {
@@ -1263,30 +1252,14 @@ function ArchivesPage() {
return <>
{header}
-
-
-
- {viewButton}
- {limitDropdown}
- {tagQuerySelector}
-
-
-
- {searchInput}
-
-
-
-
-
-
- {viewButton}
- {limitDropdown}
- {tagQuerySelector}
-
- {searchInput}
-
-
-
+
{body}
{paginator}
setLocalSearchStr(null)}
- searchStr={localSearchStr}
- onSubmit={setSearchStr}
- placeholder='Search Documents...'
- inputRef={searchInputRef}
- />;
-
return <>
{author && }
{subject && }
-
-
-
- {viewButton}
- {limitDropdown}
- {tagQuerySelector}
-
-
-
-
- {searchInput}
-
-
-
-
-
-
- {viewButton}
- {limitDropdown}
- {tagQuerySelector}
-
-
- {searchInput}
-
-
-
+
{body}
{paginator}
setView('list')}/>;
}
-export function SearchLimitDropdown({limits = []}) {
- const {limit, setLimit} = usePages();
- const limitOptions = limits.map(i => {
- return {key: i, value: i, text: i.toString()}
- });
-
- return setLimit(value)}
- />
-}
-
export function FilesView(
{
files,
@@ -373,8 +364,6 @@ export function FilesView(
onSelect,
setPage,
headlines = false,
- limitOptions = [12, 24, 48, 96],
- showAnyTag = false,
},
) {
const {view} = useSearchView();
@@ -403,15 +392,11 @@ export function FilesView(
}
const viewButton =
- const limitDropdown = ;
- const tagQuerySelector = ;
return {
body,
paginator,
viewButton,
- limitDropdown,
- tagQuerySelector,
}
}
@@ -502,33 +487,357 @@ export function TagsQuerySelector({onChange, showAny = true}) {
/>
}
-export function FileSearchFilterButton({size = 'medium'}) {
- // see `filterToMimetypes`
- const filterOptions = [
- {key: 'video', text: 'Video', value: 'video'},
- {key: 'archive', text: 'Archive', value: 'archive'},
- {key: 'pdf', text: 'PDF', value: 'pdf'},
- {key: 'ebook', text: 'eBook', value: 'ebook'},
- {key: 'doc', text: 'Document', value: 'doc'},
- {key: 'audio', text: 'Audio', value: 'audio'},
- {key: 'image', text: 'Image', value: 'image'},
- {key: 'zip', text: 'ZIP', value: 'zip'},
- {key: 'model', text: '3D Model', value: 'model'},
- {key: 'software', text: 'Software', value: 'software'},
- ];
+// see `filterToMimetypes`
+export const fileMimetypeFilterOptions = [
+ {key: 'video', text: 'Video', value: 'video'},
+ {key: 'archive', text: 'Archive', value: 'archive'},
+ {key: 'pdf', text: 'PDF', value: 'pdf'},
+ {key: 'ebook', text: 'eBook', value: 'ebook'},
+ {key: 'doc', text: 'Document', value: 'doc'},
+ {key: 'audio', text: 'Audio', value: 'audio'},
+ {key: 'image', text: 'Image', value: 'image'},
+ {key: 'zip', text: 'ZIP', value: 'zip'},
+ {key: 'model', text: '3D Model', value: 'model'},
+ {key: 'software', text: 'Software', value: 'software'},
+];
- return
+export const docMimetypeFilterOptions = [
+ {key: 'pdf', text: 'PDF', value: 'pdf'},
+ {key: 'epub', text: 'EPUB', value: 'epub'},
+ {key: 'comic', text: 'Comic', value: 'comic'},
+ {key: 'mobi', text: 'MOBI', value: 'mobi'},
+];
+
+export function FileSearchFilterButton({size = 'medium'}) {
+ return
}
export function DocSearchFilterButton({size = 'medium'}) {
- const filterOptions = [
- {key: 'pdf', text: 'PDF', value: 'pdf'},
- {key: 'epub', text: 'EPUB', value: 'epub'},
- {key: 'comic', text: 'Comic', value: 'comic'},
- {key: 'mobi', text: 'MOBI', value: 'mobi'},
- ];
+ return
+}
- return
+// The app-wide default page size (matches usePages' fallback); used to reset Results Per Page on Clear All.
+const DEFAULT_SEARCH_LIMIT = 24;
+
+// A single section within the comprehensive SearchFilterModal. Vertical spacing between sections is
+// provided by the surrounding Grid rows.
+function SearchFilterSection({header, children}) {
+ return <>
+
+ {children}
+ >
+}
+
+// A comprehensive modal which combines every search option (sort, tags, file-type filter, dates, limit)
+// into a single dialog. Which sections appear is controlled by the props, so each page can show only what
+// is relevant. All state is URL-query driven (same hooks the individual controls use), so no extra wiring
+// is required on the page.
+export function SearchFilterModal(
+ {
+ open,
+ onClose,
+ sorts = null,
+ fileFilterOptions = null,
+ showDates = false,
+ showTags = true,
+ showLimit = true,
+ limitOptions = [12, 24, 48, 96],
+ },
+) {
+ // Committed (URL) state — read only; drafts below hold the user's in-progress edits.
+ const {filter} = useSearchFilter();
+ const {sort} = useSearchOrder();
+ const {dateRange, months} = useSearchDate();
+ const {activeTags, anyTag} = useSearch();
+ const {limit} = usePages(DEFAULT_SEARCH_LIMIT);
+ const {updateQuery} = useContext(QueryContext);
+
+ const emptyRange = [null, null];
+ const seededRange = dateRange && (dateRange[0] || dateRange[1]) ? dateRange : emptyRange;
+ const seededMonths = (months || []).map(i => parseInt(i));
+
+ // Every selection is drafted locally and only applied to the URL (which performs the search) when the
+ // modal is closed — via Done, the close icon, or clicking away.
+ const [draftSort, setDraftSort] = useState(null);
+ const [draftFilter, setDraftFilter] = useState(null);
+ const [draftTags, setDraftTags] = useState([]);
+ const [draftAnyTag, setDraftAnyTag] = useState(false);
+ const [draftLimit, setDraftLimit] = useState(null);
+ const [draftMonths, setDraftMonths] = useState([]);
+ const [draftRange, setDraftRange] = useState(emptyRange);
+
+ // Seed the drafts from the URL each time the modal is opened.
+ React.useEffect(() => {
+ if (open) {
+ setDraftSort(sort || null);
+ setDraftFilter(filter || null);
+ setDraftTags(activeTags || []);
+ setDraftAnyTag(anyTag || false);
+ setDraftLimit(limit);
+ setDraftMonths(seededMonths);
+ setDraftRange(seededRange);
+ }
+ }, [open]);
+
+ const handleClose = () => {
+ // Apply every changed draft in a SINGLE updateQuery — sequential setters would each merge against
+ // the same stale searchParams and clobber one another. Only reset pagination if something changed.
+ const params = {};
+ if (sorts && (draftSort || null) !== (sort || null)) {
+ params.order = draftSort;
+ }
+ if (fileFilterOptions && (draftFilter || null) !== (filter || null)) {
+ params.filter = draftFilter;
+ }
+ if (showTags && (draftAnyTag !== anyTag || JSON.stringify(draftTags) !== JSON.stringify(activeTags))) {
+ if (draftAnyTag) {
+ params.tag = [];
+ params.anyTag = 'true';
+ } else {
+ params.tag = draftTags;
+ params.anyTag = null;
+ }
+ }
+ if (showLimit && draftLimit !== limit) {
+ params.l = draftLimit;
+ }
+ if (showDates && (JSON.stringify(draftMonths) !== JSON.stringify(seededMonths)
+ || JSON.stringify(draftRange) !== JSON.stringify(seededRange))) {
+ let newFromDate = null;
+ let newToDate = null;
+ if (draftRange[0] <= draftRange[1]) {
+ newFromDate = draftRange[0];
+ newToDate = draftRange[1];
+ }
+ params.fromDate = newFromDate;
+ params.toDate = newToDate;
+ params.month = draftMonths;
+ }
+ if (Object.keys(params).length > 0) {
+ params.o = 0;
+ updateQuery(params);
+ }
+ if (onClose) {
+ onClose();
+ }
+ }
+
+ const handleClearAll = () => {
+ // Reset the form only; nothing is applied until the modal is closed.
+ setDraftSort(null);
+ setDraftFilter(null);
+ setDraftTags([]);
+ setDraftAnyTag(false);
+ setDraftLimit(DEFAULT_SEARCH_LIMIT);
+ setDraftMonths([]);
+ setDraftRange(emptyRange);
+ }
+
+ // Sort section (operates on the draft).
+ const sortKey = draftSort ? draftSort.replace(/^-/, '') : (sorts ? sorts[0]['value'] : null);
+ const desc = draftSort ? draftSort.startsWith('-') : true;
+ const sortButtons = sorts && sorts.map(o =>
+
+ );
+ const directionToggle = ;
+
+ // File-type filter section (single-select radios; clicking the active one clears it). Laid out in
+ // columns so more options fit on each line.
+ const filterFields = fileFilterOptions && fileFilterOptions.map(i =>
+
+
+ setDraftFilter(draftFilter === i['value'] ? null : i['value'])}
+ />
+
+
+ );
+
+ // Results-per-page as buttons (matches the Sort By buttons and avoids a dropdown menu being clipped by
+ // the scrolling modal content).
+ const limitButtons = limitOptions.map(i =>
+
+ );
+
+ return
+ Search Filters
+
+
+ {sorts &&
+
+
+
+ {directionToggle}
+ {sortButtons}
+
+
+ }
+
+ {fileFilterOptions &&
+
+
+
+
+
+
+ }
+
+ {(showTags || showLimit) &&
+
+ {showTags &&
+
+
+ 0)}
+ selectedTagNames={draftTags}
+ anyTag={draftAnyTag}
+ filterByOverlap={true}
+ onChange={(tagNames, newAnyTag) => {
+ if (newAnyTag) {
+ setDraftAnyTag(true);
+ setDraftTags([]);
+ } else {
+ setDraftAnyTag(false);
+ setDraftTags(tagNames);
+ }
+ }}
+ style={{marginLeft: '0.3em', marginTop: '0.3em'}}
+ />
+
+ }
+ {showLimit &&
+
+
+ {limitButtons}
+
+ }
+ }
+
+ {showDates &&
+
+
+
+
+
+
+
+
+ }
+
+
+
+
+
+
+
+}
+
+// The trigger button which opens the comprehensive SearchFilterModal. Turns violet and displays a count
+// badge when any of its filters are active.
+export function SearchFilterButton(
+ {
+ sorts = null,
+ fileFilterOptions = null,
+ showDates = false,
+ showTags = true,
+ showLimit = true,
+ limitOptions = [12, 24, 48, 96],
+ size = 'medium',
+ content = 'Filter',
+ },
+) {
+ const [open, setOpen] = useState(false);
+ const {filter} = useSearchFilter();
+ const {sort} = useSearchOrder();
+ const {dateRange, months} = useSearchDate();
+ const {activeTags, anyTag} = useSearch();
+
+ let count = 0;
+ if (fileFilterOptions && filter) {
+ count += 1;
+ }
+ if (sorts && sort) {
+ count += 1;
+ }
+ if (showTags && ((activeTags && activeTags.length > 0) || anyTag)) {
+ count += 1;
+ }
+ if (showDates && ((months && months.length > 0) || !dateRangeIsEmpty(dateRange))) {
+ count += 1;
+ }
+ const active = count > 0;
+
+ return <>
+
+ setOpen(false)}
+ sorts={sorts}
+ fileFilterOptions={fileFilterOptions}
+ showDates={showDates}
+ showTags={showTags}
+ showLimit={showLimit}
+ limitOptions={limitOptions}
+ />
+ >
+}
+
+// The single-line search control row shared by the Videos/Archives/Docs result pages:
+// [ view toggle ] [ search input (grows) ] [ Filter button ]
+// The text input is managed locally and only submits its value on enter/clear. Filter options are
+// forwarded to the SearchFilterButton so each page shows only the sections relevant to it.
+export function SearchControlBar(
+ {
+ searchStr,
+ setSearchStr,
+ placeholder = 'Search...',
+ inputRef,
+ viewButton,
+ sorts = null,
+ fileFilterOptions = null,
+ showDates = false,
+ },
+) {
+ const [localSearchStr, setLocalSearchStr] = useState(searchStr || '');
+
+ const searchInput = setLocalSearchStr(null)}
+ onSubmit={setSearchStr}
+ placeholder={placeholder}
+ inputRef={inputRef}
+ />;
+
+ return
+ {viewButton}
+
{searchInput}
+
+
}
export function FilesSearchView({
@@ -539,7 +848,7 @@ export function FilesSearchView({
const {searchFiles, pages} = useSearchFiles(24, emptySearch, model);
- const {body, paginator, viewButton, limitDropdown, tagQuerySelector} = FilesView(
+ const {body, paginator, viewButton} = FilesView(
{
files: searchFiles,
activePage: pages.activePage,
@@ -549,31 +858,11 @@ export function FilesSearchView({
onSelect: null,
setPage: pages.setPage,
headlines: true,
- showAnyTag: true,
},
);
return <>
-
-
-
- {showView &&
- {viewButton}}
- {limitDropdown}
- {tagQuerySelector}
-
-
-
-
-
-
- {showView &&
- {viewButton}}
- {limitDropdown}
- {tagQuerySelector}
-
-
-
+ {showView && {viewButton}
}
{body}
{paginator}
>
diff --git a/app/src/components/Search.js b/app/src/components/Search.js
index a57efd7d..2c5bf5e7 100644
--- a/app/src/components/Search.js
+++ b/app/src/components/Search.js
@@ -1,6 +1,6 @@
import React, {useState} from "react";
import {Link, Route, Routes, useLocation, useNavigate} from "react-router";
-import {FilesSearchView} from "./Files";
+import {FilesSearchView, SearchViewButton} from "./Files";
import {useLatestRequest, usePages, useSearchChannels, useSearchDate, useSearchFilter} from "../hooks/customHooks";
import {ShortcutHint} from "./ShortcutHint";
import {ZimSearchView} from "./Zim";
@@ -485,10 +485,13 @@ export function SearchView({suggestions, suggestionsSums, loading}) {
{text: othersTabName, to: `/search/other${search}`, key: 'othersSearch'},
];
+ // The view toggle only applies to the Files tab; show it in the tab row's right slot there.
+ const isFilesTab = !/\/(zim|map|other)$/.test(location.pathname);
+
return
-
+ : null}/>
- }/>
+ }/>
}/>
}/>
}/>
diff --git a/app/src/components/SearchFilterModal.test.js b/app/src/components/SearchFilterModal.test.js
new file mode 100644
index 00000000..711460ca
--- /dev/null
+++ b/app/src/components/SearchFilterModal.test.js
@@ -0,0 +1,129 @@
+import React from 'react';
+import {fireEvent, render, screen} from '@testing-library/react';
+import {MemoryRouter} from 'react-router';
+import {SearchFilterButton} from './Files';
+import {QueryContext, ThemeContext} from '../contexts/contexts';
+
+// Render SearchFilterButton with a controlled QueryContext so the search hooks
+// (useSearchFilter/useSearchOrder/useSearchDate/useSearch) can read the URL
+// query params without a real router/provider.
+function renderButton(props = {}, initialParams = {}, onUpdateQuery = null) {
+ const params = new URLSearchParams(initialParams);
+
+ function Wrapper({children}) {
+ const [searchParams, setSearchParams] = React.useState(params);
+ const updateQuery = (newParams) => {
+ if (onUpdateQuery) {
+ onUpdateQuery(newParams);
+ }
+ const next = new URLSearchParams(searchParams.toString());
+ Object.entries(newParams).forEach(([k, v]) => {
+ if (v === null || v === undefined) {
+ next.delete(k);
+ } else if (Array.isArray(v)) {
+ next.delete(k);
+ v.forEach(i => next.append(k, String(i)));
+ } else {
+ next.set(k, String(v));
+ }
+ });
+ setSearchParams(next);
+ };
+ const theme = {
+ i: {}, s: {}, t: {}, theme: 'light', inverted: false, setInverted: () => {},
+ };
+ return (
+
+
+ '/'}}>
+ {children}
+
+
+
+ );
+ }
+
+ return render();
+}
+
+const videoOrders = [
+ {value: 'published_datetime', text: 'Published Date'},
+ {value: 'size', text: 'Size'},
+];
+
+describe('SearchFilterButton', () => {
+ it('renders a Filter button with no count badge when nothing is active', () => {
+ renderButton({sorts: videoOrders});
+ expect(screen.getByText('Filter')).toBeInTheDocument();
+ // No active filters -> no numeric badge.
+ expect(screen.queryByText('1')).toBeNull();
+ });
+
+ it('shows a count badge summing the active filters', () => {
+ // order + filter + tag => count of 3.
+ renderButton(
+ {sorts: videoOrders, fileFilterOptions: [{value: 'video', text: 'Video'}]},
+ {order: '-published_datetime', filter: 'video', tag: 'Food'},
+ );
+ expect(screen.getByText('3')).toBeInTheDocument();
+ });
+
+ it('opens the modal and only shows the sections enabled by props', () => {
+ renderButton({sorts: videoOrders});
+ fireEvent.click(screen.getByText('Filter'));
+
+ // Enabled sections.
+ expect(screen.getByText('Sort By')).toBeInTheDocument();
+ expect(screen.getByText('Tags')).toBeInTheDocument();
+ expect(screen.getByText('Results Per Page')).toBeInTheDocument();
+
+ // Disabled sections (no fileFilterOptions, showDates defaults false). Assert on the date range
+ // 'From Year' label, since the 'Published Date' header text collides with a sort option label.
+ expect(screen.queryByText('File Type')).toBeNull();
+ expect(screen.queryByText('From Year')).toBeNull();
+ });
+
+ it('applies changes only on close, not as fields change', () => {
+ const spy = jest.fn();
+ renderButton({sorts: videoOrders}, {}, spy);
+ fireEvent.click(screen.getByText('Filter'));
+
+ // Changing the sort field updates the draft but must NOT perform the search yet.
+ fireEvent.click(screen.getByText('Size'));
+ expect(spy).not.toHaveBeenCalled();
+
+ // Closing via Done commits the draft in a single query update.
+ fireEvent.click(screen.getByText('Done'));
+ expect(spy).toHaveBeenCalledTimes(1);
+ expect(spy.mock.calls[0][0]).toMatchObject({order: '-size'});
+ });
+
+ it('Clear All resets results-per-page to the default on close', () => {
+ const spy = jest.fn();
+ renderButton({sorts: videoOrders}, {l: '96'}, spy);
+ fireEvent.click(screen.getByText('Filter'));
+ fireEvent.click(screen.getByText('Clear All'));
+ fireEvent.click(screen.getByText('Done'));
+ expect(spy).toHaveBeenCalledTimes(1);
+ expect(spy.mock.calls[0][0]).toMatchObject({l: 24});
+ });
+
+ it('does not update the query when closed without changes', () => {
+ const spy = jest.fn();
+ renderButton({sorts: videoOrders}, {}, spy);
+ fireEvent.click(screen.getByText('Filter'));
+ fireEvent.click(screen.getByText('Done'));
+ expect(spy).not.toHaveBeenCalled();
+ });
+
+ it('shows the File Type section when fileFilterOptions are provided', () => {
+ renderButton({
+ sorts: videoOrders,
+ fileFilterOptions: [{value: 'pdf', text: 'PDF'}, {value: 'epub', text: 'EPUB'}],
+ });
+ fireEvent.click(screen.getByText('Filter'));
+ expect(screen.getByText('File Type')).toBeInTheDocument();
+ expect(screen.getByText('PDF')).toBeInTheDocument();
+ expect(screen.getByText('EPUB')).toBeInTheDocument();
+ });
+});
diff --git a/app/src/components/Videos.js b/app/src/components/Videos.js
index 26ce1285..cfc77ba1 100644
--- a/app/src/components/Videos.js
+++ b/app/src/components/Videos.js
@@ -19,9 +19,7 @@ import {
PageContainer,
PreviewLink,
scrollToTop,
- SearchInput,
secondsToFullDuration,
- SortButton,
TabLinks,
textEllipsis,
useTitle,
@@ -47,7 +45,7 @@ import {
TableCell
} from "semantic-ui-react";
import {useChannel, useSearchOrder, useSearchVideos, useVideo, useVideoStatistics} from "../hooks/customHooks";
-import {FileRowTagIcon, FilesView} from "./Files";
+import {FileRowTagIcon, FilesView, SearchControlBar} from "./Files";
import Grid from "semantic-ui-react/dist/commonjs/collections/Grid";
import Icon from "semantic-ui-react/dist/commonjs/elements/Icon";
import {Button, Card, Header, Loader, Modal, Placeholder, Popup, Segment, Statistic} from "./Theme";
@@ -65,7 +63,7 @@ import {
updateVideoDownloaderConfig,
uploadCookies,
} from "../api";
-import {Media, QueryContext, ThemeContext} from "../contexts/contexts";
+import {QueryContext, ThemeContext} from "../contexts/contexts";
import _ from "lodash";
import {defaultFileOrder, defaultSearchOrder} from "./Vars";
import {InputForm, ToggleForm, useForm} from "../hooks/useForm";
@@ -219,7 +217,7 @@ export function VideosPage() {
/>
;
- const {body, paginator, viewButton, limitDropdown, tagQuerySelector} = FilesView(
+ const {body, paginator, viewButton} = FilesView(
{
files: videos,
activePage: activePage,
@@ -233,42 +231,16 @@ export function VideosPage() {
},
);
- const [localSearchStr, setLocalSearchStr] = React.useState(searchStr || '');
- const searchInput = setLocalSearchStr(null)}
- onSubmit={setSearchStr}
- placeholder='Search Videos...'
- inputRef={searchInputRef}
- />;
-
return <>
{header}
-
-
-
- {viewButton}
- {limitDropdown}
- {tagQuerySelector}
-
-
-
- {searchInput}
-
-
-
-
-
-
- {viewButton}
- {limitDropdown}
- {tagQuerySelector}
-
- {searchInput}
-
-
-
+
{body}
{paginator}