Compare commits

...

4 Commits

Author SHA1 Message Date
Stefan Forstenlechner da273cbc87 Cleanup ImageGalleryDrawer
Build and publish docker image snapshot / build-and-publish (push) Successful in 1m10s Details
Move pathname related functions in pathnames.ts
2024-08-21 23:04:26 +02:00
Stefan Forstenlechner 4da3376d5f Fix lazy loading (Firefox)
see https://bugzilla.mozilla.org/show_bug.cgi?id=1647077

`loading` attribute needs to be before `src` attribute
2024-08-21 22:54:32 +02:00
Stefan Forstenlechner 18661afbc9 Add nginx cache recommendation 2024-08-21 22:00:31 +02:00
Stefan Forstenlechner fc32dd79cc Refactor ImageGalleryLayout
Split out main component
Move useEffect to corresponding component
2024-08-21 21:50:26 +02:00
9 changed files with 166 additions and 126 deletions

View File

@ -1,5 +1,7 @@
# Simple Picture Gallery
A simple picture gallery. No database required. Photos can simply be stored in your file system. Add and remove photos. Simple picture gallery will automatically show them and create thumbnails.
## Getting Started
### Information
@ -8,23 +10,20 @@ Folders should only contain images and folders. Folders should not contain any o
### Docker
The easiest way to run simple picture gallery is with docker.
```shell
docker build . -t simple-picture-gallery
docker run -p 3005:3001 -v /mnt/data/pictures:/usr/src/app/public --name my-picture-gallery simple-picture-gallery
docker run -p 3005:3001 -v /mnt/path/to/pictures:/usr/src/app/public --name my-picture-gallery simple-picture-gallery
```
### Customization
#### Customization
Create an environment file `.env`:
Create an environment file `.env` containing any of the following properties to customize your gallery. All properties are optional.
```properties
VITE_TITLE=My Gallery
VITE_APPBAR_COLOR=#F8AB2D
```
Other properties:
```properties
VITE_FAVICON_HREF=<URL to your favicon>
```
@ -32,4 +31,33 @@ And run docker with `--env-file .env`
```shell
docker run -p 3005:3001 -v C:/DATA/temp/bla:/usr/src/app/public --env-file .env --name my-picture-gallery simple-picture-gallery
```
### nginx
It is recommended to use a cache for the API calls so that not every request has to read the file system again.
```nginx
http {
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=picture_gallery_cache:10m max_size=100m inactive=60m use_temp_path=off;
server {
...
location / {
proxy_pass http://127.0.0.1:3005;
}
location ~ /(images|directories|folderspreview) {
proxy_cache picture_gallery_cache;
proxy_cache_valid 200 302 600m;
proxy_cache_min_uses 1;
proxy_pass http://127.0.0.1:3005;
}
...
}
}
```

View File

