Skip to content

Commit eaa42bf

Browse files
Chore: Code cleanup (#8998)
Co-authored-by: Alexander Eichhorn <alex@eichhorn.dev>
1 parent 5867556 commit eaa42bf

9 files changed

Lines changed: 118 additions & 8 deletions

File tree

invokeai/frontend/web/public/locales/en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1749,6 +1749,7 @@
17491749
"informationalPopoversDisabledDesc": "Informational popovers have been disabled. Enable them in Settings.",
17501750
"enableModelDescriptions": "Enable Model Descriptions in Dropdowns",
17511751
"enableHighlightFocusedRegions": "Highlight Focused Regions",
1752+
"middleClickOpenInNewTab": "Use Middle Click to Open Images in New Tab",
17521753
"modelDescriptionsDisabled": "Model Descriptions in Dropdowns Disabled",
17531754
"modelDescriptionsDisabledDesc": "Model descriptions in dropdowns have been disabled. Enable them in Settings.",
17541755
"enableInvisibleWatermark": "Enable Invisible Watermark",
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { useAppSelector } from 'app/store/storeHooks';
2+
import { openImageInNewTab } from 'common/util/openImageInNewTab';
3+
import { selectSystemShouldUseMiddleClickToOpenInNewTab } from 'features/system/store/systemSlice';
4+
import type { RefObject } from 'react';
5+
import { useEffect } from 'react';
6+
7+
type Options = {
8+
requireDirectTarget?: boolean;
9+
};
10+
11+
const shouldHandleMiddleClick = <T extends HTMLElement>(
12+
event: MouseEvent,
13+
element: T,
14+
requireDirectTarget: boolean
15+
) => {
16+
if (event.button !== 1) {
17+
return false;
18+
}
19+
20+
if (requireDirectTarget && event.target !== element) {
21+
return false;
22+
}
23+
24+
return true;
25+
};
26+
27+
export const useMiddleClickOpenInNewTab = <T extends HTMLElement = HTMLElement>(
28+
ref: RefObject<T>,
29+
imageUrl: string,
30+
{ requireDirectTarget = false }: Options = {}
31+
) => {
32+
const shouldUseMiddleClickToOpenInNewTab = useAppSelector(selectSystemShouldUseMiddleClickToOpenInNewTab);
33+
34+
useEffect(() => {
35+
const element = ref.current;
36+
37+
if (!element || !shouldUseMiddleClickToOpenInNewTab) {
38+
return;
39+
}
40+
41+
// If auxclick is unsupported, leave the browser's default middle-click behavior intact.
42+
if (!('onauxclick' in element)) {
43+
return;
44+
}
45+
46+
const onMouseDown = (event: MouseEvent) => {
47+
if (!shouldHandleMiddleClick(event, element, requireDirectTarget)) {
48+
return;
49+
}
50+
51+
event.preventDefault();
52+
};
53+
54+
const onAuxClick = (event: MouseEvent) => {
55+
if (!shouldHandleMiddleClick(event, element, requireDirectTarget)) {
56+
return;
57+
}
58+
59+
event.preventDefault();
60+
event.stopPropagation();
61+
openImageInNewTab(imageUrl);
62+
};
63+
64+
element.addEventListener('mousedown', onMouseDown);
65+
element.addEventListener('auxclick', onAuxClick);
66+
67+
return () => {
68+
element.removeEventListener('mousedown', onMouseDown);
69+
element.removeEventListener('auxclick', onAuxClick);
70+
};
71+
}, [imageUrl, ref, requireDirectTarget, shouldUseMiddleClickToOpenInNewTab]);
72+
};
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export const openImageInNewTab = (imageUrl: string) => {
2+
window.open(imageUrl, '_blank', 'noopener,noreferrer');
3+
};

invokeai/frontend/web/src/features/dnd/DndImage.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine';
22
import { draggable } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
33
import type { ImageProps, SystemStyleObject } from '@invoke-ai/ui-library';
44
import { Image } from '@invoke-ai/ui-library';
5-
import { useAppStore } from 'app/store/storeHooks';
5+
import { useMiddleClickOpenInNewTab } from 'common/hooks/useMiddleClickOpenInNewTab';
66
import { singleImageDndSource } from 'features/dnd/dnd';
77
import type { DndDragPreviewSingleImageState } from 'features/dnd/DndDragPreviewSingleImage';
88
import { createSingleImageDragPreview, setSingleImageDragPreview } from 'features/dnd/DndDragPreviewSingleImage';
@@ -15,7 +15,6 @@ const sx = {
1515
objectFit: 'contain',
1616
maxW: 'full',
1717
maxH: 'full',
18-
cursor: 'grab',
1918
'&[data-is-dragging=true]': {
2019
opacity: 0.3,
2120
},
@@ -28,13 +27,13 @@ type Props = {
2827

2928
export const DndImage = memo(
3029
forwardRef(({ imageDTO, asThumbnail, ...rest }: Props, forwardedRef) => {
31-
const store = useAppStore();
32-
3330
const [isDragging, setIsDragging] = useState(false);
3431
const ref = useRef<HTMLImageElement>(null);
3532
useImperativeHandle(forwardedRef, () => ref.current!, []);
3633
const [dragPreviewState, setDragPreviewState] = useState<DndDragPreviewSingleImageState | null>(null);
3734

35+
useMiddleClickOpenInNewTab(ref, imageDTO.image_url);
36+
3837
useEffect(() => {
3938
const element = ref.current;
4039
if (!element) {
@@ -62,7 +61,7 @@ export const DndImage = memo(
6261
},
6362
})
6463
);
65-
}, [forwardedRef, imageDTO, store]);
64+
}, [imageDTO]);
6665

6766
useImageContextMenu(imageDTO, ref);
6867

invokeai/frontend/web/src/features/gallery/components/ContextMenu/MenuItems/ContextMenuItemOpenInNewTab.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { IconMenuItem } from 'common/components/IconMenuItem';
2+
import { openImageInNewTab } from 'common/util/openImageInNewTab';
23
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
34
import { memo, useCallback } from 'react';
45
import { useTranslation } from 'react-i18next';
@@ -8,7 +9,7 @@ export const ContextMenuItemOpenInNewTab = memo(() => {
89
const { t } = useTranslation();
910
const imageDTO = useImageDTOContext();
1011
const onClick = useCallback(() => {
11-
window.open(imageDTO.image_url, '_blank');
12+
openImageInNewTab(imageDTO.image_url);
1213
}, [imageDTO]);
1314

1415
return (

invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Flex, Icon, Image } from '@invoke-ai/ui-library';
55
import { createSelector } from '@reduxjs/toolkit';
66
import type { AppDispatch, AppGetState } from 'app/store/store';
77
import { useAppSelector, useAppStore } from 'app/store/storeHooks';
8+
import { useMiddleClickOpenInNewTab } from 'common/hooks/useMiddleClickOpenInNewTab';
89
import { uniq } from 'es-toolkit';
910
import { multipleImageDndSource, singleImageDndSource } from 'features/dnd/dnd';
1011
import type { DndDragPreviewMultipleImageState } from 'features/dnd/DndDragPreviewMultipleImage';
@@ -187,6 +188,9 @@ export const GalleryImage = memo(({ imageDTO }: Props) => {
187188
}, []);
188189

189190
const onClick = useMemo(() => buildOnClick(imageDTO.image_name, store.dispatch, store.getState), [imageDTO, store]);
191+
useMiddleClickOpenInNewTab(ref, imageDTO.image_url, {
192+
requireDirectTarget: true,
193+
});
190194

191195
const onDoubleClick = useCallback<MouseEventHandler<HTMLDivElement>>(() => {
192196
store.dispatch(imageToCompareChanged(null));

invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import {
3939
selectSystemShouldEnableInformationalPopovers,
4040
selectSystemShouldEnableModelDescriptions,
4141
selectSystemShouldShowInvocationProgressDetail,
42+
selectSystemShouldUseMiddleClickToOpenInNewTab,
4243
selectSystemShouldUseNSFWChecker,
4344
selectSystemShouldUseWatermarker,
4445
setPrefersNumericAttentionStyle,
@@ -47,6 +48,7 @@ import {
4748
setShouldEnableModelDescriptions,
4849
setShouldHighlightFocusedRegions,
4950
setShouldShowInvocationProgressDetail,
51+
setShouldUseMiddleClickToOpenInNewTab,
5052
shouldAntialiasProgressImageChanged,
5153
shouldConfirmOnNewSessionToggled,
5254
shouldUseNSFWCheckerChanged,
@@ -100,6 +102,7 @@ const SettingsModal = (props: { children: ReactElement }) => {
100102
const shouldEnableInformationalPopovers = useAppSelector(selectSystemShouldEnableInformationalPopovers);
101103
const shouldEnableModelDescriptions = useAppSelector(selectSystemShouldEnableModelDescriptions);
102104
const shouldHighlightFocusedRegions = useAppSelector(selectSystemShouldEnableHighlightFocusedRegions);
105+
const shouldUseMiddleClickToOpenInNewTab = useAppSelector(selectSystemShouldUseMiddleClickToOpenInNewTab);
103106
const shouldConfirmOnNewSession = useAppSelector(selectSystemShouldConfirmOnNewSession);
104107
const shouldShowInvocationProgressDetail = useAppSelector(selectSystemShouldShowInvocationProgressDetail);
105108
const maxQueueHistory = runtimeConfig?.config.max_queue_history ?? null;
@@ -235,6 +238,13 @@ const SettingsModal = (props: { children: ReactElement }) => {
235238
[dispatch]
236239
);
237240

241+
const handleChangeShouldUseMiddleClickToOpenInNewTab = useCallback(
242+
(e: ChangeEvent<HTMLInputElement>) => {
243+
dispatch(setShouldUseMiddleClickToOpenInNewTab(e.target.checked));
244+
},
245+
[dispatch]
246+
);
247+
238248
const handleChangePreferAttentionStyleNumeric = useCallback(
239249
(e: ChangeEvent<HTMLInputElement>) => {
240250
dispatch(setPrefersNumericAttentionStyle(e.target.checked));
@@ -365,6 +375,13 @@ const SettingsModal = (props: { children: ReactElement }) => {
365375
onChange={handleChangeShouldHighlightFocusedRegions}
366376
/>
367377
</FormControl>
378+
<FormControl>
379+
<FormLabel>{t('settings.middleClickOpenInNewTab')}</FormLabel>
380+
<Switch
381+
isChecked={shouldUseMiddleClickToOpenInNewTab}
382+
onChange={handleChangeShouldUseMiddleClickToOpenInNewTab}
383+
/>
384+
</FormControl>
368385
</StickyScrollable>
369386

370387
<StickyScrollable title={t('settings.prompt')}>

invokeai/frontend/web/src/features/system/store/systemSlice.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { assert } from 'tsafe';
1212
import { type Language, type SystemState, zSystemState } from './types';
1313

1414
const getInitialState = (): SystemState => ({
15-
_version: 2,
15+
_version: 3,
1616
shouldConfirmOnDelete: true,
1717
shouldAntialiasProgressImage: false,
1818
shouldConfirmOnNewSession: true,
@@ -26,6 +26,7 @@ const getInitialState = (): SystemState => ({
2626
logNamespaces: [...zLogNamespace.options],
2727
shouldShowInvocationProgressDetail: false,
2828
shouldHighlightFocusedRegions: false,
29+
shouldUseMiddleClickToOpenInNewTab: false,
2930
prefersNumericAttentionWeights: false,
3031
});
3132

@@ -79,6 +80,9 @@ const slice = createSlice({
7980
setShouldHighlightFocusedRegions(state, action: PayloadAction<boolean>) {
8081
state.shouldHighlightFocusedRegions = action.payload;
8182
},
83+
setShouldUseMiddleClickToOpenInNewTab(state, action: PayloadAction<boolean>) {
84+
state.shouldUseMiddleClickToOpenInNewTab = action.payload;
85+
},
8286
},
8387
});
8488

@@ -97,6 +101,7 @@ export const {
97101
setShouldShowInvocationProgressDetail,
98102
setPrefersNumericAttentionStyle,
99103
setShouldHighlightFocusedRegions,
104+
setShouldUseMiddleClickToOpenInNewTab,
100105
} = slice.actions;
101106

102107
export const systemSliceConfig: SliceConfig<typeof slice> = {
@@ -113,6 +118,10 @@ export const systemSliceConfig: SliceConfig<typeof slice> = {
113118
state.language = (state as SystemState).language.replace('_', '-');
114119
state._version = 2;
115120
}
121+
if (state._version === 2) {
122+
state.shouldUseMiddleClickToOpenInNewTab = false;
123+
state._version = 3;
124+
}
116125
return zSystemState.parse(state);
117126
},
118127
},
@@ -141,6 +150,9 @@ export const selectSystemShouldEnableModelDescriptions = createSystemSelector(
141150
export const selectSystemShouldEnableHighlightFocusedRegions = createSystemSelector(
142151
(system) => system.shouldHighlightFocusedRegions
143152
);
153+
export const selectSystemShouldUseMiddleClickToOpenInNewTab = createSystemSelector(
154+
(system) => system.shouldUseMiddleClickToOpenInNewTab
155+
);
144156
export const selectSystemPrefersNumericAttentionWeights = createSystemSelector(
145157
(system) => system.prefersNumericAttentionWeights
146158
);

invokeai/frontend/web/src/features/system/store/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export type Language = z.infer<typeof zLanguage>;
3030
export const isLanguage = (v: unknown): v is Language => zLanguage.safeParse(v).success;
3131

3232
export const zSystemState = z.object({
33-
_version: z.literal(2),
33+
_version: z.literal(3),
3434
shouldConfirmOnDelete: z.boolean(),
3535
shouldAntialiasProgressImage: z.boolean(),
3636
shouldConfirmOnNewSession: z.boolean(),
@@ -44,6 +44,7 @@ export const zSystemState = z.object({
4444
logNamespaces: z.array(zLogNamespace),
4545
shouldShowInvocationProgressDetail: z.boolean(),
4646
shouldHighlightFocusedRegions: z.boolean(),
47+
shouldUseMiddleClickToOpenInNewTab: z.boolean(),
4748
prefersNumericAttentionWeights: z.boolean(),
4849
});
4950
export type SystemState = z.infer<typeof zSystemState>;

0 commit comments

Comments
 (0)