Skip to content

Commit

Permalink
Moving towards local-first.
Browse files Browse the repository at this point in the history
Added a new image component that loads the images via
object URL. This is a step towards loading images that are
cached locally in indexeddb.
  • Loading branch information
ashleydavis committed Apr 28, 2024
1 parent 56005dc commit 2a6ef1c
Show file tree
Hide file tree
Showing 11 changed files with 244 additions and 89 deletions.
44 changes: 24 additions & 20 deletions electron/frontend/src/context/scan-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,26 +74,30 @@ export function ScanContextProvider({ children }: IProps) {
// Scan the file system for assets.
//
function scanImages(): void {
_scanImages(async fileDetails => {
const { thumbnail, resolution, hash } = await loadThumbnail(fileDetails.path, fileDetails.contentType);
const newAsset: IGalleryItem = {
_id: `local://${fileDetails.path}`,
width: resolution.width,
height: resolution.height,
origFileName: fileDetails.path,
hash,
fileDate: dayjs().toISOString(),
sortDate: dayjs().toISOString(),
uploadDate: dayjs().toISOString(),
url: thumbnail,
makeFullUrl: async () => {
return await loadHighRes(fileDetails.path, fileDetails.contentType);
},
};
setAssets(prev => prev.concat([ newAsset ]));
})
.then(() => console.log('Scanning complete'))
.catch(error => console.error('Error scanning images', error));
//
//todo: This will be a bit different using local storage.
//
//
// _scanImages(async fileDetails => {
// const { thumbnail, resolution, hash } = await loadThumbnail(fileDetails.path, fileDetails.contentType);
// const newAsset: IGalleryItem = {
// _id: `local://${fileDetails.path}`,
// width: resolution.width,
// height: resolution.height,
// origFileName: fileDetails.path,
// hash,
// fileDate: dayjs().toISOString(),
// sortDate: dayjs().toISOString(),
// uploadDate: dayjs().toISOString(),
// url: thumbnail,
// makeFullUrl: async () => {
// return await loadHighRes(fileDetails.path, fileDetails.contentType);
// },
// };
// setAssets(prev => prev.concat([ newAsset ]));
// })
// .then(() => console.log('Scanning complete'))
// .catch(error => console.error('Error scanning images', error));
}

const value: IScanContext = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,25 @@ export function ComputerGallerySourceContextProvider({ children }: IComputerGall
//TODO: Want to store local data for an asset before it is uploaded.
}

//
// Loads data for an asset.
//
function loadAsset(assetId: string, onLoaded: (objectURL: string) => void): void {
//TODO:
}

//
// Unloads data for an asset.
//
function unloadAsset(assetId: string): void {
//TODO:
}

const value: IComputerGallerySourceContext = {
assets,
updateAsset,
loadAsset,
unloadAsset,
};

return (
Expand Down
45 changes: 24 additions & 21 deletions mobile/frontend/src/context/scan-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,27 +99,30 @@ export function ScanContextProvider({ children }: IProps) {
continue;
}

const { thumbnail, width, height, hash } = await FileUploader.loadThumbnail({ path: file.path });
const dataURL = `data:${file.contentType};base64,${thumbnail}`;
const newAsset: IGalleryItem = {
_id: `local://${file.path}`,
width,
height,
origFileName: file.path,
hash,
fileDate: dayjs().toISOString(),
sortDate: dayjs().toISOString(),
uploadDate: dayjs().toISOString(),
url: dataURL,
makeFullUrl: async () => {
const { fullImage } = await FileUploader.loadFullImage({ path: file.path, contentType: file.type });
const dataURL = `data:${file.contentType};base64,${fullImage}`;
return dataURL;
},
};

assetMap.current.set(file.path, newAsset);
setAssets(prev => prev.concat([ newAsset ]));
//
//todo: This will be a bit different using local storage.
//
// const { thumbnail, width, height, hash } = await FileUploader.loadThumbnail({ path: file.path });
// const dataURL = `data:${file.contentType};base64,${thumbnail}`;
// const newAsset: IGalleryItem = {
// _id: `local://${file.path}`,
// width,
// height,
// origFileName: file.path,
// hash,
// fileDate: dayjs().toISOString(),
// sortDate: dayjs().toISOString(),
// uploadDate: dayjs().toISOString(),
// url: dataURL,
// makeFullUrl: async () => {
// const { fullImage } = await FileUploader.loadFullImage({ path: file.path, contentType: file.type });
// const dataURL = `data:${file.contentType};base64,${fullImage}`;
// return dataURL;
// },
// };

// assetMap.current.set(file.path, newAsset);
// setAssets(prev => prev.concat([ newAsset ]));
}

syncingAssets.current = false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,25 @@ export function ComputerGallerySourceContextProvider({ children }: IComputerGall
//TODO: Want to store local data for an asset before it is uploaded.
}

//
// Loads data for an asset.
//
function loadAsset(assetId: string, onLoaded: (objectURL: string) => void): void {
//TODO:
}

//
// Unloads data for an asset.
//
function unloadAsset(assetId: string): void {
//TODO:
}

const value: IComputerGallerySourceContext = {
assets,
updateAsset,
loadAsset,
unloadAsset,
};

return (
Expand Down
32 changes: 4 additions & 28 deletions packages/user-interface/src/components/asset-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, { useEffect, useState } from "react";
import { useApi } from "../context/api-context";
import { AssetInfo } from "../pages/gallery/components/asset-info";
import { useGalleryItem } from "../context/gallery-item-context";
import { Image } from "./image";

export interface IAssetViewProps {

Expand Down Expand Up @@ -46,40 +47,15 @@ export function AssetView({ open, onClose, onNext, onPrev }: IAssetViewProps) {
//
const [openInfo, setOpenInfo] = useState<boolean>(false);

//
// The URL for the image.
//
const [url, setUrl] = useState<string>("");

useEffect(() => {
if (asset.makeFullUrl) {
asset.makeFullUrl()
.then(url => {
setUrl(url);
})
.catch(err => {
console.error(`Failed to load full asset ${asset._id}`);
console.error(err);
});
}
else if (asset.url) {
setUrl(asset.url);
}
else {
setUrl(api.makeUrl(`/display?id=${asset._id}`));
}

}, [asset]);

return (
<div className={"photo bg-black text-white text-xl " + (open ? "open" : "")}>

<div className="w-full h-full flex flex-col justify-center items-center">
{open
&& <div className="photo-container flex flex-col items-center justify-center">
<img
data-testid="fullsize-asset"
src={url}
<Image
testId="fullsize-asset"
asset={asset}
/>
</div>
}
Expand Down
16 changes: 7 additions & 9 deletions packages/user-interface/src/components/gallery-layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from "react";
import { createLayout } from "../lib/create-layout";
import { IGalleryItem, ISelectedGalleryItem } from "../lib/gallery-item";
import { useApi } from "../context/api-context";
import { Image } from "./image";

export interface IGalleryLayoutProps {
//
Expand Down Expand Up @@ -81,20 +82,17 @@ export function GalleryLayout({
}}
>
{row.items.map((item, index) => {
const url = item.url || api.makeUrl(`/thumb?id=${item._id}`);
return (
<img
data-testid="gallery-thumb"
style={{
padding: "2px",
}}
onClick={() => {
<Image
key={item._id}
testId="gallery-thumb"
imgClassName="gallery-thumb"
asset={item}
onClick={() =>{
if (onItemClick) {
onItemClick({ item, index });
}
}}
key={item._id}
src={url}
/>
);
})}
Expand Down
65 changes: 65 additions & 0 deletions packages/user-interface/src/components/image.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import React, { useEffect, useState } from "react";
import { IGalleryItem } from "../lib/gallery-item";
import { useGallery } from "../context/gallery-context";

export interface IImageProps {
//
// Test ID for the image attribute.
//
testId?: string;

//
// Class name for the image attribute.
//
imgClassName?: string;

//
// The asset being displayed.
//
asset: IGalleryItem;

//
// Event raised when an item in the gallery has been clicked.
//
onClick?: (() => void);
}

//
// Renders an image.
//
export function Image({ testId, imgClassName, asset, onClick }: IImageProps) {

const [objectURL, setObjectURL] = useState<string>("");

const { source } = useGallery();

useEffect(() => {
source.loadAsset(asset._id, objectURL => {
setObjectURL(objectURL);
});

return () => {
source.unloadAsset(asset._id);
};
}, [asset]);

return (
<>
{objectURL
&& <img
data-testid={testId}
className={imgClassName}
src={objectURL}
style={{
padding: "2px",
}}
onClick={() => {
if (onClick) {
onClick();
}
}}
/>
}
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@
// Provides a source of assets for the gallery from the cloud.
//

import React, { createContext, ReactNode, useContext, useEffect, useState } from "react";
import React, { createContext, ReactNode, useContext, useEffect, useReducer, useRef, useState } from "react";
import { IGallerySourceContext } from "./gallery-source-context";
import { useApi } from "../api-context";
import { IGalleryItem } from "../../lib/gallery-item";
import { loadImageAsObjectURL, unloadObjectURL } from "../../lib/image";

export interface ICloudGallerySourceContext extends IGallerySourceContext {
//
Expand Down Expand Up @@ -37,6 +38,26 @@ export function CloudGallerySourceContextProvider({ children }: ICloudGallerySou
//
const [ assets, setAssets ] = useState<IGalleryItem[]>([]);

//
// A cache entry for a loaded asset.
//
interface IAssetCacheEntry {
//
// Number of references to this asset.
//
numRefs: number;

//
// Object URL for the asset.
//
objectUrl: string;
}

//
// Caches loaded assets.
//
const assetCache = useRef<Map<string, IAssetCacheEntry>>(new Map<string, IAssetCacheEntry>());

//
// Resets the gallery when the search text changes.
//
Expand Down Expand Up @@ -78,11 +99,48 @@ export function CloudGallerySourceContextProvider({ children }: ICloudGallerySou
});
}

//
// Loads data for an asset.
//
function loadAsset(assetId: string, onLoaded: (objectURL: string) => void): void {
const existingCacheEntry = assetCache.current.get(assetId);
if (existingCacheEntry) {
existingCacheEntry.numRefs += 1;
onLoaded(existingCacheEntry.objectUrl);
return;
}

const url = api.makeUrl(`/thumb?id=${assetId}`);
loadImageAsObjectURL(url)
.then(objectUrl => {
assetCache.current.set(assetId, { numRefs: 1, objectUrl });
onLoaded(objectUrl);
});
}

//
// Unloads data for an asset.
//
function unloadAsset(assetId: string): void {
const cacheEntry = assetCache.current.get(assetId);
if (cacheEntry) {
if (cacheEntry.numRefs === 1) {
unloadObjectURL(cacheEntry.objectUrl);
assetCache.current.delete(assetId);
}
else {
cacheEntry.numRefs -= 1;
}
}
}

const value: ICloudGallerySourceContext = {
loadAssets,
assets,
addAsset,
updateAsset,
loadAsset,
unloadAsset,
};

return (
Expand Down
Loading

0 comments on commit 2a6ef1c

Please sign in to comment.