Compare commits
11 Commits
edward-css
...
master
Author | SHA1 | Date | |
---|---|---|---|
76b50ec462 | |||
d332635d6a | |||
1d0c3f68c5 | |||
8743133f05 | |||
dae708c765 | |||
162f21089d | |||
f3d7c7abf2 | |||
07dd094d6b | |||
1555cf37cd | |||
8a1b52749b | |||
d959c004dd |
2257
package-lock.json
generated
2257
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -47,6 +47,7 @@
|
|||||||
"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",
|
||||||
"typescript": "^5.2.2"
|
"react-router-dom": "^6.20.0",
|
||||||
|
"typescript": "4.9.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
68
src/App.css
68
src/App.css
@ -1,15 +1,14 @@
|
|||||||
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;
|
||||||
|
|
||||||
margin: 1em;
|
padding: 1em;
|
||||||
padding: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.Contents {
|
.Contents {
|
||||||
@ -24,7 +23,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;
|
||||||
@ -35,18 +34,24 @@ body {
|
|||||||
.GalleryItem > a {
|
.GalleryItem > a {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
|
|
||||||
margin: 0;
|
margin: 0.5em;
|
||||||
overflow: clip;
|
overflow: clip;
|
||||||
}
|
}
|
||||||
|
|
||||||
.GalleryItem > img,
|
.GalleryItem > .ImgLink {
|
||||||
video {
|
margin: 0;
|
||||||
|
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;
|
||||||
@ -63,35 +68,30 @@ video {
|
|||||||
background-color: #0056b3;
|
background-color: #0056b3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.PlaceholderItem {
|
||||||
|
animation: loading 1.5s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.PlaceholderItem > button,
|
||||||
.ParentDirectoryItem > button {
|
.ParentDirectoryItem > button {
|
||||||
background-color: #4d4f5d;
|
background-color: #4D4F5D;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 62em) {
|
.PlaceholderItem > button,
|
||||||
.GalleryItem {
|
.DirectoryItem > button {
|
||||||
margin: 1em;
|
height: 2.5em;
|
||||||
border-radius: 1em;
|
min-width: 6em;
|
||||||
|
}
|
||||||
|
|
||||||
overflow: hidden;
|
.PlaceholderItem > a {
|
||||||
|
height: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes loading {
|
||||||
|
0%, 100% {
|
||||||
|
filter: brightness(80%);
|
||||||
}
|
}
|
||||||
|
50% {
|
||||||
.Contents {
|
filter: brightness(100%);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
182
src/App.tsx
182
src/App.tsx
@ -1,183 +1,15 @@
|
|||||||
import React from 'react';
|
import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
||||||
//import logo from './logo.svg';
|
|
||||||
import './App.css';
|
import './App.css';
|
||||||
import { useState, useEffect } from 'react';
|
import Gallery from './Gallery/index';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [data, setData] = useState('');
|
|
||||||
const [category, setCategory] = useState('');
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
post(category, (json: string) => {
|
|
||||||
setData(json);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="App">
|
<BrowserRouter>
|
||||||
<div className="Header">
|
<Routes>
|
||||||
{getContents(data, true, category, setCategory)}
|
<Route path="/photos/*" element={<Gallery />} />
|
||||||
</div>
|
</Routes>
|
||||||
<div className="Contents">
|
</BrowserRouter>
|
||||||
{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;
|
||||||
|
223
src/Gallery/index.tsx
Normal file
223
src/Gallery/index.tsx
Normal file
@ -0,0 +1,223 @@
|
|||||||
|
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;
|
BIN
src/Gallery/placeholder.png
Normal file
BIN
src/Gallery/placeholder.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.2 KiB |
Loading…
Reference in New Issue
Block a user