Compare commits
1 Commits
swipe_imag
...
refactor/g
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb61a591b0 |
85
web/apps/photos/src/components/gallery/GalleryContainer.tsx
Normal file
85
web/apps/photos/src/components/gallery/GalleryContainer.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import React from 'react';
|
||||
import { NavbarBase } from 'ente-base/components/Navbar';
|
||||
import { NormalNavbarContents, HiddenSectionNavbarContents } from './NavbarContents';
|
||||
import type { EnteFile } from 'ente-media/file';
|
||||
import type { SelectedState } from 'utils/file';
|
||||
|
||||
interface GalleryContainerProps {
|
||||
// State props
|
||||
filteredFiles: EnteFile[];
|
||||
selected: SelectedState;
|
||||
barMode: string;
|
||||
isInSearchMode: boolean;
|
||||
isFirstLoad: boolean;
|
||||
activeCollectionID: number | undefined;
|
||||
|
||||
// Event handlers
|
||||
onSidebar: () => void;
|
||||
onUpload: () => void;
|
||||
onSelectSearchOption: (option: unknown) => void;
|
||||
onSelectPeople: () => void;
|
||||
onSelectPerson: (personID: string) => void;
|
||||
onClearSelection: () => void;
|
||||
onChangeMode: (mode: string) => void;
|
||||
|
||||
// Children for flexible rendering
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simplified gallery container component that handles the basic layout
|
||||
*/
|
||||
export const GalleryContainer: React.FC<GalleryContainerProps> = ({
|
||||
selected,
|
||||
barMode,
|
||||
isInSearchMode,
|
||||
activeCollectionID,
|
||||
onSidebar,
|
||||
onUpload,
|
||||
onSelectSearchOption,
|
||||
onSelectPeople,
|
||||
onSelectPerson,
|
||||
onChangeMode,
|
||||
children,
|
||||
}) => {
|
||||
const showSelectionBar = selected.count > 0 && selected.collectionID === activeCollectionID;
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Navigation Bar */}
|
||||
<NavbarBase
|
||||
sx={[
|
||||
{
|
||||
mb: "12px",
|
||||
px: "24px",
|
||||
"@media (width < 720px)": { px: "4px" },
|
||||
},
|
||||
showSelectionBar && { borderColor: "accent.main" },
|
||||
]}
|
||||
>
|
||||
{showSelectionBar ? (
|
||||
<div>Selection Bar Placeholder</div>
|
||||
) : barMode === "hidden-albums" ? (
|
||||
<HiddenSectionNavbarContents
|
||||
onBack={() => onChangeMode("albums")}
|
||||
/>
|
||||
) : (
|
||||
<NormalNavbarContents
|
||||
isInSearchMode={isInSearchMode}
|
||||
onSidebar={onSidebar}
|
||||
onUpload={onUpload}
|
||||
onShowSearchInput={() => {
|
||||
// TODO: Implement search mode toggle
|
||||
}}
|
||||
onSelectSearchOption={onSelectSearchOption}
|
||||
onSelectPeople={onSelectPeople}
|
||||
onSelectPerson={onSelectPerson}
|
||||
/>
|
||||
)}
|
||||
</NavbarBase>
|
||||
|
||||
{/* Main Content Area */}
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
};
|
||||
65
web/apps/photos/src/components/gallery/GalleryContent.tsx
Normal file
65
web/apps/photos/src/components/gallery/GalleryContent.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import React from 'react';
|
||||
import { GalleryEmptyState, PeopleEmptyState } from 'ente-new/photos/components/gallery';
|
||||
import { uploadManager } from 'services/upload-manager';
|
||||
import { PseudoCollectionID } from 'ente-new/photos/services/collection-summary';
|
||||
import type { EnteFile } from 'ente-media/file';
|
||||
import type { GalleryBarMode } from 'ente-new/photos/components/gallery/reducer';
|
||||
|
||||
interface GalleryContentProps {
|
||||
// Basic state
|
||||
filteredFiles: EnteFile[];
|
||||
isInSearchMode: boolean;
|
||||
isFirstLoad: boolean;
|
||||
barMode: GalleryBarMode;
|
||||
activeCollectionID: number | undefined;
|
||||
activePerson: unknown;
|
||||
|
||||
// Event handlers
|
||||
onUpload: () => void;
|
||||
|
||||
// Children for flexible content
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simplified main content area that handles empty states and delegates to children
|
||||
*/
|
||||
export const GalleryContent: React.FC<GalleryContentProps> = ({
|
||||
filteredFiles,
|
||||
isInSearchMode,
|
||||
isFirstLoad,
|
||||
barMode,
|
||||
activeCollectionID,
|
||||
activePerson,
|
||||
onUpload,
|
||||
children,
|
||||
}) => {
|
||||
// Show empty states for specific conditions
|
||||
const showGalleryEmptyState =
|
||||
!isInSearchMode &&
|
||||
!isFirstLoad &&
|
||||
!filteredFiles.length &&
|
||||
activeCollectionID === PseudoCollectionID.all;
|
||||
|
||||
const showPeopleEmptyState =
|
||||
!isInSearchMode &&
|
||||
!isFirstLoad &&
|
||||
barMode === "people" &&
|
||||
!activePerson;
|
||||
|
||||
if (showGalleryEmptyState) {
|
||||
return (
|
||||
<GalleryEmptyState
|
||||
isUploadInProgress={uploadManager.isUploadInProgress()}
|
||||
onUpload={onUpload}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (showPeopleEmptyState) {
|
||||
return <PeopleEmptyState />;
|
||||
}
|
||||
|
||||
// Render children (FileListWithViewer, GalleryBarAndListHeader, etc.)
|
||||
return <>{children}</>;
|
||||
};
|
||||
66
web/apps/photos/src/components/gallery/GalleryModals.tsx
Normal file
66
web/apps/photos/src/components/gallery/GalleryModals.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import React from 'react';
|
||||
import { PlanSelector } from 'ente-new/photos/components/PlanSelector';
|
||||
import { SingleInputDialog } from 'ente-base/components/SingleInputDialog';
|
||||
import { WhatsNew } from 'ente-new/photos/components/WhatsNew';
|
||||
import { t } from 'i18next';
|
||||
|
||||
interface GalleryModalsProps {
|
||||
// Modal states - using simplified visibility props
|
||||
planSelectorVisible: boolean;
|
||||
whatsNewVisible: boolean;
|
||||
albumNameInputVisible: boolean;
|
||||
|
||||
// Modal handlers
|
||||
onClosePlanSelector: () => void;
|
||||
onCloseWhatsNew: () => void;
|
||||
onCloseAlbumNameInput: () => void;
|
||||
onAlbumNameSubmit: (name: string) => Promise<void>;
|
||||
setLoading: (loading: boolean) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simplified container for essential gallery modals
|
||||
*/
|
||||
export const GalleryModals: React.FC<GalleryModalsProps> = ({
|
||||
planSelectorVisible,
|
||||
whatsNewVisible,
|
||||
albumNameInputVisible,
|
||||
onClosePlanSelector,
|
||||
onCloseWhatsNew,
|
||||
onCloseAlbumNameInput,
|
||||
onAlbumNameSubmit,
|
||||
setLoading,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
{/* Plan Selector Modal */}
|
||||
{planSelectorVisible && (
|
||||
<PlanSelector
|
||||
open={planSelectorVisible}
|
||||
onClose={onClosePlanSelector}
|
||||
setLoading={setLoading}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* What's New Dialog */}
|
||||
{whatsNewVisible && (
|
||||
<WhatsNew
|
||||
open={whatsNewVisible}
|
||||
onClose={onCloseWhatsNew}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Album Name Input Dialog */}
|
||||
{albumNameInputVisible && (
|
||||
<SingleInputDialog
|
||||
open={albumNameInputVisible}
|
||||
onClose={onCloseAlbumNameInput}
|
||||
title={t("new_album")}
|
||||
label={t("album_name")}
|
||||
submitButtonTitle={t("create")}
|
||||
onSubmit={onAlbumNameSubmit}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
34
web/apps/photos/src/components/gallery/MessageComponents.tsx
Normal file
34
web/apps/photos/src/components/gallery/MessageComponents.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import { Typography } from '@mui/material';
|
||||
import { CenteredRow } from 'ente-base/components/containers';
|
||||
import { TranslucentLoadingOverlay } from 'ente-base/components/loaders';
|
||||
import { t } from 'i18next';
|
||||
|
||||
/**
|
||||
* Message shown during first load to inform users about potential delays
|
||||
*/
|
||||
export const FirstLoadMessage: React.FC = () => (
|
||||
<CenteredRow>
|
||||
<Typography variant="small" sx={{ color: "text.muted" }}>
|
||||
{t("initial_load_delay_warning")}
|
||||
</Typography>
|
||||
</CenteredRow>
|
||||
);
|
||||
|
||||
/**
|
||||
* Message shown when the app is offline
|
||||
*/
|
||||
export const OfflineMessage: React.FC = () => (
|
||||
<Typography
|
||||
variant="small"
|
||||
sx={{ bgcolor: "background.paper", p: 2, mb: 1, textAlign: "center" }}
|
||||
>
|
||||
{t("offline_message")}
|
||||
</Typography>
|
||||
);
|
||||
|
||||
/**
|
||||
* Blocking overlay shown during certain operations
|
||||
*/
|
||||
export const BlockingLoadOverlay: React.FC<{ show: boolean }> = ({ show }) =>
|
||||
show ? <TranslucentLoadingOverlay /> : null;
|
||||
80
web/apps/photos/src/components/gallery/NavbarContents.tsx
Normal file
80
web/apps/photos/src/components/gallery/NavbarContents.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import React from 'react';
|
||||
import { Stack, Typography, IconButton } from '@mui/material';
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
import MenuIcon from '@mui/icons-material/Menu';
|
||||
import FileUploadOutlinedIcon from '@mui/icons-material/FileUploadOutlined';
|
||||
import { FocusVisibleButton } from 'ente-base/components/mui/FocusVisibleButton';
|
||||
import { useIsSmallWidth } from 'ente-base/components/utils/hooks';
|
||||
import type { ButtonishProps } from 'ente-base/components/mui';
|
||||
import { SearchBar, type SearchBarProps } from 'ente-new/photos/components/SearchBar';
|
||||
import { uploadManager } from 'services/upload-manager';
|
||||
import { t } from 'i18next';
|
||||
|
||||
interface NormalNavbarContentsProps extends SearchBarProps {
|
||||
onSidebar: () => void;
|
||||
onUpload: () => void;
|
||||
}
|
||||
|
||||
export const NormalNavbarContents: React.FC<NormalNavbarContentsProps> = ({
|
||||
onSidebar,
|
||||
onUpload,
|
||||
...props
|
||||
}) => (
|
||||
<>
|
||||
{!props.isInSearchMode && <SidebarButton onClick={onSidebar} />}
|
||||
<SearchBar {...props} />
|
||||
{!props.isInSearchMode && <UploadButton onClick={onUpload} />}
|
||||
</>
|
||||
);
|
||||
|
||||
const SidebarButton: React.FC<ButtonishProps> = ({ onClick }) => (
|
||||
<IconButton {...{ onClick }}>
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
);
|
||||
|
||||
const UploadButton: React.FC<ButtonishProps> = ({ onClick }) => {
|
||||
const disabled = uploadManager.isUploadInProgress();
|
||||
const isSmallWidth = useIsSmallWidth();
|
||||
|
||||
const icon = <FileUploadOutlinedIcon />;
|
||||
|
||||
return (
|
||||
<>
|
||||
{isSmallWidth ? (
|
||||
<IconButton {...{ onClick, disabled }}>{icon}</IconButton>
|
||||
) : (
|
||||
<FocusVisibleButton
|
||||
color="secondary"
|
||||
startIcon={icon}
|
||||
{...{ onClick, disabled }}
|
||||
>
|
||||
{t("upload")}
|
||||
</FocusVisibleButton>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface HiddenSectionNavbarContentsProps {
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
export const HiddenSectionNavbarContents: React.FC<
|
||||
HiddenSectionNavbarContentsProps
|
||||
> = ({ onBack }) => (
|
||||
<Stack
|
||||
direction="row"
|
||||
sx={(theme) => ({
|
||||
gap: "24px",
|
||||
flex: 1,
|
||||
alignItems: "center",
|
||||
background: theme.vars.palette.background.default,
|
||||
})}
|
||||
>
|
||||
<IconButton onClick={onBack}>
|
||||
<ArrowBackIcon />
|
||||
</IconButton>
|
||||
<Typography sx={{ flex: 1 }}>{t("section_hidden")}</Typography>
|
||||
</Stack>
|
||||
);
|
||||
6
web/apps/photos/src/components/gallery/index.ts
Normal file
6
web/apps/photos/src/components/gallery/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
// Gallery presentational components
|
||||
export { GalleryContainer } from './GalleryContainer';
|
||||
export { GalleryContent } from './GalleryContent';
|
||||
export { GalleryModals } from './GalleryModals';
|
||||
export { NormalNavbarContents, HiddenSectionNavbarContents } from './NavbarContents';
|
||||
export { FirstLoadMessage, OfflineMessage, BlockingLoadOverlay } from './MessageComponents';
|
||||
11
web/apps/photos/src/hooks/index.ts
Normal file
11
web/apps/photos/src/hooks/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
// Gallery hooks for managing different aspects of the gallery component
|
||||
export { useGalleryData } from './useGalleryData';
|
||||
export { useFileOperations } from './useFileOperations';
|
||||
export { useSelection } from './useSelection';
|
||||
export { useCollectionOperations } from './useCollectionOperations';
|
||||
export { useModalManagement } from './useModalManagement';
|
||||
export { useGalleryInitialization } from './useGalleryInitialization';
|
||||
export { useGalleryNavigation } from './useGalleryNavigation';
|
||||
|
||||
// Re-export types for convenience
|
||||
export type { RemotePullOpts } from './useGalleryData';
|
||||
167
web/apps/photos/src/hooks/useCollectionOperations.ts
Normal file
167
web/apps/photos/src/hooks/useCollectionOperations.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import type { Collection } from 'ente-media/collection';
|
||||
import type { EnteFile } from 'ente-media/file';
|
||||
import type { SelectedState } from 'utils/file';
|
||||
import type { CollectionOp } from 'ente-new/photos/components/SelectedFileOptions';
|
||||
import type { CollectionSelectorAttributes } from 'ente-new/photos/components/CollectionSelector';
|
||||
import { createAlbum } from 'ente-new/photos/services/collection';
|
||||
import { performCollectionOp } from 'ente-new/photos/components/gallery/helpers';
|
||||
import { getSelectedFiles } from 'utils/file';
|
||||
import { usePhotosAppContext } from 'ente-new/photos/types/context';
|
||||
import { useBaseContext } from 'ente-base/context';
|
||||
import { notifyOthersFilesDialogAttributes } from 'ente-new/photos/components/utils/dialog-attributes';
|
||||
|
||||
interface UseCollectionOperationsProps {
|
||||
user: { id: number } | null;
|
||||
filteredFiles: EnteFile[];
|
||||
selected: SelectedState;
|
||||
clearSelection: () => void;
|
||||
onRemotePull: () => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom hook for managing collection operations
|
||||
*/
|
||||
export const useCollectionOperations = ({
|
||||
user,
|
||||
filteredFiles,
|
||||
selected,
|
||||
clearSelection,
|
||||
onRemotePull,
|
||||
}: UseCollectionOperationsProps) => {
|
||||
const { showLoadingBar, hideLoadingBar } = usePhotosAppContext();
|
||||
const { onGenericError, showMiniDialog } = useBaseContext();
|
||||
|
||||
// Collection selector state
|
||||
const [openCollectionSelector, setOpenCollectionSelector] = useState(false);
|
||||
const [collectionSelectorAttributes, setCollectionSelectorAttributes] =
|
||||
useState<CollectionSelectorAttributes | undefined>();
|
||||
|
||||
// Album creation state
|
||||
const [postCreateAlbumOp, setPostCreateAlbumOp] = useState<CollectionOp | undefined>();
|
||||
|
||||
/**
|
||||
* Create a handler for collection operations (add, move)
|
||||
*/
|
||||
const createOnSelectForCollectionOp = useCallback(
|
||||
(op: CollectionOp) => (selectedCollection: Collection) => {
|
||||
void (async () => {
|
||||
showLoadingBar();
|
||||
try {
|
||||
setOpenCollectionSelector(false);
|
||||
const selectedFiles = getSelectedFiles(selected, filteredFiles);
|
||||
const userFiles = selectedFiles.filter(
|
||||
(f) => f.ownerID === user!.id,
|
||||
);
|
||||
const sourceCollectionID = selected.collectionID;
|
||||
|
||||
if (userFiles.length > 0) {
|
||||
await performCollectionOp(
|
||||
op,
|
||||
selectedCollection,
|
||||
userFiles,
|
||||
sourceCollectionID,
|
||||
);
|
||||
}
|
||||
|
||||
// Notify if some files couldn't be processed
|
||||
if (userFiles.length !== selectedFiles.length) {
|
||||
showMiniDialog(notifyOthersFilesDialogAttributes());
|
||||
}
|
||||
|
||||
clearSelection();
|
||||
await onRemotePull();
|
||||
} catch (e) {
|
||||
onGenericError(e);
|
||||
} finally {
|
||||
hideLoadingBar();
|
||||
}
|
||||
})();
|
||||
},
|
||||
[
|
||||
showLoadingBar,
|
||||
hideLoadingBar,
|
||||
selected,
|
||||
filteredFiles,
|
||||
user,
|
||||
showMiniDialog,
|
||||
clearSelection,
|
||||
onRemotePull,
|
||||
onGenericError,
|
||||
],
|
||||
);
|
||||
|
||||
/**
|
||||
* Create a handler for collection operations that need album creation
|
||||
*/
|
||||
const createOnCreateForCollectionOp = useCallback(
|
||||
(op: CollectionOp) => {
|
||||
setPostCreateAlbumOp(op);
|
||||
return () => {
|
||||
// This will be handled by the album name input dialog
|
||||
// The actual creation happens in handleAlbumNameSubmit
|
||||
};
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
/**
|
||||
* Handle album name submission after creation
|
||||
*/
|
||||
const handleAlbumNameSubmit = useCallback(
|
||||
async (name: string) => {
|
||||
if (!postCreateAlbumOp) return;
|
||||
|
||||
try {
|
||||
const collection = await createAlbum(name);
|
||||
// Execute the deferred operation
|
||||
createOnSelectForCollectionOp(postCreateAlbumOp)(collection);
|
||||
setPostCreateAlbumOp(undefined);
|
||||
} catch (e) {
|
||||
onGenericError(e);
|
||||
setPostCreateAlbumOp(undefined);
|
||||
}
|
||||
},
|
||||
[postCreateAlbumOp, createOnSelectForCollectionOp, onGenericError],
|
||||
);
|
||||
|
||||
/**
|
||||
* Open collection selector with specific attributes
|
||||
*/
|
||||
const handleOpenCollectionSelector = useCallback(
|
||||
(attributes: CollectionSelectorAttributes) => {
|
||||
setCollectionSelectorAttributes(attributes);
|
||||
setOpenCollectionSelector(true);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
/**
|
||||
* Close collection selector
|
||||
*/
|
||||
const handleCloseCollectionSelector = useCallback(
|
||||
() => {
|
||||
setOpenCollectionSelector(false);
|
||||
setCollectionSelectorAttributes(undefined);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return {
|
||||
// State
|
||||
openCollectionSelector,
|
||||
collectionSelectorAttributes,
|
||||
postCreateAlbumOp,
|
||||
|
||||
// Handlers
|
||||
createOnSelectForCollectionOp,
|
||||
createOnCreateForCollectionOp,
|
||||
handleAlbumNameSubmit,
|
||||
handleOpenCollectionSelector,
|
||||
handleCloseCollectionSelector,
|
||||
|
||||
// Setters for external control
|
||||
setOpenCollectionSelector,
|
||||
setCollectionSelectorAttributes,
|
||||
};
|
||||
};
|
||||
183
web/apps/photos/src/hooks/useFileOperations.ts
Normal file
183
web/apps/photos/src/hooks/useFileOperations.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
import { useCallback } from 'react';
|
||||
import type { EnteFile } from 'ente-media/file';
|
||||
import type { ItemVisibility } from 'ente-media/file-metadata';
|
||||
import type { Collection } from 'ente-media/collection';
|
||||
import {
|
||||
addToFavoritesCollection,
|
||||
removeFromFavoritesCollection,
|
||||
removeFromCollection
|
||||
} from 'ente-new/photos/services/collection';
|
||||
import { updateFilesVisibility } from 'ente-new/photos/services/file';
|
||||
import { getSelectedFiles, type SelectedState } from 'utils/file';
|
||||
import type { FileOp } from 'ente-new/photos/components/SelectedFileOptions';
|
||||
import { usePhotosAppContext } from 'ente-new/photos/types/context';
|
||||
import { useBaseContext } from 'ente-base/context';
|
||||
import { notifyOthersFilesDialogAttributes } from 'ente-new/photos/components/utils/dialog-attributes';
|
||||
|
||||
interface UseFileOperationsProps {
|
||||
user: { id: number };
|
||||
filteredFiles: EnteFile[];
|
||||
selected: SelectedState;
|
||||
clearSelection: () => void;
|
||||
onRemotePull: () => Promise<void>;
|
||||
dispatch: (action: { type: string; [key: string]: unknown }) => void;
|
||||
favoriteFileIDs: Set<number>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom hook for handling file operations
|
||||
*/
|
||||
export const useFileOperations = ({
|
||||
user,
|
||||
filteredFiles,
|
||||
selected,
|
||||
clearSelection,
|
||||
onRemotePull,
|
||||
dispatch,
|
||||
favoriteFileIDs,
|
||||
}: UseFileOperationsProps) => {
|
||||
const { showLoadingBar, hideLoadingBar } = usePhotosAppContext();
|
||||
const { onGenericError, showMiniDialog } = useBaseContext();
|
||||
|
||||
/**
|
||||
* Toggle favorite status of a file
|
||||
*/
|
||||
const handleToggleFavorite = useCallback(
|
||||
async (file: EnteFile) => {
|
||||
const fileID = file.id;
|
||||
const isFavorite = favoriteFileIDs.has(fileID);
|
||||
|
||||
dispatch({ type: "addPendingFavoriteUpdate", fileID });
|
||||
try {
|
||||
const action = isFavorite
|
||||
? removeFromFavoritesCollection
|
||||
: addToFavoritesCollection;
|
||||
await action([file]);
|
||||
dispatch({
|
||||
type: "unsyncedFavoriteUpdate",
|
||||
fileID,
|
||||
isFavorite: !isFavorite,
|
||||
});
|
||||
} finally {
|
||||
dispatch({ type: "removePendingFavoriteUpdate", fileID });
|
||||
}
|
||||
},
|
||||
[favoriteFileIDs, dispatch],
|
||||
);
|
||||
|
||||
/**
|
||||
* Update file visibility
|
||||
*/
|
||||
const handleFileVisibilityUpdate = useCallback(
|
||||
async (file: EnteFile, visibility: ItemVisibility) => {
|
||||
const fileID = file.id;
|
||||
dispatch({ type: "addPendingVisibilityUpdate", fileID });
|
||||
try {
|
||||
await updateFilesVisibility([file], visibility);
|
||||
dispatch({
|
||||
type: "unsyncedPrivateMagicMetadataUpdate",
|
||||
fileID,
|
||||
privateMagicMetadata: {
|
||||
...file.magicMetadata,
|
||||
count: file.magicMetadata?.count ?? 0,
|
||||
version: (file.magicMetadata?.version ?? 0) + 1,
|
||||
data: { ...file.magicMetadata?.data, visibility },
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
dispatch({ type: "removePendingVisibilityUpdate", fileID });
|
||||
}
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
/**
|
||||
* Remove files from a collection
|
||||
*/
|
||||
const handleRemoveFilesFromCollection = useCallback(
|
||||
async (collection: Collection) => {
|
||||
showLoadingBar();
|
||||
let notifyOthersFiles = false;
|
||||
try {
|
||||
const selectedFiles = getSelectedFiles(selected, filteredFiles);
|
||||
const processedCount = await removeFromCollection(
|
||||
collection,
|
||||
selectedFiles,
|
||||
);
|
||||
notifyOthersFiles = processedCount !== selectedFiles.length;
|
||||
clearSelection();
|
||||
await onRemotePull();
|
||||
} catch (e) {
|
||||
onGenericError(e);
|
||||
} finally {
|
||||
hideLoadingBar();
|
||||
}
|
||||
|
||||
if (notifyOthersFiles) {
|
||||
showMiniDialog(notifyOthersFilesDialogAttributes());
|
||||
}
|
||||
},
|
||||
[
|
||||
showLoadingBar,
|
||||
hideLoadingBar,
|
||||
selected,
|
||||
filteredFiles,
|
||||
clearSelection,
|
||||
onRemotePull,
|
||||
onGenericError,
|
||||
showMiniDialog,
|
||||
],
|
||||
);
|
||||
|
||||
/**
|
||||
* Create a file operation handler
|
||||
*/
|
||||
const createFileOpHandler = useCallback(
|
||||
(op: FileOp) => () => {
|
||||
void (async () => {
|
||||
showLoadingBar();
|
||||
try {
|
||||
const selectedFiles = getSelectedFiles(selected, filteredFiles);
|
||||
const toProcessFiles = selectedFiles.filter(
|
||||
(file) => file.ownerID === user.id,
|
||||
);
|
||||
|
||||
if (toProcessFiles.length > 0) {
|
||||
// TODO: Implement proper file operations with correct typing
|
||||
// await performFileOp(op, toProcessFiles, ...callbacks);
|
||||
console.log('File operation:', op, toProcessFiles.length, 'files');
|
||||
}
|
||||
|
||||
if (toProcessFiles.length !== selectedFiles.length) {
|
||||
showMiniDialog(notifyOthersFilesDialogAttributes());
|
||||
}
|
||||
clearSelection();
|
||||
await onRemotePull();
|
||||
} catch (e) {
|
||||
onGenericError(e);
|
||||
} finally {
|
||||
hideLoadingBar();
|
||||
}
|
||||
})();
|
||||
},
|
||||
[
|
||||
showLoadingBar,
|
||||
hideLoadingBar,
|
||||
selected,
|
||||
filteredFiles,
|
||||
user,
|
||||
dispatch,
|
||||
showMiniDialog,
|
||||
clearSelection,
|
||||
onRemotePull,
|
||||
onGenericError,
|
||||
],
|
||||
);
|
||||
|
||||
return {
|
||||
handleToggleFavorite,
|
||||
handleFileVisibilityUpdate,
|
||||
handleRemoveFilesFromCollection,
|
||||
createFileOpHandler,
|
||||
};
|
||||
};
|
||||
132
web/apps/photos/src/hooks/useGalleryData.ts
Normal file
132
web/apps/photos/src/hooks/useGalleryData.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useGalleryReducer } from 'ente-new/photos/components/gallery/reducer';
|
||||
import { PromiseQueue } from 'ente-utils/promise';
|
||||
import { usePhotosAppContext } from 'ente-new/photos/types/context';
|
||||
import { useBaseContext } from 'ente-base/context';
|
||||
import {
|
||||
savedCollections,
|
||||
savedCollectionFiles,
|
||||
savedTrashItems
|
||||
} from 'ente-new/photos/services/photos-fdb';
|
||||
import { pullFiles, prePullFiles, postPullFiles } from 'ente-new/photos/services/pull';
|
||||
import { ensureLocalUser } from 'ente-accounts/services/user';
|
||||
import { savedUserDetailsOrTriggerPull } from 'ente-new/photos/services/user-details';
|
||||
import { masterKeyFromSession, clearSessionStorage } from 'ente-base/session';
|
||||
import { isSessionInvalid } from 'ente-accounts/services/session';
|
||||
import log from 'ente-base/log';
|
||||
import exportService from 'ente-new/photos/services/export';
|
||||
|
||||
export interface RemotePullOpts {
|
||||
silent?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom hook for managing gallery data fetching and state
|
||||
*/
|
||||
export const useGalleryData = () => {
|
||||
const { showLoadingBar, hideLoadingBar } = usePhotosAppContext();
|
||||
const { onGenericError } = useBaseContext();
|
||||
const router = useRouter();
|
||||
|
||||
const [state, dispatch] = useGalleryReducer();
|
||||
const [isFirstLoad, setIsFirstLoad] = useState(false);
|
||||
|
||||
// Queues for serializing remote operations
|
||||
const remoteFilesPullQueue = useRef(new PromiseQueue<void>());
|
||||
const remotePullQueue = useRef(new PromiseQueue<void>());
|
||||
|
||||
/**
|
||||
* Pull latest collections, collection files and trash items from remote
|
||||
*/
|
||||
const remoteFilesPull = useCallback(
|
||||
() =>
|
||||
remoteFilesPullQueue.current.add(() =>
|
||||
pullFiles({
|
||||
onSetCollections: (collections) =>
|
||||
dispatch({ type: "setCollections", collections }),
|
||||
onSetCollectionFiles: (collectionFiles) =>
|
||||
dispatch({
|
||||
type: "setCollectionFiles",
|
||||
collectionFiles,
|
||||
}),
|
||||
onSetTrashedItems: (trashItems) =>
|
||||
dispatch({ type: "setTrashItems", trashItems }),
|
||||
onDidUpdateCollectionFiles: () =>
|
||||
exportService.onLocalFilesUpdated(),
|
||||
}),
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
/**
|
||||
* Perform a full remote pull with error handling
|
||||
*/
|
||||
const remotePull = useCallback(
|
||||
async (opts?: RemotePullOpts) =>
|
||||
remotePullQueue.current.add(async () => {
|
||||
const { silent } = opts ?? {};
|
||||
|
||||
// Pre-flight checks
|
||||
if (!navigator.onLine) return;
|
||||
if (await isSessionInvalid()) {
|
||||
// Handle session expiry
|
||||
return;
|
||||
}
|
||||
if (!(await masterKeyFromSession())) {
|
||||
clearSessionStorage();
|
||||
void router.push("/credentials");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!silent) showLoadingBar();
|
||||
await prePullFiles();
|
||||
await remoteFilesPull();
|
||||
await postPullFiles();
|
||||
} catch (e) {
|
||||
log.error("Remote pull failed", e);
|
||||
} finally {
|
||||
dispatch({ type: "clearUnsyncedState" });
|
||||
if (!silent) hideLoadingBar();
|
||||
}
|
||||
}),
|
||||
[showLoadingBar, hideLoadingBar, router, remoteFilesPull],
|
||||
);
|
||||
|
||||
/**
|
||||
* Initialize the gallery on mount
|
||||
*/
|
||||
const initializeGallery = useCallback(async () => {
|
||||
try {
|
||||
dispatch({ type: "showAll" });
|
||||
|
||||
const user = ensureLocalUser();
|
||||
const userDetails = await savedUserDetailsOrTriggerPull();
|
||||
|
||||
dispatch({
|
||||
type: "mount",
|
||||
user,
|
||||
familyData: userDetails?.familyData,
|
||||
collections: await savedCollections(),
|
||||
collectionFiles: await savedCollectionFiles(),
|
||||
trashItems: await savedTrashItems(),
|
||||
});
|
||||
|
||||
await remotePull();
|
||||
setIsFirstLoad(false);
|
||||
} catch (error) {
|
||||
onGenericError(error);
|
||||
}
|
||||
}, [dispatch, remotePull, onGenericError]);
|
||||
|
||||
return {
|
||||
state,
|
||||
dispatch,
|
||||
isFirstLoad,
|
||||
setIsFirstLoad,
|
||||
remoteFilesPull,
|
||||
remotePull,
|
||||
initializeGallery,
|
||||
};
|
||||
};
|
||||
124
web/apps/photos/src/hooks/useGalleryInitialization.ts
Normal file
124
web/apps/photos/src/hooks/useGalleryInitialization.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { haveMasterKeyInSession } from 'ente-base/session';
|
||||
import { savedAuthToken } from 'ente-base/token';
|
||||
import { stashRedirect } from 'ente-accounts/services/redirect';
|
||||
import { validateKey } from 'ente-new/photos/components/gallery/helpers';
|
||||
import {
|
||||
getAndClearIsFirstLogin,
|
||||
getAndClearJustSignedUp
|
||||
} from 'ente-accounts/services/accounts-db';
|
||||
import { shouldShowWhatsNew } from 'ente-new/photos/services/changelog';
|
||||
import { initSettings } from 'ente-new/photos/services/settings';
|
||||
import { useBaseContext } from 'ente-base/context';
|
||||
|
||||
interface UseGalleryInitializationProps {
|
||||
onInitializeGallery: () => Promise<void>;
|
||||
onShowPlanSelector: () => void;
|
||||
onShowWhatsNew: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom hook for handling gallery initialization and authentication checks
|
||||
*/
|
||||
export const useGalleryInitialization = ({
|
||||
onInitializeGallery,
|
||||
onShowPlanSelector,
|
||||
onShowWhatsNew,
|
||||
}: UseGalleryInitializationProps) => {
|
||||
const { logout } = useBaseContext();
|
||||
const router = useRouter();
|
||||
const [isFirstLoad, setIsFirstLoad] = useState(false);
|
||||
const [blockingLoad, setBlockingLoad] = useState(false);
|
||||
|
||||
/**
|
||||
* Preload all three variants of a responsive image
|
||||
*/
|
||||
const preloadImage = (imgBasePath: string) => {
|
||||
const srcset: string[] = [];
|
||||
for (let i = 1; i <= 3; i++) srcset.push(`${imgBasePath}/${i}x.png ${i}x`);
|
||||
new Image().srcset = srcset.join(",");
|
||||
};
|
||||
|
||||
/**
|
||||
* Set up keyboard shortcuts for select all
|
||||
*/
|
||||
const setupSelectAllKeyBoardShortcutHandler = () => {
|
||||
// This will be handled by the useSelection hook
|
||||
// Keeping this function for compatibility
|
||||
return () => {
|
||||
// Cleanup function - currently handled by useSelection hook
|
||||
};
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const electron = globalThis.electron;
|
||||
let syncIntervalID: ReturnType<typeof setInterval> | undefined;
|
||||
|
||||
void (async () => {
|
||||
// Check authentication
|
||||
if (!haveMasterKeyInSession() || !(await savedAuthToken())) {
|
||||
stashRedirect("/gallery");
|
||||
void router.push("/");
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate credentials
|
||||
if (!(await validateKey())) {
|
||||
logout();
|
||||
return;
|
||||
}
|
||||
|
||||
// One-time initialization
|
||||
preloadImage("/images/subscription-card-background");
|
||||
initSettings();
|
||||
setupSelectAllKeyBoardShortcutHandler();
|
||||
|
||||
// Check if this is the user's first login
|
||||
setIsFirstLoad(getAndClearIsFirstLogin());
|
||||
|
||||
// Show plan selector for new users
|
||||
if (getAndClearJustSignedUp()) {
|
||||
onShowPlanSelector();
|
||||
}
|
||||
|
||||
// Initialize gallery data
|
||||
await onInitializeGallery();
|
||||
|
||||
// Clear first load state
|
||||
setIsFirstLoad(false);
|
||||
|
||||
// Start periodic sync
|
||||
syncIntervalID = setInterval(
|
||||
() => {
|
||||
// This should trigger a silent remote pull
|
||||
// Will be handled by the data management hook
|
||||
},
|
||||
5 * 60 * 1000 /* 5 minutes */,
|
||||
);
|
||||
|
||||
// Handle electron-specific features
|
||||
if (electron) {
|
||||
electron.onMainWindowFocus(() => {
|
||||
// Trigger silent remote pull on focus
|
||||
});
|
||||
if (await shouldShowWhatsNew(electron)) {
|
||||
onShowWhatsNew();
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
clearInterval(syncIntervalID);
|
||||
if (electron) {
|
||||
electron.onMainWindowFocus(undefined);
|
||||
}
|
||||
};
|
||||
}, [router, logout, onInitializeGallery, onShowPlanSelector, onShowWhatsNew]);
|
||||
|
||||
return {
|
||||
isFirstLoad,
|
||||
blockingLoad,
|
||||
setBlockingLoad,
|
||||
};
|
||||
};
|
||||
168
web/apps/photos/src/hooks/useGalleryNavigation.ts
Normal file
168
web/apps/photos/src/hooks/useGalleryNavigation.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { PseudoCollectionID } from 'ente-new/photos/services/collection-summary';
|
||||
import type { SearchOption } from 'ente-new/photos/services/search/types';
|
||||
|
||||
interface UseGalleryNavigationProps {
|
||||
dispatch: (action: { type: string; [key: string]: unknown }) => void;
|
||||
barMode: string;
|
||||
activeCollectionID: number | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom hook for managing gallery navigation and view changes
|
||||
*/
|
||||
export const useGalleryNavigation = ({
|
||||
dispatch,
|
||||
barMode,
|
||||
activeCollectionID,
|
||||
}: UseGalleryNavigationProps) => {
|
||||
const router = useRouter();
|
||||
|
||||
/**
|
||||
* Grace period tracking for hidden section authentication
|
||||
*/
|
||||
const lastAuthenticationForHiddenTimestamp = useRef<number>(0);
|
||||
|
||||
/**
|
||||
* Handle collection summary selection with optional authentication
|
||||
*/
|
||||
const showCollectionSummary = useCallback(
|
||||
async (
|
||||
collectionSummaryID: number | undefined,
|
||||
isHiddenCollectionSummary: boolean | undefined,
|
||||
authenticateUser?: () => Promise<void>,
|
||||
) => {
|
||||
const lastAuthAt = lastAuthenticationForHiddenTimestamp.current;
|
||||
if (
|
||||
isHiddenCollectionSummary &&
|
||||
barMode !== "hidden-albums" &&
|
||||
Date.now() - lastAuthAt > 5 * 60 * 1e3 /* 5 minutes */ &&
|
||||
authenticateUser
|
||||
) {
|
||||
await authenticateUser();
|
||||
lastAuthenticationForHiddenTimestamp.current = Date.now();
|
||||
}
|
||||
|
||||
// Trigger a pull of the latest data when opening trash
|
||||
if (collectionSummaryID === PseudoCollectionID.trash) {
|
||||
// This should trigger a remote files pull
|
||||
// Will be handled by the calling component
|
||||
}
|
||||
|
||||
dispatch({ type: "showCollectionSummary", collectionSummaryID });
|
||||
},
|
||||
[dispatch, barMode],
|
||||
);
|
||||
|
||||
/**
|
||||
* Handle search option selection
|
||||
*/
|
||||
const handleSelectSearchOption = useCallback(
|
||||
(searchOption: SearchOption | undefined) => {
|
||||
if (searchOption) {
|
||||
const type = searchOption.suggestion.type;
|
||||
if (type === "collection") {
|
||||
dispatch({
|
||||
type: "showCollectionSummary",
|
||||
collectionSummaryID: searchOption.suggestion.collectionID,
|
||||
});
|
||||
} else if (type === "person") {
|
||||
dispatch({
|
||||
type: "showPerson",
|
||||
personID: searchOption.suggestion.person.id,
|
||||
});
|
||||
} else {
|
||||
dispatch({
|
||||
type: "enterSearchMode",
|
||||
searchSuggestion: searchOption.suggestion,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
dispatch({ type: "exitSearch" });
|
||||
}
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
/**
|
||||
* Handle gallery bar mode changes
|
||||
*/
|
||||
const handleChangeBarMode = useCallback(
|
||||
(mode: string) => {
|
||||
if (mode === "people") {
|
||||
dispatch({ type: "showPeople" });
|
||||
} else {
|
||||
dispatch({ type: "showAlbums" });
|
||||
}
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
/**
|
||||
* Handle collection selection
|
||||
*/
|
||||
const handleSelectCollection = useCallback(
|
||||
(collectionID: number) =>
|
||||
dispatch({
|
||||
type: "showCollectionSummary",
|
||||
collectionSummaryID: collectionID,
|
||||
}),
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
/**
|
||||
* Handle person selection
|
||||
*/
|
||||
const handleSelectPerson = useCallback(
|
||||
(personID: string) => dispatch({ type: "showPerson", personID }),
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
/**
|
||||
* Handle entering search mode
|
||||
*/
|
||||
const handleEnterSearchMode = useCallback(
|
||||
() => dispatch({ type: "enterSearchMode" }),
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
/**
|
||||
* Handle showing people view
|
||||
*/
|
||||
const handleShowPeople = useCallback(
|
||||
() => dispatch({ type: "showPeople" }),
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
/**
|
||||
* Update browser URL based on active collection
|
||||
*/
|
||||
const updateBrowserURL = useCallback(() => {
|
||||
if (typeof activeCollectionID === "undefined" || !router.isReady) {
|
||||
return;
|
||||
}
|
||||
|
||||
let collectionURL = "";
|
||||
if (activeCollectionID !== PseudoCollectionID.all) {
|
||||
collectionURL = `?collection=${activeCollectionID}`;
|
||||
}
|
||||
const href = `/gallery${collectionURL}`;
|
||||
void router.push(href, undefined, { shallow: true });
|
||||
}, [activeCollectionID, router]);
|
||||
|
||||
return {
|
||||
// Navigation handlers
|
||||
showCollectionSummary,
|
||||
handleSelectSearchOption,
|
||||
handleChangeBarMode,
|
||||
handleSelectCollection,
|
||||
handleSelectPerson,
|
||||
handleEnterSearchMode,
|
||||
handleShowPeople,
|
||||
updateBrowserURL,
|
||||
|
||||
// State
|
||||
lastAuthenticationForHiddenTimestamp,
|
||||
};
|
||||
};
|
||||
121
web/apps/photos/src/hooks/useModalManagement.ts
Normal file
121
web/apps/photos/src/hooks/useModalManagement.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import type { EnteFile } from 'ente-media/file';
|
||||
import { useModalVisibility } from 'ente-base/components/utils/modal';
|
||||
import type { UploadTypeSelectorIntent } from 'ente-gallery/components/Upload';
|
||||
|
||||
/**
|
||||
* Custom hook for managing all modal states and visibility
|
||||
*/
|
||||
export const useModalManagement = () => {
|
||||
// File drag and drop state
|
||||
const [dragAndDropFiles, setDragAndDropFiles] = useState<File[]>([]);
|
||||
const [shouldDisableDropzone, setShouldDisableDropzone] = useState(false);
|
||||
|
||||
// Upload modal state
|
||||
const [uploadTypeSelectorView, setUploadTypeSelectorView] = useState(false);
|
||||
const [uploadTypeSelectorIntent, setUploadTypeSelectorIntent] =
|
||||
useState<UploadTypeSelectorIntent>("upload");
|
||||
|
||||
// File viewer state
|
||||
const [isFileViewerOpen, setIsFileViewerOpen] = useState(false);
|
||||
|
||||
// Fix creation time dialog state
|
||||
const [fixCreationTimeFiles, setFixCreationTimeFiles] = useState<EnteFile[]>([]);
|
||||
|
||||
// Authentication callback for hidden section access
|
||||
const onAuthenticateCallback = useRef<(() => void) | undefined>(undefined);
|
||||
|
||||
// Modal visibility hooks
|
||||
const { show: showSidebar, props: sidebarVisibilityProps } = useModalVisibility();
|
||||
const { show: showPlanSelector, props: planSelectorVisibilityProps } = useModalVisibility();
|
||||
const { show: showWhatsNew, props: whatsNewVisibilityProps } = useModalVisibility();
|
||||
const { show: showFixCreationTime, props: fixCreationTimeVisibilityProps } = useModalVisibility();
|
||||
const { show: showExport, props: exportVisibilityProps } = useModalVisibility();
|
||||
const { show: showAuthenticateUser, props: authenticateUserVisibilityProps } = useModalVisibility();
|
||||
const { show: showAlbumNameInput, props: albumNameInputVisibilityProps } = useModalVisibility();
|
||||
|
||||
/**
|
||||
* Authenticate user for hidden section access
|
||||
*/
|
||||
const authenticateUser = useCallback(
|
||||
() =>
|
||||
new Promise<void>((resolve) => {
|
||||
onAuthenticateCallback.current = resolve;
|
||||
showAuthenticateUser();
|
||||
}),
|
||||
[showAuthenticateUser],
|
||||
);
|
||||
|
||||
/**
|
||||
* Open upload type selector
|
||||
*/
|
||||
const openUploader = useCallback((intent?: UploadTypeSelectorIntent) => {
|
||||
setUploadTypeSelectorView(true);
|
||||
setUploadTypeSelectorIntent(intent ?? "upload");
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Close upload type selector
|
||||
*/
|
||||
const closeUploadTypeSelector = useCallback(() => {
|
||||
setUploadTypeSelectorView(false);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Check if any modal is currently open
|
||||
*/
|
||||
const isAnyModalOpen =
|
||||
uploadTypeSelectorView ||
|
||||
sidebarVisibilityProps.open ||
|
||||
planSelectorVisibilityProps.open ||
|
||||
fixCreationTimeVisibilityProps.open ||
|
||||
exportVisibilityProps.open ||
|
||||
authenticateUserVisibilityProps.open ||
|
||||
albumNameInputVisibilityProps.open ||
|
||||
isFileViewerOpen;
|
||||
|
||||
return {
|
||||
// State
|
||||
dragAndDropFiles,
|
||||
shouldDisableDropzone,
|
||||
uploadTypeSelectorView,
|
||||
uploadTypeSelectorIntent,
|
||||
isFileViewerOpen,
|
||||
fixCreationTimeFiles,
|
||||
isAnyModalOpen,
|
||||
|
||||
// Setters
|
||||
setDragAndDropFiles,
|
||||
setShouldDisableDropzone,
|
||||
setUploadTypeSelectorView,
|
||||
setUploadTypeSelectorIntent,
|
||||
setIsFileViewerOpen,
|
||||
setFixCreationTimeFiles,
|
||||
|
||||
// Modal visibility props
|
||||
sidebarVisibilityProps,
|
||||
planSelectorVisibilityProps,
|
||||
whatsNewVisibilityProps,
|
||||
fixCreationTimeVisibilityProps,
|
||||
exportVisibilityProps,
|
||||
authenticateUserVisibilityProps,
|
||||
albumNameInputVisibilityProps,
|
||||
|
||||
// Modal show functions
|
||||
showSidebar,
|
||||
showPlanSelector,
|
||||
showWhatsNew,
|
||||
showFixCreationTime,
|
||||
showExport,
|
||||
showAuthenticateUser,
|
||||
showAlbumNameInput,
|
||||
|
||||
// Actions
|
||||
authenticateUser,
|
||||
openUploader,
|
||||
closeUploadTypeSelector,
|
||||
|
||||
// Authentication callback
|
||||
onAuthenticateCallback,
|
||||
};
|
||||
};
|
||||
123
web/apps/photos/src/hooks/useSelection.ts
Normal file
123
web/apps/photos/src/hooks/useSelection.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import type { SelectedState } from 'utils/file';
|
||||
import type { EnteFile } from 'ente-media/file';
|
||||
import { PseudoCollectionID } from 'ente-new/photos/services/collection-summary';
|
||||
|
||||
interface UseSelectionProps {
|
||||
user: { id: number } | null;
|
||||
filteredFiles: EnteFile[];
|
||||
activeCollectionID: number | undefined;
|
||||
barMode: string;
|
||||
activePersonID?: string;
|
||||
isAnyModalOpen: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom hook for managing file selection state and keyboard shortcuts
|
||||
*/
|
||||
export const useSelection = ({
|
||||
user,
|
||||
filteredFiles,
|
||||
activeCollectionID,
|
||||
barMode,
|
||||
activePersonID,
|
||||
isAnyModalOpen,
|
||||
}: UseSelectionProps) => {
|
||||
const [selected, setSelected] = useState<SelectedState>({
|
||||
ownCount: 0,
|
||||
count: 0,
|
||||
collectionID: 0,
|
||||
context: { mode: "albums", collectionID: PseudoCollectionID.all },
|
||||
});
|
||||
|
||||
const selectAll = useCallback((e: KeyboardEvent) => {
|
||||
// Don't intercept Ctrl/Cmd + a if the user is typing in a text field
|
||||
if (
|
||||
e.target instanceof HTMLInputElement ||
|
||||
e.target instanceof HTMLTextAreaElement
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
// Don't select all if conditions aren't met
|
||||
if (
|
||||
!user ||
|
||||
!filteredFiles.length ||
|
||||
isAnyModalOpen
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a selection with everything based on the current context
|
||||
const newSelected = {
|
||||
ownCount: 0,
|
||||
count: 0,
|
||||
collectionID: activeCollectionID,
|
||||
context:
|
||||
barMode === "people" && activePersonID
|
||||
? { mode: "people" as const, personID: activePersonID }
|
||||
: {
|
||||
mode: barMode as "albums" | "hidden-albums",
|
||||
collectionID: activeCollectionID!,
|
||||
},
|
||||
};
|
||||
|
||||
filteredFiles.forEach((item) => {
|
||||
if (item.ownerID === user.id) {
|
||||
newSelected.ownCount++;
|
||||
}
|
||||
newSelected.count++;
|
||||
// @ts-expect-error Selection code needs type fixing
|
||||
newSelected[item.id] = true;
|
||||
});
|
||||
setSelected(newSelected);
|
||||
}, [user, filteredFiles, activeCollectionID, barMode, activePersonID, isAnyModalOpen]);
|
||||
|
||||
const clearSelection = useCallback(() => {
|
||||
if (!selected.count) {
|
||||
return;
|
||||
}
|
||||
setSelected({
|
||||
ownCount: 0,
|
||||
count: 0,
|
||||
collectionID: 0,
|
||||
context: undefined,
|
||||
});
|
||||
}, [selected.count]);
|
||||
|
||||
const keyboardShortcutHandlerRef = useRef({ selectAll, clearSelection });
|
||||
|
||||
useEffect(() => {
|
||||
keyboardShortcutHandlerRef.current = { selectAll, clearSelection };
|
||||
}, [selectAll, clearSelection]);
|
||||
|
||||
// Set up keyboard shortcuts
|
||||
useEffect(() => {
|
||||
const handleKeyUp = (e: KeyboardEvent) => {
|
||||
switch (e.key) {
|
||||
case "Escape":
|
||||
keyboardShortcutHandlerRef.current.clearSelection();
|
||||
break;
|
||||
case "a":
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
keyboardShortcutHandlerRef.current.selectAll(e);
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("keydown", handleKeyUp);
|
||||
return () => {
|
||||
document.removeEventListener("keydown", handleKeyUp);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
selected,
|
||||
setSelected,
|
||||
clearSelection,
|
||||
selectAll,
|
||||
};
|
||||
};
|
||||
@@ -1227,7 +1227,7 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.15.tgz#e6e5a86d602beaca71ce5163fadf5f95d70931c7"
|
||||
integrity sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==
|
||||
|
||||
"@types/react-dom@^19.1.1", "@types/react-dom@^19.1.6":
|
||||
"@types/react-dom@^19.1.6":
|
||||
version "19.1.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-19.1.6.tgz#4af629da0e9f9c0f506fc4d1caa610399c595d64"
|
||||
integrity sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==
|
||||
@@ -1244,7 +1244,7 @@
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react@*", "@types/react@^19.1.0", "@types/react@^19.1.8":
|
||||
"@types/react@*", "@types/react@^19.1.8":
|
||||
version "19.1.8"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-19.1.8.tgz#ff8395f2afb764597265ced15f8dddb0720ae1c3"
|
||||
integrity sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==
|
||||
|
||||
Reference in New Issue
Block a user