-
Notifications
You must be signed in to change notification settings - Fork 2.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(ui): gallery image selection ux
The selection logic is a bit complicated. We have image selection and pagination, both of which can be triggered using the mouse or hotkeys. We have viewer image selection and comparison image selection, which is determined by the alt key. This change ties the room together with these behaviours: - Changing the page using pagination buttons never changes the selection. - Changing the selected image using arrows may change the page, if the arrow key pressed would select an image off the current page. - `right` on the last image of the current page goes to the next page - `down` on the last row of images goes to the next page - `left` on the first image of the current page goes to the previous page - `up` on the first row of images goes to the previous page - If `alt` is held when using arrow keys, we change the page, but we only change the comparison image selection. - When using arrow keys, if the page has changed since the last image was selected, the selection is reset to the first image on the page. - The next/previous buttons on the image viewer do the same thing as `left` and `right` without `alt`. - When clicking an image in the gallery: - If no modifier keys are held, the image is exclusively selected. - If `ctrl` or `meta` are held, the image's selection status is toggled. - If `shift` is held, all images from the last-selected image to the image are selected. If there are no images on the current page, the selection is unchanged. - If `alt` is held, the image is set as the compare image. - `ctrl+a` and `meta+a` add the current page to the selection. The logic for gallery navigation and selection is now pretty hairy. It's spread across 3 hooks, a listener, redux slice, components. When we next make changes to this part of the app, we should consider consolidating some of the related logic. Probably most of it can go into a single listener and make it much simpler to grok.
- Loading branch information
1 parent
78663c6
commit 5ed185c
Showing
10 changed files
with
243 additions
and
94 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
119 changes: 119 additions & 0 deletions
119
...rontend/web/src/app/store/middleware/listenerMiddleware/listeners/galleryOffsetChanged.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; | ||
import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors'; | ||
import { imageToCompareChanged, offsetChanged, selectionChanged } from 'features/gallery/store/gallerySlice'; | ||
import { imagesApi } from 'services/api/endpoints/images'; | ||
|
||
export const addGalleryOffsetChangedListener = (startAppListening: AppStartListening) => { | ||
/** | ||
* When the user changes pages in the gallery, we need to wait until the next page of images is loaded, then maybe | ||
* update the selection. | ||
* | ||
* There are a three scenarios: | ||
* | ||
* 1. The page is changed by clicking the pagination buttons. No changes to selection are needed. | ||
* | ||
* 2. The page is changed by using the arrow keys (without alt). | ||
* - When going backwards, select the last image. | ||
* - When going forwards, select the first image. | ||
* | ||
* 3. The page is changed by using the arrows keys with alt. This means the user is changing the comparison image. | ||
* - When going backwards, select the last image _as the comparison image_. | ||
* - When going forwards, select the first image _as the comparison image_. | ||
*/ | ||
startAppListening({ | ||
actionCreator: offsetChanged, | ||
effect: async (action, { dispatch, getState, getOriginalState, take, cancelActiveListeners }) => { | ||
// Cancel any active listeners to prevent the selection from changing without user input | ||
cancelActiveListeners(); | ||
|
||
const { withHotkey } = action.payload; | ||
|
||
if (!withHotkey) { | ||
// User changed pages by clicking the pagination buttons - no changes to selection | ||
return; | ||
} | ||
|
||
const originalState = getOriginalState(); | ||
const prevOffset = originalState.gallery.offset; | ||
const offset = getState().gallery.offset; | ||
|
||
if (offset === prevOffset) { | ||
// The page didn't change - bail | ||
return; | ||
} | ||
|
||
/** | ||
* We need to wait until the next page of images is loaded before updating the selection, so we use the correct | ||
* page of images. | ||
* | ||
* The simplest way to do it would be to use `take` to wait for the next fulfilled action, but RTK-Q doesn't | ||
* dispatch an action on cache hits. This means the `take` will only return if the cache is empty. If the user | ||
* changes to a cached page - a common situation - the `take` will never resolve. | ||
* | ||
* So we need to take a two-step approach. First, check if we have data in the cache for the page of images. If | ||
* we have data cached, use it to update the selection. If we don't have data cached, wait for the next fulfilled | ||
* action, which updates the cache, then use the cache to update the selection. | ||
*/ | ||
|
||
// Check if we have data in the cache for the page of images | ||
const queryArgs = selectListImagesQueryArgs(getState()); | ||
let { data } = imagesApi.endpoints.listImages.select(queryArgs)(getState()); | ||
|
||
// No data yet - wait for the network request to complete | ||
if (!data) { | ||
const takeResult = await take(imagesApi.endpoints.listImages.matchFulfilled, 5000); | ||
if (!takeResult) { | ||
// The request didn't complete in time - bail | ||
return; | ||
} | ||
data = takeResult[0].payload; | ||
} | ||
|
||
// We awaited a network request - state could have changed, get fresh state | ||
const state = getState(); | ||
const { selection, imageToCompare } = state.gallery; | ||
const imageDTOs = data?.items; | ||
|
||
if (!imageDTOs) { | ||
// The page didn't load - bail | ||
return; | ||
} | ||
|
||
if (withHotkey === 'arrow') { | ||
// User changed pages by using the arrow keys - selection changes to first or last image depending | ||
if (offset < prevOffset) { | ||
// We've gone backwards | ||
const lastImage = imageDTOs[imageDTOs.length - 1]; | ||
if (!selection.some((selectedImage) => selectedImage.image_name === lastImage?.image_name)) { | ||
dispatch(selectionChanged(lastImage ? [lastImage] : [])); | ||
} | ||
} else { | ||
// We've gone forwards | ||
const firstImage = imageDTOs[0]; | ||
if (!selection.some((selectedImage) => selectedImage.image_name === firstImage?.image_name)) { | ||
dispatch(selectionChanged(firstImage ? [firstImage] : [])); | ||
} | ||
} | ||
return; | ||
} | ||
|
||
if (withHotkey === 'alt+arrow') { | ||
// User changed pages by using the arrow keys with alt - comparison image changes to first or last depending | ||
if (offset < prevOffset) { | ||
// We've gone backwards | ||
const lastImage = imageDTOs[imageDTOs.length - 1]; | ||
if (lastImage && imageToCompare?.image_name !== lastImage.image_name) { | ||
dispatch(imageToCompareChanged(lastImage)); | ||
} | ||
} else { | ||
// We've gone forwards | ||
const firstImage = imageDTOs[0]; | ||
if (firstImage && imageToCompare?.image_name !== firstImage.image_name) { | ||
dispatch(imageToCompareChanged(firstImage)); | ||
} | ||
} | ||
return; | ||
} | ||
}, | ||
}); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.