mirror of
https://github.com/stashapp/stash.git
synced 2025-12-06 08:26:00 +01:00
Save sidebar state (#6217)
* Save sidebar section open state in browser history state This means that state is saved when going back, but not when navigating to the scenes page from elsewhere.
This commit is contained in:
parent
299e1ac1f9
commit
336fa3b70e
10 changed files with 182 additions and 96 deletions
|
|
@ -54,6 +54,7 @@ interface ISidebarFilter {
|
|||
option: CriterionOption;
|
||||
filter: ListFilterModel;
|
||||
setFilter: (f: ListFilterModel) => void;
|
||||
sectionID?: string;
|
||||
}
|
||||
|
||||
export const SidebarBooleanFilter: React.FC<ISidebarFilter> = ({
|
||||
|
|
@ -61,6 +62,7 @@ export const SidebarBooleanFilter: React.FC<ISidebarFilter> = ({
|
|||
option,
|
||||
filter,
|
||||
setFilter,
|
||||
sectionID,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
|
|
@ -127,6 +129,7 @@ export const SidebarBooleanFilter: React.FC<ISidebarFilter> = ({
|
|||
onUnselect={onUnselect}
|
||||
selected={selected}
|
||||
singleValue
|
||||
sectionID={sectionID}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import ScreenUtils from "src/utils/screen";
|
|||
import Mousetrap from "mousetrap";
|
||||
import { Button } from "react-bootstrap";
|
||||
|
||||
const savedFiltersSectionID = "saved-filters";
|
||||
|
||||
export const FilteredSidebarHeader: React.FC<{
|
||||
sidebarOpen: boolean;
|
||||
showEditFilter: () => void;
|
||||
|
|
@ -60,6 +62,7 @@ export const FilteredSidebarHeader: React.FC<{
|
|||
<SidebarSection
|
||||
className="sidebar-saved-filters"
|
||||
text={<FormattedMessage id="search_filter.saved_filters" />}
|
||||
sectionID={savedFiltersSectionID}
|
||||
>
|
||||
<SidebarSavedFilterList
|
||||
filter={filter}
|
||||
|
|
|
|||
|
|
@ -110,7 +110,8 @@ export const SidebarPerformersFilter: React.FC<{
|
|||
filter: ListFilterModel;
|
||||
setFilter: (f: ListFilterModel) => void;
|
||||
filterHook?: (f: ListFilterModel) => ListFilterModel;
|
||||
}> = ({ title, option, filter, setFilter, filterHook }) => {
|
||||
sectionID?: string;
|
||||
}> = ({ title, option, filter, setFilter, filterHook, sectionID }) => {
|
||||
const state = useLabeledIdFilterState({
|
||||
filter,
|
||||
setFilter,
|
||||
|
|
@ -119,7 +120,7 @@ export const SidebarPerformersFilter: React.FC<{
|
|||
useQuery: usePerformerQueryFilter,
|
||||
});
|
||||
|
||||
return <SidebarListFilter {...state} title={title} />;
|
||||
return <SidebarListFilter {...state} title={title} sectionID={sectionID} />;
|
||||
};
|
||||
|
||||
export default PerformersFilter;
|
||||
|
|
|
|||
|
|
@ -77,6 +77,7 @@ interface ISidebarFilter {
|
|||
option: CriterionOption;
|
||||
filter: ListFilterModel;
|
||||
setFilter: (f: ListFilterModel) => void;
|
||||
sectionID?: string;
|
||||
}
|
||||
|
||||
const any = "any";
|
||||
|
|
@ -87,6 +88,7 @@ export const SidebarRatingFilter: React.FC<ISidebarFilter> = ({
|
|||
option,
|
||||
filter,
|
||||
setFilter,
|
||||
sectionID,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
|
|
@ -199,6 +201,7 @@ export const SidebarRatingFilter: React.FC<ISidebarFilter> = ({
|
|||
singleValue
|
||||
preCandidates={ratingValue === null ? ratingStars : undefined}
|
||||
preSelected={ratingValue !== null ? ratingStars : undefined}
|
||||
sectionID={sectionID}
|
||||
/>
|
||||
<div></div>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -275,7 +275,8 @@ export const SidebarListFilter: React.FC<{
|
|||
postSelected?: React.ReactNode;
|
||||
preCandidates?: React.ReactNode;
|
||||
postCandidates?: React.ReactNode;
|
||||
onOpen?: () => void;
|
||||
// used to store open/closed state in SidebarStateContext
|
||||
sectionID?: string;
|
||||
}> = ({
|
||||
title,
|
||||
selected,
|
||||
|
|
@ -291,7 +292,7 @@ export const SidebarListFilter: React.FC<{
|
|||
postCandidates,
|
||||
preSelected,
|
||||
postSelected,
|
||||
onOpen,
|
||||
sectionID,
|
||||
}) => {
|
||||
// TODO - sort items?
|
||||
|
||||
|
|
@ -325,6 +326,7 @@ export const SidebarListFilter: React.FC<{
|
|||
<SidebarSection
|
||||
className="sidebar-list-filter"
|
||||
text={title}
|
||||
sectionID={sectionID}
|
||||
outsideCollapse={
|
||||
<>
|
||||
{preSelected ? <div className="extra">{preSelected}</div> : null}
|
||||
|
|
@ -342,7 +344,6 @@ export const SidebarListFilter: React.FC<{
|
|||
{postSelected ? <div className="extra">{postSelected}</div> : null}
|
||||
</>
|
||||
}
|
||||
onOpen={onOpen}
|
||||
>
|
||||
{preCandidates ? <div className="extra">{preCandidates}</div> : null}
|
||||
<CandidateList
|
||||
|
|
|
|||
|
|
@ -98,7 +98,8 @@ export const SidebarStudiosFilter: React.FC<{
|
|||
filter: ListFilterModel;
|
||||
setFilter: (f: ListFilterModel) => void;
|
||||
filterHook?: (f: ListFilterModel) => ListFilterModel;
|
||||
}> = ({ title, option, filter, setFilter, filterHook }) => {
|
||||
sectionID?: string;
|
||||
}> = ({ title, option, filter, setFilter, filterHook, sectionID }) => {
|
||||
const state = useLabeledIdFilterState({
|
||||
filter,
|
||||
setFilter,
|
||||
|
|
@ -110,7 +111,7 @@ export const SidebarStudiosFilter: React.FC<{
|
|||
includeSubMessageID: "subsidiary_studios",
|
||||
});
|
||||
|
||||
return <SidebarListFilter {...state} title={title} />;
|
||||
return <SidebarListFilter {...state} title={title} sectionID={sectionID} />;
|
||||
};
|
||||
|
||||
export default StudiosFilter;
|
||||
|
|
|
|||
|
|
@ -103,7 +103,8 @@ export const SidebarTagsFilter: React.FC<{
|
|||
filter: ListFilterModel;
|
||||
setFilter: (f: ListFilterModel) => void;
|
||||
filterHook?: (f: ListFilterModel) => ListFilterModel;
|
||||
}> = ({ title, option, filter, setFilter, filterHook }) => {
|
||||
sectionID?: string;
|
||||
}> = ({ title, option, filter, setFilter, filterHook, sectionID }) => {
|
||||
const state = useLabeledIdFilterState({
|
||||
filter,
|
||||
setFilter,
|
||||
|
|
@ -114,7 +115,7 @@ export const SidebarTagsFilter: React.FC<{
|
|||
includeSubMessageID: "sub_tags",
|
||||
});
|
||||
|
||||
return <SidebarListFilter {...state} title={title} />;
|
||||
return <SidebarListFilter {...state} title={title} sectionID={sectionID} />;
|
||||
};
|
||||
|
||||
export default TagsFilter;
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ import {
|
|||
Sidebar,
|
||||
SidebarPane,
|
||||
SidebarPaneContent,
|
||||
SidebarStateContext,
|
||||
useSidebarState,
|
||||
} from "../Shared/Sidebar";
|
||||
import { SidebarPerformersFilter } from "../List/Filters/PerformersFilter";
|
||||
|
|
@ -290,6 +291,7 @@ const SidebarContent: React.FC<{
|
|||
filter={filter}
|
||||
setFilter={setFilter}
|
||||
filterHook={filterHook}
|
||||
sectionID="studios"
|
||||
/>
|
||||
)}
|
||||
<SidebarPerformersFilter
|
||||
|
|
@ -299,6 +301,7 @@ const SidebarContent: React.FC<{
|
|||
filter={filter}
|
||||
setFilter={setFilter}
|
||||
filterHook={filterHook}
|
||||
sectionID="performers"
|
||||
/>
|
||||
<SidebarTagsFilter
|
||||
title={<FormattedMessage id="tags" />}
|
||||
|
|
@ -307,6 +310,7 @@ const SidebarContent: React.FC<{
|
|||
filter={filter}
|
||||
setFilter={setFilter}
|
||||
filterHook={filterHook}
|
||||
sectionID="tags"
|
||||
/>
|
||||
<SidebarRatingFilter
|
||||
title={<FormattedMessage id="rating" />}
|
||||
|
|
@ -314,6 +318,7 @@ const SidebarContent: React.FC<{
|
|||
option={RatingCriterionOption}
|
||||
filter={filter}
|
||||
setFilter={setFilter}
|
||||
sectionID="rating"
|
||||
/>
|
||||
<SidebarBooleanFilter
|
||||
title={<FormattedMessage id="organized" />}
|
||||
|
|
@ -321,6 +326,7 @@ const SidebarContent: React.FC<{
|
|||
option={OrganizedCriterionOption}
|
||||
filter={filter}
|
||||
setFilter={setFilter}
|
||||
sectionID="organized"
|
||||
/>
|
||||
</ScenesFilterSidebarSections>
|
||||
|
||||
|
|
@ -447,6 +453,8 @@ export const FilteredSceneList = (props: IFilteredScenes) => {
|
|||
showSidebar,
|
||||
setShowSidebar,
|
||||
loading: sidebarStateLoading,
|
||||
sectionOpen,
|
||||
setSectionOpen,
|
||||
} = useSidebarState(view);
|
||||
|
||||
const { filterState, queryResult, modalState, listSelect, showEditFilter } =
|
||||
|
|
@ -695,6 +703,7 @@ export const FilteredSceneList = (props: IFilteredScenes) => {
|
|||
>
|
||||
{modal}
|
||||
|
||||
<SidebarStateContext.Provider value={{ sectionOpen, setSectionOpen }}>
|
||||
<SidebarPane hideSidebar={!showSidebar}>
|
||||
<Sidebar hide={!showSidebar} onHide={() => setShowSidebar(false)}>
|
||||
<SidebarContent
|
||||
|
|
@ -727,7 +736,9 @@ export const FilteredSceneList = (props: IFilteredScenes) => {
|
|||
setShowSidebar(true);
|
||||
setSearchFocus(true);
|
||||
}}
|
||||
onRemoveSearchTerm={() => setFilter(filter.clearSearchTerm())}
|
||||
onRemoveSearchTerm={() =>
|
||||
setFilter(filter.clearSearchTerm())
|
||||
}
|
||||
view={view}
|
||||
/>
|
||||
}
|
||||
|
|
@ -775,6 +786,7 @@ export const FilteredSceneList = (props: IFilteredScenes) => {
|
|||
)}
|
||||
</SidebarPaneContent>
|
||||
</SidebarPane>
|
||||
</SidebarStateContext.Provider>
|
||||
</div>
|
||||
</TaggerContext>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import {
|
|||
faChevronRight,
|
||||
faChevronUp,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import React, { useState } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Button, Collapse, CollapseProps } from "react-bootstrap";
|
||||
import { Icon } from "./Icon";
|
||||
|
||||
|
|
@ -12,21 +12,26 @@ interface IProps {
|
|||
text: React.ReactNode;
|
||||
collapseProps?: Partial<CollapseProps>;
|
||||
outsideCollapse?: React.ReactNode;
|
||||
onOpen?: () => void;
|
||||
onOpenChanged?: (o: boolean) => void;
|
||||
open?: boolean;
|
||||
}
|
||||
|
||||
export const CollapseButton: React.FC<React.PropsWithChildren<IProps>> = (
|
||||
props: React.PropsWithChildren<IProps>
|
||||
) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [open, setOpen] = useState(props.open ?? false);
|
||||
|
||||
function toggleOpen() {
|
||||
const nv = !open;
|
||||
setOpen(nv);
|
||||
if (props.onOpen && nv) {
|
||||
props.onOpen();
|
||||
props.onOpenChanged?.(nv);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (props.open !== undefined) {
|
||||
setOpen(props.open);
|
||||
}
|
||||
}, [props.open]);
|
||||
|
||||
return (
|
||||
<div className={props.className}>
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@ import { Button, CollapseProps } from "react-bootstrap";
|
|||
import { useIntl } from "react-intl";
|
||||
import { Icon } from "./Icon";
|
||||
import { faSliders } from "@fortawesome/free-solid-svg-icons";
|
||||
import { useHistory } from "react-router-dom";
|
||||
|
||||
export type SidebarSectionStates = Record<string, boolean>;
|
||||
|
||||
// this needs to correspond to the CSS media query that overlaps the sidebar over content
|
||||
const fixedSidebarMediaQuery = "only screen and (max-width: 767px)";
|
||||
|
|
@ -61,14 +64,35 @@ export const SidebarPaneContent: React.FC = ({ children }) => {
|
|||
return <div className="sidebar-pane-content">{children}</div>;
|
||||
};
|
||||
|
||||
interface IContext {
|
||||
sectionOpen: SidebarSectionStates;
|
||||
setSectionOpen: (section: string, open: boolean) => void;
|
||||
}
|
||||
|
||||
export const SidebarStateContext = React.createContext<IContext | null>(null);
|
||||
|
||||
export const SidebarSection: React.FC<
|
||||
PropsWithChildren<{
|
||||
text: React.ReactNode;
|
||||
className?: string;
|
||||
outsideCollapse?: React.ReactNode;
|
||||
onOpen?: () => void;
|
||||
// used to store open/closed state in SidebarStateContext
|
||||
sectionID?: string;
|
||||
}>
|
||||
> = ({ className = "", text, outsideCollapse, onOpen, children }) => {
|
||||
> = ({ className = "", text, outsideCollapse, sectionID = "", children }) => {
|
||||
// this is optional
|
||||
const contextState = React.useContext(SidebarStateContext);
|
||||
const openState =
|
||||
!contextState || !sectionID
|
||||
? undefined
|
||||
: contextState.sectionOpen[sectionID] ?? undefined;
|
||||
|
||||
function onOpenInternal(open: boolean) {
|
||||
if (contextState && sectionID) {
|
||||
contextState.setSectionOpen(sectionID, open);
|
||||
}
|
||||
}
|
||||
|
||||
const collapseProps: Partial<CollapseProps> = {
|
||||
mountOnEnter: true,
|
||||
unmountOnExit: true,
|
||||
|
|
@ -79,7 +103,8 @@ export const SidebarSection: React.FC<
|
|||
collapseProps={collapseProps}
|
||||
text={text}
|
||||
outsideCollapse={outsideCollapse}
|
||||
onOpen={onOpen}
|
||||
onOpenChanged={onOpenInternal}
|
||||
open={openState}
|
||||
>
|
||||
{children}
|
||||
</CollapseButton>
|
||||
|
|
@ -110,6 +135,7 @@ export function defaultShowSidebar() {
|
|||
export function useSidebarState(view?: View) {
|
||||
const [interfaceLocalForage, setInterfaceLocalForage] =
|
||||
useInterfaceLocalForage();
|
||||
const history = useHistory();
|
||||
|
||||
const { data: interfaceLocalForageData, loading } = interfaceLocalForage;
|
||||
|
||||
|
|
@ -118,6 +144,7 @@ export function useSidebarState(view?: View) {
|
|||
}, [view, interfaceLocalForageData]);
|
||||
|
||||
const [showSidebar, setShowSidebar] = useState<boolean>();
|
||||
const [sectionOpen, setSectionOpen] = useState<SidebarSectionStates>();
|
||||
|
||||
// set initial state once loading is done
|
||||
useEffect(() => {
|
||||
|
|
@ -132,7 +159,17 @@ export function useSidebarState(view?: View) {
|
|||
|
||||
// only show sidebar by default on large screens
|
||||
setShowSidebar(!!viewConfig.showSidebar && defaultShowSidebar());
|
||||
}, [view, loading, showSidebar, viewConfig.showSidebar]);
|
||||
setSectionOpen(
|
||||
(history.location.state as { sectionOpen?: SidebarSectionStates })
|
||||
?.sectionOpen || {}
|
||||
);
|
||||
}, [
|
||||
view,
|
||||
loading,
|
||||
showSidebar,
|
||||
viewConfig.showSidebar,
|
||||
history.location.state,
|
||||
]);
|
||||
|
||||
const onSetShowSidebar = useCallback(
|
||||
(show: boolean | ((prevState: boolean | undefined) => boolean)) => {
|
||||
|
|
@ -154,9 +191,28 @@ export function useSidebarState(view?: View) {
|
|||
[showSidebar, setInterfaceLocalForage, view, viewConfig]
|
||||
);
|
||||
|
||||
const onSetSectionOpen = useCallback(
|
||||
(section: string, open: boolean) => {
|
||||
const newSectionOpen = { ...sectionOpen, [section]: open };
|
||||
setSectionOpen(newSectionOpen);
|
||||
if (view === undefined) return;
|
||||
|
||||
history.replace({
|
||||
...history.location,
|
||||
state: {
|
||||
...(history.location.state as {}),
|
||||
sectionOpen: newSectionOpen,
|
||||
},
|
||||
});
|
||||
},
|
||||
[sectionOpen, view, history]
|
||||
);
|
||||
|
||||
return {
|
||||
showSidebar: showSidebar ?? defaultShowSidebar(),
|
||||
sectionOpen: sectionOpen || {},
|
||||
setShowSidebar: onSetShowSidebar,
|
||||
setSectionOpen: onSetSectionOpen,
|
||||
loading: showSidebar === undefined,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue