Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
edd390f
created ContextProvider collector function and extracted AppSettings …
lucaengelhard Mar 4, 2026
b7540a6
extracted all useState calls in App.tsx into ContextProvider
lucaengelhard Mar 4, 2026
777d1ad
moved history state into context
lucaengelhard Mar 4, 2026
48438a4
simplify context state creation typing
lucaengelhard Mar 4, 2026
f1e4e23
removed prop drilling to Editor
lucaengelhard Mar 4, 2026
402e4fb
removed prop drilling to EditorToolbar
lucaengelhard Mar 4, 2026
e21627e
removed prop drilling to ImageCanvas
lucaengelhard Mar 4, 2026
f5fc2b6
removed prop unused variables
lucaengelhard Mar 4, 2026
46ea2ea
removed prop drilling to FolderTree
lucaengelhard Mar 4, 2026
1fd2b21
removed prop drilling to MainLibrary
lucaengelhard Mar 4, 2026
396b763
removed prop drilling to LibraryExportPanel
lucaengelhard Mar 4, 2026
2941566
removed prop drilling to modals
lucaengelhard Mar 4, 2026
5330c85
fixing main library
lucaengelhard Mar 4, 2026
eba8ff1
removed prop drilling to panels
lucaengelhard Mar 4, 2026
640778c
started extracting global functions and effects
lucaengelhard Mar 5, 2026
0f47ff7
extracting global functions and effects
lucaengelhard Mar 5, 2026
663d20f
methods extracted
lucaengelhard Mar 5, 2026
a2f68fc
removed unused imports
lucaengelhard Mar 5, 2026
e6123a0
restore app.tsx
lucaengelhard Mar 5, 2026
fc033d5
restore old prop drilling
lucaengelhard Mar 5, 2026
0189314
fix imports
lucaengelhard Mar 5, 2026
af20dd4
extracted memoized components
lucaengelhard Mar 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5,053 changes: 658 additions & 4,395 deletions src/App.tsx

Large diffs are not rendered by default.

331 changes: 331 additions & 0 deletions src/context/ContextProviders.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,331 @@
import { createContext, PropsWithChildren, useContext, useMemo, useRef, useState } from 'react';
import { ContextMenuProvider } from './ContextMenuContext';
import { ClerkProvider } from '@clerk/clerk-react';
import { CLERK_PUBLISHABLE_KEY } from '../utils/constants';
import {
AppSettings,
BrushSettings,
FilterCriteria,
ImageFile,
LibraryViewMode,
Panel,
Progress,
RawStatus,
SortCriteria,
SortDirection,
SupportedTypes,
Theme,
ThumbnailAspectRatio,
ThumbnailSize,
UiVisibility,
WaveformData,
} from '../components/ui/AppProperties';
import { useSelectedImage } from './state/SelectedImageContext';
import { Adjustments, INITIAL_ADJUSTMENTS, MaskContainer } from '../utils/adjustments';
import { ChannelConfig } from '../components/adjustments/Curves';
import { DEFAULT_THEME_ID } from '../utils/themes';
import { ImageDimensions } from '../hooks/useImageRenderSize';
import { OverlayMode } from '../components/panel/right/CropPanel';
import {
CollageModalState,
CollapsibleSectionsState,
ConfirmModalState,
CullingModalState,
DenoiseModalState,
HdrModalState,
NegativeConversionModalState,
PanoramaModalState,
SearchCriteria,
} from '../App';
import { ToolType } from '../components/panel/right/Masks';
import { useHistoryState } from '../hooks/useHistoryState';
import { ExportState, ImportState, Status } from '../components/ui/ExportImportProperties';

export function ContextProviders({ children }: PropsWithChildren) {
return (
<ClerkProvider publishableKey={CLERK_PUBLISHABLE_KEY}>
<AppStateContextProvider>
<ContextMenuProvider>{children}</ContextMenuProvider>
</AppStateContextProvider>
</ClerkProvider>
);
}

const AppStateContext = createContext<ReturnType<typeof createAppStateContext> | null>(null);

