Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rework main navbar #1769

Merged
merged 10 commits into from
Nov 4, 2021
Merged
1 change: 1 addition & 0 deletions ui/v2.5/src/components/Changelog/versions/v0110.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
* Added interface options to disable creating performers/studios/tags from dropdown selectors. ([#1814](https://github.com/stashapp/stash/pull/1814))

### 🎨 Improvements
* Reworked main navbar and positioned at bottom for mobile devices. ([#1769](https://github.com/stashapp/stash/pull/1769))
* Show files being deleted in the Delete dialogs. ([#1852](https://github.com/stashapp/stash/pull/1852))
* Added specific page titles. ([#1831](https://github.com/stashapp/stash/pull/1831))
* Added es-ES language option. ([#1886](https://github.com/stashapp/stash/pull/1886))
Expand Down
250 changes: 142 additions & 108 deletions ui/v2.5/src/components/MainNavbar.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect, useRef, useState } from "react";
import React, { useEffect, useRef, useState, useCallback } from "react";
import {
defineMessages,
FormattedMessage,
Expand All @@ -21,6 +21,8 @@ interface IMenuItem {
message: MessageDescriptor;
href: string;
icon: IconName;
hotkey: string;
userCreatable?: boolean;
}

const messages = defineMessages({
Expand Down Expand Up @@ -72,51 +74,68 @@ const allMenuItems: IMenuItem[] = [
message: messages.scenes,
href: "/scenes",
icon: "play-circle",
hotkey: "g s",
},
{
name: "images",
message: messages.images,
href: "/images",
icon: "image",
hotkey: "g i",
},
{
name: "movies",
message: messages.movies,
href: "/movies",
icon: "film",
hotkey: "g v",
userCreatable: true,
},
{
name: "markers",
message: messages.markers,
href: "/scenes/markers",
icon: "map-marker-alt",
hotkey: "g k",
},
{
name: "galleries",
message: messages.galleries,
href: "/galleries",
icon: "images",
hotkey: "g l",
userCreatable: true,
},
{
name: "performers",
message: messages.performers,
href: "/performers",
icon: "user",
hotkey: "g p",
userCreatable: true,
},
{
name: "studios",
message: messages.studios,
href: "/studios",
icon: "video",
hotkey: "g u",
userCreatable: true,
},
{
name: "tags",
message: messages.tags,
href: "/tags",
icon: "tag",
hotkey: "g t",
userCreatable: true,
},
];

const newPathsList = allMenuItems
.filter((item) => item.userCreatable)
.map((item) => item.href);

export const MainNavbar: React.FC = () => {
const history = useHistory();
const location = useLocation();
Expand Down Expand Up @@ -144,15 +163,18 @@ export const MainNavbar: React.FC = () => {
const navbarRef = useRef<any>();
const intl = useIntl();

const maybeCollapse = (event: Event) => {
if (
navbarRef.current &&
event.target instanceof Node &&
!navbarRef.current.contains(event.target)
) {
setExpanded(false);
}
};
const maybeCollapse = useCallback(
(event: Event) => {
if (
navbarRef.current &&
event.target instanceof Node &&
!navbarRef.current.contains(event.target)
) {
setExpanded(false);
}
},
[setExpanded]
);

useEffect(() => {
if (expanded) {
Expand All @@ -163,65 +185,38 @@ export const MainNavbar: React.FC = () => {
document.removeEventListener("click", maybeCollapse);
document.removeEventListener("touchstart", maybeCollapse);
};
}, [expanded]);
}, [expanded, maybeCollapse]);

function goto(page: string) {
history.push(page);
if (document.activeElement instanceof HTMLElement) {
document.activeElement.blur();
}
}
const goto = useCallback(
(page: string) => {
history.push(page);
if (document.activeElement instanceof HTMLElement) {
document.activeElement.blur();
}
},
[history]
);

const newPath =
location.pathname === "/performers"
? "/performers/new"
: location.pathname === "/studios"
? "/studios/new"
: location.pathname === "/movies"
? "/movies/new"
: location.pathname === "/tags"
? "/tags/new"
: location.pathname === "/galleries"
? "/galleries/new"
: null;
const newButton =
newPath === null ? (
""
) : (
<Link to={newPath}>
<Button variant="primary">
<FormattedMessage id="new" defaultMessage="New" />
</Button>
</Link>
);
const { pathname } = location;
const newPath = newPathsList.includes(pathname) ? `${pathname}/new` : null;

// set up hotkeys
useEffect(() => {
Mousetrap.bind("?", () => setShowManual(!showManual));
Mousetrap.bind("g s", () => goto("/scenes"));
Mousetrap.bind("g i", () => goto("/images"));
Mousetrap.bind("g v", () => goto("/movies"));
Mousetrap.bind("g k", () => goto("/scenes/markers"));
Mousetrap.bind("g l", () => goto("/galleries"));
Mousetrap.bind("g p", () => goto("/performers"));
Mousetrap.bind("g u", () => goto("/studios"));
Mousetrap.bind("g t", () => goto("/tags"));
Mousetrap.bind("g z", () => goto("/settings"));

menuItems.forEach((item) =>
Mousetrap.bind(item.hotkey, () => goto(item.href))
);

if (newPath) {
Mousetrap.bind("n", () => history.push(newPath));
}

return () => {
Mousetrap.unbind("?");
Mousetrap.unbind("g s");
Mousetrap.unbind("g v");
Mousetrap.unbind("g k");
Mousetrap.unbind("g l");
Mousetrap.unbind("g p");
Mousetrap.unbind("g u");
Mousetrap.unbind("g t");
Mousetrap.unbind("g z");
menuItems.forEach((item) => Mousetrap.unbind(item.hotkey));

if (newPath) {
Mousetrap.unbind("n");
Expand All @@ -232,13 +227,60 @@ export const MainNavbar: React.FC = () => {
function maybeRenderLogout() {
if (SessionUtils.isLoggedIn()) {
return (
<Button className="minimal logout-button" href="/logout">
<Button
className="minimal logout-button d-flex align-items-center"
href="/logout"
title="Log out"
>
<Icon icon="sign-out-alt" />
</Button>
);
}
}

const handleDismiss = useCallback(() => setExpanded(false), [setExpanded]);

function renderUtilityButtons() {
return (
<>
<Nav.Link
className="nav-utility"
href="https://opencollective.com/stashapp"
target="_blank"
onClick={handleDismiss}
>
<Button className="minimal donate" title="Donate">
<Icon icon="heart" />
<span className="d-none d-sm-inline">
{intl.formatMessage(messages.donate)}
</span>
</Button>
</Nav.Link>
<NavLink
className="nav-utility"
exact
to="/settings"
onClick={handleDismiss}
>
<Button
className="minimal d-flex align-items-center h-100"
title="Settings"
>
<Icon icon="cog" />
</Button>
</NavLink>
<Button
className="nav-utility minimal"
onClick={() => setShowManual(true)}
title="Help"
>
<Icon icon="question-circle" />
</Button>
{maybeRenderLogout()}
</>
);
}

return (
<>
<Manual show={showManual} onClose={() => setShowManual(false)} />
Expand All @@ -253,62 +295,54 @@ export const MainNavbar: React.FC = () => {
onToggle={setExpanded}
ref={navbarRef}
>
<Navbar.Brand
as="div"
className="order-1 order-md-0"
onClick={() => setExpanded(false)}
>
<Link to="/">
<Button className="minimal brand-link d-none d-md-inline-block">
Stash
</Button>
<Button className="minimal brand-icon d-inline d-md-none">
<img src="favicon.ico" alt="" />
</Button>
</Link>
</Navbar.Brand>
<Navbar.Toggle className="order-0" />
<Navbar.Collapse className="order-3 order-md-1">
<Navbar.Collapse className="bg-dark order-sm-1">
<Fade in={!loading}>
<Nav className="mr-md-auto">
{menuItems.map((i) => (
<Nav.Link eventKey={i.href} as="div" key={i.href}>
<LinkContainer activeClassName="active" exact to={i.href}>
<Button className="minimal w-100">
<Icon icon={i.icon} />
<span>{intl.formatMessage(i.message)}</span>
</Button>
</LinkContainer>
</Nav.Link>
))}
</Nav>
<>
<Nav>
{menuItems.map(({ href, icon, message }) => (
<Nav.Link
eventKey={href}
as="div"
key={href}
className="col-4 col-sm-3 col-md-2 col-lg-auto"
>
<LinkContainer activeClassName="active" exact to={href}>
<Button className="minimal p-4 p-xl-2 d-flex d-xl-inline-block flex-column justify-content-between align-items-center">
<Icon
{...{ icon }}
className="nav-menu-icon d-block d-xl-inline mb-2 mb-xl-0"
/>
<span>{intl.formatMessage(message)}</span>
</Button>
</LinkContainer>
</Nav.Link>
))}
</Nav>
<Nav>{renderUtilityButtons()}</Nav>
</>
</Fade>
</Navbar.Collapse>
<Nav className="order-2 flex-row">
<div>{newButton}</div>
<Nav.Link
href="https://opencollective.com/stashapp"
target="_blank"
onClick={() => setExpanded(false)}
>
<Button className="minimal donate" title="Donate">
<Icon icon="heart" />
<span>{intl.formatMessage(messages.donate)}</span>
</Button>
</Nav.Link>
<NavLink exact to="/settings" onClick={() => setExpanded(false)}>
<Button className="minimal settings-button" title="Settings">
<Icon icon="cog" />
</Button>
</NavLink>
<Button
className="minimal help-button"
onClick={() => setShowManual(true)}
title="Help"
>
<Icon icon="question-circle" />
</Button>
{maybeRenderLogout()}

<Navbar.Brand as="div" onClick={handleDismiss}>
<Link to="/">
<Button className="minimal brand-link d-inline-block">Stash</Button>
</Link>
</Navbar.Brand>

<Nav className="navbar-buttons flex-row ml-auto order-xl-2">
{!!newPath && (
<div className="mr-2">
<Link to={newPath}>
<Button variant="primary">
<FormattedMessage id="new" defaultMessage="New" />
</Button>
</Link>
</div>
)}
{renderUtilityButtons()}
<Navbar.Toggle className="nav-menu-toggle ml-sm-2">
<Icon icon={expanded ? "times" : "bars"} />
</Navbar.Toggle>
</Nav>
</Navbar>
</>
Expand Down
Loading