Compare commits
17 Commits
bee9c1cc00
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 76b50ec462 | |||
| d332635d6a | |||
| 1d0c3f68c5 | |||
| 8743133f05 | |||
| dae708c765 | |||
| 162f21089d | |||
| f3d7c7abf2 | |||
| 07dd094d6b | |||
| 1555cf37cd | |||
| 8a1b52749b | |||
| d959c004dd | |||
| 098777bb02 | |||
| 2c31cf8820 | |||
| 9d3d3530ea | |||
| a0b85dfbca | |||
| 039c2948c4 | |||
| a26dd9741a |
Generated
+1190
-1067
File diff suppressed because it is too large
Load Diff
+3
-1
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"name": "react-photo-viewer",
|
||||
"homepage": "https://dundun.ddns.net/photos",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
@@ -46,6 +47,7 @@
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-plugin-prettier": "^5.0.1",
|
||||
"prettier": "^3.0.3",
|
||||
"typescript": "^5.2.2"
|
||||
"react-router-dom": "^6.20.0",
|
||||
"typescript": "4.9.5"
|
||||
}
|
||||
}
|
||||
|
||||
-52
@@ -1,52 +0,0 @@
|
||||
<?php
|
||||
define("ROOT_URL", "http://localhost/react-photo-viewer/");
|
||||
define("RELATIVE_IMG", "../img/");
|
||||
define("ABSOLUTE_IMG", "img/");
|
||||
|
||||
main();
|
||||
|
||||
function main(): void {
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
$array = [];
|
||||
|
||||
$category = $data["category"];
|
||||
$directory = RELATIVE_IMG.$category;
|
||||
if (!is_dir($directory)) return;
|
||||
foreach (scandir($directory) as $file) {
|
||||
if ($file === ".") continue;
|
||||
if ($category === "" && $file === "..") continue;
|
||||
|
||||
if ($category != "") $file = $category.'/'.$file;
|
||||
$is_dir = is_dir(RELATIVE_IMG.$file);
|
||||
if (!$is_dir && !is_valid_file_type($file)) continue;
|
||||
|
||||
$file_item = [];
|
||||
$file_item["is_dir"] = $is_dir;
|
||||
$file_item["url"] = ROOT_URL.ABSOLUTE_IMG.$file;
|
||||
array_push($array, $file_item);
|
||||
}
|
||||
echo json_encode($array);
|
||||
}
|
||||
|
||||
function is_valid_file_type(string $filename): bool {
|
||||
$file_extensions = [
|
||||
// Image extensions
|
||||
'jpg',
|
||||
'jpeg',
|
||||
'png',
|
||||
'gif',
|
||||
'tiff',
|
||||
'bmp',
|
||||
'webp',
|
||||
'svg',
|
||||
// Video extensions
|
||||
'mp4',
|
||||
];
|
||||
|
||||
foreach ($file_extensions as $file_extension) {
|
||||
if (str_ends_with($filename, $file_extension)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
?>
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>React App</title>
|
||||
<title>Photos</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
+36
-4
@@ -8,8 +8,7 @@ body {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
margin: 1em;
|
||||
padding: 0;
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
.Contents {
|
||||
@@ -32,11 +31,21 @@ body {
|
||||
max-width: calc(100vw - 2em);
|
||||
}
|
||||
|
||||
.GalleryItem > p {
|
||||
.GalleryItem > a {
|
||||
color: inherit;
|
||||
|
||||
margin: 0.5em;
|
||||
overflow: clip;
|
||||
}
|
||||
|
||||
.GalleryItem > img, video {
|
||||
.GalleryItem > .ImgLink {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.GalleryItem > .ImgLink > img,
|
||||
.GalleryItem > video {
|
||||
object-fit: contain;
|
||||
max-height: 80vh;
|
||||
}
|
||||
|
||||
@@ -59,7 +68,30 @@ body {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
.PlaceholderItem {
|
||||
animation: loading 1.5s infinite;
|
||||
}
|
||||
|
||||
.PlaceholderItem > button,
|
||||
.ParentDirectoryItem > button {
|
||||
background-color: #4D4F5D;
|
||||
}
|
||||
|
||||
.PlaceholderItem > button,
|
||||
.DirectoryItem > button {
|
||||
height: 2.5em;
|
||||
min-width: 6em;
|
||||
}
|
||||
|
||||
.PlaceholderItem > a {
|
||||
height: 1em;
|
||||
}
|
||||
|
||||
@keyframes loading {
|
||||
0%, 100% {
|
||||
filter: brightness(80%);
|
||||
}
|
||||
50% {
|
||||
filter: brightness(100%);
|
||||
}
|
||||
}
|
||||
|
||||
+7
-144
@@ -1,152 +1,15 @@
|
||||
import React from 'react';
|
||||
//import logo from './logo.svg';
|
||||
import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
||||
import './App.css';
|
||||
import { useState, useEffect } from 'react';
|
||||
import Gallery from './Gallery/index';
|
||||
|
||||
function App() {
|
||||
const [data, setData] = useState('');
|
||||
const [category, setCategory] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
post(category, (json: string) => {
|
||||
setData(json);
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="App">
|
||||
<div className="Header">
|
||||
{getContents(data, true, category, setCategory)}
|
||||
</div>
|
||||
<div className="Contents">
|
||||
{getContents(data, false, category, setCategory)}
|
||||
</div>
|
||||
</div>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/photos/*" element={<Gallery />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
|
||||
interface galleryItemInfo {
|
||||
is_dir: boolean;
|
||||
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;
|
||||
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} url={url} onClick={onClick} />;
|
||||
}
|
||||
if (url.endsWith('.mp4')) {
|
||||
return <VideoItem key={index} url={url} onClick={onClick} />;
|
||||
}
|
||||
return <ImageItem key={index} url={url} onClick={onClick} />;
|
||||
});
|
||||
}
|
||||
|
||||
interface galleryItem {
|
||||
url: string;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
function ImageItem({ url }: galleryItem) {
|
||||
return (
|
||||
<div className="GalleryItem">
|
||||
<p>{getFileName(url)}</p>
|
||||
<img src={url} loading="lazy" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function VideoItem({ url }: galleryItem) {
|
||||
return (
|
||||
<div className="GalleryItem">
|
||||
<p>{getFileName(url)}</p>
|
||||
<video controls>
|
||||
<source src={url} type="video/mp4" />
|
||||
</video>
|
||||
</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/react-photo-viewer/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;
|
||||
|
||||
@@ -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;
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 8.2 KiB |
Reference in New Issue
Block a user