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; | ||||||
|  | } | ||||||
|  | ?>
 | ||||||
|  | 
 | ||||||
							
								
								
									
										81
									
								
								src/App.css
									
									
									
									
									
								
							
							
						
						
									
										81
									
								
								src/App.css
									
									
									
									
									
								
							| @ -1,38 +1,65 @@ | |||||||
| .App { | body { | ||||||
|   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; |  | ||||||
|   min-height: 100vh; |   min-height: 100vh; | ||||||
|  |   background-color: #282c34; | ||||||
|  |   color: #B9B8D6; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .Header { | ||||||
|  |   display: flex; | ||||||
|  |   flex-direction: row; | ||||||
|  | 
 | ||||||
|  |   margin: 1em; | ||||||
|  |   padding: 0; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .Contents { | ||||||
|   display: flex; |   display: flex; | ||||||
|   flex-direction: column; |   flex-direction: column; | ||||||
|   align-items: center; |   align-items: center; | ||||||
|   justify-content: center; |   white-space: pre-wrap; | ||||||
|   font-size: calc(10px + 2vmin); |  | ||||||
|   color: white; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .App-link { | .GalleryItem { | ||||||
|   color: #61dafb; |   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 { | .GalleryItem > p { | ||||||
|   from { |   margin: 0.5em; | ||||||
|     transform: rotate(0deg); |  | ||||||
| } | } | ||||||
|   to { | 
 | ||||||
|     transform: rotate(360deg); | .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 React from 'react'; | ||||||
| import logo from './logo.svg'; | //import logo from './logo.svg';
 | ||||||
| import './App.css'; | import './App.css'; | ||||||
|  | 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 ( | ||||||
|     <div className="App"> |     <div className="App"> | ||||||
|       <header className="App-header"> |       <div className="Header"> | ||||||
|         <img src={logo} className="App-logo" alt="logo" /> |         {getContents(data, true, category, setCategory)} | ||||||
|         <p> |       </div> | ||||||
|           Edit <code>src/App.tsx</code> and save to reload. |       <div className="Contents"> | ||||||
|         </p> |         {getContents(data, false, category, setCategory)} | ||||||
|         <a |       </div> | ||||||
|           className="App-link" |  | ||||||
|           href="https://reactjs.org" |  | ||||||
|           target="_blank" |  | ||||||
|           rel="noopener noreferrer" |  | ||||||
|         > |  | ||||||
|           Learn React |  | ||||||
|         </a> |  | ||||||
|       </header> |  | ||||||
|     </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; | export default App; | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user