mirror of
https://github.com/stashapp/stash.git
synced 2026-06-11 07:41:08 -05:00
Compare commits
7 Commits
localisati
...
releases/0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b3d84187a3 | ||
|
|
b8263535ea | ||
|
|
c78aa7d99c | ||
|
|
c8379c0281 | ||
|
|
a95bf348f4 | ||
|
|
0fc55d15fa | ||
|
|
cbb1c04fd5 |
5
.github/workflows/build.yml
vendored
5
.github/workflows/build.yml
vendored
@@ -2,7 +2,10 @@ name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ develop, master ]
|
||||
branches:
|
||||
- develop
|
||||
- master
|
||||
- 'releases/**'
|
||||
pull_request:
|
||||
release:
|
||||
types: [ published ]
|
||||
|
||||
1
.github/workflows/golangci-lint.yml
vendored
1
.github/workflows/golangci-lint.yml
vendored
@@ -6,6 +6,7 @@ on:
|
||||
branches:
|
||||
- master
|
||||
- develop
|
||||
- 'releases/**'
|
||||
pull_request:
|
||||
|
||||
env:
|
||||
|
||||
@@ -322,6 +322,11 @@ func (s *Manager) BackupDatabase(download bool) (string, string, error) {
|
||||
backupPath = f.Name()
|
||||
backupName = s.Database.DatabaseBackupPath("")
|
||||
f.Close()
|
||||
|
||||
// delete the temp file so that the backup operation can create it
|
||||
if err := os.Remove(backupPath); err != nil {
|
||||
return "", "", fmt.Errorf("could not remove temporary backup file %v: %w", backupPath, err)
|
||||
}
|
||||
} else {
|
||||
backupDir := s.Config.GetBackupDirectoryPathOrDefault()
|
||||
if backupDir != "" {
|
||||
|
||||
@@ -195,7 +195,6 @@ export const GalleryList: React.FC<IGalleryList> = ({
|
||||
selectable
|
||||
>
|
||||
<ItemList
|
||||
zoomable
|
||||
view={view}
|
||||
otherOperations={otherOperations}
|
||||
addKeybinds={addKeybinds}
|
||||
|
||||
@@ -226,7 +226,6 @@ export const GroupList: React.FC<IGroupList> = ({
|
||||
selectable={selectable}
|
||||
>
|
||||
<ItemList
|
||||
zoomable
|
||||
view={view}
|
||||
otherOperations={otherOperations}
|
||||
addKeybinds={addKeybinds}
|
||||
|
||||
@@ -169,6 +169,7 @@ export const ImageCard: React.FC<IImageCardProps> = (
|
||||
<ImagePreview
|
||||
loop={video}
|
||||
autoPlay={video}
|
||||
playsInline={video}
|
||||
className="image-card-preview-image"
|
||||
alt={props.image.title ?? ""}
|
||||
src={source}
|
||||
|
||||
@@ -370,6 +370,7 @@ const ImagePage: React.FC<IProps> = ({ image }) => {
|
||||
<ImageView
|
||||
loop={image.visual_files[0].__typename == "VideoFile"}
|
||||
autoPlay={image.visual_files[0].__typename == "VideoFile"}
|
||||
playsInline={image.visual_files[0].__typename == "VideoFile"}
|
||||
controls={image.visual_files[0].__typename == "VideoFile"}
|
||||
className="m-sm-auto no-gutter image-image"
|
||||
style={
|
||||
|
||||
@@ -464,7 +464,6 @@ export const ImageList: React.FC<IImageList> = ({
|
||||
selectable
|
||||
>
|
||||
<ItemList
|
||||
zoomable
|
||||
view={view}
|
||||
otherOperations={otherOperations}
|
||||
addKeybinds={addKeybinds}
|
||||
|
||||
@@ -39,6 +39,7 @@ export const ImageWallItem: React.FC<RenderImageProps & IExtraProps> = (
|
||||
<ImagePreview
|
||||
loop={video}
|
||||
muted={video}
|
||||
playsInline={video}
|
||||
autoPlay={video}
|
||||
key={props.photo.key}
|
||||
style={imgStyle}
|
||||
|
||||
@@ -2,8 +2,8 @@ import React from "react";
|
||||
import { QueryResult } from "@apollo/client";
|
||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
|
||||
import { ListFilter } from "./ListFilter";
|
||||
import { ListViewOptions } from "./ListViewOptions";
|
||||
import { PageSizeSelector, SearchTermInput, SortBySelect } from "./ListFilter";
|
||||
import { ListViewButtonGroup } from "./ListViewOptions";
|
||||
import {
|
||||
IListFilterOperation,
|
||||
ListOperationButtons,
|
||||
@@ -11,6 +11,8 @@ import {
|
||||
import { ButtonGroup, ButtonToolbar } from "react-bootstrap";
|
||||
import { View } from "./views";
|
||||
import { IListSelect, useFilterOperations } from "./util";
|
||||
import { SavedFilterDropdown } from "./SavedFilterList";
|
||||
import { FilterButton } from "./Filters/FilterButton";
|
||||
|
||||
export interface IItemListOperation<T extends QueryResult> {
|
||||
text: string;
|
||||
@@ -63,34 +65,47 @@ export const FilteredListToolbar: React.FC<IFilteredListToolbar> = ({
|
||||
|
||||
return (
|
||||
<ButtonToolbar className="filtered-list-toolbar">
|
||||
<SearchTermInput filter={filter} onFilterUpdate={setFilter} />
|
||||
|
||||
<ButtonGroup>
|
||||
{showEditFilter && (
|
||||
<ListFilter
|
||||
onFilterUpdate={setFilter}
|
||||
filter={filter}
|
||||
openFilterDialog={() => showEditFilter()}
|
||||
view={view}
|
||||
/>
|
||||
)}
|
||||
<ListOperationButtons
|
||||
onSelectAll={onSelectAll}
|
||||
onSelectNone={onSelectNone}
|
||||
otherOperations={operations}
|
||||
itemsSelected={selectedIds.size > 0}
|
||||
onEdit={onEdit}
|
||||
onDelete={onDelete}
|
||||
<SavedFilterDropdown
|
||||
filter={filter}
|
||||
onSetFilter={setFilter}
|
||||
view={view}
|
||||
/>
|
||||
<ButtonGroup>
|
||||
<ListViewOptions
|
||||
displayMode={filter.displayMode}
|
||||
displayModeOptions={filterOptions.displayModeOptions}
|
||||
onSetDisplayMode={setDisplayMode}
|
||||
zoomIndex={zoomable ? filter.zoomIndex : undefined}
|
||||
onSetZoom={zoomable ? setZoom : undefined}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
<FilterButton onClick={() => showEditFilter()} count={filter.count()} />
|
||||
</ButtonGroup>
|
||||
<ButtonGroup></ButtonGroup>
|
||||
|
||||
<SortBySelect
|
||||
sortBy={filter.sortBy}
|
||||
sortDirection={filter.sortDirection}
|
||||
options={filterOptions.sortByOptions}
|
||||
onChangeSortBy={(e) => setFilter(filter.setSortBy(e ?? undefined))}
|
||||
onChangeSortDirection={() => setFilter(filter.toggleSortDirection())}
|
||||
onReshuffleRandomSort={() => setFilter(filter.reshuffleRandomSort())}
|
||||
/>
|
||||
|
||||
<PageSizeSelector
|
||||
pageSize={filter.itemsPerPage}
|
||||
setPageSize={(size) => setFilter(filter.setPageSize(size))}
|
||||
/>
|
||||
|
||||
<ListOperationButtons
|
||||
onSelectAll={onSelectAll}
|
||||
onSelectNone={onSelectNone}
|
||||
otherOperations={operations}
|
||||
itemsSelected={selectedIds.size > 0}
|
||||
onEdit={onEdit}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
|
||||
<ListViewButtonGroup
|
||||
displayMode={filter.displayMode}
|
||||
displayModeOptions={filterOptions.displayModeOptions}
|
||||
onSetDisplayMode={setDisplayMode}
|
||||
zoomIndex={zoomable ? filter.zoomIndex : undefined}
|
||||
onSetZoom={zoomable ? setZoom : undefined}
|
||||
/>
|
||||
</ButtonToolbar>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -44,6 +44,8 @@ import {
|
||||
} from "./FilteredListToolbar";
|
||||
import { PagedList } from "./PagedList";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { useZoomKeybinds } from "./ZoomSlider";
|
||||
import { DisplayMode } from "src/models/list-filter/types";
|
||||
|
||||
interface IFilteredItemList<T extends QueryResult, E extends IHasID = IHasID> {
|
||||
filterStateProps: IFilterStateHook;
|
||||
@@ -113,7 +115,6 @@ export function useFilteredItemList<
|
||||
|
||||
interface IItemListProps<T extends QueryResult, E extends IHasID> {
|
||||
view?: View;
|
||||
zoomable?: boolean;
|
||||
otherOperations?: IItemListOperation<T>[];
|
||||
renderContent: (
|
||||
result: T,
|
||||
@@ -145,7 +146,6 @@ export const ItemList = <T extends QueryResult, E extends IHasID>(
|
||||
) => {
|
||||
const {
|
||||
view,
|
||||
zoomable,
|
||||
otherOperations,
|
||||
renderContent,
|
||||
renderEditDialog,
|
||||
@@ -217,6 +217,15 @@ export const ItemList = <T extends QueryResult, E extends IHasID>(
|
||||
showEditFilter,
|
||||
});
|
||||
|
||||
const zoomable =
|
||||
filter.displayMode === DisplayMode.Grid ||
|
||||
filter.displayMode === DisplayMode.Wall;
|
||||
|
||||
useZoomKeybinds({
|
||||
zoomIndex: zoomable ? filter.zoomIndex : undefined,
|
||||
onChangeZoom: (zoom) => updateFilter(filter.setZoom(zoom)),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (addKeybinds) {
|
||||
const unbindExtras = addKeybinds(result, effectiveFilter, selectedIds);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import cloneDeep from "lodash-es/cloneDeep";
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
@@ -23,17 +22,14 @@ import {
|
||||
import { Icon } from "../Shared/Icon";
|
||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||
import useFocus from "src/utils/focus";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { SavedFilterDropdown } from "./SavedFilterList";
|
||||
import { useIntl } from "react-intl";
|
||||
import {
|
||||
faCaretDown,
|
||||
faCaretUp,
|
||||
faCheck,
|
||||
faRandom,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FilterButton } from "./Filters/FilterButton";
|
||||
import { useDebounce } from "src/hooks/debounce";
|
||||
import { View } from "./views";
|
||||
import { ClearableInput } from "../Shared/ClearableInput";
|
||||
import { useStopWheelScroll } from "src/utils/form";
|
||||
import { ISortByOption } from "src/models/list-filter/filter-options";
|
||||
@@ -318,109 +314,3 @@ export const SortBySelect: React.FC<{
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
interface IListFilterProps {
|
||||
onFilterUpdate: (newFilter: ListFilterModel) => void;
|
||||
filter: ListFilterModel;
|
||||
view?: View;
|
||||
openFilterDialog: () => void;
|
||||
}
|
||||
|
||||
export const ListFilter: React.FC<IListFilterProps> = ({
|
||||
onFilterUpdate,
|
||||
filter,
|
||||
openFilterDialog,
|
||||
view,
|
||||
}) => {
|
||||
const filterOptions = filter.options;
|
||||
|
||||
useEffect(() => {
|
||||
Mousetrap.bind("r", () => onReshuffleRandomSort());
|
||||
|
||||
return () => {
|
||||
Mousetrap.unbind("r");
|
||||
};
|
||||
});
|
||||
|
||||
function onChangePageSize(pp: number) {
|
||||
const newFilter = cloneDeep(filter);
|
||||
newFilter.itemsPerPage = pp;
|
||||
newFilter.currentPage = 1;
|
||||
onFilterUpdate(newFilter);
|
||||
}
|
||||
|
||||
function onChangeSortDirection() {
|
||||
const newFilter = cloneDeep(filter);
|
||||
if (filter.sortDirection === SortDirectionEnum.Asc) {
|
||||
newFilter.sortDirection = SortDirectionEnum.Desc;
|
||||
} else {
|
||||
newFilter.sortDirection = SortDirectionEnum.Asc;
|
||||
}
|
||||
|
||||
onFilterUpdate(newFilter);
|
||||
}
|
||||
|
||||
function onChangeSortBy(eventKey: string | null) {
|
||||
const newFilter = cloneDeep(filter);
|
||||
newFilter.sortBy = eventKey ?? undefined;
|
||||
newFilter.currentPage = 1;
|
||||
onFilterUpdate(newFilter);
|
||||
}
|
||||
|
||||
function onReshuffleRandomSort() {
|
||||
const newFilter = cloneDeep(filter);
|
||||
newFilter.currentPage = 1;
|
||||
newFilter.randomSeed = -1;
|
||||
onFilterUpdate(newFilter);
|
||||
}
|
||||
|
||||
function render() {
|
||||
return (
|
||||
<>
|
||||
<div className="d-flex">
|
||||
<SearchTermInput filter={filter} onFilterUpdate={onFilterUpdate} />
|
||||
</div>
|
||||
|
||||
<ButtonGroup className="mr-2">
|
||||
<SavedFilterDropdown
|
||||
filter={filter}
|
||||
onSetFilter={(f) => {
|
||||
onFilterUpdate(f);
|
||||
}}
|
||||
view={view}
|
||||
/>
|
||||
<OverlayTrigger
|
||||
placement="top"
|
||||
overlay={
|
||||
<Tooltip id="filter-tooltip">
|
||||
<FormattedMessage id="search_filter.name" />
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<FilterButton
|
||||
onClick={() => openFilterDialog()}
|
||||
count={filter.count()}
|
||||
/>
|
||||
</OverlayTrigger>
|
||||
</ButtonGroup>
|
||||
|
||||
<SortBySelect
|
||||
className="mr-2"
|
||||
sortBy={filter.sortBy}
|
||||
sortDirection={filter.sortDirection}
|
||||
options={filterOptions.sortByOptions}
|
||||
onChangeSortBy={onChangeSortBy}
|
||||
onChangeSortDirection={onChangeSortDirection}
|
||||
onReshuffleRandomSort={onReshuffleRandomSort}
|
||||
/>
|
||||
|
||||
<PageSizeSelector
|
||||
pageSize={filter.itemsPerPage}
|
||||
setPageSize={onChangePageSize}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return render();
|
||||
};
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import React, { PropsWithChildren, useEffect } from "react";
|
||||
import {
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Dropdown,
|
||||
OverlayTrigger,
|
||||
Tooltip,
|
||||
} from "react-bootstrap";
|
||||
import React, { PropsWithChildren, useEffect, useMemo } from "react";
|
||||
import { Button, ButtonGroup, Dropdown } from "react-bootstrap";
|
||||
import Mousetrap from "mousetrap";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
|
||||
@@ -108,8 +102,8 @@ export const ListOperationButtons: React.FC<IListOperationButtonsProps> = ({
|
||||
};
|
||||
});
|
||||
|
||||
function maybeRenderButtons() {
|
||||
const buttons = (otherOperations ?? []).filter((o) => {
|
||||
const buttons = useMemo(() => {
|
||||
const ret = (otherOperations ?? []).filter((o) => {
|
||||
if (!o.icon) {
|
||||
return false;
|
||||
}
|
||||
@@ -120,16 +114,17 @@ export const ListOperationButtons: React.FC<IListOperationButtonsProps> = ({
|
||||
|
||||
return o.isDisplayed();
|
||||
});
|
||||
|
||||
if (itemsSelected) {
|
||||
if (onEdit) {
|
||||
buttons.push({
|
||||
ret.push({
|
||||
icon: faPencilAlt,
|
||||
text: intl.formatMessage({ id: "actions.edit" }),
|
||||
onClick: onEdit,
|
||||
});
|
||||
}
|
||||
if (onDelete) {
|
||||
buttons.push({
|
||||
ret.push({
|
||||
icon: faTrash,
|
||||
text: intl.formatMessage({ id: "actions.delete" }),
|
||||
onClick: onDelete,
|
||||
@@ -138,58 +133,57 @@ export const ListOperationButtons: React.FC<IListOperationButtonsProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
if (buttons.length > 0) {
|
||||
return (
|
||||
<ButtonGroup className="ml-2">
|
||||
{buttons.map((button) => {
|
||||
return (
|
||||
<OverlayTrigger
|
||||
overlay={<Tooltip id="edit">{button.text}</Tooltip>}
|
||||
key={button.text}
|
||||
>
|
||||
<Button
|
||||
variant={button.buttonVariant ?? "secondary"}
|
||||
onClick={button.onClick}
|
||||
>
|
||||
{button.icon ? <Icon icon={button.icon} /> : undefined}
|
||||
</Button>
|
||||
</OverlayTrigger>
|
||||
);
|
||||
})}
|
||||
</ButtonGroup>
|
||||
);
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}, [otherOperations, itemsSelected, onEdit, onDelete, intl]);
|
||||
|
||||
function renderSelectAll() {
|
||||
if (onSelectAll) {
|
||||
return (
|
||||
<Dropdown.Item
|
||||
key="select-all"
|
||||
className="bg-secondary text-white"
|
||||
onClick={() => onSelectAll?.()}
|
||||
>
|
||||
<FormattedMessage id="actions.select_all" />
|
||||
</Dropdown.Item>
|
||||
);
|
||||
}
|
||||
}
|
||||
const operationButtons = useMemo(() => {
|
||||
return (
|
||||
<>
|
||||
{buttons.map((button) => {
|
||||
return (
|
||||
<Button
|
||||
key={button.text}
|
||||
variant={button.buttonVariant ?? "secondary"}
|
||||
onClick={button.onClick}
|
||||
title={button.text}
|
||||
>
|
||||
<Icon icon={button.icon!} />
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}, [buttons]);
|
||||
|
||||
function renderSelectNone() {
|
||||
if (onSelectNone) {
|
||||
return (
|
||||
<Dropdown.Item
|
||||
key="select-none"
|
||||
className="bg-secondary text-white"
|
||||
onClick={() => onSelectNone?.()}
|
||||
>
|
||||
<FormattedMessage id="actions.select_none" />
|
||||
</Dropdown.Item>
|
||||
);
|
||||
const moreDropdown = useMemo(() => {
|
||||
function renderSelectAll() {
|
||||
if (onSelectAll) {
|
||||
return (
|
||||
<Dropdown.Item
|
||||
key="select-all"
|
||||
className="bg-secondary text-white"
|
||||
onClick={() => onSelectAll?.()}
|
||||
>
|
||||
<FormattedMessage id="actions.select_all" />
|
||||
</Dropdown.Item>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function renderSelectNone() {
|
||||
if (onSelectNone) {
|
||||
return (
|
||||
<Dropdown.Item
|
||||
key="select-none"
|
||||
className="bg-secondary text-white"
|
||||
onClick={() => onSelectNone?.()}
|
||||
>
|
||||
<FormattedMessage id="actions.select_none" />
|
||||
</Dropdown.Item>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function renderMore() {
|
||||
const options = [renderSelectAll(), renderSelectNone()].filter((o) => o);
|
||||
|
||||
if (otherOperations) {
|
||||
@@ -224,13 +218,19 @@ export const ListOperationButtons: React.FC<IListOperationButtonsProps> = ({
|
||||
{options.length > 0 ? options : undefined}
|
||||
</OperationDropdown>
|
||||
);
|
||||
}, [otherOperations, onSelectAll, onSelectNone]);
|
||||
|
||||
// don't render anything if there are no buttons or operations
|
||||
if (buttons.length === 0 && !moreDropdown) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{maybeRenderButtons()}
|
||||
|
||||
<ButtonGroup className="ml-2">{renderMore()}</ButtonGroup>
|
||||
<ButtonGroup>
|
||||
{operationButtons}
|
||||
{moreDropdown}
|
||||
</ButtonGroup>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import Mousetrap from "mousetrap";
|
||||
import { Button, Dropdown, Overlay, Popover } from "react-bootstrap";
|
||||
import {
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Dropdown,
|
||||
Overlay,
|
||||
OverlayTrigger,
|
||||
Popover,
|
||||
Tooltip,
|
||||
} from "react-bootstrap";
|
||||
import { DisplayMode } from "src/models/list-filter/types";
|
||||
import { useIntl } from "react-intl";
|
||||
import { IntlShape, useIntl } from "react-intl";
|
||||
import { Icon } from "../Shared/Icon";
|
||||
import {
|
||||
faChevronDown,
|
||||
@@ -53,6 +61,10 @@ function getLabelId(option: DisplayMode) {
|
||||
return `display_mode.${displayModeId}`;
|
||||
}
|
||||
|
||||
function getLabel(intl: IntlShape, option: DisplayMode) {
|
||||
return intl.formatMessage({ id: getLabelId(option) });
|
||||
}
|
||||
|
||||
export const ListViewOptions: React.FC<IListViewOptionsProps> = ({
|
||||
zoomIndex,
|
||||
onSetZoom,
|
||||
@@ -60,9 +72,6 @@ export const ListViewOptions: React.FC<IListViewOptionsProps> = ({
|
||||
onSetDisplayMode,
|
||||
displayModeOptions,
|
||||
}) => {
|
||||
const minZoom = 0;
|
||||
const maxZoom = 3;
|
||||
|
||||
const intl = useIntl();
|
||||
|
||||
const overlayTarget = useRef(null);
|
||||
@@ -84,18 +93,20 @@ export const ListViewOptions: React.FC<IListViewOptionsProps> = ({
|
||||
onSetDisplayMode(DisplayMode.Wall);
|
||||
}
|
||||
});
|
||||
Mousetrap.bind("v t", () => {
|
||||
if (displayModeOptions.includes(DisplayMode.Tagger)) {
|
||||
onSetDisplayMode(DisplayMode.Tagger);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
Mousetrap.unbind("v g");
|
||||
Mousetrap.unbind("v l");
|
||||
Mousetrap.unbind("v w");
|
||||
Mousetrap.unbind("v t");
|
||||
};
|
||||
});
|
||||
|
||||
function getLabel(option: DisplayMode) {
|
||||
return intl.formatMessage({ id: getLabelId(option) });
|
||||
}
|
||||
|
||||
function onChangeZoom(v: number) {
|
||||
if (onSetZoom) {
|
||||
onSetZoom(v);
|
||||
@@ -110,7 +121,7 @@ export const ListViewOptions: React.FC<IListViewOptionsProps> = ({
|
||||
variant="secondary"
|
||||
title={intl.formatMessage(
|
||||
{ id: "display_mode.label_current" },
|
||||
{ current: getLabel(displayMode) }
|
||||
{ current: getLabel(intl, displayMode) }
|
||||
)}
|
||||
onClick={() => setShowOptions(!showOptions)}
|
||||
>
|
||||
@@ -134,8 +145,6 @@ export const ListViewOptions: React.FC<IListViewOptionsProps> = ({
|
||||
displayMode === DisplayMode.Wall) ? (
|
||||
<div className="zoom-slider-container">
|
||||
<ZoomSelect
|
||||
minZoom={minZoom}
|
||||
maxZoom={maxZoom}
|
||||
zoomIndex={zoomIndex}
|
||||
onChangeZoom={onChangeZoom}
|
||||
/>
|
||||
@@ -150,7 +159,7 @@ export const ListViewOptions: React.FC<IListViewOptionsProps> = ({
|
||||
onSetDisplayMode(option);
|
||||
}}
|
||||
>
|
||||
<Icon icon={getIcon(option)} /> {getLabel(option)}
|
||||
<Icon icon={getIcon(option)} /> {getLabel(intl, option)}
|
||||
</Dropdown.Item>
|
||||
))}
|
||||
</div>
|
||||
@@ -161,3 +170,48 @@ export const ListViewOptions: React.FC<IListViewOptionsProps> = ({
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const ListViewButtonGroup: React.FC<IListViewOptionsProps> = ({
|
||||
zoomIndex,
|
||||
onSetZoom,
|
||||
displayMode,
|
||||
onSetDisplayMode,
|
||||
displayModeOptions,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
return (
|
||||
<>
|
||||
{displayModeOptions.length > 1 && (
|
||||
<ButtonGroup>
|
||||
{displayModeOptions.map((option) => (
|
||||
<OverlayTrigger
|
||||
key={option}
|
||||
overlay={
|
||||
<Tooltip id="display-mode-tooltip">
|
||||
{getLabel(intl, option)}
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
variant="secondary"
|
||||
active={displayMode === option}
|
||||
onClick={() => onSetDisplayMode(option)}
|
||||
>
|
||||
<Icon icon={getIcon(option)} />
|
||||
</Button>
|
||||
</OverlayTrigger>
|
||||
))}
|
||||
</ButtonGroup>
|
||||
)}
|
||||
<div className="zoom-slider-container">
|
||||
{onSetZoom &&
|
||||
zoomIndex !== undefined &&
|
||||
(displayMode === DisplayMode.Grid ||
|
||||
displayMode === DisplayMode.Wall) ? (
|
||||
<ZoomSelect zoomIndex={zoomIndex} onChangeZoom={onSetZoom} />
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,19 +2,14 @@ import React, { useEffect } from "react";
|
||||
import Mousetrap from "mousetrap";
|
||||
import { Form } from "react-bootstrap";
|
||||
|
||||
export interface IZoomSelectProps {
|
||||
minZoom: number;
|
||||
maxZoom: number;
|
||||
zoomIndex: number;
|
||||
onChangeZoom: (v: number) => void;
|
||||
}
|
||||
const minZoom = 0;
|
||||
const maxZoom = 3;
|
||||
|
||||
export const ZoomSelect: React.FC<IZoomSelectProps> = ({
|
||||
minZoom,
|
||||
maxZoom,
|
||||
zoomIndex,
|
||||
onChangeZoom,
|
||||
}) => {
|
||||
export function useZoomKeybinds(props: {
|
||||
zoomIndex: number | undefined;
|
||||
onChangeZoom: (v: number) => void;
|
||||
}) {
|
||||
const { zoomIndex, onChangeZoom } = props;
|
||||
useEffect(() => {
|
||||
Mousetrap.bind("+", () => {
|
||||
if (zoomIndex !== undefined && zoomIndex < maxZoom) {
|
||||
@@ -32,7 +27,17 @@ export const ZoomSelect: React.FC<IZoomSelectProps> = ({
|
||||
Mousetrap.unbind("-");
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export interface IZoomSelectProps {
|
||||
zoomIndex: number;
|
||||
onChangeZoom: (v: number) => void;
|
||||
}
|
||||
|
||||
export const ZoomSelect: React.FC<IZoomSelectProps> = ({
|
||||
zoomIndex,
|
||||
onChangeZoom,
|
||||
}) => {
|
||||
return (
|
||||
<Form.Control
|
||||
className="zoom-slider"
|
||||
|
||||
@@ -93,7 +93,8 @@
|
||||
|
||||
// hide zoom slider in xs viewport
|
||||
@include media-breakpoint-down(xs) {
|
||||
.display-mode-menu .zoom-slider-container {
|
||||
.display-mode-menu .zoom-slider-container,
|
||||
.zoom-slider-container {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -916,6 +917,8 @@ input[type="range"].zoom-slider {
|
||||
}
|
||||
|
||||
.filtered-list-toolbar {
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
justify-content: center;
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
@@ -933,8 +936,10 @@ input[type="range"].zoom-slider {
|
||||
}
|
||||
}
|
||||
|
||||
.btn.display-mode-select {
|
||||
margin-left: 0.5rem;
|
||||
// set the width of the zoom-slider-container to prevent buttons moving when
|
||||
// the slider appears/disappears
|
||||
.zoom-slider-container {
|
||||
min-width: 60px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -946,10 +951,6 @@ input[type="range"].zoom-slider {
|
||||
}
|
||||
}
|
||||
|
||||
.search-term-input {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.custom-field-filter {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
|
||||
@@ -146,6 +146,22 @@ export const PerformerScrapeDialog: React.FC<IPerformerScrapeDialogProps> = (
|
||||
return;
|
||||
}
|
||||
|
||||
// #6257 - it is possible (though unsupported) to have multiple stash IDs for the same
|
||||
// endpoint; in that case, we should prefer the one matching the scraped remote site ID
|
||||
// if it exists
|
||||
const stashIDs = (props.performer.stash_ids ?? []).filter(
|
||||
(s) => s.endpoint === endpoint
|
||||
);
|
||||
if (stashIDs.length > 1 && props.scraped.remote_site_id) {
|
||||
const matchingID = stashIDs.find(
|
||||
(s) => s.stash_id === props.scraped.remote_site_id
|
||||
);
|
||||
if (matchingID) {
|
||||
return matchingID.stash_id;
|
||||
}
|
||||
}
|
||||
|
||||
// otherwise, return the first stash ID for the endpoint
|
||||
return props.performer.stash_ids?.find((s) => s.endpoint === endpoint)
|
||||
?.stash_id;
|
||||
}
|
||||
|
||||
@@ -329,7 +329,6 @@ export const PerformerList: React.FC<IPerformerList> = ({
|
||||
selectable
|
||||
>
|
||||
<ItemList
|
||||
zoomable
|
||||
view={view}
|
||||
otherOperations={otherOperations}
|
||||
addKeybinds={addKeybinds}
|
||||
|
||||
@@ -70,6 +70,7 @@ import {
|
||||
ToolbarSelectionSection,
|
||||
} from "../List/ListToolbar";
|
||||
import { ListResultsHeader } from "../List/ListResultsHeader";
|
||||
import { useZoomKeybinds } from "../List/ZoomSlider";
|
||||
|
||||
function renderMetadataByline(result: GQL.FindScenesQueryResult) {
|
||||
const duration = result?.data?.findScenes?.duration;
|
||||
@@ -519,6 +520,10 @@ export const FilteredSceneList = (props: IFilteredScenes) => {
|
||||
Mousetrap.unbind("d d");
|
||||
};
|
||||
});
|
||||
useZoomKeybinds({
|
||||
zoomIndex: filter.zoomIndex,
|
||||
onChangeZoom: (zoom) => setFilter(filter.setZoom(zoom)),
|
||||
});
|
||||
|
||||
const onCloseEditDelete = useCloseEditDelete({
|
||||
closeModal,
|
||||
|
||||
@@ -138,7 +138,6 @@ export const SceneMarkerList: React.FC<ISceneMarkerList> = ({
|
||||
selectable
|
||||
>
|
||||
<ItemList
|
||||
zoomable
|
||||
view={view}
|
||||
otherOperations={otherOperations}
|
||||
addKeybinds={addKeybinds}
|
||||
|
||||
@@ -96,6 +96,7 @@ export const MarkerWallItem: React.FC<
|
||||
loop={video}
|
||||
muted={!video || !playSound || !active}
|
||||
autoPlay={video}
|
||||
playsInline={video}
|
||||
key={props.photo.key}
|
||||
src={props.photo.src}
|
||||
width={width}
|
||||
|
||||
@@ -89,6 +89,7 @@ export const SceneWallItem: React.FC<
|
||||
loop={video}
|
||||
muted={!video || !playSound || !active}
|
||||
autoPlay={video}
|
||||
playsInline={video}
|
||||
key={props.photo.key}
|
||||
src={props.photo.src}
|
||||
width={width}
|
||||
|
||||
@@ -188,7 +188,6 @@ export const StudioList: React.FC<IStudioList> = ({
|
||||
selectable
|
||||
>
|
||||
<ItemList
|
||||
zoomable
|
||||
view={view}
|
||||
otherOperations={otherOperations}
|
||||
addKeybinds={addKeybinds}
|
||||
|
||||
@@ -367,7 +367,6 @@ export const TagList: React.FC<ITagList> = ({ filterHook, alterQuery }) => {
|
||||
>
|
||||
<ItemList
|
||||
view={view}
|
||||
zoomable
|
||||
otherOperations={otherOperations}
|
||||
addKeybinds={addKeybinds}
|
||||
renderContent={renderContent}
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
|
||||
|
||||
### 🎨 Improvements
|
||||
* **[0.29.4]** Restored display mode button group to non-scene list pages. ([#6317](https://github.com/stashapp/stash/pull/6317))
|
||||
* **[0.29.4]** Added keyboard shortcut for tagger view (`v t`). ([#6261](https://github.com/stashapp/stash/pull/6261))
|
||||
* **[0.29.2]** Returned saved filters button to the top toolbar in the Scene list. ([#6215](https://github.com/stashapp/stash/pull/6215))
|
||||
* **[0.29.2]** Top pagination can now be optionally shown in the scene list with [custom css](https://github.com/stashapp/stash/pull/6234#issue-3593190476). ([#6234](https://github.com/stashapp/stash/pull/6234))
|
||||
* **[0.29.2]** Restyled the scene list toolbar based on user feedback. ([#6215](https://github.com/stashapp/stash/pull/6215))
|
||||
@@ -33,6 +35,10 @@
|
||||
* Include IP address in login errors in log. ([#5760](https://github.com/stashapp/stash/pull/5760))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
* **[0.29.4]** Fixed zoom keyboard shortcuts not working. ([#6317](https://github.com/stashapp/stash/pull/6317))
|
||||
* **[0.29.4]** Fixed existing match stash ID sometimes not being displayed in the performer scrape dialog. ([#6257](https://github.com/stashapp/stash/pull/6257))
|
||||
* **[0.29.4]** Fixed inline videos showing as full-screen on iPhone devices. ([#6259](https://github.com/stashapp/stash/pull/6259))
|
||||
* **[0.29.4]** Fixed download backup function not working when generated directory is on a different filesystem. ([#6244](https://github.com/stashapp/stash/pull/6244))
|
||||
* **[0.29.3]** Fixed sidebar filter contents not loading. ([#6240](https://github.com/stashapp/stash/pull/6240))
|
||||
* **[0.29.2]** Fixed Play Random not playing from the current filtered scenes on scene list sub-pages. ([#6202](https://github.com/stashapp/stash/pull/6202))
|
||||
* **[0.29.2]** Fixed infinite loop in Group Sub-Groups panel. ([#6212](https://github.com/stashapp/stash/pull/6212))
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
| `v g` | Set view to grid |
|
||||
| `v l` | Set view to list |
|
||||
| `v w` | Set view to wall |
|
||||
| `v t` | Set view to tagger |
|
||||
| `+` | Increase zoom slider |
|
||||
| `-` | Decrease zoom slider |
|
||||
| `←` | Previous page of results |
|
||||
|
||||
@@ -455,6 +455,7 @@ export const LightboxComponent: React.FC<IProps> = ({
|
||||
React.createElement(image.paths.preview != "" ? "video" : "img", {
|
||||
loop: image.paths.preview != "",
|
||||
autoPlay: image.paths.preview != "",
|
||||
playsInline: image.paths.preview != "",
|
||||
src:
|
||||
image.paths.preview != ""
|
||||
? image.paths.preview ?? ""
|
||||
|
||||
Reference in New Issue
Block a user