From f0101b408f25073598b5df321aca453ad37ac10a Mon Sep 17 00:00:00 2001 From: Floh <48927090+Flohhhhh@users.noreply.github.com> Date: Sat, 18 Apr 2026 17:14:12 -0400 Subject: [PATCH 1/3] feat(ui): open app settings from the editor without leaving the session Mount SettingsPanel from App behind a full-screen overlay so splash and in-session flows share one path. Add a bottom control on the editor right rail (Cog + tooltip) and wire splash gear to the same opener. Center and pad the overlay content for readability. Clear the overlay when returning to the library or home so it cannot stick open. Defer reading theme CSS variables into the Editor WGPU ref until after the current commit finishes passive effects, so child effects no longer observe stale :root tokens before App applies the new theme (fixes wrong letterbox colors until navigation). Rename the settings header back tooltip from "Go to Home" to "Back". Refs: https://github.com/CyberTimon/RapidRAW/issues/128 Made-with: Cursor --- src/App.tsx | 26 +++++++ src/components/panel/Editor.tsx | 34 +++++---- src/components/panel/MainLibrary.tsx | 18 +---- src/components/panel/SettingsPanel.tsx | 2 +- .../panel/right/RightPanelSwitcher.tsx | 73 ++++++++++++------- 5 files changed, 96 insertions(+), 57 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index cc4fe9607..4093e6b9c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -45,6 +45,7 @@ import { import TitleBar from './window/TitleBar'; import CommunityPage from './components/panel/CommunityPage'; import MainLibrary, { ColumnWidths } from './components/panel/MainLibrary'; +import SettingsPanel from './components/panel/SettingsPanel'; import FolderTree from './components/panel/FolderTree'; import Editor from './components/panel/Editor'; import Controls from './components/panel/right/ControlsPanel'; @@ -409,6 +410,7 @@ function App() { const [renameTargetPaths, setRenameTargetPaths] = useState>([]); const [isImportModalOpen, setIsImportModalOpen] = useState(false); const [isCopyPasteSettingsModalOpen, setIsCopyPasteSettingsModalOpen] = useState(false); + const [isSettingsOpen, setIsSettingsOpen] = useState(false); const [importTargetFolder, setImportTargetFolder] = useState(null); const [importSourcePaths, setImportSourcePaths] = useState>([]); const [folderActionTarget, setFolderActionTarget] = useState(null); @@ -2403,6 +2405,7 @@ function App() { }, []); const handleBackToLibrary = useCallback(() => { + setIsSettingsOpen(false); if (selectedImage?.path && cachedEditStateRef.current) { imageCacheRef.current.set(selectedImage.path, cachedEditStateRef.current); } @@ -3927,6 +3930,7 @@ function App() { ]); const handleGoHome = () => { + setIsSettingsOpen(false); setRootPath(null); setCurrentFolderPath(null); setImageList([]); @@ -5126,6 +5130,7 @@ function App() { onImageDoubleClick={handleImageSelect} onLibraryRefresh={handleLibraryRefresh} onOpenFolder={handleOpenFolder} + onOpenSettings={() => setIsSettingsOpen(true)} onSettingsChange={handleSettingsChange} onThumbnailAspectRatioChange={setThumbnailAspectRatio} onThumbnailSizeChange={setThumbnailSize} @@ -5480,6 +5485,7 @@ function App() { setIsSettingsOpen(true)} isInstantTransition={isInstantTransition} /> @@ -5709,6 +5715,26 @@ function App() { sourceImages={collageModalState.sourceImages} thumbnails={thumbnails} /> + {isSettingsOpen && appSettings && ( +
+
+
+ setIsSettingsOpen(false)} + onLibraryRefresh={handleLibraryRefresh} + onSettingsChange={handleSettingsChange} + rootPath={rootPath} + /> +
+
+
+ )} { - const rootStyle = getComputedStyle(document.documentElement); - const bgPrimaryStr = rootStyle.getPropertyValue('--app-bg-primary') || 'rgb(24, 24, 24)'; - const bgSecondaryStr = rootStyle.getPropertyValue('--app-bg-secondary') || 'rgb(35, 35, 35)'; - - wgpuStateRef.current = { - useWgpuRenderer: appSettings?.useWgpuRenderer, - isReady: selectedImage?.isReady ?? false, - hasRenderedFirstFrame, - isCropping, - uncroppedAdjustedPreviewUrl, - showOriginal, - bgPrimary: parseRgb(bgPrimaryStr), - bgSecondary: parseRgb(bgSecondaryStr), + let isCancelled = false; + const applyDocumentThemeToWgpuRef = () => { + if (isCancelled) return; + const rootStyle = getComputedStyle(document.documentElement); + const bgPrimaryStr = rootStyle.getPropertyValue('--app-bg-primary') || 'rgb(24, 24, 24)'; + const bgSecondaryStr = rootStyle.getPropertyValue('--app-bg-secondary') || 'rgb(35, 35, 35)'; + + wgpuStateRef.current = { + useWgpuRenderer: appSettings?.useWgpuRenderer, + isReady: selectedImage?.isReady ?? false, + hasRenderedFirstFrame, + isCropping, + uncroppedAdjustedPreviewUrl, + showOriginal, + bgPrimary: parseRgb(bgPrimaryStr), + bgSecondary: parseRgb(bgSecondaryStr), + }; + }; + queueMicrotask(applyDocumentThemeToWgpuRef); + return () => { + isCancelled = true; }; }, [ appSettings?.useWgpuRenderer, diff --git a/src/components/panel/MainLibrary.tsx b/src/components/panel/MainLibrary.tsx index decc1da74..19fa9c55b 100644 --- a/src/components/panel/MainLibrary.tsx +++ b/src/components/panel/MainLibrary.tsx @@ -24,7 +24,6 @@ import { import { motion, AnimatePresence } from 'framer-motion'; import { List, useListCallbackRef } from 'react-window'; import Button from '../ui/Button'; -import SettingsPanel from './SettingsPanel'; import { ThemeProps, THEMES, DEFAULT_THEME_ID } from '../../utils/themes'; import { AppSettings, @@ -103,6 +102,7 @@ interface MainLibraryProps { onImageDoubleClick(path: string): void; onLibraryRefresh(): void; onOpenFolder(): void; + onOpenSettings(): void; onSettingsChange(settings: AppSettings): Promise; onThumbnailAspectRatioChange(aspectRatio: ThumbnailAspectRatio): void; onThumbnailSizeChange(size: ThumbnailSize): void; @@ -1518,6 +1518,7 @@ export default function MainLibrary({ onImageDoubleClick, onLibraryRefresh, onOpenFolder, + onOpenSettings, onSettingsChange, onThumbnailAspectRatioChange, onThumbnailSizeChange, @@ -1539,7 +1540,6 @@ export default function MainLibrary({ listColumnWidths, setListColumnWidths, }: MainLibraryProps) { - const [showSettings, setShowSettings] = useState(false); const [appVersion, setAppVersion] = useState(''); const [, setSupportedTypes] = useState(null); const libraryContainerRef = useRef(null); @@ -1915,16 +1915,6 @@ export default function MainLibrary({
- {showSettings ? ( - setShowSettings(false)} - onLibraryRefresh={onLibraryRefresh} - onSettingsChange={onSettingsChange} - rootPath={rootPath} - /> - ) : ( - <>
RapidRAW
)} - - )}
); diff --git a/src/components/panel/SettingsPanel.tsx b/src/components/panel/SettingsPanel.tsx index 4831c39ed..891fd05f7 100644 --- a/src/components/panel/SettingsPanel.tsx +++ b/src/components/panel/SettingsPanel.tsx @@ -688,7 +688,7 @@ export default function SettingsPanel({ onClick={onBack} size="icon" variant="ghost" - data-tooltip="Go to Home" + data-tooltip="Back" > diff --git a/src/components/panel/right/RightPanelSwitcher.tsx b/src/components/panel/right/RightPanelSwitcher.tsx index 1dfc42ce9..44576af3d 100644 --- a/src/components/panel/right/RightPanelSwitcher.tsx +++ b/src/components/panel/right/RightPanelSwitcher.tsx @@ -1,5 +1,5 @@ import { motion } from 'framer-motion'; -import { SlidersHorizontal, Info, Scaling, BrushCleaning, Bookmark, Save, Layers } from 'lucide-react'; +import { SlidersHorizontal, Info, Scaling, BrushCleaning, Bookmark, Save, Layers, Cog } from 'lucide-react'; import { Panel } from '../../ui/AppProperties'; interface PanelOptions { @@ -11,6 +11,7 @@ interface PanelOptions { interface RightPanelSwitcherProps { activePanel: Panel | null; onPanelSelect(id: Panel): void; + onOpenAppSettings(): void; isInstantTransition: boolean; } @@ -31,36 +32,52 @@ const panelGroups: Array> = [ export default function RightPanelSwitcher({ activePanel, onPanelSelect, + onOpenAppSettings, isInstantTransition, }: RightPanelSwitcherProps) { return ( -
- {panelGroups.map((group, groupIndex) => ( -
- {groupIndex > 0 &&
} - {group.map(({ id, icon: Icon, title }) => ( - - ))} -
- ))} +
+
+ {panelGroups.map((group, groupIndex) => ( +
+ {groupIndex > 0 &&
} + {group.map(({ id, icon: Icon, title }) => ( + + ))} +
+ ))} +
+
+
+ +
); } From 29dce53bb29376694d9e643ef611b92f6cf9431b Mon Sep 17 00:00:00 2001 From: Floh <48927090+Flohhhhh@users.noreply.github.com> Date: Sat, 18 Apr 2026 17:27:23 -0400 Subject: [PATCH 2/3] Fix width --- src/App.tsx | 202 ++++++++++++++++++++++++++-------------------------- 1 file changed, 101 insertions(+), 101 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 4093e6b9c..c0692771b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -756,11 +756,11 @@ function App() { aiPatches: prev.aiPatches.map((p: AiPatch) => p.id === patchId ? { - ...p, - patchData: newPatchData, - isLoading: false, - name: useFastInpaint ? 'Inpaint' : prompt && prompt.trim() ? prompt.trim() : p.name, - } + ...p, + patchData: newPatchData, + isLoading: false, + name: useFastInpaint ? 'Inpaint' : prompt && prompt.trim() ? prompt.trim() : p.name, + } : p, ), })); @@ -848,11 +848,11 @@ function App() { aiPatches: adjustments.aiPatches.map((p: AiPatch) => p.id === patchId ? { - ...p, - subMasks: p.subMasks.map((sm: SubMask) => - sm.id === subMaskId ? { ...sm, parameters: finalSubMaskParams } : sm, - ), - } + ...p, + subMasks: p.subMasks.map((sm: SubMask) => + sm.id === subMaskId ? { ...sm, parameters: finalSubMaskParams } : sm, + ), + } : p, ), }; @@ -876,13 +876,13 @@ function App() { aiPatches: prev.aiPatches?.map((p: AiPatch) => p.id === patchId ? { - ...p, - patchData: newPatchData, - isLoading: false, - subMasks: p.subMasks.map((sm: SubMask) => - sm.id === subMaskId ? { ...sm, parameters: finalSubMaskParams } : sm, - ), - } + ...p, + patchData: newPatchData, + isLoading: false, + subMasks: p.subMasks.map((sm: SubMask) => + sm.id === subMaskId ? { ...sm, parameters: finalSubMaskParams } : sm, + ), + } : p, ), })); @@ -1244,32 +1244,32 @@ function App() { searchTags.length === 0 && lowerCaseSearchText === '' ? filteredList : filteredList.filter((image: ImageFile) => { - const lowerCaseImageTags = (image.tags || []).map((t) => t.toLowerCase().replace('user:', '')); - const filename = image?.path?.split(/[\\/]/)?.pop()?.toLowerCase() || ''; - - let tagsMatch = true; - if (searchTags.length > 0) { - const lowerCaseSearchTags = searchTags.map((t) => t.toLowerCase()); - if (searchMode === 'OR') { - tagsMatch = lowerCaseSearchTags.some((searchTag) => - lowerCaseImageTags.some((imgTag) => imgTag.includes(searchTag)), - ); - } else { - tagsMatch = lowerCaseSearchTags.every((searchTag) => - lowerCaseImageTags.some((imgTag) => imgTag.includes(searchTag)), - ); - } + const lowerCaseImageTags = (image.tags || []).map((t) => t.toLowerCase().replace('user:', '')); + const filename = image?.path?.split(/[\\/]/)?.pop()?.toLowerCase() || ''; + + let tagsMatch = true; + if (searchTags.length > 0) { + const lowerCaseSearchTags = searchTags.map((t) => t.toLowerCase()); + if (searchMode === 'OR') { + tagsMatch = lowerCaseSearchTags.some((searchTag) => + lowerCaseImageTags.some((imgTag) => imgTag.includes(searchTag)), + ); + } else { + tagsMatch = lowerCaseSearchTags.every((searchTag) => + lowerCaseImageTags.some((imgTag) => imgTag.includes(searchTag)), + ); } + } - let textMatch = true; - if (lowerCaseSearchText !== '') { - textMatch = - filename.includes(lowerCaseSearchText) || - lowerCaseImageTags.some((t) => t.includes(lowerCaseSearchText)); - } + let textMatch = true; + if (lowerCaseSearchText !== '') { + textMatch = + filename.includes(lowerCaseSearchText) || + lowerCaseImageTags.some((t) => t.includes(lowerCaseSearchText)); + } - return tagsMatch && textMatch; - }); + return tagsMatch && textMatch; + }); const list = [...filteredBySearch]; @@ -1791,7 +1791,7 @@ function App() { const { searchCriteria: _searchCriteria, ...settingsToSave } = newSettings as any; setAppSettings(newSettings); return invoke(Invokes.SaveSettings, { settings: settingsToSave }) - .then(() => {}) + .then(() => { }) .catch((err) => { console.error('Failed to save settings:', err); }); @@ -4479,7 +4479,7 @@ function App() { let deleteSubmenu; if (selectionHasVirtualCopies) { deleteSubmenu = [ - { label: 'Cancel', icon: X, onClick: () => {} }, + { label: 'Cancel', icon: X, onClick: () => { } }, { label: 'Confirm Delete + Virtual Copies', icon: Check, @@ -4489,7 +4489,7 @@ function App() { ]; } else if (hasAssociatedFiles) { deleteSubmenu = [ - { label: 'Cancel', icon: X, onClick: () => {} }, + { label: 'Cancel', icon: X, onClick: () => { } }, { label: 'Delete Selected Only', icon: Check, @@ -4505,7 +4505,7 @@ function App() { ]; } else { deleteSubmenu = [ - { label: 'Cancel', icon: X, onClick: () => {} }, + { label: 'Cancel', icon: X, onClick: () => { } }, { label: 'Confirm', icon: Check, @@ -4593,27 +4593,27 @@ function App() { const options = [ ...(!isEditingThisImage ? [ - { - disabled: !isSingleSelection, - icon: Edit, - label: 'Edit Image', - onClick: () => handleImageSelect(finalSelection[0]), - }, - { - icon: Save, - label: exportLabel, - onClick: onExportClick, - }, - { type: OPTION_SEPARATOR }, - ] + { + disabled: !isSingleSelection, + icon: Edit, + label: 'Edit Image', + onClick: () => handleImageSelect(finalSelection[0]), + }, + { + icon: Save, + label: exportLabel, + onClick: onExportClick, + }, + { type: OPTION_SEPARATOR }, + ] : [ - { - icon: Save, - label: exportLabel, - onClick: onExportClick, - }, - { type: OPTION_SEPARATOR }, - ]), + { + icon: Save, + label: exportLabel, + onClick: onExportClick, + }, + { type: OPTION_SEPARATOR }, + ]), { disabled: !isSingleSelection, icon: Copy, @@ -4894,15 +4894,15 @@ function App() { const pinOption = isCurrentlyPinned ? { - icon: PinOff, - label: 'Unpin Folder', - onClick: () => handleTogglePinFolder(targetPath), - } + icon: PinOff, + label: 'Unpin Folder', + onClick: () => handleTogglePinFolder(targetPath), + } : { - icon: Pin, - label: 'Pin Folder', - onClick: () => handleTogglePinFolder(targetPath), - }; + icon: Pin, + label: 'Pin Folder', + onClick: () => handleTogglePinFolder(targetPath), + }; const options = [ pinOption, @@ -4967,30 +4967,30 @@ function App() { }, ...(path ? [ - { - disabled: isRoot, - icon: Trash2, - isDestructive: true, - label: 'Delete Folder', - submenu: [ - { label: 'Cancel', icon: X, onClick: () => {} }, - { - label: 'Confirm', - icon: Check, - isDestructive: true, - onClick: async () => { - try { - await invoke(Invokes.DeleteFolder, { path: targetPath }); - if (currentFolderPath?.startsWith(targetPath)) await handleSelectSubfolder(rootPath); - refreshAllFolderTrees(); - } catch (err) { - setError(`Failed to delete folder: ${err}`); - } - }, + { + disabled: isRoot, + icon: Trash2, + isDestructive: true, + label: 'Delete Folder', + submenu: [ + { label: 'Cancel', icon: X, onClick: () => { } }, + { + label: 'Confirm', + icon: Check, + isDestructive: true, + onClick: async () => { + try { + await invoke(Invokes.DeleteFolder, { path: targetPath }); + if (currentFolderPath?.startsWith(targetPath)) await handleSelectSubfolder(rootPath); + refreshAllFolderTrees(); + } catch (err) { + setError(`Failed to delete folder: ${err}`); + } }, - ], - }, - ] + }, + ], + }, + ] : []), ]; showContextMenu(event.clientX, event.clientY, options); @@ -5577,8 +5577,8 @@ function App() { loadingImageUrl={ panoramaModalState.stitchingSourcePaths.length > 0 ? thumbnails[ - panoramaModalState.stitchingSourcePaths[Math.floor(panoramaModalState.stitchingSourcePaths.length / 2)] - ] || null + panoramaModalState.stitchingSourcePaths[Math.floor(panoramaModalState.stitchingSourcePaths.length / 2)] + ] || null : null } onClose={() => @@ -5605,8 +5605,8 @@ function App() { loadingImageUrl={ hdrModalState.stitchingSourcePaths.length > 0 ? thumbnails[ - hdrModalState.stitchingSourcePaths[Math.floor(hdrModalState.stitchingSourcePaths.length / 2)] - ] || null + hdrModalState.stitchingSourcePaths[Math.floor(hdrModalState.stitchingSourcePaths.length / 2)] + ] || null : null } onClose={() => @@ -5656,7 +5656,7 @@ function App() { loadingImageUrl={ denoiseModalState.targetPaths.length > 0 ? thumbnails[denoiseModalState.targetPaths[0]] || - (selectedImage?.path === denoiseModalState.targetPaths[0] ? finalPreviewUrl : null) + (selectedImage?.path === denoiseModalState.targetPaths[0] ? finalPreviewUrl : null) : null } /> @@ -5723,7 +5723,7 @@ function App() { aria-label="Settings" >
-
+
setIsSettingsOpen(false)} From 05fe406f97ca42e8cea6d227f65cd3d191cab7f2 Mon Sep 17 00:00:00 2001 From: Floh <48927090+Flohhhhh@users.noreply.github.com> Date: Fri, 8 May 2026 12:47:50 -0400 Subject: [PATCH 3/3] Close settings overlay on Escape --- src/hooks/useKeyboardShortcuts.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/hooks/useKeyboardShortcuts.tsx b/src/hooks/useKeyboardShortcuts.tsx index 092afea6b..264884257 100644 --- a/src/hooks/useKeyboardShortcuts.tsx +++ b/src/hooks/useKeyboardShortcuts.tsx @@ -441,7 +441,8 @@ export const useKeyboardShortcuts = ({ match: (e: KeyboardEvent) => e.code === 'Escape', execute: (e: KeyboardEvent, s: any) => { e.preventDefault(); - if (s.editor.isStraightenActive) s.editor.setEditor({ isStraightenActive: false }); + if (s.ui.isSettingsOpen) s.ui.setUI({ isSettingsOpen: false }); + else if (s.editor.isStraightenActive) s.editor.setEditor({ isStraightenActive: false }); else if (s.ui.customEscapeHandler) s.ui.customEscapeHandler(); else if (s.editor.activeAiSubMaskId) s.editor.setEditor({ activeAiSubMaskId: null }); else if (s.editor.activeAiPatchContainerId) s.editor.setEditor({ activeAiPatchContainerId: null }); @@ -504,7 +505,14 @@ export const useKeyboardShortcuts = ({ const handleKeyDown = (event: KeyboardEvent) => { const state = getStoreState(); + if (state.ui.isSettingsOpen && event.code === 'Escape') { + event.preventDefault(); + state.ui.setUI({ isSettingsOpen: false }); + return; + } + const isModalOpen = + state.ui.isSettingsOpen || state.ui.isCreateFolderModalOpen || state.ui.isRenameFolderModalOpen || state.ui.isRenameFileModalOpen ||