diff --git a/src/components/FixLargeThumbnail.tsx b/src/components/FixLargeThumbnail.tsx index 12de7731b..bc9f6f2c2 100644 --- a/src/components/FixLargeThumbnail.tsx +++ b/src/components/FixLargeThumbnail.tsx @@ -24,6 +24,7 @@ interface Props { export enum FIX_STATE { NOT_STARTED, FIX_LATER, + NOOP, RUNNING, COMPLETED, COMPLETED_WITH_ERRORS, @@ -38,6 +39,9 @@ function Message(props: { fixState: FIX_STATE }) { case FIX_STATE.COMPLETED: message = constants.REPLACE_THUMBNAIL_COMPLETED(); break; + case FIX_STATE.NOOP: + message = constants.REPLACE_THUMBNAIL_NOOP(); + break; case FIX_STATE.COMPLETED_WITH_ERRORS: message = constants.REPLACE_THUMBNAIL_COMPLETED_WITH_ERROR(); break; @@ -64,6 +68,10 @@ export default function FixLargeThumbnails(props: Props) { fixState = FIX_STATE.NOT_STARTED; updateFixState(fixState); } + if (fixState === FIX_STATE.COMPLETED) { + fixState = FIX_STATE.NOOP; + updateFixState(fixState); + } setFixState(fixState); return fixState; }; @@ -83,14 +91,14 @@ export default function FixLargeThumbnails(props: Props) { props.show(); } if ( - fixState === FIX_STATE.COMPLETED && + (fixState === FIX_STATE.COMPLETED || fixState === FIX_STATE.NOOP) && largeThumbnailFiles.length > 0 ) { updateFixState(FIX_STATE.NOT_STARTED); logError(Error(), 'large thumbnail files left after migration'); } - if (largeThumbnailFiles.length === 0) { - updateFixState(FIX_STATE.COMPLETED); + if (largeThumbnailFiles.length === 0 && fixState !== FIX_STATE.NOOP) { + updateFixState(FIX_STATE.NOOP); } }; useEffect(() => { diff --git a/src/components/PhotoFrame.tsx b/src/components/PhotoFrame.tsx index 17de5d863..58618384e 100644 --- a/src/components/PhotoFrame.tsx +++ b/src/components/PhotoFrame.tsx @@ -86,6 +86,7 @@ const getTemplateColumns = (columns: number, groups?: number[]): string => { }; const ListContainer = styled.div<{ columns: number; groups?: number[] }>` + user-select: none; display: grid; grid-template-columns: ${({ columns, groups }) => getTemplateColumns(columns, groups)}; @@ -100,6 +101,7 @@ const ListContainer = styled.div<{ columns: number; groups?: number[] }>` `; const DateContainer = styled.div<{ span: number }>` + user-select: none; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; @@ -181,6 +183,28 @@ const PhotoFrame = ({ const startTime = Date.now(); const galleryContext = useContext(GalleryContext); const listRef = useRef(null); + const [rangeStart, setRangeStart] = useState(null); + const [currentHover, setCurrentHover] = useState(null); + const [isShiftKeyPressed, setIsShiftKeyPressed] = useState(false); + + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Shift') { + setIsShiftKeyPressed(true); + } + }; + const handleKeyUp = (e: KeyboardEvent) => { + if (e.key === 'Shift') { + setIsShiftKeyPressed(false); + } + }; + document.addEventListener('keydown', handleKeyDown, false); + document.addEventListener('keyup', handleKeyUp, false); + return () => { + document.addEventListener('keydown', handleKeyDown, false); + document.addEventListener('keyup', handleKeyUp, false); + }; + }, []); useEffect(() => { if (isInSearchMode) { @@ -204,6 +228,12 @@ const PhotoFrame = ({ setFetching({}); }, [files, search, deleted]); + useEffect(() => { + if (selected.count === 0) { + setRangeStart(null); + } + }, [selected]); + const updateUrl = (index: number) => (url: string) => { files[index] = { ...files[index], @@ -276,10 +306,16 @@ const PhotoFrame = ({ setOpen(true); }; - const handleSelect = (id: number) => (checked: boolean) => { + const handleSelect = (id: number, index?: number) => (checked: boolean) => { if (selected.collectionID !== activeCollection) { setSelected({ count: 0, collectionID: 0 }); } + if (rangeStart || rangeStart === 0) { + setRangeStart(null); + } else if (checked) { + setRangeStart(index); + } + setSelected((selected) => ({ ...selected, [id]: checked, @@ -287,19 +323,50 @@ const PhotoFrame = ({ collectionID: activeCollection, })); }; + const onHoverOver = (index: number) => () => { + setCurrentHover(index); + }; + + const handleRangeSelect = (index: number) => () => { + if (rangeStart !== index) { + let leftEnd = -1; + let rightEnd = -1; + if (index < rangeStart) { + leftEnd = index; + rightEnd = rangeStart; + } else { + leftEnd = rangeStart; + rightEnd = index; + } + for (let i = leftEnd; i <= rightEnd; i++) { + handleSelect(filteredData[i].id)(true); + } + } + }; const getThumbnail = (file: File[], index: number) => ( 0} + onHover={onHoverOver(index)} + onRangeSelect={handleRangeSelect(index)} + isRangeSelectActive={ + isShiftKeyPressed && (rangeStart || rangeStart === 0) + } + isInsSelectRange={ + (index >= rangeStart + 1 && index <= currentHover) || + (index >= currentHover && index <= rangeStart - 1) + } /> ); diff --git a/src/components/pages/gallery/PreviewCard.tsx b/src/components/pages/gallery/PreviewCard.tsx index bacbff199..f3dcd4a31 100644 --- a/src/components/pages/gallery/PreviewCard.tsx +++ b/src/components/pages/gallery/PreviewCard.tsx @@ -15,13 +15,18 @@ interface IProps { selectable?: boolean; selected?: boolean; onSelect?: (checked: boolean) => void; + onHover?: () => void; + onRangeSelect?: () => void; + isRangeSelectActive?: boolean; selectOnClick?: boolean; + isInsSelectRange?: boolean; } -const Check = styled.input` +const Check = styled.input<{ active: boolean }>` appearance: none; position: absolute; - right: 0; + z-index: 10; + left: 0; opacity: 0; outline: none; cursor: pointer; @@ -34,7 +39,7 @@ const Check = styled.input` width: 16px; height: 16px; border: 2px solid #fff; - background-color: rgba(0, 0, 0, 0.5); + background-color: #ddd; display: inline-block; border-radius: 50%; vertical-align: bottom; @@ -43,18 +48,19 @@ const Check = styled.input` line-height: 16px; transition: background-color 0.3s ease; pointer-events: inherit; + color: #aaa; } &::after { content: ''; width: 5px; height: 10px; - border-right: 2px solid #fff; - border-bottom: 2px solid #fff; + border-right: 2px solid #333; + border-bottom: 2px solid #333; transform: translate(-18px, 8px); - opacity: 0; transition: transform 0.3s ease; position: absolute; pointer-events: inherit; + transform: translate(-18px, 10px) rotate(45deg); } /** checked */ @@ -65,15 +71,50 @@ const Check = styled.input` color: #fff; } &:checked::after { - opacity: 1; - transform: translate(-18px, 10px) rotate(45deg); + content: ''; + border-right: 2px solid #ddd; + border-bottom: 2px solid #ddd; } - + ${(props) => props.active && 'opacity: 0.5 '}; &:checked { - opacity: 1; + opacity: 1 !important; } `; +export const HoverOverlay = styled.div<{ checked: boolean }>` + opacity: 0; + left: 0; + top: 0; + outline: none; + height: 40%; + width: 100%; + display: flex; + justify-content: center; + align-items: center; + color: #fff; + font-weight: 900; + position: absolute; + ${(props) => + !props.checked && + 'background:linear-gradient(rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0))'}; +`; + +export const InSelectRangeOverLay = styled.div<{ active: boolean }>` + opacity: ${(props) => (!props.active ? 0 : 1)}); + left: 0; + top: 0; + outline: none; + height: 100%; + width: 100%; + display: flex; + justify-content: center; + align-items: center; + color: #fff; + font-weight: 900; + position: absolute; + ${(props) => props.active && 'background:rgba(81, 205, 124, 0.25)'}; +`; + const Cont = styled.div<{ disabled: boolean; selected: boolean }>` background: #222; display: flex; @@ -107,6 +148,9 @@ const Cont = styled.div<{ disabled: boolean; selected: boolean }>` } &:hover ${Check} { + opacity: 0.5; + } + &:hover ${HoverOverlay} { opacity: 1; } `; @@ -123,6 +167,10 @@ export default function PreviewCard(props: IProps) { selected, onSelect, selectOnClick, + onHover, + onRangeSelect, + isRangeSelectActive, + isInsSelectRange, } = props; const isMounted = useRef(true); useLayoutEffect(() => { @@ -167,24 +215,36 @@ export default function PreviewCard(props: IProps) { const handleClick = () => { if (selectOnClick) { - onSelect?.(!selected); + if (isRangeSelectActive) { + onRangeSelect(); + } else { + onSelect?.(!selected); + } } else if (file?.msrc || imgSrc) { onClick?.(); } }; const handleSelect: React.ChangeEventHandler = (e) => { + if (isRangeSelectActive) { + onRangeSelect?.(); + } onSelect?.(e.target.checked); }; const longPressCallback = () => { onSelect(!selected); }; - + const handleHover = () => { + if (selectOnClick) { + onHover(); + } + }; return ( @@ -193,11 +253,16 @@ export default function PreviewCard(props: IProps) { type="checkbox" checked={selected} onChange={handleSelect} + active={isRangeSelectActive && isInsSelectRange} onClick={(e) => e.stopPropagation()} /> )} {(file?.msrc || imgSrc) && } {file?.metadata.fileType === 1 && } + + ); } diff --git a/src/services/fileService.ts b/src/services/fileService.ts index e16a5aa4f..9c2c183c1 100644 --- a/src/services/fileService.ts +++ b/src/services/fileService.ts @@ -14,7 +14,6 @@ import { decryptFile, mergeMetadata, sortFiles } from 'utils/file'; import CryptoWorker from 'utils/crypto'; const ENDPOINT = getEndpoint(); -const DIFF_LIMIT: number = 1000; const FILES = 'files'; @@ -148,13 +147,7 @@ export const syncFiles = async ( continue; } const fetchedFiles = - (await getFiles( - collection, - lastSyncTime, - DIFF_LIMIT, - files, - setFiles - )) ?? []; + (await getFiles(collection, lastSyncTime, files, setFiles)) ?? []; files.push(...fetchedFiles); const latestVersionFiles = new Map(); files.forEach((file) => { @@ -198,7 +191,6 @@ export const syncFiles = async ( export const getFiles = async ( collection: Collection, sinceTime: number, - limit: number, files: File[], setFiles: (files: File[]) => void ): Promise => { @@ -215,11 +207,10 @@ export const getFiles = async ( break; } resp = await HTTPService.get( - `${ENDPOINT}/collections/diff`, + `${ENDPOINT}/collections/v2/diff`, { collectionID: collection.id, sinceTime: time, - limit, }, { 'X-Auth-Token': token, @@ -249,7 +240,7 @@ export const getFiles = async ( ) ) ); - } while (resp.data.diff.length === limit); + } while (resp.data.hasMore); return decryptedFiles; } catch (e) { logError(e, 'Get files failed'); diff --git a/src/utils/strings/englishConstants.tsx b/src/utils/strings/englishConstants.tsx index fbf1bca96..03a5b4544 100644 --- a/src/utils/strings/englishConstants.tsx +++ b/src/utils/strings/englishConstants.tsx @@ -590,6 +590,9 @@ const englishConstants = { REPLACE_THUMBNAIL_COMPLETED: () => ( <>successfully compressed all thumbnails ), + REPLACE_THUMBNAIL_NOOP: () => ( + <>you have no thumbnails that can be compressed further + ), REPLACE_THUMBNAIL_COMPLETED_WITH_ERROR: () => ( <>could not compress some of your thumbnails, please retry ),