Fix various issues with v2.5 UI (#390)

* Fix navbar collapse breakpoint
* Fix list filter colors and height
* Make styling similar to v2
* Fix scene card zoom and orientation
* Keep p tag even without details
* Fix custom css
* Default settings tab to tasks
* Fix flickering progress bar. Fix percentage.
* Fix unsetting studio
* Fix scene gallery select
* Don't hide edit on small devices
* Fix log dropdown style
* Use monospace for custom css input
This commit is contained in:
WithoutPants
2020-03-06 20:02:02 +11:00
committed by GitHub
parent 088ddc9df4
commit cb594f0e43
20 changed files with 5381 additions and 92 deletions

View File

@@ -107,7 +107,7 @@ export const ListFilter: React.FC<IListFilterProps> = (
function renderSortByOptions() {
return props.filter.sortByOptions.map(option => (
<Dropdown.Item onClick={onChangeSortBy} key={option}>
<Dropdown.Item onClick={onChangeSortBy} key={option} className="bg-secondary text-white">
{option}
</Dropdown.Item>
));
@@ -186,7 +186,7 @@ export const ListFilter: React.FC<IListFilterProps> = (
function renderSelectAll() {
if (props.onSelectAll) {
return (
<Dropdown.Item key="select-all" onClick={() => onSelectAll()}>
<Dropdown.Item key="select-all" className="bg-secondary text-white" onClick={() => onSelectAll()}>
Select All
</Dropdown.Item>
);
@@ -196,7 +196,7 @@ export const ListFilter: React.FC<IListFilterProps> = (
function renderSelectNone() {
if (props.onSelectNone) {
return (
<Dropdown.Item key="select-none" onClick={() => onSelectNone()}>
<Dropdown.Item key="select-none" className="bg-secondary text-white" onClick={() => onSelectNone()}>
Select None
</Dropdown.Item>
);
@@ -209,7 +209,7 @@ export const ListFilter: React.FC<IListFilterProps> = (
if (props.otherOperations) {
props.otherOperations.forEach(o => {
options.push(
<Dropdown.Item key={o.text} onClick={o.onClick}>
<Dropdown.Item key={o.text} className="bg-secondary text-white" onClick={o.onClick}>
{o.text}
</Dropdown.Item>
);
@@ -222,7 +222,7 @@ export const ListFilter: React.FC<IListFilterProps> = (
<Dropdown.Toggle variant="secondary" id="more-menu">
<Icon icon="ellipsis-h" />
</Dropdown.Toggle>
<Dropdown.Menu>{options}</Dropdown.Menu>
<Dropdown.Menu className="bg-secondary text-white">{options}</Dropdown.Menu>
</Dropdown>
);
}
@@ -259,13 +259,13 @@ export const ListFilter: React.FC<IListFilterProps> = (
placeholder="Search..."
defaultValue={props.filter.searchTerm}
onChange={onChangeQuery}
className="filter-item col-5 col-sm-2"
className="filter-item col-5 col-sm-2 bg-secondary text-white border-secondary"
/>
<Form.Control
as="select"
onChange={onChangePageSize}
value={props.filter.itemsPerPage.toString()}
className="filter-item col-1 d-none d-sm-inline"
className="btn-secondary filter-item col-1 d-none d-sm-inline"
>
{PAGE_SIZE_OPTIONS.map(s => (
<option value={s} key={s}>
@@ -278,7 +278,7 @@ export const ListFilter: React.FC<IListFilterProps> = (
<Dropdown.Toggle split variant="secondary" id="more-menu">
{props.filter.sortBy}
</Dropdown.Toggle>
<Dropdown.Menu>{renderSortByOptions()}</Dropdown.Menu>
<Dropdown.Menu className="bg-secondary text-white">{renderSortByOptions()}</Dropdown.Menu>
<OverlayTrigger
overlay={
<Tooltip id="sort-direction-tooltip">

View File

@@ -73,20 +73,20 @@ export const MainNavbar: React.FC = () => {
variant="dark"
bg="dark"
className="top-nav"
expand="sm"
expand="md"
>
<Navbar.Brand as="div" className="order-1 order-sm-0">
<Navbar.Brand as="div" className="order-1 order-md-0">
<Link to="/">
<Button className="minimal brand-link d-none d-sm-inline-block">
<Button className="minimal brand-link d-none d-md-inline-block">
Stash
</Button>
<Button className="minimal brand-icon d-inline d-sm-none">
<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-sm-1">
<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}>

View File

@@ -298,6 +298,7 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
</td>
<td>
<Form.Control
className="text-input"
value={url ?? ""}
readOnly={!isEditing}
plaintext={!isEditing}

View File

@@ -235,11 +235,9 @@ export const SceneCard: React.FC<ISceneCardProps> = (
: TextUtils.fileNameFromPath(props.scene.path)}
</h5>
<span>{props.scene.date}</span>
{props.scene.details && (
<p>
{TextUtils.truncate(props.scene.details, 100, "... (continued)")}
{props.scene.details && TextUtils.truncate(props.scene.details, 100, "... (continued)")}
</p>
)}
</div>
{maybeRenderPopoverButtonGroup()}

View File

@@ -140,7 +140,6 @@ export const Scene: React.FC = () => {
<Tab
eventKey="scene-edit-panel"
title="Edit"
tabClassName="d-none d-sm-block"
>
<SceneEditPanel
scene={scene}

View File

@@ -310,6 +310,7 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
}
value={url}
placeholder="URL"
className="text-input"
/>
{maybeRenderScrapeButton()}
</td>
@@ -318,7 +319,8 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
title: "Date",
value: date,
isEditing: true,
onChange: setDate
onChange: setDate,
placeholder: "YYYY-MM-DD"
})}
{TableUtils.renderHtmlSelect({
title: "Rating",
@@ -342,7 +344,7 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
<td>Studio</td>
<td>
<StudioSelect
onSelect={items => items.length && setStudioId(items[0]?.id)}
onSelect={items => setStudioId(items.length > 0 ? items[0]?.id : undefined)}
ids={studioId ? [studioId] : []}
/>
</td>
@@ -377,7 +379,7 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
<Form.Label>Details</Form.Label>
<Form.Control
as="textarea"
className="scene-description"
className="scene-description text-input"
onChange={(newValue: React.FormEvent<HTMLTextAreaElement>) =>
setDetails(newValue.currentTarget.value)
}

View File

@@ -11,7 +11,7 @@ import { SettingsTasksPanel } from "./SettingsTasksPanel/SettingsTasksPanel";
export const Settings: React.FC = () => {
const location = useLocation();
const history = useHistory();
const defaultTab = queryString.parse(location.search).tab ?? "configuration";
const defaultTab = queryString.parse(location.search).tab ?? "tasks";
const onSelect = (val: string) => history.push(`?tab=${val}`);
@@ -23,7 +23,7 @@ export const Settings: React.FC = () => {
onSelect={onSelect}
>
<Row>
<Col sm={2}>
<Col sm={3} md={2}>
<Nav variant="pills" className="flex-column">
<Nav.Item>
<Nav.Link eventKey="configuration">Configuration</Nav.Link>
@@ -43,7 +43,7 @@ export const Settings: React.FC = () => {
<hr className="d-sm-none" />
</Nav>
</Col>
<Col sm={10}>
<Col sm={9} md={10}>
<Tab.Content>
<Tab.Pane eventKey="configuration">
<SettingsConfigurationPanel />

View File

@@ -173,7 +173,7 @@ export const SettingsConfigurationPanel: React.FC = () => {
<Form.Group id="database-path">
<h6>Database Path</h6>
<Form.Control
className="col col-sm-6"
className="col col-sm-6 text-input"
defaultValue={databasePath}
onChange={(e: React.FormEvent<HTMLInputElement>) =>
setDatabasePath(e.currentTarget.value)
@@ -187,7 +187,7 @@ export const SettingsConfigurationPanel: React.FC = () => {
<Form.Group id="generated-path">
<h6>Generated Path</h6>
<Form.Control
className="col col-sm-6"
className="col col-sm-6 text-input"
defaultValue={generatedPath}
onChange={(e: React.FormEvent<HTMLInputElement>) =>
setGeneratedPath(e.currentTarget.value)
@@ -206,7 +206,7 @@ export const SettingsConfigurationPanel: React.FC = () => {
excludes.map((regexp, i) => (
<InputGroup>
<Form.Control
className="col col-sm-6"
className="col col-sm-6 text-input"
value={regexp}
onChange={(e: React.FormEvent<HTMLInputElement>) =>
excludeRegexChanged(i, e.currentTarget.value)
@@ -226,15 +226,13 @@ export const SettingsConfigurationPanel: React.FC = () => {
<Button className="minimal" onClick={() => excludeAddRegex()}>
<Icon icon="plus" />
</Button>
<Form.Text>
<Form.Text className="text-muted">
Regexps of files/paths to exclude from Scan and add to Clean
<a
href="https://github.com/stashapp/stash/wiki/Exclude-file-configuration"
rel="noopener noreferrer"
target="_blank"
>
<span>
Regexps of files/paths to exclude from Scan and add to Clean
</span>
<Icon icon="question-circle" />
</a>
</Form.Text>
@@ -248,7 +246,7 @@ export const SettingsConfigurationPanel: React.FC = () => {
<Form.Group id="transcode-size">
<h6>Maximum transcode size</h6>
<Form.Control
className="col col-sm-6"
className="col col-sm-6 input-control"
as="select"
onChange={(event: React.FormEvent<HTMLSelectElement>) =>
setMaxTranscodeSize(translateQuality(event.currentTarget.value))
@@ -268,7 +266,7 @@ export const SettingsConfigurationPanel: React.FC = () => {
<Form.Group id="streaming-transcode-size">
<h6>Maximum streaming transcode size</h6>
<Form.Control
className="col col-sm-6"
className="col col-sm-6 input-control"
as="select"
onChange={(event: React.FormEvent<HTMLSelectElement>) =>
setMaxStreamingTranscodeSize(
@@ -296,7 +294,7 @@ export const SettingsConfigurationPanel: React.FC = () => {
<Form.Group id="username">
<h6>Username</h6>
<Form.Control
className="col col-sm-6"
className="col col-sm-6 text-input"
defaultValue={username}
onChange={(e: React.FormEvent<HTMLInputElement>) =>
setUsername(e.currentTarget.value)
@@ -309,7 +307,7 @@ export const SettingsConfigurationPanel: React.FC = () => {
<Form.Group id="password">
<h6>Password</h6>
<Form.Control
className="col col-sm-6"
className="col col-sm-6 text-input"
type="password"
defaultValue={password}
onChange={(e: React.FormEvent<HTMLInputElement>) =>
@@ -328,7 +326,7 @@ export const SettingsConfigurationPanel: React.FC = () => {
<Form.Group id="log-file">
<h6>Log file</h6>
<Form.Control
className="col col-sm-6"
className="col col-sm-6 text-input"
defaultValue={logFile}
onChange={(e: React.FormEvent<HTMLInputElement>) =>
setLogFile(e.currentTarget.value)
@@ -356,7 +354,7 @@ export const SettingsConfigurationPanel: React.FC = () => {
<Form.Group id="log-level">
<h6>Log Level</h6>
<Form.Control
className="col col-sm-6"
className="col col-sm-6 input-control"
as="select"
onChange={(event: React.FormEvent<HTMLSelectElement>) =>
setLogLevel(event.currentTarget.value)

View File

@@ -56,11 +56,11 @@ export const SettingsInterfacePanel: React.FC = () => {
return (
<>
<h4>User Interface</h4>
<Form.Group controlId="language" className="row">
<Form.Label className="col-2">Language</Form.Label>
<Form.Group controlId="language">
<h6>Language</h6>
<Form.Control
as="select"
className="col-4"
className="col-4 input-control"
value={language}
onChange={(e: React.FormEvent<HTMLSelectElement>) =>
setLanguage(e.currentTarget.value)
@@ -72,7 +72,7 @@ export const SettingsInterfacePanel: React.FC = () => {
</Form.Control>
</Form.Group>
<Form.Group>
<Form.Label>Scene / Marker Wall</Form.Label>
<h5>Scene / Marker Wall</h5>
<Form.Check
id="wall-show-title"
checked={wallShowTitle}
@@ -104,19 +104,20 @@ export const SettingsInterfacePanel: React.FC = () => {
<Form.Group>
<h5>Scene Player</h5>
<Form.Group id="auto-start-video">
<Form.Check
id="auto-start-video"
checked={autostartVideo}
label="Auto-start video"
onChange={() => {
setAutostartVideo(!autostartVideo);
}}
/>
</Form.Group>
<Form.Group id="max-loop-duration">
<Form.Label>Maximum loop duration</Form.Label>
<h6>Maximum loop duration</h6>
<DurationInput
className="col col-sm-4"
className="row col col-4"
numericValue={maximumLoopDuration}
onValueChange={duration => setMaximumLoopDuration(duration)}
/>
@@ -145,7 +146,7 @@ export const SettingsInterfacePanel: React.FC = () => {
setCSS(e.currentTarget.value)
}
rows={16}
className="col col-sm-6"
className="col col-sm-6 text-input code"
></Form.Control>
<Form.Text className="text-muted">
Page must be reloaded for changes to take effect.

View File

@@ -101,7 +101,7 @@ export const SettingsLogsPanel: React.FC = () => {
<Form.Row id="log-level">
<Form.Label className="col-6 col-sm-2">Log Level</Form.Label>
<Form.Control
className="col-6 col-sm-2"
className="col-6 col-sm-2 input-control"
as="select"
defaultValue={logLevel}
onChange={event => setLogLevel(event.currentTarget.value)}

View File

@@ -167,8 +167,8 @@ export const SettingsTasksPanel: React.FC = () => {
<>
<Form.Group>
<h5>Status: {status}</h5>
{status !== "Idle" ? (
<ProgressBar now={progress} label={`${progress}%`} />
{!!status && status !== "Idle" ? (
<ProgressBar animated now={progress} label={`${progress.toFixed(0)}%`} />
) : (
""
)}
@@ -183,7 +183,7 @@ export const SettingsTasksPanel: React.FC = () => {
{renderImportAlert()}
{renderCleanAlert()}
<h5>Running Jobs</h5>
<h4>Running Jobs</h4>
{renderJobStatus()}

View File

@@ -31,3 +31,7 @@
.log-time {
margin-right: 1rem;
}
#configuration-tabs-tabpane-about .table {
width: initial;
}

View File

@@ -75,7 +75,7 @@ export const DurationInput: React.FC<IProps> = (props: IProps) => {
<Form.Group className={`duration-input ${props.className}`}>
<InputGroup>
<Form.Control
className="duration-control"
className="duration-control text-input"
disabled={props.disabled}
value={value}
onChange={(e: React.FormEvent<HTMLInputElement>) =>

View File

@@ -73,16 +73,20 @@ export const SceneGallerySelect: React.FC<ISceneGallerySelect> = props => {
const onChange = (selectedItems: ValueType<Option>) => {
const selectedItem = getSelectedValues(selectedItems)[0];
props.onSelect(galleries.find(g => g.id === selectedItem.value));
props.onSelect(selectedItem ? galleries.find(g => g.id === selectedItem) : undefined);
};
const initialId = props.initialId ? [props.initialId] : [];
const selectedOptions: Option[] = props.initialId
? items.filter(item => props.initialId?.indexOf(item.value) !== -1)
: [];
return (
<SelectComponent
className="input-control"
onChange={onChange}
isLoading={loading}
items={items}
initialIds={initialId}
selectedOptions={selectedOptions}
/>
);
};
@@ -207,10 +211,15 @@ export const StudioSelect: React.FC<IFilterProps> = props => {
const { data, loading } = StashService.useAllStudiosForFilter();
const normalizedData = data?.allStudios ?? [];
const items: Option[] = normalizedData.map(item => ({
const items = (normalizedData.length > 0
? [{ name: "None", id: "0" }, ...normalizedData]
: []
).map(item => ({
value: item.id,
label: item.name
}));
const placeholder = props.noSelectionString ?? "Select studio...";
const selectedOptions: Option[] = props.ids
? items.filter(item => props.ids?.indexOf(item.value) !== -1)
@@ -358,6 +367,7 @@ const SelectComponent: React.FC<ISelectProps & ITypeProps> = ({
options,
value: selectedOptions,
className,
classNamePrefix: "react-select",
onChange,
isMulti,
isClearable,

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -23,16 +23,40 @@ body {
-moz-osx-font-smoothing: grayscale;
margin: 0;
padding: 4rem 0 0 0;
@media (min-width: 600px) {
min-width: 845px;
}
}
code {
a {
color: $primary;
}
code, .code {
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace;
}
.input-control, .text-input {
color: $text-color;
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);
&:focus {
color: $text-color;
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);
}
}
.text-input, .text-input:focus {
background-color: $textfield-bg;
}
.input-control, .input-control:focus {
background-color: $secondary;
}
.table-list a {
color: $text-color;
}
.table-list table {
width: inherit;
}
@@ -56,40 +80,84 @@ code {
@media (min-width: 576px) {
.zoom-0 {
width: 15rem;
width: 240px;
.scene-card-video {
max-height: 180px;
}
.previewable.portrait {
height: 180px;
}
}
.zoom-1 {
width: 20rem;
width: 320px;
.scene-card-video {
max-height: 240px;
}
.previewable.portrait {
height: 240px;
}
}
.zoom-2 {
width: 30rem;
width: 480px;
.scene-card-video {
max-height: 360px;
}
.previewable.portrait {
height: 360px;
}
}
.zoom-3 {
width: 40rem;
width: 640px;
.scene-card-video {
max-height: 480px;
}
.portrait {
height: 480px;
}
}
}
.scene-card-video {
height: auto;
width: 100%;
.zoom-0 {
height: 11.25rem;
}
.zoom-1 {
height: 15rem;
/* this is a bit of a hack, because we can't supply direct class names
to the react-select controls */
div.react-select__control {
background-color: $secondary;
border-color: $secondary;
color: $text-color;
cursor: pointer;
.react-select__single-value, .react-select__input {
color: $text-color;
}
.zoom-2 {
height: 22.5rem;
.react-select__multi-value {
background-color: $muted-gray;
color: $text-color;
}
}
.zoom-3 {
height: 30rem;
div.react-select__menu {
background-color: $secondary;
color: $text-color;
.react-select__option {
color: $text-color;
}
.react-select__option--is-focused {
background-color: #8a9ba826;
cursor: pointer;
}
}
@@ -155,7 +223,8 @@ code {
.operation-container {
display: flex;
justify-content: center;
margin: 10px auto;
align-items: center;
margin: 0 auto 10px;
}
.filter-item,
@@ -294,6 +363,10 @@ code {
padding: 0;
}
.fa-icon {
margin-left: 0;
}
.btn {
white-space: nowrap;
}

View File

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

View File

@@ -25,6 +25,7 @@ $pre-color: $text-color;
$navbar-dark-color: rgb(245, 248, 250);
$popover-bg: $secondary;
$dark-text: #182026;
$textfield-bg: rgba(16,22,26,.3);
@import "node_modules/bootstrap/scss/bootstrap";
@@ -129,10 +130,6 @@ hr {
padding-right: 0;
}
}
a {
color: $text-color;
}
}
.popover {

View File

@@ -63,6 +63,7 @@ const renderInputGroup = (options: {
<td>{options.title}</td>
<td>
<Form.Control
className="text-input"
readOnly={!options.isEditing}
plaintext={!options.isEditing}
defaultValue={options.value}
@@ -87,6 +88,7 @@ const renderHtmlSelect = (options: {
<td>
<Form.Control
as="select"
className="input-control"
disabled={!options.isEditing}
plaintext={!options.isEditing}
value={options.value?.toString()}