-
- {searchTerm}
-
- }
- onClick={() => onEditSearchTerm?.()}
- onRemove={() => onRemoveSearchTerm?.()}
- />
+ if (cutoff && filterTags.length > cutoff) {
+ const visibleCriteria = filterTags.slice(0, cutoff);
+ const hiddenCriteria = filterTags.slice(cutoff);
+
+ return (
+
+ {visibleCriteria}
+
+ {criteria.length >= 3 && (
+
+ )}
+
);
}
- const visibleCriteria = cutoff ? filterTags.slice(0, cutoff) : filterTags;
- const hiddenCriteria = cutoff ? filterTags.slice(cutoff) : [];
-
return (
- {visibleCriteria}
-
- {filterTags.length >= 3 && (
+ {filterTags}
+ {criteria.length >= 3 && (
@@ -533,9 +520,6 @@ export const FilteredSceneList = (props: IFilteredScenes) => {
const intl = useIntl();
const history = useHistory();
- const searchFocus = useFocus();
- const [, setSearchFocus] = searchFocus;
-
const { filterHook, defaultSort, view, alterQuery, fromGroupId } = props;
// States
@@ -790,7 +774,6 @@ export const FilteredSceneList = (props: IFilteredScenes) => {
sidebarOpen={showSidebar}
onClose={() => setShowSidebar(false)}
count={cachedResult.loading ? undefined : totalCount}
- focus={searchFocus}
/>
@@ -800,7 +783,6 @@ export const FilteredSceneList = (props: IFilteredScenes) => {
})}
>
{
onToggleSidebar={() => setShowSidebar(!showSidebar)}
onEditCriterion={(c) => showEditFilter(c.criterionOption.type)}
onRemoveCriterion={removeCriterion}
- onRemoveAllCriterion={() => clearAllCriteria(true)}
- onEditSearchTerm={() => {
- setShowSidebar(true);
- setSearchFocus(true);
- }}
- onRemoveSearchTerm={() => setFilter(filter.clearSearchTerm())}
+ onRemoveAllCriterion={() => clearAllCriteria()}
onSelectAll={() => onSelectAll()}
onSelectNone={() => onSelectNone()}
onEdit={onEdit}
diff --git a/ui/v2.5/src/index.scss b/ui/v2.5/src/index.scss
index fb4d82a1c..c013b852a 100755
--- a/ui/v2.5/src/index.scss
+++ b/ui/v2.5/src/index.scss
@@ -698,10 +698,8 @@ div.dropdown-menu {
}
.tag-item {
- align-items: center;
background-color: $muted-gray;
color: $dark-text;
- display: flex;
font-size: 12px;
font-weight: 400;
line-height: 16px;
@@ -712,20 +710,17 @@ div.dropdown-menu {
cursor: pointer;
}
- .search-term svg {
- margin-left: 0;
- }
-
.btn {
background: none;
border: none;
bottom: 2px;
color: $dark-text;
font-size: 12px;
- line-height: 16px;
+ line-height: 1rem;
margin-right: -0.5rem;
opacity: 0.5;
padding: 0 0.5rem;
+ position: relative;
&:active,
&:hover {
diff --git a/ui/v2.5/src/models/list-filter/filter.ts b/ui/v2.5/src/models/list-filter/filter.ts
index 2a68cd6a2..ac9d9de1e 100644
--- a/ui/v2.5/src/models/list-filter/filter.ts
+++ b/ui/v2.5/src/models/list-filter/filter.ts
@@ -476,23 +476,13 @@ export class ListFilterModel {
return this.setCriteria(criteria);
}
- public clearCriteria(clearSearchTerm = false) {
+ public clearCriteria() {
const ret = this.clone();
- if (clearSearchTerm) {
- ret.searchTerm = "";
- }
ret.criteria = [];
ret.currentPage = 1;
return ret;
}
- public clearSearchTerm() {
- const ret = this.clone();
- ret.searchTerm = "";
- ret.currentPage = 1; // reset to first page
- return ret;
- }
-
public setCriteria(criteria: Criterion[]) {
const ret = this.clone();
ret.criteria = criteria;
diff --git a/ui/v2.5/src/utils/focus.ts b/ui/v2.5/src/utils/focus.ts
index cf1b20a88..f1ede47f9 100644
--- a/ui/v2.5/src/utils/focus.ts
+++ b/ui/v2.5/src/utils/focus.ts
@@ -2,14 +2,10 @@ import { useRef, useEffect, useCallback } from "react";
const useFocus = () => {
const htmlElRef = useRef(null);
- const setFocus = useCallback((selectAll?: boolean) => {
+ const setFocus = useCallback(() => {
const currentEl = htmlElRef.current;
if (currentEl) {
- if (selectAll) {
- currentEl.select();
- } else {
- currentEl.focus();
- }
+ currentEl.focus();
}
}, []);
From e23bdfa2046d682586d9bbded3de4ac4d2028866 Mon Sep 17 00:00:00 2001
From: feederbox826 <144178721+feederbox826@users.noreply.github.com>
Date: Tue, 9 Sep 2025 01:03:55 -0400
Subject: [PATCH 033/405] Add media hardware key support (#6031)
---
.../src/components/ScenePlayer/ScenePlayer.tsx | 16 ++++++++++++++++
1 file changed, 16 insertions(+)
diff --git a/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx b/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx
index 5749f6331..4440f80df 100644
--- a/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx
+++ b/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx
@@ -120,6 +120,22 @@ function handleHotkeys(player: VideoJsPlayer, event: videojs.KeyboardEvent) {
return;
}
+ const skipButtons = player.skipButtons();
+ if (skipButtons) {
+ // handle multimedia keys
+ switch (event.key) {
+ case "MediaTrackNext":
+ if (!skipButtons.onNext) return;
+ skipButtons.onNext();
+ break;
+ case "MediaTrackPrevious":
+ if (!skipButtons.onPrevious) return;
+ skipButtons.onPrevious();
+ break;
+ // MediaPlayPause handled by videojs
+ }
+ }
+
switch (event.which) {
case 32: // space
case 13: // enter
From c0ba119ebf94fddbc711f680e69e2b333781e3a8 Mon Sep 17 00:00:00 2001
From: feederbox826 <144178721+feederbox826@users.noreply.github.com>
Date: Tue, 9 Sep 2025 01:04:39 -0400
Subject: [PATCH 034/405] exclude empty regex exclude (#6023)
---
internal/manager/exclude_files.go | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/internal/manager/exclude_files.go b/internal/manager/exclude_files.go
index 6c5452d0d..7ab24b51c 100644
--- a/internal/manager/exclude_files.go
+++ b/internal/manager/exclude_files.go
@@ -60,6 +60,10 @@ func generateRegexps(patterns []string) []*regexp.Regexp {
var fileRegexps []*regexp.Regexp
for _, pattern := range patterns {
+ if pattern == "" || pattern == " " {
+ logger.Warnf("Skipping empty exclude pattern")
+ continue
+ }
if !strings.HasPrefix(pattern, "(?i)") {
pattern = "(?i)" + pattern
}
From b5b207c940b1bae137127dfeb3579e65a6a1d461 Mon Sep 17 00:00:00 2001
From: feederbox826 <144178721+feederbox826@users.noreply.github.com>
Date: Tue, 9 Sep 2025 01:07:00 -0400
Subject: [PATCH 035/405] remove ruby and faraday gem (#6020)
---
docker/ci/x86_64/Dockerfile | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/docker/ci/x86_64/Dockerfile b/docker/ci/x86_64/Dockerfile
index f0f1e242b..957da347c 100644
--- a/docker/ci/x86_64/Dockerfile
+++ b/docker/ci/x86_64/Dockerfile
@@ -12,9 +12,8 @@ RUN if [ "$TARGETPLATFORM" = "linux/arm/v6" ]; then BIN=stash-linux-arm32v6; \
FROM --platform=$TARGETPLATFORM alpine:latest AS app
COPY --from=binary /stash /usr/bin/
-RUN apk add --no-cache ca-certificates python3 py3-requests py3-requests-toolbelt py3-lxml py3-pip ffmpeg ruby tzdata vips vips-tools \
- && pip install --user --break-system-packages mechanicalsoup cloudscraper stashapp-tools \
- && gem install faraday
+RUN apk add --no-cache ca-certificates python3 py3-requests py3-requests-toolbelt py3-lxml py3-pip ffmpeg tzdata vips vips-tools \
+ && pip install --user --break-system-packages mechanicalsoup cloudscraper stashapp-tools
ENV STASH_CONFIG_FILE=/root/.stash/config.yml
# Basic build-time metadata as defined at https://github.com/opencontainers/image-spec/blob/main/annotations.md#pre-defined-annotation-keys
From fd36c0fac7051ff6dae40f3808af4a6aefa143ed Mon Sep 17 00:00:00 2001
From: gregpetersonanon
Date: Mon, 8 Sep 2025 22:10:13 -0700
Subject: [PATCH 036/405] Allow scan to continue when encountering an error
(#6073)
---
pkg/file/folder_rename_detect.go | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/pkg/file/folder_rename_detect.go b/pkg/file/folder_rename_detect.go
index 4f6d31bd5..4c057461b 100644
--- a/pkg/file/folder_rename_detect.go
+++ b/pkg/file/folder_rename_detect.go
@@ -107,7 +107,8 @@ func (s *scanJob) detectFolderMove(ctx context.Context, file scanFile) (*models.
info, err := d.Info()
if err != nil {
- return fmt.Errorf("reading info for %q: %w", path, err)
+ logger.Errorf("reading info for %q: %v", path, err)
+ return nil
}
if !s.acceptEntry(ctx, path, info) {
From b1883f3df57d868ade3fe20b9339e39755b86acd Mon Sep 17 00:00:00 2001
From: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
Date: Tue, 9 Sep 2025 16:44:51 +1000
Subject: [PATCH 037/405] Add gallery link to image lightbox (#6012)
---
ui/v2.5/src/core/files.ts | 2 +-
ui/v2.5/src/core/galleries.ts | 2 +-
ui/v2.5/src/hooks/Lightbox/Lightbox.tsx | 30 ++++++++++++++++++++----
ui/v2.5/src/hooks/Lightbox/lightbox.scss | 17 +++++++++++++-
ui/v2.5/src/hooks/Lightbox/types.ts | 12 ++++++++++
5 files changed, 55 insertions(+), 8 deletions(-)
diff --git a/ui/v2.5/src/core/files.ts b/ui/v2.5/src/core/files.ts
index d17d34d16..b90d10193 100644
--- a/ui/v2.5/src/core/files.ts
+++ b/ui/v2.5/src/core/files.ts
@@ -6,7 +6,7 @@ export interface IFile {
}
interface IObjectWithFiles {
- files?: IFile[];
+ files?: GQL.Maybe;
}
export interface IObjectWithTitleFiles extends IObjectWithFiles {
diff --git a/ui/v2.5/src/core/galleries.ts b/ui/v2.5/src/core/galleries.ts
index bedc2453e..722ba8d3b 100644
--- a/ui/v2.5/src/core/galleries.ts
+++ b/ui/v2.5/src/core/galleries.ts
@@ -6,7 +6,7 @@ interface IFile {
}
interface IGallery {
- files: IFile[];
+ files: GQL.Maybe;
folder?: GQL.Maybe;
}
diff --git a/ui/v2.5/src/hooks/Lightbox/Lightbox.tsx b/ui/v2.5/src/hooks/Lightbox/Lightbox.tsx
index 0af1e835b..6e4eb856a 100644
--- a/ui/v2.5/src/hooks/Lightbox/Lightbox.tsx
+++ b/ui/v2.5/src/hooks/Lightbox/Lightbox.tsx
@@ -44,11 +44,13 @@ import {
faSearchMinus,
faTimes,
faBars,
+ faImages,
} from "@fortawesome/free-solid-svg-icons";
import { RatingSystem } from "src/components/Shared/Rating/RatingSystem";
import { useDebounce } from "../debounce";
import { isVideo } from "src/utils/visualFile";
import { imageTitle } from "src/core/files";
+import { galleryTitle } from "src/core/galleries";
const CLASSNAME = "Lightbox";
const CLASSNAME_HEADER = `${CLASSNAME}-header`;
@@ -62,6 +64,8 @@ const CLASSNAME_OPTIONS_INLINE = `${CLASSNAME_OPTIONS}-inline`;
const CLASSNAME_RIGHT = `${CLASSNAME_HEADER}-right`;
const CLASSNAME_FOOTER = `${CLASSNAME}-footer`;
const CLASSNAME_FOOTER_LEFT = `${CLASSNAME_FOOTER}-left`;
+const CLASSNAME_FOOTER_CENTER = `${CLASSNAME_FOOTER}-center`;
+const CLASSNAME_FOOTER_RIGHT = `${CLASSNAME_FOOTER}-right`;
const CLASSNAME_DISPLAY = `${CLASSNAME}-display`;
const CLASSNAME_CAROUSEL = `${CLASSNAME}-carousel`;
const CLASSNAME_INSTANT = `${CLASSNAME_CAROUSEL}-instant`;
@@ -933,14 +937,30 @@ export const LightboxComponent: React.FC = ({
>
)}
-
+
{currentImage && (
- close()}>
- {title ?? ""}
-
+ <>
+ close()}
+ >
+ {title ?? ""}
+
+ {currentImage.galleries?.length ? (
+ close()}
+ >
+
+ {galleryTitle(currentImage.galleries[0])}
+
+ ) : null}
+ >
)}
-
+
>
);
diff --git a/ui/v2.5/src/hooks/Lightbox/lightbox.scss b/ui/v2.5/src/hooks/Lightbox/lightbox.scss
index b12de3cf9..95a5fbc42 100644
--- a/ui/v2.5/src/hooks/Lightbox/lightbox.scss
+++ b/ui/v2.5/src/hooks/Lightbox/lightbox.scss
@@ -105,10 +105,25 @@
padding-left: 1em;
}
+ &-center {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ padding-left: 1em;
+ text-align: center;
+ }
+
a {
color: $text-color;
- font-weight: bold;
text-decoration: none;
+
+ .fa-icon {
+ margin-right: 0.5rem;
+ }
+
+ &.image-link {
+ font-weight: bold;
+ }
}
}
diff --git a/ui/v2.5/src/hooks/Lightbox/types.ts b/ui/v2.5/src/hooks/Lightbox/types.ts
index 56c9d6b71..58cdc8434 100644
--- a/ui/v2.5/src/hooks/Lightbox/types.ts
+++ b/ui/v2.5/src/hooks/Lightbox/types.ts
@@ -14,6 +14,17 @@ interface IFiles {
video_codec?: GQL.Maybe;
}
+interface IWithPath {
+ path: string;
+}
+
+export interface IGallery {
+ id: string;
+ title?: GQL.Maybe;
+ files?: GQL.Maybe;
+ folder?: GQL.Maybe;
+}
+
export interface ILightboxImage {
id?: string;
title?: GQL.Maybe;
@@ -21,6 +32,7 @@ export interface ILightboxImage {
o_counter?: GQL.Maybe;
paths: IImagePaths;
visual_files?: IFiles[];
+ galleries?: GQL.Maybe;
}
export interface IChapter {
From cc97e96eaad64bae50c013affe914fc5356b3935 Mon Sep 17 00:00:00 2001
From: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
Date: Tue, 9 Sep 2025 16:45:29 +1000
Subject: [PATCH 038/405] Add wall zoom functionality (#6011)
* Show zoom slider when wall view active
* Add zoom functionality to scene wall
* Add zoom functionality to image wall
* Add zoom functionality to gallery wall
* Add zoom functionality for marker wall
---
.../src/components/Galleries/GalleryList.tsx | 2 +-
ui/v2.5/src/components/Galleries/styles.scss | 80 +++++++++++++------
ui/v2.5/src/components/Images/ImageList.tsx | 29 ++++++-
.../src/components/List/ListViewOptions.tsx | 3 +-
ui/v2.5/src/components/Scenes/SceneList.tsx | 8 +-
.../src/components/Scenes/SceneMarkerList.tsx | 5 +-
.../Scenes/SceneMarkerWallPanel.tsx | 26 +++++-
.../src/components/Scenes/SceneWallPanel.tsx | 32 +++++++-
8 files changed, 148 insertions(+), 37 deletions(-)
diff --git a/ui/v2.5/src/components/Galleries/GalleryList.tsx b/ui/v2.5/src/components/Galleries/GalleryList.tsx
index 7becbe93a..a0930b927 100644
--- a/ui/v2.5/src/components/Galleries/GalleryList.tsx
+++ b/ui/v2.5/src/components/Galleries/GalleryList.tsx
@@ -149,7 +149,7 @@ export const GalleryList: React.FC = ({
if (filter.displayMode === DisplayMode.Wall) {
return (
-
+
{result.data.findGalleries.galleries.map((gallery) => (
))}
diff --git a/ui/v2.5/src/components/Galleries/styles.scss b/ui/v2.5/src/components/Galleries/styles.scss
index 12439a94d..58116e936 100644
--- a/ui/v2.5/src/components/Galleries/styles.scss
+++ b/ui/v2.5/src/components/Galleries/styles.scss
@@ -206,7 +206,7 @@ $galleryTabWidth: 450px;
}
}
-.GalleryWall {
+div.GalleryWall {
display: flex;
flex-wrap: wrap;
margin: 0 auto;
@@ -249,28 +249,6 @@ $galleryTabWidth: 450px;
z-index: 1;
}
- @mixin galleryWidth($width) {
- height: math.div($width, 3) * 2;
-
- &-landscape {
- width: $width;
- }
-
- &-portrait {
- width: math.div($width, 2);
- }
- }
-
- @media (min-width: 576px) {
- @include galleryWidth(96vw);
- }
- @media (min-width: 768px) {
- @include galleryWidth(48vw);
- }
- @media (min-width: 1200px) {
- @include galleryWidth(32vw);
- }
-
&-img {
height: 100%;
object-fit: cover;
@@ -355,6 +333,62 @@ $galleryTabWidth: 450px;
}
}
+div.GalleryWall {
+ @mixin galleryWidth($width) {
+ height: math.div($width, 3) * 2;
+
+ &-landscape {
+ width: $width;
+ }
+
+ &-portrait {
+ width: math.div($width, 2);
+ }
+ }
+
+ .GalleryWallCard {
+ @media (min-width: 576px) {
+ @include galleryWidth(96vw);
+ }
+ }
+
+ &.zoom-0 .GalleryWallCard {
+ @media (min-width: 768px) {
+ @include galleryWidth(16vw);
+ }
+ @media (min-width: 1200px) {
+ @include galleryWidth(10vw);
+ }
+ }
+
+ &.zoom-1 .GalleryWallCard {
+ @media (min-width: 768px) {
+ @include galleryWidth(24vw);
+ }
+ @media (min-width: 1200px) {
+ @include galleryWidth(16vw);
+ }
+ }
+
+ &.zoom-2 .GalleryWallCard {
+ @media (min-width: 768px) {
+ @include galleryWidth(32vw);
+ }
+ @media (min-width: 1200px) {
+ @include galleryWidth(24vw);
+ }
+ }
+
+ &.zoom-3 .GalleryWallCard {
+ @media (min-width: 768px) {
+ @include galleryWidth(48vw);
+ }
+ @media (min-width: 1200px) {
+ @include galleryWidth(32vw);
+ }
+ }
+}
+
.gallery-file-card.card {
margin: 0;
padding: 0;
diff --git a/ui/v2.5/src/components/Images/ImageList.tsx b/ui/v2.5/src/components/Images/ImageList.tsx
index 12eb264b1..a468c2815 100644
--- a/ui/v2.5/src/components/Images/ImageList.tsx
+++ b/ui/v2.5/src/components/Images/ImageList.tsx
@@ -35,9 +35,22 @@ interface IImageWallProps {
currentPage: number;
pageCount: number;
handleImageOpen: (index: number) => void;
+ zoomIndex: number;
}
-const ImageWall: React.FC
= ({ images, handleImageOpen }) => {
+const zoomWidths = [280, 340, 480, 640];
+const breakpointZoomHeights = [
+ { minWidth: 576, heights: [100, 120, 240, 360] },
+ { minWidth: 768, heights: [120, 160, 240, 480] },
+ { minWidth: 1200, heights: [120, 160, 240, 300] },
+ { minWidth: 1400, heights: [160, 240, 300, 480] },
+];
+
+const ImageWall: React.FC = ({
+ images,
+ zoomIndex,
+ handleImageOpen,
+}) => {
const { configuration } = useContext(ConfigurationContext);
const uiConfig = configuration?.ui;
@@ -76,11 +89,21 @@ const ImageWall: React.FC = ({ images, handleImageOpen }) => {
);
function columns(containerWidth: number) {
- let preferredSize = 300;
+ let preferredSize = zoomWidths[zoomIndex];
let columnCount = containerWidth / preferredSize;
return Math.round(columnCount);
}
+ function targetRowHeight(containerWidth: number) {
+ let zoomHeight = 280;
+ breakpointZoomHeights.forEach((e) => {
+ if (containerWidth >= e.minWidth) {
+ zoomHeight = e.heights[zoomIndex];
+ }
+ });
+ return zoomHeight;
+ }
+
return (
{photos.length ? (
@@ -91,6 +114,7 @@ const ImageWall: React.FC = ({ images, handleImageOpen }) => {
margin={uiConfig?.imageWallOptions?.margin!}
direction={uiConfig?.imageWallOptions?.direction!}
columns={columns}
+ targetRowHeight={targetRowHeight}
/>
) : null}
@@ -211,6 +235,7 @@ const ImageListImages: React.FC = ({
currentPage={filter.currentPage}
pageCount={pageCount}
handleImageOpen={handleImageOpen}
+ zoomIndex={filter.zoomIndex}
/>
);
}
diff --git a/ui/v2.5/src/components/List/ListViewOptions.tsx b/ui/v2.5/src/components/List/ListViewOptions.tsx
index e83ff9290..1ea928983 100644
--- a/ui/v2.5/src/components/List/ListViewOptions.tsx
+++ b/ui/v2.5/src/components/List/ListViewOptions.tsx
@@ -130,7 +130,8 @@ export const ListViewOptions: React.FC = ({