Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions src-tauri/src/file_management.rs
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,10 @@ pub struct AppSettings {
pub processing_backend: Option<String>,
#[serde(default)]
pub linux_gpu_optimization: Option<bool>,
#[serde(default)]
pub group_associated_files: Option<bool>,
#[serde(default)]
pub preferred_associated_type: Option<String>,
}

fn default_adjustment_visibility() -> HashMap<String, bool> {
Expand Down Expand Up @@ -309,6 +313,8 @@ impl Default for AppSettings {
linux_gpu_optimization: Some(true),
#[cfg(not(target_os = "linux"))]
linux_gpu_optimization: Some(false),
group_associated_files: Some(true),
preferred_associated_type: Some("jpeg".to_string()),
}
}
}
Expand Down
208 changes: 197 additions & 11 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ import {
} from './components/panel/right/ExportImportProperties';
import {
AppSettings,
AssociationInfo,
AssociatedPrimaryPreference,
BrushSettings,
FilterCriteria,
Invokes,
Expand All @@ -115,6 +117,103 @@ import { ChannelConfig } from './components/adjustments/Curves';

const CLERK_PUBLISHABLE_KEY = 'pk_test_YnJpZWYtc2Vhc25haWwtMTIuY2xlcmsuYWNjb3VudHMuZGV2JA'; // local dev key

const JPEG_EXTENSIONS = new Set(['jpg', 'jpeg']);

type LibraryImageFile = ImageFile & { associatedPaths?: Array<string> };

const getFileExtension = (path: string) => {
const normalized = path.toLowerCase();
const lastDot = normalized.lastIndexOf('.');
if (lastDot === -1) {
return '';
}
return normalized.substring(lastDot + 1);
};

const getStemKey = (path: string) => {
const normalized = path.replace(/\\/g, '/');
const lastDot = normalized.lastIndexOf('.');
if (lastDot === -1) {
return normalized;
}
return normalized.substring(0, lastDot);
};

const buildAssociations = (
images: Array<ImageFile>,
supportedTypes: SupportedTypes | null,
preferredType: AssociatedPrimaryPreference,
) => {
const associationMap: Record<string, AssociationInfo> = {};
const groupedList: Array<LibraryImageFile> = [];
const groups = new Map<
string,
{ variants: Array<ImageFile>; jpeg?: ImageFile; raw?: ImageFile }
>();
const rawExtensions = new Set((supportedTypes?.raw || []).map((ext) => ext.toLowerCase()));

images.forEach((image) => {
const stem = getStemKey(image.path);
let bucket = groups.get(stem);
if (!bucket) {
bucket = { variants: [] };
groups.set(stem, bucket);
}
bucket.variants.push(image);
const ext = getFileExtension(image.path);
if (JPEG_EXTENSIONS.has(ext)) {
bucket.jpeg = image;
} else if (rawExtensions.has(ext) && !bucket.raw) {
bucket.raw = image;
}
});

groups.forEach((bucket) => {
if (bucket.variants.length === 0) {
return;
}
const pickPrimary = () => {
const jpegCandidate = bucket.jpeg;
const rawCandidate = bucket.raw;
const fallback = bucket.variants[0];
switch (preferredType) {
case 'raw':
return rawCandidate || jpegCandidate || fallback;
case 'jpeg':
return jpegCandidate || rawCandidate || fallback;
case 'auto':
default:
return jpegCandidate || rawCandidate || fallback;
}
};
const primary = pickPrimary();
const variantPaths = bucket.variants.map((variant) => variant.path);
const info: AssociationInfo = {
primaryPath: primary.path,
variantPaths,
jpegPath: bucket.jpeg?.path,
rawPath: bucket.raw?.path,
};
bucket.variants.forEach((variant) => {
associationMap[variant.path] = info;
});
groupedList.push({ ...primary, associatedPaths: variantPaths });
});

return { associationMap, groupedList };
};

const getVariantLabel = (path: string, supportedTypes: SupportedTypes | null) => {
const ext = getFileExtension(path);
if (JPEG_EXTENSIONS.has(ext)) {
return 'JPEG';
}
if (supportedTypes?.raw?.some((rawExt) => rawExt.toLowerCase() === ext)) {
return 'RAW';
}
return ext ? ext.toUpperCase() : 'FILE';
};

interface CollapsibleSectionsState {
basic: boolean;
color: boolean;
Expand Down Expand Up @@ -372,11 +471,25 @@ function App() {
progress: { current: 0, total: 0 },
status: Status.Idle,
});
const [groupAssociatedFiles, setGroupAssociatedFiles] = useState(true);
const [preferredAssociatedType, setPreferredAssociatedType] = useState<AssociatedPrimaryPreference>('jpeg');

useEffect(() => {
currentFolderPathRef.current = currentFolderPath;
}, [currentFolderPath]);

const associationData = useMemo(
() => buildAssociations(imageList, supportedTypes, preferredAssociatedType),
[imageList, supportedTypes, preferredAssociatedType],
);
const associationsByPath = associationData.associationMap;
const groupedImageList = associationData.groupedList;
const isGroupingEnabled = groupAssociatedFiles && filterCriteria.rawStatus === RawStatus.All;
const visibleImageList = useMemo(
() => (isGroupingEnabled ? groupedImageList : imageList),
[isGroupingEnabled, groupedImageList, imageList],
);

useEffect(() => {
if (!isCopied) {
return;
Expand Down Expand Up @@ -818,7 +931,7 @@ function App() {
};

const sortedImageList = useMemo(() => {
const filteredList = imageList.filter((image) => {
const filteredList = visibleImageList.filter((image) => {
if (filterCriteria.rating > 0) {
const rating = imageRatings[image.path] || 0;
if (filterCriteria.rating === 5) {
Expand Down Expand Up @@ -972,7 +1085,25 @@ function App() {
return order === SortDirection.Ascending ? comparison : -comparison;
});
return list;
}, [imageList, sortCriteria, imageRatings, filterCriteria, supportedTypes, searchQuery, appSettings]);
}, [visibleImageList, sortCriteria, imageRatings, filterCriteria, supportedTypes, searchQuery, appSettings]);

const variantOptions = useMemo(() => {
if (!selectedImage) {
return [];
}
const association = associationsByPath[selectedImage.path];
if (!association || association.variantPaths.length < 2) {
return [];
}
return association.variantPaths.map((path) => ({
path,
label: getVariantLabel(path, supportedTypes),
}));
}, [selectedImage?.path, associationsByPath, supportedTypes]);

const filmstripActivePath = selectedImage
? associationsByPath[selectedImage.path]?.primaryPath || selectedImage.path
: null;

const applyAdjustments = useCallback(
debounce((currentAdjustments) => {
Expand Down Expand Up @@ -1140,6 +1271,16 @@ function App() {
if (settings?.thumbnailAspectRatio) {
setThumbnailAspectRatio(settings.thumbnailAspectRatio);
}
if (settings?.groupAssociatedFiles !== undefined) {
setGroupAssociatedFiles(settings.groupAssociatedFiles);
} else {
setGroupAssociatedFiles(true);
}
if (settings?.preferredAssociatedType) {
setPreferredAssociatedType(settings.preferredAssociatedType);
} else {
setPreferredAssociatedType('jpeg');
}
if (settings?.activeTreeSection) {
setActiveTreeSection(settings.activeTreeSection);
}
Expand Down Expand Up @@ -1174,6 +1315,14 @@ function App() {
setIsWaveformVisible((prev: boolean) => !prev);
}, []);

const handleGroupAssociationsChange = useCallback((value: boolean) => {
setGroupAssociatedFiles(value);
}, []);

const handlePreferredAssociatedTypeChange = useCallback((value: AssociatedPrimaryPreference) => {
setPreferredAssociatedType(value);
}, []);

useEffect(() => {
if (isInitialMount.current || !appSettings) {
return;
Expand Down Expand Up @@ -1216,6 +1365,24 @@ function App() {
}
}, [filterCriteria, appSettings, handleSettingsChange]);

useEffect(() => {
if (isInitialMount.current || !appSettings) {
return;
}
if (appSettings.groupAssociatedFiles !== groupAssociatedFiles) {
handleSettingsChange({ ...appSettings, groupAssociatedFiles });
}
}, [groupAssociatedFiles, appSettings, handleSettingsChange]);

useEffect(() => {
if (isInitialMount.current || !appSettings) {
return;
}
if (appSettings.preferredAssociatedType !== preferredAssociatedType) {
handleSettingsChange({ ...appSettings, preferredAssociatedType });
}
}, [preferredAssociatedType, appSettings, handleSettingsChange]);

useEffect(() => {
if (appSettings?.adaptiveEditorTheme && selectedImage && finalPreviewUrl) {
generatePaletteFromImage(finalPreviewUrl)
Expand Down Expand Up @@ -1467,6 +1634,10 @@ function App() {

const handleBackToLibrary = useCallback(() => {
const lastActivePath = selectedImage?.path ?? null;
const primaryPath =
lastActivePath && associationsByPath[lastActivePath]
? associationsByPath[lastActivePath].primaryPath
: lastActivePath;
setSelectedImage(null);
setFinalPreviewUrl(null);
setUncroppedAdjustedPreviewUrl(null);
Expand All @@ -1477,8 +1648,9 @@ function App() {
setActiveMaskContainerId(null);
setActiveAiPatchContainerId(null);
setActiveAiSubMaskId(null);
setLibraryActivePath(lastActivePath);
}, [selectedImage?.path]);
setLibraryActivePath(primaryPath);
setMultiSelectedPaths(primaryPath ? [primaryPath] : []);
}, [associationsByPath, selectedImage?.path]);

const executeDelete = useCallback(
async (pathsToDelete: Array<string>, options = { includeAssociated: false }) => {
Expand Down Expand Up @@ -1970,6 +2142,16 @@ function App() {
[selectedImage?.path, applyAdjustments, debouncedSave, thumbnails, resetAdjustmentsHistory],
);

const handleVariantSelect = useCallback(
(path: string) => {
if (selectedImage?.path === path) {
return;
}
handleImageSelect(path);
},
[handleImageSelect, selectedImage?.path],
);

useKeyboardShortcuts({
activeAiPatchContainerId,
activeAiSubMaskId,
Expand Down Expand Up @@ -2000,6 +2182,7 @@ function App() {
multiSelectedPaths,
redo,
selectedImage,
selectedDisplayPath: filmstripActivePath,
setActiveAiSubMaskId,
setActiveMaskContainerId,
setActiveMaskId,
Expand Down Expand Up @@ -2807,12 +2990,8 @@ function App() {
const stitchLabel = `Stitch Panorama`;

const hasAssociatedFiles = finalSelection.some((selectedPath) => {
const lastDotIndex = selectedPath.lastIndexOf('.');
if (lastDotIndex === -1) return false;
const basePath = selectedPath.substring(0, lastDotIndex);
return imageList.some(
(image) => image.path.startsWith(basePath + '.') && image.path !== selectedPath,
);
const association = associationsByPath[selectedPath];
return association && association.variantPaths.length > 1;
});

const deleteOption = {
Expand Down Expand Up @@ -3301,12 +3480,15 @@ function App() {
isFullResolution={isFullResolution}
fullResolutionUrl={fullResolutionUrl}
isLoadingFullRes={isLoadingFullRes}
variantOptions={variantOptions}
onVariantSelect={handleVariantSelect}
/>
<Resizer
direction={Orientation.Horizontal}
onMouseDown={createResizeHandler(setBottomPanelHeight, bottomPanelHeight)}
/>
<BottomBar
activeDisplayPath={filmstripActivePath}
filmstripHeight={bottomPanelHeight}
imageList={sortedImageList}
imageRatings={imageRatings}
Expand Down Expand Up @@ -3520,6 +3702,10 @@ function App() {
thumbnails={thumbnails}
thumbnailSize={thumbnailSize}
onNavigateToCommunity={() => setActiveView('community')}
groupAssociatedFiles={groupAssociatedFiles}
preferredAssociatedType={preferredAssociatedType}
onGroupAssociationsChange={handleGroupAssociationsChange}
onPreferredAssociationTypeChange={handlePreferredAssociatedTypeChange}
/>
)}
{rootPath && (
Expand Down Expand Up @@ -3714,4 +3900,4 @@ const AppWrapper = () => (
</ClerkProvider>
);

export default AppWrapper;
export default AppWrapper;
6 changes: 4 additions & 2 deletions src/components/panel/BottomBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Filmstrip from './Filmstrip';
import { GLOBAL_KEYS, ImageFile, SelectedImage, ThumbnailAspectRatio } from '../ui/AppProperties';

interface BottomBarProps {
activeDisplayPath?: string | null;
filmstripHeight?: number;
imageList?: Array<ImageFile>;
imageRatings?: Record<string, number> | null;
Expand Down Expand Up @@ -79,6 +80,7 @@ const StarRating = ({ rating, onRate, disabled }: StarRatingProps) => {
};

export default function BottomBar({
activeDisplayPath,
filmstripHeight,
imageList = [],
imageRatings,
Expand Down Expand Up @@ -220,7 +222,7 @@ export default function BottomBar({
onClearSelection={onClearSelection}
onContextMenu={onContextMenu}
onImageSelect={onImageSelect}
selectedImage={selectedImage}
activePath={activeDisplayPath || selectedImage?.path || null}
thumbnails={thumbnails}
thumbnailAspectRatio={thumbnailAspectRatio}
/>
Expand Down Expand Up @@ -356,4 +358,4 @@ export default function BottomBar({
</div>
</div>
);
}
}
Loading
Loading