@ -13,9 +13,9 @@ export interface PhotoWithFolder extends ImageWithThumbnail {
const PreviewFolder = ({ folder }: { folder: FolderPreview }) => {
return (
<img
loading="lazy"
src={folder.imagePreview.thumbnail}
alt={folder.name}
loading="lazy"
style={{
objectFit: "cover",
width: "100%",

View File

@ -22,7 +22,7 @@ export const ImageGallery = ({ images }: { images: ImageWithThumbnail[] }) => {
photos={images}
render={{
image: (props, context) => (
<img {...props} src={context.photo.thumbnail} loading={"lazy"} />
<img loading={"lazy"} {...props} src={context.photo.thumbnail} />
),
}}
onClick={({ index }) => setIndex(index)}

View File

@ -10,7 +10,7 @@ import Toolbar from "@mui/material/Toolbar";
import { Chip, useTheme } from "@mui/material";
import useMediaQuery from "@mui/material/useMediaQuery";
import { smallScreenMediaQuery } from "../ImageGalleryLayout";
import { getDefaultExpanded } from "./PathToExpaned";
import { getDefaultExpanded, getParentAndChildPath } from "./pathnames";
import Typography from "@mui/material/Typography";
function generateTreeViewChildren(
@ -61,17 +61,9 @@ const GenerateTreeView = ({ root }: { root: Folders }) => {
);
const [selectedItem, setSelectedItem] = useState<string>(location.pathname);
// TODO: clean this effect up. See also `getDefaultExpanded`
useEffect(() => {
let curPathname = location.pathname.startsWith("/")
? location.pathname.slice(1)
: location.pathname;
while (curPathname.endsWith("/")) {
curPathname = curPathname.slice(0, -1);
}
const parentPathname = curPathname.substring(
0,
curPathname.lastIndexOf("/"),
const { parent: parentPathname, cur: curPathname } = getParentAndChildPath(
location.pathname,
);
if (!expandedItems.includes(parentPathname)) {
setExpandedItems([parentPathname, ...expandedItems]);
@ -132,16 +124,25 @@ const GenerateTreeView = ({ root }: { root: Folders }) => {
export const ImageGalleryDrawer = ({
open,
drawerWidth,
folders,
handleDrawerToggle,
}: {
open: boolean;
drawerWidth: number;
folders: Folders | undefined;
handleDrawerToggle: () => void;
}) => {
const theme = useTheme();
const smallScreen = !useMediaQuery(smallScreenMediaQuery);
const [folders, setFolders] = useState<Folders | undefined>(undefined);
useEffect(() => {
fetch("/directories", {
headers: {
Accept: "application/json",
},
})
.then((res) => res.json())
.then((data) => setFolders(data));
}, []);
const drawerContent =
folders != undefined ? (

View File

@ -0,0 +1,89 @@
import React, { useEffect, useState } from "react";
import Box from "@mui/material/Box";
import { useLocation, useNavigate } from "react-router-dom";
import Toolbar from "@mui/material/Toolbar";
import { Chip, Divider } from "@mui/material";
import { FolderPreview, ImageWithThumbnail } from "./models";
import { Spinner } from "./Spinner";
import { FolderGallery } from "./FolderGallery";
import { ImageGallery } from "./ImageGallery";
export const ImageGalleryMain = ({
setError,
}: {
setError: (_: boolean) => void;
}) => {
const [imagesLoaded, setImagesLoaded] = useState(false);
const [images, setImages] = useState<ImageWithThumbnail[]>([]);
const [foldersPreview, setFoldersPreview] = useState<
FolderPreview[] | undefined
>([]);
const location = useLocation();
const navigate = useNavigate();
useEffect(() => {
setFoldersPreview(undefined);
setImages([]);
setError(false);
setImagesLoaded(false);
fetch(`/folderspreview${location.pathname}`, {
headers: {
Accept: "application/json",
},
})
.then((res) => res.json())
.then((data) => {
setFoldersPreview(data);
});
fetch(`/images${location.pathname}`, {
headers: {
Accept: "application/json",
},
})
.then((res) => res.json())
.then((data) => {
if (data.images === undefined) {
if (location.pathname !== "/") {
navigate("/");
} else {
setError(true);
}
} else {
setImages(data.images);
setImagesLoaded(true);
}
});
}, [location.pathname]);
return (
<Box component="main" sx={{ flexGrow: 1, p: 3 }}>
<Toolbar />
{!imagesLoaded || !foldersPreview ? (
<Spinner />
) : (
<>
{foldersPreview.length > 0 && (
<>
<Divider style={{ marginBottom: "10px" }}>
<Chip label="Folders" size="small" />
</Divider>
<FolderGallery folders={foldersPreview} />
</>
)}
{images.length > 0 && foldersPreview.length > 0 && (
<Divider style={{ marginBottom: "10px" }}>
<Chip label="Images" size="small" />
</Divider>
)}
{images.length > 0 && <ImageGallery images={images} />}
{images.length == 0 && foldersPreview.length == 0 && (
<p>
No images available. You may want to add images to this directory.
</p>
)}
</>
)}
</Box>
);
};

View File

@ -1,4 +1,4 @@
import { getDefaultExpanded } from "../PathToExpaned";
import { getDefaultExpanded } from "../pathnames";
interface pathWithExpanded {
pathname: string;

View File

@ -1,12 +1,28 @@
export const getDefaultExpanded = (pathname: string): string[] => {
const pathParts = [];
const cleanupPath = (pathname: string): string => {
let curPathname = pathname.startsWith("/") ? pathname.slice(1) : pathname;
while (curPathname.endsWith("/")) {
curPathname = curPathname.slice(0, -1);
}
return curPathname;
};
export const getDefaultExpanded = (pathname: string): string[] => {
let curPathname = cleanupPath(pathname);
const pathParts = [];
while (curPathname.length > 0) {
pathParts.push(curPathname);
curPathname = curPathname.substring(0, curPathname.lastIndexOf("/"));
}
return pathParts;
};
export const getParentAndChildPath = (
pathname: string,
): { parent: string; cur: string } => {
let curPathname = cleanupPath(pathname);
const parentPathname = curPathname.substring(0, curPathname.lastIndexOf("/"));
return {
parent: parentPathname,
cur: curPathname,
};
};

View File

@ -1,84 +1,21 @@
import React, { useEffect, useState } from "react";
import React, { useState } from "react";
import CssBaseline from "@mui/material/CssBaseline";
import Box from "@mui/material/Box";
import { useLocation, useNavigate } from "react-router-dom";
import {
FolderPreview,
Folders,
ImageWithThumbnail,
} from "./ImageGallery/models";
import { ImageGalleryAppBar } from "./ImageGallery/ImageGalleryAppBar";
import { ImageGalleryDrawer } from "./ImageGallery/ImageGalleryDrawer";
import { ImageGallery } from "./ImageGallery/ImageGallery";
import { Spinner } from "./ImageGallery/Spinner";
import Toolbar from "@mui/material/Toolbar";
import { Chip, Divider } from "@mui/material";
import { FolderGallery } from "./ImageGallery/FolderGallery";
import { ImageGalleryMain } from "./ImageGallery/ImageGalleryMain";
const drawerWidth = 240;
export const smallScreenMediaQuery = `(min-width:${drawerWidth * 3}px)`;
function ImageGalleryLayout() {
export const ImageGalleryLayout = (): React.JSX.Element => {
const [drawerOpen, setDrawerOpen] = useState(false);
const [error, setError] = useState(false);
const [imagesLoaded, setImagesLoaded] = useState(false);
const [images, setImages] = useState<ImageWithThumbnail[]>([]);
const [folders, setFolders] = useState<Folders | undefined>(undefined);
const [foldersPreview, setFoldersPreview] = useState<
FolderPreview[] | undefined
>([]);
const location = useLocation();
const navigate = useNavigate();
function handleDrawerToggle() {
setDrawerOpen(!drawerOpen);
}
useEffect(() => {
fetch("/directories", {
headers: {
Accept: "application/json",
},
})
.then((res) => res.json())
.then((data) => setFolders(data));
}, []);
useEffect(() => {
setFoldersPreview(undefined);
setImages([]);
setError(false);
setImagesLoaded(false);
fetch(`/folderspreview${location.pathname}`, {
headers: {
Accept: "application/json",
},
})
.then((res) => res.json())
.then((data) => {
setFoldersPreview(data);
});
fetch(`/images${location.pathname}`, {
headers: {
Accept: "application/json",
},
})
.then((res) => res.json())
.then((data) => {
if (data.images === undefined) {
if (location.pathname !== "/") {
navigate("/");
} else {
setError(true);
}
} else {
setImages(data.images);
setImagesLoaded(true);
}
});
}, [location.pathname]);
if (error) {
return (
<p>
@ -98,40 +35,9 @@ function ImageGalleryLayout() {
<ImageGalleryDrawer
open={drawerOpen}
drawerWidth={drawerWidth}
folders={folders}
handleDrawerToggle={handleDrawerToggle}
/>
<Box component="main" sx={{ flexGrow: 1, p: 3 }}>
<Toolbar />
{!imagesLoaded || !foldersPreview ? (
<Spinner />
) : (
<>
{foldersPreview.length > 0 && (
<>
<Divider style={{ marginBottom: "10px" }}>
<Chip label="Folders" size="small" />
</Divider>
<FolderGallery folders={foldersPreview} />
</>
)}
{images.length > 0 && foldersPreview.length > 0 && (
<Divider style={{ marginBottom: "10px" }}>
<Chip label="Images" size="small" />
</Divider>
)}
{images.length > 0 && <ImageGallery images={images} />}
{images.length == 0 && foldersPreview.length == 0 && (
<p>
No images available. You may want to add images to this
directory.
</p>
)}
</>
)}
</Box>
<ImageGalleryMain setError={setError} />
</Box>
);
}
export default ImageGalleryLayout;
};

View File

@ -3,7 +3,7 @@ import ReactDOM from "react-dom/client";
import "./index.css";
import { BrowserRouter } from "react-router-dom";
import reportWebVitals from "./reportWebVitals";
import ImageGalleryLayout from "./ImageGalleryLayout";
import { ImageGalleryLayout } from "./ImageGalleryLayout";
import { setGalleryTitleAndFavicon } from "./env";
setGalleryTitleAndFavicon();