Compare commits

..

1 Commits

Author SHA1 Message Date
1a7786b458 make grid 2023-11-13 20:45:24 +00:00
6 changed files with 1276 additions and 1455 deletions

2255
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -47,7 +47,6 @@
"eslint-config-prettier": "^9.0.0", "eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.1", "eslint-plugin-prettier": "^5.0.1",
"prettier": "^3.0.3", "prettier": "^3.0.3",
"react-router-dom": "^6.20.0", "typescript": "^5.2.2"
"typescript": "4.9.5"
} }
} }

View File

@ -1,14 +1,15 @@
body { body {
min-height: 100vh; min-height: 100vh;
background-color: #282c34; background-color: #282c34;
color: #B9B8D6; color: #b9b8d6;
} }
.Header { .Header {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
padding: 1em; margin: 1em;
padding: 0;
} }
.Contents { .Contents {
@ -23,7 +24,7 @@ body {
flex-direction: column; flex-direction: column;
text-align: center; text-align: center;
background-color: #3B3E49; background-color: #3b3e49;
border-radius: 10px 10px 0px 0px; border-radius: 10px 10px 0px 0px;
margin: 0 0 1em 0; margin: 0 0 1em 0;
@ -34,24 +35,18 @@ body {
.GalleryItem > a { .GalleryItem > a {
color: inherit; color: inherit;
margin: 0.5em; margin: 0;
overflow: clip; overflow: clip;
} }
.GalleryItem > .ImgLink { .GalleryItem > img,
margin: 0; video {
padding: 0;
}
.GalleryItem > .ImgLink > img,
.GalleryItem > video {
object-fit: contain;
max-height: 80vh; max-height: 80vh;
} }
.DirectoryItem > button { .DirectoryItem > button {
background-color: #44655D; background-color: #44655d;
color: #B9B8D6; color: #b9b8d6;
border: none; border: none;
padding: 10px 20px; padding: 10px 20px;
text-align: center; text-align: center;
@ -68,30 +63,35 @@ body {
background-color: #0056b3; background-color: #0056b3;
} }
.PlaceholderItem {
animation: loading 1.5s infinite;
}
.PlaceholderItem > button,
.ParentDirectoryItem > button { .ParentDirectoryItem > button {
background-color: #4D4F5D; background-color: #4d4f5d;
} }
.PlaceholderItem > button, @media (min-width: 62em) {
.DirectoryItem > button { .GalleryItem {
height: 2.5em; margin: 1em;
min-width: 6em; border-radius: 1em;
}
.PlaceholderItem > a { overflow: hidden;
height: 1em;
}
@keyframes loading {
0%, 100% {
filter: brightness(80%);
} }
50% {
filter: brightness(100%); .Contents {
display: flex;
flex-direction: row;
align-items: start;
align-content: center;
flex-wrap: wrap;
}
.GalleryImg {
object-fit: cover;
width: 12em;
height: 9em;
}
.GalleryVid {
object-fit: cover;
width: 12em;
height: 9em;
} }
} }

View File

@ -1,15 +1,183 @@
import { BrowserRouter, Routes, Route } from 'react-router-dom'; import React from 'react';
//import logo from './logo.svg';
import './App.css'; import './App.css';
import Gallery from './Gallery/index'; import { useState, useEffect } from 'react';
function App() { function App() {
const [data, setData] = useState('');
const [category, setCategory] = useState('');
useEffect(() => {
post(category, (json: string) => {
setData(json);
});
});
return ( return (
<BrowserRouter> <div className="App">
<Routes> <div className="Header">
<Route path="/photos/*" element={<Gallery />} /> {getContents(data, true, category, setCategory)}
</Routes> </div>
</BrowserRouter> <div className="Contents">
{getContents(data, false, category, setCategory)}
</div>
</div>
); );
} }
interface galleryItemInfo {
is_dir: boolean;
url: string;
thumbnail_url: string;
}
function getContents(
data: string,
getDir: boolean,
category: string,
setCategory: (category: string) => void
) {
if (data === '') return;
let obj = null;
try {
obj = JSON.parse(data);
} catch (error: unknown) {
if (!(error instanceof SyntaxError)) {
throw new Error(error as unknown as undefined);
}
return data;
}
return obj
.sort((a: galleryItemInfo, b: galleryItemInfo) => {
if (a.is_dir && b.is_dir) return 0;
if (a.is_dir && !b.is_dir) return -1;
if (!a.is_dir && b.is_dir) return 1;
if (!a.is_dir && !b.is_dir) return 0;
})
.filter((item: galleryItemInfo) => {
return getDir == item.is_dir;
})
.map((item: galleryItemInfo, index: number) => {
const url = item.url;
const thumbnail_url =
item.thumbnail_url === null ? item.url : item.thumbnail_url;
let onClick = () => {};
if (item.is_dir) {
onClick = () => {
const subCategory = getFileName(url);
if (category !== '') category = `${category}/${subCategory}`;
else category = subCategory;
if (subCategory === '..')
category = category.split('/').slice(0, -2).join('/');
setCategory(category);
};
return (
<DirectoryItem
key={index}
thumbnail_url={thumbnail_url}
url={url}
onClick={onClick}
/>
);
}
if (url.endsWith('.mp4')) {
return (
<VideoItem
key={index}
thumbnail_url={thumbnail_url}
url={url}
onClick={onClick}
/>
);
}
return (
<ImageItem
key={index}
thumbnail_url={thumbnail_url}
url={url}
onClick={onClick}
/>
);
});
}
interface galleryItem {
url: string;
thumbnail_url: string;
onClick: () => void;
}
function ImageItem({ thumbnail_url, url }: galleryItem) {
return (
<div className="GalleryItem">
<a href={url} target="_blank">
<img className="GalleryImg" src={thumbnail_url} loading="lazy" />
</a>
</div>
);
}
function VideoItem({ url }: galleryItem) {
return (
<div className="GalleryItem">
<a href={url} target="_blank">
<video controls>
<source className="GalleryVid" src={url} type="video/mp4" />
</video>
</a>
</div>
);
}
function DirectoryItem({ url, onClick }: galleryItem) {
let buttonText = getFileName(url);
const isBackButton = buttonText === '..';
buttonText = isBackButton ? 'Back' : `Category: ${buttonText}`;
const backButtonClass = isBackButton ? 'ParentDirectoryItem' : '';
return (
<div className={`DirectoryItem ${backButtonClass}`}>
<button className="DirectoryItem" onClick={onClick}>
{buttonText}
</button>
</div>
);
}
function getFileName(string: string): string {
return string.split('/').at(-1) as unknown as string;
}
function post(category: string, callback: (text: string) => void) {
fetch(
//'http://localhost/photo-viewer-backend/php/get.php',
'https://dundun.ddns.net/photo-viewer/photo-viewer-backend/php/get.php',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
category: category,
}),
}
)
.then((response) => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response;
})
.then((response) => response.text())
.then((data) => {
callback(data as unknown as string);
})
.catch((error) => {
console.error('There was a problem with the fetch operation:', error);
});
}
export default App; export default App;

View File

@ -1,223 +0,0 @@
import '../App.css';
import placeholderPng from './placeholder.png';
import { useState, useEffect } from 'react';
import { useParams, useNavigate, useLocation } from 'react-router-dom';
function Gallery() {
const params = useParams<string>()['*'];
const location = useLocation();
const navigate = useNavigate();
const [data, setData] = useState<galleryItemInfo[]>([]);
const [category, setCategory] = useState(params === undefined ? '' : params);
useEffect(() => {
post(category, (data: galleryItemInfo[]) => {
if (data.length === 0) {
updateCategory('');
}
setData(data);
});
}, [category]);
useEffect(() => {
setData(getPlaceholderData());
setCategory(params as string);
}, [location]);
const updateCategory = (category: string) => {
setData(getPlaceholderData());
setCategory(category);
navigate(`/photos/${category}`);
};
return (
<div className="Gallery">
<div className="Header">
{Contents(data, true, category, updateCategory)}
</div>
<div className="Contents">
{Contents(data, false, category, updateCategory)}
</div>
</div>
);
}
interface galleryItemInfo {
is_dir: boolean;
url: string;
thumbnail_url: string;
isPlaceholder: boolean;
}
function Contents(
data: galleryItemInfo[],
getDir: boolean,
category: string,
setCategory: (category: string) => void
) {
if (data === null) return;
return data
.sort((a: galleryItemInfo, b: galleryItemInfo): number => {
if (a.is_dir && b.is_dir) return 0;
if (a.is_dir && !b.is_dir) return -1;
if (!a.is_dir && b.is_dir) return 1;
if (!a.is_dir && !b.is_dir) return 0;
return 0;
})
.filter((item: galleryItemInfo) => {
return getDir == item.is_dir;
})
.map((item: galleryItemInfo, index: number) => {
const itemProps = {
key: index,
url: item.url,
thumbnail_url:
item.thumbnail_url === null ? item.url : item.thumbnail_url,
onClick: () => {},
isPlaceholder: item.isPlaceholder,
};
if (item.is_dir) {
itemProps.onClick = item.isPlaceholder
? () => {}
: () => {
const subCategory = getFileName(itemProps.url);
if (category !== '') category = `${category}/${subCategory}`;
else category = subCategory;
if (subCategory === '..')
category = category.split('/').slice(0, -2).join('/');
setCategory(category);
};
return <DirectoryItem {...itemProps} />;
}
if (itemProps.url.endsWith('.mp4')) {
return <VideoItem {...itemProps} />;
}
return <ImageItem {...itemProps} />;
});
}
interface galleryItem {
url: string;
thumbnail_url: string;
onClick: () => void;
isPlaceholder: boolean;
}
function ImageItem({ thumbnail_url, url, isPlaceholder }: galleryItem) {
const placeholderClass = isPlaceholder ? 'PlaceholderItem' : '';
return (
<div className={`GalleryItem ${placeholderClass}`}>
<a href={url} target="_blank">
{getFileName(url)}
</a>
<a href={url} target="_blank" className="ImgLink">
<img src={thumbnail_url} loading="lazy" />
</a>
</div>
);
}
function VideoItem({ url }: galleryItem) {
return (
<div className="GalleryItem">
<a href={url} target="_blank">
{getFileName(url)}
</a>
<video controls>
<source src={url} type="video/mp4" />
</video>
</div>
);
}
function DirectoryItem({ url, onClick, isPlaceholder }: galleryItem) {
let buttonText = getFileName(url);
const isBackButton = buttonText === '..';
const placeholderClass = isPlaceholder ? 'PlaceholderItem' : '';
buttonText = isBackButton ? 'Back' : `${buttonText}`;
const backButtonClass = isBackButton ? 'ParentDirectoryItem' : '';
return (
<div className={`DirectoryItem ${backButtonClass} ${placeholderClass}`}>
<button onClick={onClick}>{buttonText}</button>
</div>
);
}
function getFileName(string: string): string {
return string.split('/').at(-1) as unknown as string;
}
function getPlaceholderData(): galleryItemInfo[] {
const backDir = (): galleryItemInfo => {
return {
is_dir: true,
url: '..',
thumbnail_url: '',
isPlaceholder: true,
};
};
const placeholderDir = (): galleryItemInfo => {
return {
is_dir: true,
url: '',
thumbnail_url: '',
isPlaceholder: true,
};
};
const placeholderImg = (): galleryItemInfo => {
return {
is_dir: false,
url: '',
thumbnail_url: `${placeholderPng}`,
isPlaceholder: true,
};
};
return [
backDir(),
placeholderDir(),
placeholderDir(),
placeholderImg(),
placeholderImg(),
placeholderImg(),
];
}
async function post(
category: string,
setDataCallback: (data: galleryItemInfo[]) => void
) {
try {
const response = await fetch(
//'http://localhost/photo-viewer-backend/php/get.php',
'https://dundun.ddns.net/photo-viewer/photo-viewer-backend/php/get.php',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
category: category,
}),
}
);
setDataCallback(await response.json());
} catch (error) {
console.error('Error fetching data: ', error);
setDataCallback([
{
is_dir: false,
url: 'error_fetching',
thumbnail_url: '',
isPlaceholder: false,
},
]);
}
}
export default Gallery;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB