Rework main navbar (#1769)

* Fix responsive layout
* Refactor MainNavbar
* Stick the navbar to the bottom on mobile
* Fix menu item icon-text vertical alignment

Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
This commit is contained in:
liquid-flow
2021-11-04 14:12:48 +07:00
committed by GitHub
parent 602183cca9
commit 3671388b8d
3 changed files with 247 additions and 128 deletions

View File

@@ -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))

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useRef, useState } from "react";
import React, { useEffect, useRef, useState, useCallback } from "react";
import {
defineMessages,
FormattedMessage,
@@ -21,6 +21,8 @@ interface IMenuItem {
message: MessageDescriptor;
href: string;
icon: IconName;
hotkey: string;
userCreatable?: boolean;
}
const messages = defineMessages({
@@ -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();
@@ -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) {
@@ -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");
@@ -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)} />
@@ -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>
</>

View File

@@ -38,6 +38,12 @@ body {
-moz-osx-font-smoothing: grayscale;
margin: 0;
padding: 4rem 0 0 0;
@include media-breakpoint-down(xs) {
@media (orientation: portrait) {
padding: 1rem 0 5rem;
}
}
}
code,
@@ -464,44 +470,122 @@ div.dropdown-menu {
}
.top-nav {
justify-content: start;
padding: 0.25rem 1rem;
@include media-breakpoint-down(xs) {
padding: 0.25rem 2rem;
@media (orientation: portrait) {
bottom: 0;
top: auto;
}
}
.navbar-toggler {
padding: 0.5em 0;
text-align: center;
width: 3em;
svg {
margin: auto;
}
}
.navbar-collapse {
justify-content: space-between;
max-height: calc(100vh - 4rem);
@include media-breakpoint-down(xs) {
@media (orientation: landscape) {
overflow-y: scroll;
}
}
.navbar-nav {
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
padding-bottom: 0.5rem;
@include media-breakpoint-up(xl) {
flex-wrap: nowrap;
padding-bottom: 0;
}
&:last-child {
display: none;
}
@include media-breakpoint-down(xs) {
&:last-child {
display: flex;
min-height: 3rem;
}
}
}
}
.navbar-buttons .btn {
align-items: center;
display: flex;
}
@include media-breakpoint-down(xs) {
.navbar-buttons .nav-utility {
display: none;
}
}
.nav-link {
padding: 0;
}
.fa-icon {
margin-left: 0;
@include media-breakpoint-down(xs) {
margin: 0;
}
&.nav-menu-icon {
@include media-breakpoint-down(lg) {
height: 100%;
max-height: min(10vw, 55px);
width: 100%;
}
}
}
.btn {
white-space: nowrap;
}
@media (max-width: 576px) {
@include media-breakpoint-down(lg) {
.navbar-brand {
margin-left: -8px;
}
.navbar-buttons {
margin: 0 -8px;
}
.btn {
padding: 6px;
}
.settings-button {
padding-left: 1rem;
padding-right: 1rem;
}
.donate {
span {
display: none;
}
svg {
margin: 0;
}
padding: 6px 12px;
}
}
}
.donate svg {
color: #ff7373;
.donate {
align-items: center;
display: flex;
height: 100%;
svg {
color: #ff7373;
@include media-breakpoint-down(xs) {
margin: 0;
}
}
}
.error-message {