Skip to content

Commit 83f8bc0

Browse files
authored
In-app help manual (stashapp#628)
1 parent 534d475 commit 83f8bc0

19 files changed

+1555
-48
lines changed

ui/v2.5/src/components/Changelog/Version.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ const Version: React.FC<IVersionProps> = ({
4949
</Card.Header>
5050
<Card.Body>
5151
<Collapse in={open}>
52-
<div className="changelog-version-body">{children}</div>
52+
<div className="changelog-version-body markdown">{children}</div>
5353
</Collapse>
5454
</Card.Body>
5555
</Card>

ui/v2.5/src/components/Changelog/versions/v030.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import ReactMarkdown from "react-markdown";
33

44
const markup = `
55
### ✨ New Features
6+
* Add in-app help manual.
67
* Add support for custom served folders.
78
* Add support for parent/child studios.
89
+150
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import React, { useState } from "react";
2+
import { Modal, Container, Row, Col, Nav, Tab } from "react-bootstrap";
3+
import Introduction from "src/docs/en/Introduction.md";
4+
import Tasks from "src/docs/en/Tasks.md";
5+
import AutoTagging from "src/docs/en/AutoTagging.md";
6+
import JSONSpec from "src/docs/en/JSONSpec.md";
7+
import Configuration from "src/docs/en/Configuration.md";
8+
import Interface from "src/docs/en/Interface.md";
9+
import Galleries from "src/docs/en/Galleries.md";
10+
import Scraping from "src/docs/en/Scraping.md";
11+
import Contributing from "src/docs/en/Contributing.md";
12+
import SceneFilenameParser from "src/docs/en/SceneFilenameParser.md";
13+
import Help from "src/docs/en/Help.md";
14+
import { Page } from "./Page";
15+
16+
interface IManualProps {
17+
show: boolean;
18+
onClose: () => void;
19+
}
20+
21+
export const Manual: React.FC<IManualProps> = ({ show, onClose }) => {
22+
const content = [
23+
{
24+
key: "Introduction.md",
25+
title: "Introduction",
26+
content: Introduction,
27+
},
28+
{
29+
key: "Configuration.md",
30+
title: "Configuration",
31+
content: Configuration,
32+
},
33+
{
34+
key: "Interface.md",
35+
title: "Interface",
36+
content: Interface,
37+
},
38+
{
39+
key: "Tasks.md",
40+
title: "Tasks",
41+
content: Tasks,
42+
},
43+
{
44+
key: "AutoTagging.md",
45+
title: "Auto Tagging",
46+
content: AutoTagging,
47+
className: "indent-1",
48+
},
49+
{
50+
key: "SceneFilenameParser.md",
51+
title: "Scene Filename Parser",
52+
content: SceneFilenameParser,
53+
className: "indent-1",
54+
},
55+
{
56+
key: "JSONSpec.md",
57+
title: "JSON Specification",
58+
content: JSONSpec,
59+
className: "indent-1",
60+
},
61+
{
62+
key: "Galleries.md",
63+
title: "Image Galleries",
64+
content: Galleries,
65+
},
66+
{
67+
key: "Scraping.md",
68+
title: "Metadata Scraping",
69+
content: Scraping,
70+
},
71+
{
72+
key: "Contributing.md",
73+
title: "Contributing",
74+
content: Contributing,
75+
},
76+
{
77+
key: "Help.md",
78+
title: "Further Help",
79+
content: Help,
80+
},
81+
];
82+
83+
const [activeTab, setActiveTab] = useState(content[0].key);
84+
85+
// links to other manual pages are specified as "/help/page.md"
86+
// intercept clicks to these pages and set the tab accordingly
87+
function interceptLinkClick(
88+
event: React.MouseEvent<HTMLDivElement, MouseEvent>
89+
) {
90+
if (event.target instanceof HTMLAnchorElement) {
91+
const href = (event.target as HTMLAnchorElement).getAttribute("href");
92+
if (href && href.startsWith("/help")) {
93+
const newKey = (event.target as HTMLAnchorElement).pathname.substring(
94+
"/help/".length
95+
);
96+
setActiveTab(newKey);
97+
event.preventDefault();
98+
}
99+
}
100+
}
101+
102+
return (
103+
<Modal
104+
show={show}
105+
onHide={onClose}
106+
dialogClassName="modal-dialog-scrollable manual modal-xl"
107+
>
108+
<Modal.Header closeButton>
109+
<Modal.Title>Help</Modal.Title>
110+
</Modal.Header>
111+
<Modal.Body>
112+
<Container className="manual-container">
113+
<Tab.Container
114+
activeKey={activeTab}
115+
onSelect={(k) => setActiveTab(k)}
116+
id="manual-tabs"
117+
>
118+
<Row>
119+
<Col lg={3} className="mb-3 mb-lg-0">
120+
<Nav variant="pills" className="flex-column">
121+
{content.map((c) => {
122+
return (
123+
<Nav.Item>
124+
<Nav.Link className={c.className} eventKey={c.key}>
125+
{c.title}
126+
</Nav.Link>
127+
</Nav.Item>
128+
);
129+
})}
130+
<hr className="d-sm-none" />
131+
</Nav>
132+
</Col>
133+
<Col lg={9} className="manual-content">
134+
<Tab.Content>
135+
{content.map((c) => {
136+
return (
137+
<Tab.Pane eventKey={c.key} onClick={interceptLinkClick}>
138+
<Page page={c.content} />
139+
</Tab.Pane>
140+
);
141+
})}
142+
</Tab.Content>
143+
</Col>
144+
</Row>
145+
</Tab.Container>
146+
</Container>
147+
</Modal.Body>
148+
</Modal>
149+
);
150+
};

ui/v2.5/src/components/Help/Page.tsx

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import React, { useEffect, useState } from "react";
2+
import ReactMarkdown from "react-markdown";
3+
4+
interface IPageProps {
5+
// page is a markdown module
6+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
7+
page: any;
8+
}
9+
10+
export const Page: React.FC<IPageProps> = ({ page }) => {
11+
const [markdown, setMarkdown] = useState("");
12+
13+
useEffect(() => {
14+
if (!markdown) {
15+
fetch(page)
16+
.then((res) => res.text())
17+
.then((text) => setMarkdown(text));
18+
}
19+
}, [page, markdown]);
20+
21+
return <ReactMarkdown className="markdown" source={markdown} />;
22+
};
+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
.manual {
2+
background-color: #30404d;
3+
color: $text-color;
4+
5+
.close {
6+
color: $text-color;
7+
}
8+
9+
.manual-container {
10+
padding-left: 1px;
11+
padding-right: 5px;
12+
}
13+
14+
.modal-header,
15+
.modal-body {
16+
background-color: #30404d;
17+
color: $text-color;
18+
overflow-y: hidden;
19+
}
20+
}
21+
22+
.manual .manual-content {
23+
max-height: calc(100vh - 10rem);
24+
overflow-y: auto;
25+
26+
.indent-1 {
27+
padding-left: 2rem;
28+
}
29+
}
30+
31+
@media (max-width: 992px) {
32+
.manual .modal-body {
33+
overflow-y: auto;
34+
35+
.manual-content {
36+
max-height: inherit;
37+
overflow-y: hidden;
38+
}
39+
}
40+
}

ui/v2.5/src/components/MainNavbar.tsx

+60-47
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { Link, NavLink, useLocation } from "react-router-dom";
1212
import { SessionUtils } from "src/utils";
1313

1414
import { Icon } from "src/components/Shared";
15+
import { Manual } from "./Help/Manual";
1516

1617
interface IMenuItem {
1718
message: MessageDescriptor;
@@ -91,6 +92,8 @@ const menuItems: IMenuItem[] = [
9192
export const MainNavbar: React.FC = () => {
9293
const location = useLocation();
9394
const [expanded, setExpanded] = useState(false);
95+
const [showManual, setShowManual] = useState(false);
96+
9497
// react-bootstrap typing bug
9598
// eslint-disable-next-line @typescript-eslint/no-explicit-any
9699
const navbarRef = useRef<any>();
@@ -147,55 +150,65 @@ export const MainNavbar: React.FC = () => {
147150
}
148151

149152
return (
150-
<Navbar
151-
collapseOnSelect
152-
fixed="top"
153-
variant="dark"
154-
bg="dark"
155-
className="top-nav"
156-
expand="lg"
157-
expanded={expanded}
158-
onToggle={setExpanded}
159-
ref={navbarRef}
160-
>
161-
<Navbar.Brand
162-
as="div"
163-
className="order-1 order-md-0"
164-
onClick={() => setExpanded(false)}
153+
<>
154+
<Manual show={showManual} onClose={() => setShowManual(false)} />
155+
<Navbar
156+
collapseOnSelect
157+
fixed="top"
158+
variant="dark"
159+
bg="dark"
160+
className="top-nav"
161+
expand="lg"
162+
expanded={expanded}
163+
onToggle={setExpanded}
164+
ref={navbarRef}
165165
>
166-
<Link to="/">
167-
<Button className="minimal brand-link d-none d-md-inline-block">
168-
Stash
169-
</Button>
170-
<Button className="minimal brand-icon d-inline d-md-none">
171-
<img src="favicon.ico" alt="" />
166+
<Navbar.Brand
167+
as="div"
168+
className="order-1 order-md-0"
169+
onClick={() => setExpanded(false)}
170+
>
171+
<Link to="/">
172+
<Button className="minimal brand-link d-none d-md-inline-block">
173+
Stash
174+
</Button>
175+
<Button className="minimal brand-icon d-inline d-md-none">
176+
<img src="favicon.ico" alt="" />
177+
</Button>
178+
</Link>
179+
</Navbar.Brand>
180+
<Navbar.Toggle className="order-0" />
181+
<Navbar.Collapse className="order-3 order-md-1">
182+
<Nav className="mr-md-auto">
183+
{menuItems.map((i) => (
184+
<Nav.Link eventKey={i.href} as="div" key={i.href}>
185+
<LinkContainer activeClassName="active" exact to={i.href}>
186+
<Button className="minimal w-100">
187+
<Icon icon={i.icon} />
188+
<span>{intl.formatMessage(i.message)}</span>
189+
</Button>
190+
</LinkContainer>
191+
</Nav.Link>
192+
))}
193+
</Nav>
194+
</Navbar.Collapse>
195+
<Nav className="order-2 flex-row">
196+
<div className="d-none d-sm-block">{newButton}</div>
197+
<NavLink exact to="/settings" onClick={() => setExpanded(false)}>
198+
<Button className="minimal settings-button" title="Settings">
199+
<Icon icon="cog" />
200+
</Button>
201+
</NavLink>
202+
<Button
203+
className="minimal help-button"
204+
onClick={() => setShowManual(true)}
205+
title="Help"
206+
>
207+
<Icon icon="question-circle" />
172208
</Button>
173-
</Link>
174-
</Navbar.Brand>
175-
<Navbar.Toggle className="order-0" />
176-
<Navbar.Collapse className="order-3 order-md-1">
177-
<Nav className="mr-md-auto">
178-
{menuItems.map((i) => (
179-
<Nav.Link eventKey={i.href} as="div" key={i.href}>
180-
<LinkContainer activeClassName="active" exact to={i.href}>
181-
<Button className="minimal w-100">
182-
<Icon icon={i.icon} />
183-
<span>{intl.formatMessage(i.message)}</span>
184-
</Button>
185-
</LinkContainer>
186-
</Nav.Link>
187-
))}
209+
{maybeRenderLogout()}
188210
</Nav>
189-
</Navbar.Collapse>
190-
<Nav className="order-2 flex-row">
191-
<div className="d-none d-sm-block">{newButton}</div>
192-
<NavLink exact to="/settings" onClick={() => setExpanded(false)}>
193-
<Button className="minimal settings-button">
194-
<Icon icon="cog" />
195-
</Button>
196-
</NavLink>
197-
{maybeRenderLogout()}
198-
</Nav>
199-
</Navbar>
211+
</Navbar>
212+
</>
200213
);
201214
};

ui/v2.5/src/docs/en/AutoTagging.md

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Auto Tagging
2+
3+
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.
4+
5+
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.
6+
7+
For example, auto tagging for performer `Jane Doe` will match the following filenames:
8+
* `Jane.Doe.1.mp4`
9+
* `Jane_Doe.2.mp4`
10+
* `Jane-Doe.3.mp4`
11+
* `Jane Doe.4.mp4`
12+
13+
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`.
14+
15+
Auto tagging for specific Performers, Studios and Tags can be performed from the individual Performer/Studio/Tag page.

0 commit comments

Comments
 (0)