Make mobile menu behavior more consistent, and stats styles responsive (#391)

This commit is contained in:
InfiniteTF
2020-03-08 02:55:42 +01:00
committed by GitHub
parent cb594f0e43
commit b3fab3cfef
15 changed files with 135 additions and 49 deletions

View File

@@ -111,6 +111,7 @@ func Start() {
r.Mount("/studio", studioRoutes{}.Routes()) r.Mount("/studio", studioRoutes{}.Routes())
r.HandleFunc("/css", func(w http.ResponseWriter, r *http.Request) { r.HandleFunc("/css", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/css")
if !config.GetCSSEnabled() { if !config.GetCSSEnabled() {
return return
} }

View File

@@ -50,7 +50,6 @@
"length-zero-no-unit": true, "length-zero-no-unit": true,
"max-empty-lines": 1, "max-empty-lines": 1,
"max-nesting-depth": 4, "max-nesting-depth": 4,
"max-line-length": 100,
"media-feature-colon-space-after": "always", "media-feature-colon-space-after": "always",
"media-feature-colon-space-before": "never", "media-feature-colon-space-before": "never",
"media-feature-range-operator-space-after": "always", "media-feature-range-operator-space-after": "always",

View File

@@ -107,7 +107,11 @@ export const ListFilter: React.FC<IListFilterProps> = (
function renderSortByOptions() { function renderSortByOptions() {
return props.filter.sortByOptions.map(option => ( return props.filter.sortByOptions.map(option => (
<Dropdown.Item onClick={onChangeSortBy} key={option} className="bg-secondary text-white"> <Dropdown.Item
onClick={onChangeSortBy}
key={option}
className="bg-secondary text-white"
>
{option} {option}
</Dropdown.Item> </Dropdown.Item>
)); ));
@@ -186,7 +190,11 @@ export const ListFilter: React.FC<IListFilterProps> = (
function renderSelectAll() { function renderSelectAll() {
if (props.onSelectAll) { if (props.onSelectAll) {
return ( return (
<Dropdown.Item key="select-all" className="bg-secondary text-white" onClick={() => onSelectAll()}> <Dropdown.Item
key="select-all"
className="bg-secondary text-white"
onClick={() => onSelectAll()}
>
Select All Select All
</Dropdown.Item> </Dropdown.Item>
); );
@@ -196,7 +204,11 @@ export const ListFilter: React.FC<IListFilterProps> = (
function renderSelectNone() { function renderSelectNone() {
if (props.onSelectNone) { if (props.onSelectNone) {
return ( return (
<Dropdown.Item key="select-none" className="bg-secondary text-white" onClick={() => onSelectNone()}> <Dropdown.Item
key="select-none"
className="bg-secondary text-white"
onClick={() => onSelectNone()}
>
Select None Select None
</Dropdown.Item> </Dropdown.Item>
); );
@@ -209,7 +221,11 @@ export const ListFilter: React.FC<IListFilterProps> = (
if (props.otherOperations) { if (props.otherOperations) {
props.otherOperations.forEach(o => { props.otherOperations.forEach(o => {
options.push( options.push(
<Dropdown.Item key={o.text} className="bg-secondary text-white" onClick={o.onClick}> <Dropdown.Item
key={o.text}
className="bg-secondary text-white"
onClick={o.onClick}
>
{o.text} {o.text}
</Dropdown.Item> </Dropdown.Item>
); );
@@ -222,7 +238,9 @@ export const ListFilter: React.FC<IListFilterProps> = (
<Dropdown.Toggle variant="secondary" id="more-menu"> <Dropdown.Toggle variant="secondary" id="more-menu">
<Icon icon="ellipsis-h" /> <Icon icon="ellipsis-h" />
</Dropdown.Toggle> </Dropdown.Toggle>
<Dropdown.Menu className="bg-secondary text-white">{options}</Dropdown.Menu> <Dropdown.Menu className="bg-secondary text-white">
{options}
</Dropdown.Menu>
</Dropdown> </Dropdown>
); );
} }
@@ -278,7 +296,9 @@ export const ListFilter: React.FC<IListFilterProps> = (
<Dropdown.Toggle split variant="secondary" id="more-menu"> <Dropdown.Toggle split variant="secondary" id="more-menu">
{props.filter.sortBy} {props.filter.sortBy}
</Dropdown.Toggle> </Dropdown.Toggle>
<Dropdown.Menu className="bg-secondary text-white">{renderSortByOptions()}</Dropdown.Menu> <Dropdown.Menu className="bg-secondary text-white">
{renderSortByOptions()}
</Dropdown.Menu>
<OverlayTrigger <OverlayTrigger
overlay={ overlay={
<Tooltip id="sort-direction-tooltip"> <Tooltip id="sort-direction-tooltip">

View File

@@ -1,4 +1,4 @@
import React from "react"; import React, { useEffect, useRef, useState } from "react";
import { FormattedMessage } from "react-intl"; import { FormattedMessage } from "react-intl";
import { Nav, Navbar, Button } from "react-bootstrap"; import { Nav, Navbar, Button } from "react-bootstrap";
import { IconName } from "@fortawesome/fontawesome-svg-core"; import { IconName } from "@fortawesome/fontawesome-svg-core";
@@ -48,6 +48,31 @@ const menuItems: IMenuItem[] = [
export const MainNavbar: React.FC = () => { export const MainNavbar: React.FC = () => {
const location = useLocation(); const location = useLocation();
const [expanded, setExpanded] = useState(false);
// react-bootstrap typing bug
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const navbarRef = useRef<any>();
const maybeCollapse = (event: Event) => {
if (
navbarRef.current &&
event.target instanceof Node &&
!navbarRef.current.contains(event.target)
) {
setExpanded(false);
}
};
useEffect(() => {
if (expanded) {
document.addEventListener("click", maybeCollapse);
document.addEventListener("touchstart", maybeCollapse);
}
return () => {
document.removeEventListener("click", maybeCollapse);
document.removeEventListener("touchstart", maybeCollapse);
};
}, [expanded]);
const path = const path =
location.pathname === "/performers" location.pathname === "/performers"
@@ -74,8 +99,15 @@ export const MainNavbar: React.FC = () => {
bg="dark" bg="dark"
className="top-nav" className="top-nav"
expand="md" expand="md"
expanded={expanded}
onToggle={setExpanded}
ref={navbarRef}
> >
<Navbar.Brand as="div" className="order-1 order-md-0"> <Navbar.Brand
as="div"
className="order-1 order-md-0"
onClick={() => setExpanded(false)}
>
<Link to="/"> <Link to="/">
<Button className="minimal brand-link d-none d-md-inline-block"> <Button className="minimal brand-link d-none d-md-inline-block">
Stash Stash
@@ -109,8 +141,8 @@ export const MainNavbar: React.FC = () => {
</Navbar.Collapse> </Navbar.Collapse>
<Nav className="order-2"> <Nav className="order-2">
<div className="d-none d-sm-block">{newButton}</div> <div className="d-none d-sm-block">{newButton}</div>
<LinkContainer exact to="/settings"> <LinkContainer exact to="/settings" onClick={() => setExpanded(false)}>
<Button className="minimal"> <Button className="minimal settings-button">
<Icon icon="cog" /> <Icon icon="cog" />
</Button> </Button>
</LinkContainer> </LinkContainer>

View File

@@ -236,7 +236,8 @@ export const SceneCard: React.FC<ISceneCardProps> = (
</h5> </h5>
<span>{props.scene.date}</span> <span>{props.scene.date}</span>
<p> <p>
{props.scene.details && TextUtils.truncate(props.scene.details, 100, "... (continued)")} {props.scene.details &&
TextUtils.truncate(props.scene.details, 100, "... (continued)")}
</p> </p>
</div> </div>

View File

@@ -137,10 +137,7 @@ export const Scene: React.FC = () => {
> >
<SceneFileInfoPanel scene={scene} /> <SceneFileInfoPanel scene={scene} />
</Tab> </Tab>
<Tab <Tab eventKey="scene-edit-panel" title="Edit">
eventKey="scene-edit-panel"
title="Edit"
>
<SceneEditPanel <SceneEditPanel
scene={scene} scene={scene}
onUpdate={newScene => setScene(newScene)} onUpdate={newScene => setScene(newScene)}

View File

@@ -344,7 +344,9 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
<td>Studio</td> <td>Studio</td>
<td> <td>
<StudioSelect <StudioSelect
onSelect={items => setStudioId(items.length > 0 ? items[0]?.id : undefined)} onSelect={items =>
setStudioId(items.length > 0 ? items[0]?.id : undefined)
}
ids={studioId ? [studioId] : []} ids={studioId ? [studioId] : []}
/> />
</td> </td>

View File

@@ -168,7 +168,11 @@ export const SettingsTasksPanel: React.FC = () => {
<Form.Group> <Form.Group>
<h5>Status: {status}</h5> <h5>Status: {status}</h5>
{!!status && status !== "Idle" ? ( {!!status && status !== "Idle" ? (
<ProgressBar animated now={progress} label={`${progress.toFixed(0)}%`} /> <ProgressBar
animated
now={progress}
label={`${progress.toFixed(0)}%`}
/>
) : ( ) : (
"" ""
)} )}

View File

@@ -73,7 +73,9 @@ export const SceneGallerySelect: React.FC<ISceneGallerySelect> = props => {
const onChange = (selectedItems: ValueType<Option>) => { const onChange = (selectedItems: ValueType<Option>) => {
const selectedItem = getSelectedValues(selectedItems)[0]; const selectedItem = getSelectedValues(selectedItems)[0];
props.onSelect(selectedItem ? galleries.find(g => g.id === selectedItem) : undefined); props.onSelect(
selectedItem ? galleries.find(g => g.id === selectedItem) : undefined
);
}; };
const selectedOptions: Option[] = props.initialId const selectedOptions: Option[] = props.initialId
@@ -211,7 +213,7 @@ export const StudioSelect: React.FC<IFilterProps> = props => {
const { data, loading } = StashService.useAllStudiosForFilter(); const { data, loading } = StashService.useAllStudiosForFilter();
const normalizedData = data?.allStudios ?? []; const normalizedData = data?.allStudios ?? [];
const items = (normalizedData.length > 0 const items = (normalizedData.length > 0
? [{ name: "None", id: "0" }, ...normalizedData] ? [{ name: "None", id: "0" }, ...normalizedData]
: [] : []

View File

@@ -13,7 +13,7 @@ export const Stats: React.FC = () => {
return ( return (
<div className="mt-5"> <div className="mt-5">
<div className="col col-sm-8 m-sm-auto row stats"> <div className="col col-sm-8 m-sm-auto row stats">
<div className="flex-grow-1"> <div className="stats-element">
<p className="title"> <p className="title">
<FormattedNumber value={data.stats.scene_count} /> <FormattedNumber value={data.stats.scene_count} />
</p> </p>
@@ -21,7 +21,7 @@ export const Stats: React.FC = () => {
<FormattedMessage id="scenes" defaultMessage="Scenes" /> <FormattedMessage id="scenes" defaultMessage="Scenes" />
</p> </p>
</div> </div>
<div className="flex-grow-1"> <div className="stats-element">
<p className="title"> <p className="title">
<FormattedNumber value={data.stats.gallery_count} /> <FormattedNumber value={data.stats.gallery_count} />
</p> </p>
@@ -29,7 +29,7 @@ export const Stats: React.FC = () => {
<FormattedMessage id="galleries" defaultMessage="Galleries" /> <FormattedMessage id="galleries" defaultMessage="Galleries" />
</p> </p>
</div> </div>
<div className="flex-grow-1"> <div className="stats-element">
<p className="title"> <p className="title">
<FormattedNumber value={data.stats.performer_count} /> <FormattedNumber value={data.stats.performer_count} />
</p> </p>
@@ -37,7 +37,7 @@ export const Stats: React.FC = () => {
<FormattedMessage id="performers" defaultMessage="Performers" /> <FormattedMessage id="performers" defaultMessage="Performers" />
</p> </p>
</div> </div>
<div className="flex-grow-1"> <div className="stats-element">
<p className="title"> <p className="title">
<FormattedNumber value={data.stats.studio_count} /> <FormattedNumber value={data.stats.studio_count} />
</p> </p>
@@ -45,7 +45,7 @@ export const Stats: React.FC = () => {
<FormattedMessage id="studios" defaultMessage="Studios" /> <FormattedMessage id="studios" defaultMessage="Studios" />
</p> </p>
</div> </div>
<div className="flex-grow-1"> <div className="stats-element">
<p className="title"> <p className="title">
<FormattedNumber value={data.stats.tag_count} /> <FormattedNumber value={data.stats.tag_count} />
</p> </p>

View File

@@ -11,7 +11,7 @@ export class StashService {
public static client: ApolloClient<NormalizedCacheObject>; public static client: ApolloClient<NormalizedCacheObject>;
private static cache: InMemoryCache; private static cache: InMemoryCache;
public static getPlatformURL(ws? : boolean) { public static getPlatformURL(ws?: boolean) {
const platformUrl = new URL(window.location.origin); const platformUrl = new URL(window.location.origin);
if (!process.env.NODE_ENV || process.env.NODE_ENV === "development") { if (!process.env.NODE_ENV || process.env.NODE_ENV === "development") {
@@ -36,7 +36,7 @@ export class StashService {
if (platformUrl.protocol === "https:") { if (platformUrl.protocol === "https:") {
wsPlatformUrl.protocol = "wss:"; wsPlatformUrl.protocol = "wss:";
} }
const url = `${platformUrl.toString().slice(0, -1)}/graphql`; const url = `${platformUrl.toString().slice(0, -1)}/graphql`;
const wsUrl = `${wsPlatformUrl.toString().slice(0, -1)}/graphql`; const wsUrl = `${wsPlatformUrl.toString().slice(0, -1)}/graphql`;

View File

@@ -41,8 +41,7 @@ function useLocalForage(
if (!Object.is(parsed, null)) { if (!Object.is(parsed, null)) {
setData(parsed); setData(parsed);
Cache[key] = parsed; Cache[key] = parsed;
} } else {
else {
setData({}); setData({});
Cache[key] = {}; Cache[key] = {};
} }

View File

@@ -29,27 +29,31 @@ a {
color: $primary; color: $primary;
} }
code, .code { code,
.code {
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace; font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace;
} }
.input-control, .text-input { .input-control,
color: $text-color; .text-input {
border: 0; border: 0;
box-shadow:0 0 0 0 rgba(19, 124, 189, 0), 0 0 0 0 rgba(19, 124, 189, 0), 0 0 0 0 rgba(19, 124, 189, 0), inset 0 0 0 1px rgba(16, 22, 26, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.4); box-shadow: 0 0 0 0 rgba(19, 124, 189, 0), 0 0 0 0 rgba(19, 124, 189, 0), 0 0 0 0 rgba(19, 124, 189, 0), inset 0 0 0 1px rgba(16, 22, 26, .3), inset 0 1px 1px rgba(16, 22, 26, .4);
color: $text-color;
&:focus { &:focus {
color: $text-color;
border: 0; border: 0;
box-shadow: 0 0 0 1px $primary, 0 0 0 1px $primary, 0 0 0 3px rgba(19,124,189,.3), inset 0 0 0 1px rgba(16,22,26,.3), inset 0 1px 1px rgba(16,22,26,.4); box-shadow: 0 0 0 1px $primary, 0 0 0 1px $primary, 0 0 0 3px rgba(19, 124, 189, .3), inset 0 0 0 1px rgba(16, 22, 26, .3), inset 0 1px 1px rgba(16, 22, 26, .4);
color: $text-color;
} }
} }
.text-input, .text-input:focus { .text-input,
.text-input:focus {
background-color: $textfield-bg; background-color: $textfield-bg;
} }
.input-control, .input-control:focus { .input-control,
.input-control:focus {
background-color: $secondary; background-color: $secondary;
} }
@@ -85,6 +89,7 @@ code, .code {
.scene-card-video { .scene-card-video {
max-height: 180px; max-height: 180px;
} }
.previewable.portrait { .previewable.portrait {
height: 180px; height: 180px;
} }
@@ -96,6 +101,7 @@ code, .code {
.scene-card-video { .scene-card-video {
max-height: 240px; max-height: 240px;
} }
.previewable.portrait { .previewable.portrait {
height: 240px; height: 240px;
} }
@@ -107,6 +113,7 @@ code, .code {
.scene-card-video { .scene-card-video {
max-height: 360px; max-height: 360px;
} }
.previewable.portrait { .previewable.portrait {
height: 360px; height: 360px;
} }
@@ -118,6 +125,7 @@ code, .code {
.scene-card-video { .scene-card-video {
max-height: 480px; max-height: 480px;
} }
.portrait { .portrait {
height: 480px; height: 480px;
} }
@@ -131,13 +139,15 @@ code, .code {
/* this is a bit of a hack, because we can't supply direct class names /* this is a bit of a hack, because we can't supply direct class names
to the react-select controls */ to the react-select controls */
/* stylelint-disable selector-class-pattern */
div.react-select__control { div.react-select__control {
background-color: $secondary; background-color: $secondary;
border-color: $secondary; border-color: $secondary;
color: $text-color; color: $text-color;
cursor: pointer; cursor: pointer;
.react-select__single-value, .react-select__input { .react-select__single-value,
.react-select__input {
color: $text-color; color: $text-color;
} }
@@ -160,6 +170,7 @@ div.react-select__menu {
cursor: pointer; cursor: pointer;
} }
} }
/* stylelint-enable selector-class-pattern */
.image-thumbnail { .image-thumbnail {
height: 100px; height: 100px;
@@ -221,9 +232,9 @@ div.react-select__menu {
.filter-container, .filter-container,
.operation-container { .operation-container {
align-items: center;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center;
margin: 0 auto 10px; margin: 0 auto 10px;
} }
@@ -375,6 +386,11 @@ div.react-select__menu {
.btn { .btn {
padding: 6px; padding: 6px;
} }
.settings-button {
padding-left: 1rem;
padding-right: 1rem;
}
} }
} }
@@ -383,9 +399,18 @@ div.react-select__menu {
} }
.stats { .stats {
&-element {
flex-grow: 1;
margin: auto .5rem;
}
.title { .title {
font-size: 3vw; font-size: 3vw;
text-align: center; text-align: center;
@media (max-width: 576px) {
font-size: 16px;
}
} }
.heading { .heading {

View File

@@ -7,16 +7,20 @@ import { StashService } from "./core/StashService";
import "./index.scss"; import "./index.scss";
import * as serviceWorker from "./serviceWorker"; import * as serviceWorker from "./serviceWorker";
ReactDOM.render(( ReactDOM.render(
<> <>
<link rel="stylesheet" type="text/css" href={StashService.getPlatformURL() + "css"}/> <link
<BrowserRouter> rel="stylesheet"
<ApolloProvider client={StashService.initialize()!}> type="text/css"
<App /> href={`${StashService.getPlatformURL()}css`}
</ApolloProvider> />
</BrowserRouter> <BrowserRouter>
</> <ApolloProvider client={StashService.initialize()!}>
), document.getElementById("root") <App />
</ApolloProvider>
</BrowserRouter>
</>,
document.getElementById("root")
); );
// If you want your app to work offline and load faster, you can change // If you want your app to work offline and load faster, you can change

View File

@@ -25,7 +25,7 @@ $pre-color: $text-color;
$navbar-dark-color: rgb(245, 248, 250); $navbar-dark-color: rgb(245, 248, 250);
$popover-bg: $secondary; $popover-bg: $secondary;
$dark-text: #182026; $dark-text: #182026;
$textfield-bg: rgba(16,22,26,.3); $textfield-bg: rgba(16, 22, 26, .3);
@import "node_modules/bootstrap/scss/bootstrap"; @import "node_modules/bootstrap/scss/bootstrap";