function AppStateContextProvider({ children }: PropsWithChildren) {
return <AppStateContext.Provider value={createAppStateContext()}>{children}</AppStateContext.Provider>;
}

export function useAppState() {
const ctx = useContext(AppStateContext);

if (!ctx) {
throw new Error(`${useAppState.name} must be used within a ${AppStateContextProvider.name}`);
}

return ctx;
}

function createAppStateContext() {
const refs = {
dragIdleTimer: useRef<ReturnType<typeof setTimeout> | null>(null),
isInitialThemeMount: useRef(true),
fullResCacheKeyRef: useRef<string | null>(null),
patchesSentToBackend: useRef<Set<string>>(new Set()),
transformWrapperRef: useRef<any>(null),
isProgrammaticZoom: useRef(false),
isInitialMount: useRef(true),
preloadedDataRef: useRef<{
tree?: Promise<any>;
images?: Promise<ImageFile[]>;
rootPath?: string;
currentPath?: string;
}>({}),
previewJobIdRef: useRef<number>(0),
latestRenderedJobIdRef: useRef<number>(0),
};

const state = {
...useSelectedImage(),
history: useHistoryState(INITIAL_ADJUSTMENTS),
...useContextState<AppSettings>()('appSettings'),
...useContextState<string | null>()('rootPath'),
...useContextState<string>()('activeView', 'library'),
...useContextState<boolean>()('isWindowFullScreen', false),
...useContextState<boolean>()('isLayoutReady', false),
...useContextState<string>()('currentFolderPath'),
...useContextState<Set<string>>()('expandedFolders', new Set()),
...useContextState<any>()('folderTree'),
...useContextState<any[]>()('pinnedFolderTrees', []),
...useContextState<Array<ImageFile>>()('imageList', []),
...useContextState<Record<string, number>>()('imageRatings', {}),
...useContextState<SortCriteria>()('sortCriteria', { key: 'name', order: SortDirection.Ascending }),
...useContextState<FilterCriteria>()('filterCriteria', {
colors: [],
rating: 0,
rawStatus: RawStatus.All,
}),
...useContextState<SupportedTypes | null>()('supportedTypes'),
...useContextState<Array<string>>()('multiSelectedPaths', []),
...useContextState<string | null>()('libraryActivePath'),
...useContextState<Adjustments>()('libraryActiveAdjustments', INITIAL_ADJUSTMENTS),
...useContextState<string | null>()('finalPreviewUrl'),
...useContextState<string | null>()('uncroppedAdjustedPreviewUrl'),
...useContextState<Adjustments>()('adjustments', INITIAL_ADJUSTMENTS),
...useContextState<boolean>()('showOriginal', false),
...useContextState<boolean>()('isTreeLoading', false),
...useContextState<boolean>()('isViewLoading', false),
...useContextState<string | null>()('initialFileToOpen'),
...useContextState<string | null>()('error'),
...useContextState<ChannelConfig | null>()('histogram'),
...useContextState<WaveformData | null>()('waveform'),
...useContextState<boolean>()('isWaveformVisible', false),
...useContextState<UiVisibility>()('uiVisibility', {
folderTree: true,
filmstrip: true,
}),
...useContextState<boolean>()('isSliderDragging', false),
...useContextState<boolean>()('isFullScreen', false),
...useContextState<boolean>()('isHighResNeeded', false),
...useContextState<boolean>()('isAnimatingTheme', false),
...useContextState<Theme>()('theme', DEFAULT_THEME_ID),
...useContextState<any>()('adaptivePalette'),
...useContextState<Panel | null>()('activeRightPanel', Panel.Adjustments),
...useContextState<number>()('slideDirection', 1),
...useContextState<string | null>()('activeMaskContainerId'),
...useContextState<string | null>()('activeMaskId'),
...useContextState<string | null>()('activeAiPatchContainerId'),
...useContextState<string | null>()('activeAiSubMaskId'),
...useContextState<number>()('zoom', 1),
...useContextState<ImageDimensions>()('displaySize', { width: 0, height: 0 }),
...useContextState<ImageDimensions>()('previewSize', { width: 0, height: 0 }),
...useContextState<ImageDimensions>()('baseRenderSize', { width: 0, height: 0 }),
...useContextState<ImageDimensions>()('originalSize', { width: 0, height: 0 }),
...useContextState<boolean>()('isLoadingFullRes', false),
...useContextState<boolean>()('isRotationActive', false),
...useContextState<OverlayMode>()('overlayMode', 'thirds'),
...useContextState<number>()('overlayRotation', 0),
...useContextState<string | null>()('transformedOriginalUrl'),
...useContextState<number | null>()('initialFitScale'),
...useContextState<CollapsibleSectionsState>()('collapsibleSectionsState', {
basic: true,
color: false,
curves: true,
details: false,
effects: false,
}),
...useContextState<boolean>()('isLibraryExportPanelVisible', false),
...useContextState<LibraryViewMode>()('libraryViewMode', LibraryViewMode.Flat),
...useContextState<number>()('leftPanelWidth', 256),
...useContextState<number>()('rightPanelWidth', 320),
...useContextState<number>()('bottomPanelHeight', 144),
...useContextState<string | null>()('activeTreeSection', 'current'),
...useContextState<boolean>()('isResizing', false),
...useContextState<ThumbnailSize>()('thumbnailSize', ThumbnailSize.Medium),
...useContextState<ThumbnailAspectRatio>()('thumbnailAspectRatio', ThumbnailAspectRatio.Cover),
...useContextState<Adjustments | null>()('copiedAdjustments', null),
...useContextState<boolean>()('isStraightenActive', false),
...useContextState<boolean>()('isWbPickerActive', false),
...useContextState<Array<string>>()('copiedFilePaths', []),
...useContextState<string | null>()('aiModelDownloadStatus'),
...useContextState<Adjustments | null>()('copiedSectionAdjustments'),
...useContextState<MaskContainer | null>()('copiedMask'),
...useContextState<boolean>()('isCopied', false),
...useContextState<boolean>()('isPasted', false),
...useContextState<boolean>()('isIndexing', false),
...useContextState<Progress>()('indexingProgress', { current: 0, total: 0 }),
...useContextState<SearchCriteria>()('searchCriteria', {
tags: [],
text: '',
mode: 'OR',
}),
...useContextState<BrushSettings | null>()('brushSettings', {
size: 50,
feather: 50,
tool: ToolType.Brush,
}),
...useContextState<boolean>()('isCreateFolderModalOpen', false),
...useContextState<boolean>()('isRenameFolderModalOpen', false),
...useContextState<boolean>()('isRenameFileModalOpen', false),
...useContextState<Array<string>>()('renameTargetPaths', []),
...useContextState<boolean>()('isImportModalOpen', false),
...useContextState<boolean>()('isCopyPasteSettingsModalOpen', false),
...useContextState<string | null>()('importTargetFolder'),
...useContextState<Array<string>>()('importSourcePaths', []),
...useContextState<string | null>()('folderActionTarget'),
...useContextState<ConfirmModalState>()('confirmModalState', { isOpen: false }),
...useContextState<PanoramaModalState>()('panoramaModalState', {
error: null,
finalImageBase64: null,
isOpen: false,
progressMessage: '',
stitchingSourcePaths: [],
}),
...useContextState<HdrModalState>()('hdrModalState', {
error: null,
finalImageBase64: null,
isOpen: false,
progressMessage: '',
stitchingSourcePaths: [],
}),
...useContextState<NegativeConversionModalState>()('negativeModalState', {
isOpen: false,
targetPath: null,
}),
...useContextState<DenoiseModalState>()('denoiseModalState', {
isOpen: false,
isProcessing: false,
previewBase64: null,
error: null,
targetPath: null,
progressMessage: null,
}),
...useContextState<CullingModalState>()('cullingModalState', {
isOpen: false,
suggestions: null,
progress: null,
error: null,
pathsToCull: [],
}),
...useContextState<CollageModalState>()('collageModalState', {
isOpen: false,
sourceImages: [],
}),
...useContextState<null>()('customEscapeHandler'),
...useContextState<boolean>()('isGeneratingAiMask', false),
...useContextState<boolean>()('isAIConnectorConnected', false),
...useContextState<boolean>()('isGeneratingAi', false),
...useContextState<boolean>()('isMaskControlHovered', false),
...useContextState<number>()('libraryScrollTop', 0),
...useContextState<Record<string, string>>()('thumbnails', {}),
...useContextState<ExportState>()('exportState', {
errorMessage: '',
progress: { current: 0, total: 0 },
status: Status.Idle,
}),
...useContextState<ImportState>()('importState', {
errorMessage: '',
path: '',
progress: { current: 0, total: 0 },
status: Status.Idle,
}),
};

const {
activeRightPanel,
theme,
adjustments,
appSettings,
isCreateFolderModalOpen,
isRenameFolderModalOpen,
isRenameFileModalOpen,
isImportModalOpen,
isCopyPasteSettingsModalOpen,
confirmModalState,
panoramaModalState,
cullingModalState,
collageModalState,
denoiseModalState,
negativeModalState,
} = state;

const dependants = {
...useContextState<Panel | null>()('renderedRightPanel', activeRightPanel),
currentFolderPathRef: useRef<string>(state.currentFolderPath),
isLightTheme: useMemo(() => [Theme.Light, Theme.Snow, Theme.Arctic].includes(theme as Theme), [theme]),
geometricAdjustmentsKey: useMemo(() => {
if (!adjustments) return '';
const { crop, rotation, flipHorizontal, flipVertical, orientationSteps } = adjustments;
return JSON.stringify({ crop, rotation, flipHorizontal, flipVertical, orientationSteps });
}, [
adjustments?.crop,
adjustments?.rotation,
adjustments?.flipHorizontal,
adjustments?.flipVertical,
adjustments?.orientationSteps,
]),
visualAdjustmentsKey: useMemo(() => {
if (!adjustments) return '';
const { rating: _rating, sectionVisibility: _sectionVisibility, ...visualAdjustments } = adjustments;
return JSON.stringify(visualAdjustments);
}, [adjustments]),
pinnedFolders: useMemo(() => appSettings?.pinnedFolders || [], [appSettings]),
isAnyModalOpen:
isCreateFolderModalOpen ||
isRenameFolderModalOpen ||
isRenameFileModalOpen ||
isImportModalOpen ||
isCopyPasteSettingsModalOpen ||
confirmModalState.isOpen ||
panoramaModalState.isOpen ||
cullingModalState.isOpen ||
collageModalState.isOpen ||
denoiseModalState.isOpen ||
negativeModalState.isOpen,
};

return {
...refs,
...state,
...dependants,
};
}

