Skip to content

Commit

Permalink
Add selection, delete and cancel to gallery.
Browse files Browse the repository at this point in the history
  • Loading branch information
Lillifee committed Jan 5, 2022
1 parent 6c8eb6f commit 0d0173d
Show file tree
Hide file tree
Showing 12 changed files with 248 additions and 89 deletions.
13 changes: 8 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ https://www.prusaprinters.org/prints/61556-raspberry-pi4-hq-camera-aluminium-mix

![picture](screenshots/raspiCamSettingsShot2.png)

![picture](screenshots/raspiCamGallery1.png)

# Installation

## Raspberry PI OS
Expand Down Expand Up @@ -207,14 +209,19 @@ You can find the latest command sent to raspiCam in the terminal output:

![picture](screenshots/raspiCamSettingsShot5.png)

![picture](screenshots/raspiCamSettingsShot6.png)

![picture](screenshots/raspiCamGallery1.png)

![picture](screenshots/raspiCamGallery2.png)

# Theme

![picture](screenshots/raspiCamTheme1.png)

![picture](screenshots/raspiCamTheme2.png)

![picture](screenshots/raspiCamTheme3.png)

# Roadmap and ideas

## Stream
Expand All @@ -226,11 +233,7 @@ You can find the latest command sent to raspiCam in the terminal output:
- Setting explanation

## Gallery
- sort order
- loading indication
- support videos
- group timelapse photos
- select and delete items
- download multiple items (zip)

## Keywords
Expand Down
Binary file added screenshots/raspiCamGallery1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added screenshots/raspiCamGallery2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added screenshots/raspiCamTheme3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 14 additions & 1 deletion src/server/server.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import express from 'express';
import path from 'path';
import { isDefined } from '../shared/helperFunctions';
import {
RaspiGallery,
RaspiControlStatus,
Expand All @@ -25,7 +26,7 @@ const server = (
const app = express();

// Serve the static content from public
app.use(express.static(path.join(__dirname, './public')));
app.use(express.static(path.join(__dirname, 'public')));
app.use('/photos', express.static(fileWatcher.getPath()));
app.use(express.json());

Expand Down Expand Up @@ -116,6 +117,18 @@ const server = (
res.status(200).send(gallery);
});

app.post('/api/gallery/delete', (req, res) => {
const files =
Array.isArray(req.body) &&
req.body.map((value) => (typeof value === 'string' ? value : undefined)).filter(isDefined);

files && fileWatcher.deleteFiles(files);
res.status(200).send('Gallery files deleted');
});

// All other requests to index html
app.get('*', (_, res) => res.sendFile(path.resolve(__dirname, 'public', 'index.html')));

return app;
};

Expand Down
35 changes: 25 additions & 10 deletions src/server/watcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export interface FileWatcher {
getFiles: () => RaspiFile[];
getLatestFile: () => RaspiFile | undefined;
getPath: () => string;
deleteFiles: (fileNames: string[]) => void;
}

