Compare commits
4 Commits
5d0a9fdefe
...
da273cbc87
| Author | SHA1 | Date |
|---|---|---|
|
|
da273cbc87 | |
|
|
4da3376d5f | |
|
|
18661afbc9 | |
|
|
fc32dd79cc |
44
README.md
44
README.md
|
|
@ -1,5 +1,7 @@
|
||||||
# Simple Picture Gallery
|
# 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
|
## Getting Started
|
||||||
|
|
||||||
### Information
|
### Information
|
||||||
|
|
@ -8,23 +10,20 @@ Folders should only contain images and folders. Folders should not contain any o
|
||||||
|
|
||||||
### Docker
|
### Docker
|
||||||
|
|
||||||
|
The easiest way to run simple picture gallery is with docker.
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
docker build . -t simple-picture-gallery
|
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
|
```properties
|
||||||
VITE_TITLE=My Gallery
|
VITE_TITLE=My Gallery
|
||||||
VITE_APPBAR_COLOR=#F8AB2D
|
VITE_APPBAR_COLOR=#F8AB2D
|
||||||
```
|
|
||||||
|
|
||||||
Other properties:
|
|
||||||
|
|
||||||
```properties
|
|
||||||
VITE_FAVICON_HREF=<URL to your favicon>
|
VITE_FAVICON_HREF=<URL to your favicon>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -32,4 +31,33 @@ And run docker with `--env-file .env`
|
||||||
|
|
||||||
```shell
|
```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
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
...
|
||||||
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
@ -13,9 +13,9 @@ export interface PhotoWithFolder extends ImageWithThumbnail {
|
||||||
const PreviewFolder = ({ folder }: { folder: FolderPreview }) => {
|
const PreviewFolder = ({ folder }: { folder: FolderPreview }) => {
|
||||||
return (
|
return (
|
||||||
<img
|
<img
|
||||||
|
loading="lazy"
|
||||||
src={folder.imagePreview.thumbnail}
|
src={folder.imagePreview.thumbnail}
|
||||||
alt={folder.name}
|
alt={folder.name}
|
||||||
loading="lazy"
|
|
||||||
style={{
|
style={{
|
||||||
objectFit: "cover",
|
objectFit: "cover",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ export const ImageGallery = ({ images }: { images: ImageWithThumbnail[] }) => {
|
||||||
photos={images}
|
photos={images}
|
||||||
render={{
|
render={{
|
||||||
image: (props, context) => (
|
image: (props, context) => (
|
||||||
<img {...props} src={context.photo.thumbnail} loading={"lazy"} />
|
<img loading={"lazy"} {...props} src={context.photo.thumbnail} />
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
onClick={({ index }) => setIndex(index)}
|
onClick={({ index }) => setIndex(index)}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import Toolbar from "@mui/material/Toolbar";
|
||||||
import { Chip, useTheme } from "@mui/material";
|
import { Chip, useTheme } from "@mui/material";
|
||||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||||
import { smallScreenMediaQuery } from "../ImageGalleryLayout";
|
import { smallScreenMediaQuery } from "../ImageGalleryLayout";
|
||||||
import { getDefaultExpanded } from "./PathToExpaned";
|
import { getDefaultExpanded, getParentAndChildPath } from "./pathnames";
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
|
|
||||||
function generateTreeViewChildren(
|
function generateTreeViewChildren(
|
||||||
|
|
@ -61,17 +61,9 @@ const GenerateTreeView = ({ root }: { root: Folders }) => {
|
||||||
);
|
);
|
||||||
const [selectedItem, setSelectedItem] = useState<string>(location.pathname);
|
const [selectedItem, setSelectedItem] = useState<string>(location.pathname);
|
||||||
|
|
||||||
// TODO: clean this effect up. See also `getDefaultExpanded`
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let curPathname = location.pathname.startsWith("/")
|
const { parent: parentPathname, cur: curPathname } = getParentAndChildPath(
|
||||||
? location.pathname.slice(1)
|
location.pathname,
|
||||||
: location.pathname;
|
|
||||||
while (curPathname.endsWith("/")) {
|
|
||||||
curPathname = curPathname.slice(0, -1);
|
|
||||||
}
|
|
||||||
const parentPathname = curPathname.substring(
|
|
||||||
0,
|
|
||||||
curPathname.lastIndexOf("/"),
|
|
||||||
);
|
);
|
||||||
if (!expandedItems.includes(parentPathname)) {
|
if (!expandedItems.includes(parentPathname)) {
|
||||||
setExpandedItems([parentPathname, ...expandedItems]);
|
setExpandedItems([parentPathname, ...expandedItems]);
|
||||||
|
|
@ -132,16 +124,25 @@ const GenerateTreeView = ({ root }: { root: Folders }) => {
|
||||||
export const ImageGalleryDrawer = ({
|
export const ImageGalleryDrawer = ({
|
||||||
open,
|
open,
|
||||||
drawerWidth,
|
drawerWidth,
|
||||||
folders,
|
|
||||||
handleDrawerToggle,
|
handleDrawerToggle,
|
||||||
}: {
|
}: {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
drawerWidth: number;
|
drawerWidth: number;
|
||||||
folders: Folders | undefined;
|
|
||||||
handleDrawerToggle: () => void;
|
handleDrawerToggle: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const smallScreen = !useMediaQuery(smallScreenMediaQuery);
|
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 =
|
const drawerContent =
|
||||||
folders != undefined ? (
|
folders != undefined ? (
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { getDefaultExpanded } from "../PathToExpaned";
|
import { getDefaultExpanded } from "../pathnames";
|
||||||
|
|
||||||
interface pathWithExpanded {
|
interface pathWithExpanded {
|
||||||
pathname: string;
|
pathname: string;
|
||||||
|
|
@ -1,12 +1,28 @@
|
||||||
export const getDefaultExpanded = (pathname: string): string[] => {
|
const cleanupPath = (pathname: string): string => {
|
||||||
const pathParts = [];
|
|
||||||
let curPathname = pathname.startsWith("/") ? pathname.slice(1) : pathname;
|
let curPathname = pathname.startsWith("/") ? pathname.slice(1) : pathname;
|
||||||
while (curPathname.endsWith("/")) {
|
while (curPathname.endsWith("/")) {
|
||||||
curPathname = curPathname.slice(0, -1);
|
curPathname = curPathname.slice(0, -1);
|
||||||
}
|
}
|
||||||
|
return curPathname;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getDefaultExpanded = (pathname: string): string[] => {
|
||||||
|
let curPathname = cleanupPath(pathname);
|
||||||
|
const pathParts = [];
|
||||||
while (curPathname.length > 0) {
|
while (curPathname.length > 0) {
|
||||||
pathParts.push(curPathname);
|
pathParts.push(curPathname);
|
||||||
curPathname = curPathname.substring(0, curPathname.lastIndexOf("/"));
|
curPathname = curPathname.substring(0, curPathname.lastIndexOf("/"));
|
||||||
}
|
}
|
||||||
return pathParts;
|
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,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
@ -1,84 +1,21 @@
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useState } from "react";
|
||||||
import CssBaseline from "@mui/material/CssBaseline";
|
import CssBaseline from "@mui/material/CssBaseline";
|
||||||
import Box from "@mui/material/Box";
|
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 { ImageGalleryAppBar } from "./ImageGallery/ImageGalleryAppBar";
|
||||||
import { ImageGalleryDrawer } from "./ImageGallery/ImageGalleryDrawer";
|
import { ImageGalleryDrawer } from "./ImageGallery/ImageGalleryDrawer";
|
||||||
import { ImageGallery } from "./ImageGallery/ImageGallery";
|
import { ImageGalleryMain } from "./ImageGallery/ImageGalleryMain";
|
||||||
import { Spinner } from "./ImageGallery/Spinner";
|
|
||||||
import Toolbar from "@mui/material/Toolbar";
|
|
||||||
import { Chip, Divider } from "@mui/material";
|
|
||||||
import { FolderGallery } from "./ImageGallery/FolderGallery";
|
|
||||||
|
|
||||||
const drawerWidth = 240;
|
const drawerWidth = 240;
|
||||||
export const smallScreenMediaQuery = `(min-width:${drawerWidth * 3}px)`;
|
export const smallScreenMediaQuery = `(min-width:${drawerWidth * 3}px)`;
|
||||||
|
|
||||||
function ImageGalleryLayout() {
|
export const ImageGalleryLayout = (): React.JSX.Element => {
|
||||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||||
const [error, setError] = 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() {
|
function handleDrawerToggle() {
|
||||||
setDrawerOpen(!drawerOpen);
|
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) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<p>
|
<p>
|
||||||
|
|
@ -98,40 +35,9 @@ function ImageGalleryLayout() {
|
||||||
<ImageGalleryDrawer
|
<ImageGalleryDrawer
|
||||||
open={drawerOpen}
|
open={drawerOpen}
|
||||||
drawerWidth={drawerWidth}
|
drawerWidth={drawerWidth}
|
||||||
folders={folders}
|
|
||||||
handleDrawerToggle={handleDrawerToggle}
|
handleDrawerToggle={handleDrawerToggle}
|
||||||
/>
|
/>
|
||||||
<Box component="main" sx={{ flexGrow: 1, p: 3 }}>
|
<ImageGalleryMain setError={setError} />
|
||||||
<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>
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default ImageGalleryLayout;
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import ReactDOM from "react-dom/client";
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
import { BrowserRouter } from "react-router-dom";
|
import { BrowserRouter } from "react-router-dom";
|
||||||
import reportWebVitals from "./reportWebVitals";
|
import reportWebVitals from "./reportWebVitals";
|
||||||
import ImageGalleryLayout from "./ImageGalleryLayout";
|
import { ImageGalleryLayout } from "./ImageGalleryLayout";
|
||||||
import { setGalleryTitleAndFavicon } from "./env";
|
import { setGalleryTitleAndFavicon } from "./env";
|
||||||
|
|
||||||
setGalleryTitleAndFavicon();
|
setGalleryTitleAndFavicon();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue