Implement photo viewer with categories/directories
This commit is contained in:
		
							parent
							
								
									592d65b66e
								
							
						
					
					
						commit
						bee9c1cc00
					
				
							
								
								
									
										52
									
								
								php/get.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								php/get.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,52 @@ | ||||
| <?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; | ||||
| } | ||||
| ?>
 | ||||
| 
 | ||||
							
								
								
									
										85
									
								
								src/App.css
									
									
									
									
									
								
							
							
						
						
									
										85
									
								
								src/App.css
									
									
									
									
									
								
							| @ -1,38 +1,65 @@ | ||||
| .App { | ||||
|   text-align: center; | ||||
| } | ||||
| 
 | ||||
| .App-logo { | ||||
|   height: 40vmin; | ||||
|   pointer-events: none; | ||||
| } | ||||
| 
 | ||||
| @media (prefers-reduced-motion: no-preference) { | ||||
|   .App-logo { | ||||
|     animation: App-logo-spin infinite 20s linear; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .App-header { | ||||
|   background-color: #282c34; | ||||
| body { | ||||
|   min-height: 100vh; | ||||
|   background-color: #282c34; | ||||
|   color: #B9B8D6; | ||||
| } | ||||
| 
 | ||||
| .Header { | ||||
|   display: flex; | ||||
|   flex-direction: row; | ||||
| 
 | ||||
|   margin: 1em; | ||||
|   padding: 0; | ||||
| } | ||||
| 
 | ||||
| .Contents { | ||||
|   display: flex; | ||||
|   flex-direction: column; | ||||
|   align-items: center; | ||||
|   justify-content: center; | ||||
|   font-size: calc(10px + 2vmin); | ||||
|   color: white; | ||||
|   white-space: pre-wrap; | ||||
| } | ||||
| 
 | ||||
| .App-link { | ||||
|   color: #61dafb; | ||||
| .GalleryItem { | ||||
|   display: flex; | ||||
|   flex-direction: column; | ||||
|   text-align: center; | ||||
| 
 | ||||
| 	background-color: #3B3E49; | ||||
| 
 | ||||
|   border-radius: 10px 10px 0px 0px; | ||||
|   margin: 0 0 1em 0; | ||||
|   padding: 0em; | ||||
|   max-width: calc(100vw - 2em); | ||||
| } | ||||
| 
 | ||||
| @keyframes App-logo-spin { | ||||
|   from { | ||||
|     transform: rotate(0deg); | ||||
|   } | ||||
|   to { | ||||
|     transform: rotate(360deg); | ||||
|   } | ||||
| .GalleryItem > p { | ||||
|   margin: 0.5em; | ||||
| } | ||||
| 
 | ||||
| .GalleryItem > img, video { | ||||
|   max-height: 80vh; | ||||
| } | ||||
| 
 | ||||
| .DirectoryItem > button { | ||||
| 	background-color: #44655D; | ||||
|   color: #B9B8D6; | ||||
|   border: none; | ||||
|   padding: 10px 20px; | ||||
|   text-align: center; | ||||
|   text-decoration: none; /* No underline for text */ | ||||
|   display: inline-block; | ||||
|   font-size: 16px; | ||||
|   margin: 0px 4px 0px 0px; | ||||
|   cursor: pointer; | ||||
|   border-radius: 10px; /* Slightly rounded corners for a modern feel */ | ||||
|   transition: background-color 0.3s; /* Smooth transition for hover effect */ | ||||
| } | ||||
| 
 | ||||
| .DirectoryItem > button:hover { | ||||
|   background-color: #0056b3; | ||||
| } | ||||
| 
 | ||||
| .ParentDirectoryItem > button { | ||||
| 	background-color: #4D4F5D; | ||||
| } | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										156
									
								
								src/App.tsx
									
									
									
									
									
								
							
							
						
						
									
										156
									
								
								src/App.tsx
									
									
									
									
									
								
							| @ -1,26 +1,152 @@ | ||||
| import React from 'react'; | ||||
| import logo from './logo.svg'; | ||||
| //import logo from './logo.svg';
 | ||||
| import './App.css'; | ||||
| import { useState, useEffect } from 'react'; | ||||
| 
 | ||||
| function App() { | ||||
|   const [data, setData] = useState(''); | ||||
|   const [category, setCategory] = useState(''); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     post(category, (json: string) => { | ||||
|       setData(json); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   return ( | ||||
|     <div className="App"> | ||||
|       <header className="App-header"> | ||||
|         <img src={logo} className="App-logo" alt="logo" /> | ||||
|         <p> | ||||
|           Edit <code>src/App.tsx</code> and save to reload. | ||||
|         </p> | ||||
|         <a | ||||
|           className="App-link" | ||||
|           href="https://reactjs.org" | ||||
|           target="_blank" | ||||
|           rel="noopener noreferrer" | ||||
|         > | ||||
|           Learn React | ||||
|         </a> | ||||
|       </header> | ||||
|       <div className="Header"> | ||||
|         {getContents(data, true, category, setCategory)} | ||||
|       </div> | ||||
|       <div className="Contents"> | ||||
|         {getContents(data, false, category, setCategory)} | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| 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; | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user