export const fileWatcher = (): FileWatcher => {
Expand All @@ -35,25 +36,39 @@ export const fileWatcher = (): FileWatcher => {
const getPath = () => photosAbsPath;
const getFiles = () => files;
const getLatestFile = () => files[files.length - 1];
const deleteFiles = (fileNames: string[]) =>
fileNames.forEach((fileName) => {
const file = files.find((x) => x.base === fileName);
if (!file) return;

try {
logger.info('delete file', file.base);
fs.unlinkSync(path.join(photosAbsPath, file.base));
file.thumb && fs.unlinkSync(path.join(photosAbsPath, file.thumb));
return false;
} catch (err) {
logger.error('failed to delete file', err);
return true;
}
});

const addFile = (fileName: string) => {
const watchFile = (fileName: string) => {
const { name, base, ext } = path.parse(fileName);
const type = fileTypes[ext.substring(1)];
const file: RaspiFile = { name, base, ext, type, date: 0 };

// Invalid type or thumbnail
if (!type || isThumbnail(file)) return;

// File already exists
if (files.findIndex((x) => x.base === file.base) >= 0) return;

const filePath = path.join(photosAbsPath, fileName);

fs.stat(filePath, (err, stats) => {
if (err) {
logger.warning('remove file', fileName);
files = files.filter((x) => x.base === fileName);
// File removed
files = files.filter((x) => x.base !== fileName);
} else {
// File already exists
if (files.findIndex((x) => x.base === file.base) >= 0) return;

file.date = stats.ctime.getTime();
extractThumbnail(file);
files.push(file);
Expand All @@ -63,14 +78,14 @@ export const fileWatcher = (): FileWatcher => {

fs.readdir(photosAbsPath, (err, files) => {
if (err) logger.error('failed to read photo directory', err.message);
files.forEach(addFile);
files.forEach(watchFile);
});

fs.watch(photosAbsPath, {}, (_, fileName) => {
addFile(fileName);
watchFile(fileName);
});

return { getFiles, getLatestFile, getPath };
return { getFiles, getLatestFile, getPath, deleteFiles };
};

const isThumbnail = (file: RaspiFile): boolean => file.name.includes(thumbnailExt);
Expand Down
24 changes: 24 additions & 0 deletions src/site/components/common/Icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,30 @@ export const IconMap = {
<path d="M6.85 12.65h2.3L8 9l-1.15 3.65zM22 7l-1.2 6.29L19.3 7h-1.6l-1.49 6.29L15 7h-.76C12.77 5.17 10.53 4 8 4c-4.42 0-8 3.58-8 8s3.58 8 8 8c3.13 0 5.84-1.81 7.15-4.43l.1.43H17l1.5-6.1L20 16h1.75l2.05-9H22zm-11.7 9l-.7-2H6.4l-.7 2H3.8L7 7h2l3.2 9h-1.9z" />
</svg>
),
Checked: (
<svg width="24" height="24" viewBox="0 0 24 24">
<path d="M0 0h24v24H0z" fill="none" />
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z" />
</svg>
),
Unchecked: (
<svg width="24" height="24" viewBox="0 0 24 24">
<path d="M0 0h24v24H0V0z" fill="none" />
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z" />
</svg>
),
Delete: (
<svg width="24" height="24" viewBox="0 0 24 24">
<path d="M0 0h24v24H0V0z" fill="none" />
<path d="M16 9v10H8V9h8m-1.5-6h-5l-1 1H5v2h14V4h-3.5l-1-1zM18 7H6v12c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7z" />
</svg>
),
Clear: (
<svg width="24" height="24" viewBox="0 0 24 24">
<path d="M0 0h24v24H0V0z" fill="none" />
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z" />
</svg>
),
};

export type IconType = keyof typeof IconMap;
Expand Down
96 changes: 37 additions & 59 deletions src/site/components/gallery/Gallery.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import * as React from 'react';
import styled from 'styled-components';
import { RaspiGallery, RaspiFile, photosPath } from '../../../shared/settings/types';
import { RaspiGallery, RaspiFile } from '../../../shared/settings/types';
import { useFetch } from '../common/hooks/useFetch';
import { Icon } from '../common/Icon';
import { GalleryItem } from './GalleryItem';
import { Toolbar } from './Toolbar';

const GalleryContainer = styled.div`
Expand All @@ -24,43 +24,10 @@ const GroupContainer = styled.div`
align-items: stretch;
@media (max-width: 500px) {
grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
}
`;

const Thumbnail = styled.img`
max-width: 100%;
height: auto;
object-fit: cover;
flex: 1;
`;

const PreviewContainer = styled.div`
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
min-height: 80px;
fill: ${(p) => p.theme.Foreground};
background: ${(p) => p.theme.LayerBackground};
`;

const FallbackIcon = styled.div`
margin: 0.2em;
`;

const FallbackText = styled.div`
margin: 0.2em;
color: ${({ theme }) => theme.Foreground};
font-size: ${({ theme }) => theme.FontSize.s};
`;

const PreviewLink = styled.a`
display: flex;
text-decoration: none;
`;

const Group = styled.div`
display: flex;
flex-direction: column;
Expand All @@ -70,9 +37,7 @@ const Group = styled.div`
const Header = styled.div`
font-size: ${(p) => p.theme.FontSize.l};
font-weight: 300;
position: sticky;
top: 0px;
margin: 0 1.5em;
/* top: 0px; */
padding: 0.45em;
`;

Expand All @@ -81,7 +46,8 @@ const dateTimeFormat = new Intl.DateTimeFormat('en-GB', {
} as Intl.DateTimeFormatOptions);

export const Gallery: React.FC = () => {
const [gallery] = useFetch<RaspiGallery>('/api/gallery', { files: [] });
const [gallery, , refresh] = useFetch<RaspiGallery>('/api/gallery', { files: [] });
const [selectedFiles, setSelectFiles] = React.useState<string[]>([]);

const groupedFiles = gallery.data.files
.sort((a, b) => b.date - a.date)
Expand All @@ -92,34 +58,46 @@ export const Gallery: React.FC = () => {
return result;
}, {});

const toggleSelection = (fileBase: string) =>
setSelectFiles((files) =>
files.find((x) => x === fileBase)
? files.filter((x) => x !== fileBase)
: [...files, fileBase],
);

const clearSelection = () => setSelectFiles([]);

const deleteFiles = React.useCallback(() => {
fetch('/api/gallery/delete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(selectedFiles),
})
.finally(refresh)
.catch((error) => console.error('Delete failed', error));

clearSelection();
}, [selectedFiles, refresh]);

return (
<GalleryContainer>
<Toolbar />
<Toolbar
showActions={selectedFiles.length > 0}
deleteFiles={deleteFiles}
clearSelection={clearSelection}
/>

{Object.entries(groupedFiles).map(([date, files]) => (
<Group key={date}>
<Header>{date}</Header>
<GroupContainer>
{files.map((file) => (
<PreviewLink
<GalleryItem
key={file.base}
target="_blank"
rel="noreferrer"
href={`${photosPath}/${file.base}`}
>
<PreviewContainer>
{file.thumb ? (
<Thumbnail src={`${photosPath}/${file.thumb || ''}`} />
) : (
<React.Fragment>
<FallbackIcon>
<Icon type={file.type === 'VIDEO' ? 'Video' : 'Photo'} />
</FallbackIcon>
<FallbackText>{file.base}</FallbackText>
</React.Fragment>
)}
</PreviewContainer>
</PreviewLink>
file={file}
selected={selectedFiles.includes(file.base)}
toggleSelection={toggleSelection}
/>
))}
</GroupContainer>
</Group>
Expand Down
Loading

0 comments on commit 0d0173d

Please sign in to comment.