type ContextStateObj<ValueName extends string, T> = {
[K in ValueName]: T;
} & {
[K in `set${Capitalize<ValueName>}`]: React.Dispatch<React.SetStateAction<T>>;
};

function useContextState<T>() {
function create<ValueName extends string>(valueName: ValueName, defaultValue?: T) {
const [value, setter] = useState<T | null>(defaultValue === undefined ? null : defaultValue);

const setterName = 'set' + valueName.charAt(0).toUpperCase() + valueName.slice(1);

return { [valueName]: value, [setterName]: setter } as ContextStateObj<ValueName, T>;
}

return create;
}
27 changes: 27 additions & 0 deletions src/context/state/SelectedImageContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React, {
createContext,
PropsWithChildren,
RefObject,
SetStateAction,
useContext,
useEffect,
useRef,
useState,
} from 'react';
import { SelectedImage } from '../../components/ui/AppProperties';

export interface SelectedImageContext {
selectedImage: SelectedImage | null;
selectedImagePathRef: RefObject<string | null>;
setSelectedImage: React.Dispatch<SetStateAction<SelectedImage | null>>;
}

export function useSelectedImage() {
const [selectedImage, setSelectedImage] = useState<SelectedImage | null>(null);
const selectedImagePathRef = useRef<string | null>(null);
useEffect(() => {
selectedImagePathRef.current = selectedImage?.path ?? null;
}, [selectedImage?.path]);

return { selectedImage, setSelectedImage, selectedImagePathRef };
}
Loading
Loading