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

In-app help manual #628

Merged
merged 9 commits into from
Jun 23, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ui/v2.5/src/components/Changelog/Version.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ const Version: React.FC<IVersionProps> = ({
</Card.Header>
<Card.Body>
<Collapse in={open}>
<div className="changelog-version-body">{children}</div>
<div className="changelog-version-body markdown">{children}</div>
</Collapse>
</Card.Body>
</Card>
Expand Down
1 change: 1 addition & 0 deletions ui/v2.5/src/components/Changelog/versions/v030.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import ReactMarkdown from "react-markdown";

const markup = `
### ✨ New Features
* Add in-app help manual.
* Add support for custom served folders.
* Add support for parent/child studios.

Expand Down
150 changes: 150 additions & 0 deletions ui/v2.5/src/components/Help/Manual.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import React, { useState } from "react";
import { Modal, Container, Row, Col, Nav, Tab } from "react-bootstrap";
import Introduction from "src/docs/en/Introduction.md";
import Tasks from "src/docs/en/Tasks.md";
import AutoTagging from "src/docs/en/AutoTagging.md";
import JSONSpec from "src/docs/en/JSONSpec.md";
import Configuration from "src/docs/en/Configuration.md";
import Interface from "src/docs/en/Interface.md";
import Galleries from "src/docs/en/Galleries.md";
import Scraping from "src/docs/en/Scraping.md";
import Contributing from "src/docs/en/Contributing.md";
import SceneFilenameParser from "src/docs/en/SceneFilenameParser.md";
import Help from "src/docs/en/Help.md";
import { Page } from "./Page";

interface IManualProps {
show: boolean;
onClose: () => void;
}

export const Manual: React.FC<IManualProps> = ({ show, onClose }) => {
const content = [
{
key: "Introduction.md",
title: "Introduction",
content: Introduction,
},
{
key: "Configuration.md",
title: "Configuration",
content: Configuration,
},
{
key: "Interface.md",
title: "Interface",
content: Interface,
},
{
key: "Tasks.md",
title: "Tasks",
content: Tasks,
},
{
key: "AutoTagging.md",
title: "Auto Tagging",
content: AutoTagging,
className: "indent-1",
},
{
key: "SceneFilenameParser.md",
title: "Scene Filename Parser",
content: SceneFilenameParser,
className: "indent-1",
},
{
key: "JSONSpec.md",
title: "JSON Specification",
content: JSONSpec,
className: "indent-1",
},
{
key: "Galleries.md",
title: "Image Galleries",
content: Galleries,
},
{
key: "Scraping.md",
title: "Metadata Scraping",
content: Scraping,
},
{
key: "Contributing.md",
title: "Contributing",
content: Contributing,
},
{
key: "Help.md",
title: "Further Help",
content: Help,
},
];

const [activeTab, setActiveTab] = useState(content[0].key);

// links to other manual pages are specified as "/help/page.md"
// intercept clicks to these pages and set the tab accordingly
function interceptLinkClick(
event: React.MouseEvent<HTMLDivElement, MouseEvent>
) {
if (event.target instanceof HTMLAnchorElement) {
const href = (event.target as HTMLAnchorElement).getAttribute("href");
if (href && href.startsWith("/help")) {
const newKey = (event.target as HTMLAnchorElement).pathname.substring(
"/help/".length
);
setActiveTab(newKey);
event.preventDefault();
}
}
}

return (
<Modal
show={show}
onHide={onClose}
dialogClassName="modal-dialog-scrollable manual modal-xl"
>
<Modal.Header closeButton>
<Modal.Title>Help</Modal.Title>
</Modal.Header>
<Modal.Body>
<Container className="manual-container">
<Tab.Container
activeKey={activeTab}
onSelect={(k) => setActiveTab(k)}
id="manual-tabs"
>
<Row>
<Col lg={3} className="mb-3 mb-lg-0">
<Nav variant="pills" className="flex-column">
{content.map((c) => {
return (
<Nav.Item>
<Nav.Link className={c.className} eventKey={c.key}>
{c.title}
</Nav.Link>
</Nav.Item>
);
})}
<hr className="d-sm-none" />
</Nav>
</Col>
<Col lg={9} className="manual-content">
<Tab.Content>
{content.map((c) => {
return (
<Tab.Pane eventKey={c.key} onClick={interceptLinkClick}>
<Page page={c.content} />
</Tab.Pane>
);
})}
</Tab.Content>
</Col>
</Row>
</Tab.Container>
</Container>
</Modal.Body>
</Modal>
);
};
22 changes: 22 additions & 0 deletions ui/v2.5/src/components/Help/Page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React, { useEffect, useState } from "react";
import ReactMarkdown from "react-markdown";

interface IPageProps {
// page is a markdown module
// eslint-disable-next-line @typescript-eslint/no-explicit-any
page: any;
}

export const Page: React.FC<IPageProps> = ({ page }) => {
const [markdown, setMarkdown] = useState("");

useEffect(() => {
if (!markdown) {
fetch(page)
.then((res) => res.text())
.then((text) => setMarkdown(text));
}
}, [page, markdown]);

return <ReactMarkdown className="markdown" source={markdown} />;
};
40 changes: 40 additions & 0 deletions ui/v2.5/src/components/Help/styles.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
.manual {
background-color: #30404d;
color: $text-color;

.close {
color: $text-color;
}

.manual-container {
padding-left: 1px;
padding-right: 5px;
}

.modal-header,
.modal-body {
background-color: #30404d;
color: $text-color;
overflow-y: hidden;
}
}

.manual .manual-content {
max-height: calc(100vh - 10rem);
overflow-y: auto;

.indent-1 {
padding-left: 2rem;
}
}

@media (max-width: 992px) {
.manual .modal-body {
overflow-y: auto;

.manual-content {
max-height: inherit;
overflow-y: hidden;
}
}
}
107 changes: 60 additions & 47 deletions ui/v2.5/src/components/MainNavbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { Link, NavLink, useLocation } from "react-router-dom";
import { SessionUtils } from "src/utils";

import { Icon } from "src/components/Shared";
import { Manual } from "./Help/Manual";

interface IMenuItem {
message: MessageDescriptor;
Expand Down Expand Up @@ -91,6 +92,8 @@ const menuItems: IMenuItem[] = [
export const MainNavbar: React.FC = () => {
const location = useLocation();
const [expanded, setExpanded] = useState(false);
const [showManual, setShowManual] = useState(false);

// react-bootstrap typing bug
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const navbarRef = useRef<any>();
Expand Down Expand Up @@ -147,55 +150,65 @@ export const MainNavbar: React.FC = () => {
}

return (
<Navbar
collapseOnSelect
fixed="top"
variant="dark"
bg="dark"
className="top-nav"
expand="lg"
expanded={expanded}
onToggle={setExpanded}
ref={navbarRef}
>
<Navbar.Brand
as="div"
className="order-1 order-md-0"
onClick={() => setExpanded(false)}
<>
<Manual show={showManual} onClose={() => setShowManual(false)} />
<Navbar
collapseOnSelect
fixed="top"
variant="dark"
bg="dark"
className="top-nav"
expand="lg"
expanded={expanded}
onToggle={setExpanded}
ref={navbarRef}
>
<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="" />
<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">
<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>
</Navbar.Collapse>
<Nav className="order-2 flex-row">
<div className="d-none d-sm-block">{newButton}</div>
<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>
</Link>
</Navbar.Brand>
<Navbar.Toggle className="order-0" />
<Navbar.Collapse className="order-3 order-md-1">
<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>
))}
{maybeRenderLogout()}
</Nav>
</Navbar.Collapse>
<Nav className="order-2 flex-row">
<div className="d-none d-sm-block">{newButton}</div>
<NavLink exact to="/settings" onClick={() => setExpanded(false)}>
<Button className="minimal settings-button">
<Icon icon="cog" />
</Button>
</NavLink>
{maybeRenderLogout()}
</Nav>
</Navbar>
</Navbar>
</>
);
};
15 changes: 15 additions & 0 deletions ui/v2.5/src/docs/en/AutoTagging.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Auto Tagging

This task iterates through your created Performers, Studios and Tags - based on what options you ticked. For each, it finds scenes where the filename contains the Performer/Studio/Tag name. For each scene it finds that matches, it sets the applicable field.

Where the Performer/Studio/Tag name has multiple words, the search will include filenames where the Performer/Studio/Tag name is separated with `.`, `-` or `_` characters, as well as whitespace.

For example, auto tagging for performer `Jane Doe` will match the following filenames:
* `Jane.Doe.1.mp4`
* `Jane_Doe.2.mp4`
* `Jane-Doe.3.mp4`
* `Jane Doe.4.mp4`

Matching is case insensitive, and should only match exact wording within word boundaries. For example, `Jane Doe` will not match `Maryjane-Doe`, but may match `Mary-Jane-Doe`.

Auto tagging for specific Performers, Studios and Tags can be performed from the individual Performer/Studio/Tag page.
Loading