DreamSong.jsx
1 import React, { useState, useEffect, useCallback } from 'react'; 2 import { BLACK, WHITE, BLUE } from '../constants/colors'; 3 import { readDreamSongCanvas, listFiles } from '../utils/fileUtils'; 4 import { processDreamSongData } from '../utils/dreamSongUtils'; 5 import FileContextMenu from './FileContextMenu'; 6 import DisplayContent from './DisplayContent'; 7 8 const DreamSong = ({ repoName, dreamSongMedia, onClick, onRightClick, onFileRightClick, onMouseEnter, onMouseLeave, borderColor, onFlip }) => { 9 const [processedNodes, setProcessedNodes] = useState([]); 10 const [files, setFiles] = useState([]); 11 const [showDreamSong, setShowDreamSong] = useState(true); 12 const [contextMenu, setContextMenu] = useState(null); 13 const [circlePackingData, setCirclePackingData] = useState(null); 14 15 useEffect(() => { 16 const fetchData = async () => { 17 const canvasData = await readDreamSongCanvas(repoName); 18 const fileList = await listFiles(repoName); 19 setFiles(fileList); 20 21 if (canvasData) { 22 const processed = processDreamSongData(canvasData); 23 setProcessedNodes(processed); 24 } else { 25 setProcessedNodes([]); 26 setShowDreamSong(false); 27 } 28 }; 29 30 fetchData(); 31 }, [repoName]); 32 33 useEffect(() => { 34 // Prepare data for circle packing 35 const prepareCirclePackingData = () => { 36 return { 37 name: "root", 38 children: files.map(file => ({ name: file, value: 1 })) 39 }; 40 }; 41 42 if (files.length > 0) { 43 setCirclePackingData(prepareCirclePackingData()); 44 } 45 }, [files]); 46 47 const handleMediaClick = (event) => { 48 event.stopPropagation(); 49 const mediaFile = event.target.alt; 50 51 if (typeof mediaFile === 'string') { 52 const pathParts = mediaFile.split('/'); 53 let targetRepo; 54 55 if (pathParts.length === 2) { 56 targetRepo = repoName; 57 } else if (pathParts.length > 2) { 58 targetRepo = pathParts[pathParts.length - 2]; 59 } 60 61 if (targetRepo) { 62 onClick(targetRepo); 63 } 64 } 65 }; 66 67 const renderMediaElement = (file, index) => { 68 const mediaItem = dreamSongMedia.find(item => item.filePath === file); 69 if (!mediaItem) return null; 70 71 const isVideo = /\.(mp4|webm|ogg)$/i.test(file); 72 if (isVideo) { 73 return ( 74 <video 75 key={`video-${index}`} 76 src={`data:${mediaItem.mimeType};base64,${mediaItem.data}`} 77 style={{ maxWidth: '100%', height: 'auto' }} 78 controls 79 onClick={handleMediaClick} 80 /> 81 ); 82 } else { 83 return ( 84 <img 85 key={`img-${index}`} 86 src={`data:${mediaItem.mimeType};base64,${mediaItem.data}`} 87 alt={file} 88 style={{ maxWidth: '100%', height: 'auto' }} 89 onClick={handleMediaClick} 90 /> 91 ); 92 } 93 }; 94 95 const renderNode = (node, index) => { 96 if (node.type === 'file') { 97 return renderMediaElement(node.file, `file-${index}`); 98 } else if (node.type === 'text') { 99 return <div key={`text-${index}`} dangerouslySetInnerHTML={{ __html: node.text }} />; 100 } 101 return null; 102 }; 103 104 const toggleView = () => { 105 setShowDreamSong(!showDreamSong); 106 }; 107 108 const handleFileRightClick = useCallback((event, file) => { 109 event.preventDefault(); 110 event.stopPropagation(); 111 console.log('Right-click detected on file:', file); 112 setContextMenu({ 113 x: event.clientX, 114 y: event.clientY, 115 file: file 116 }); 117 }, []); 118 119 const handleCloseContextMenu = useCallback(() => { 120 setContextMenu(null); 121 }, []); 122 123 return ( 124 <div 125 className="dream-song" 126 style={{ 127 position: 'relative', 128 width: '100%', 129 height: '100%', 130 overflow: 'hidden', 131 borderRadius: '50%', 132 border: `5px solid ${borderColor || BLUE}`, 133 backgroundColor: BLACK, 134 color: WHITE, 135 boxSizing: 'border-box', 136 }} 137 onClick={onClick} 138 onContextMenu={(e) => { 139 if (!e.defaultPrevented) { 140 e.preventDefault(); 141 onRightClick(e); 142 } 143 }} 144 onMouseEnter={onMouseEnter} 145 onMouseLeave={onMouseLeave} 146 > 147 <div 148 className="dream-song-content" 149 style={{ 150 position: 'absolute', 151 top: 0, 152 left: '50%', 153 transform: 'translateX(-50%)', 154 width: '80%', 155 height: '100%', 156 overflowY: 'auto', 157 overflowX: 'hidden', 158 scrollbarWidth: 'none', // Firefox 159 msOverflowStyle: 'none', // Internet Explorer 10+ 160 padding: '16px', 161 boxSizing: 'border-box', 162 display: 'flex', 163 flexDirection: 'column', 164 alignItems: 'center', 165 gap: '16px', 166 }} 167 > 168 <style> 169 {` 170 .dream-song-content::-webkit-scrollbar { 171 display: none; 172 } 173 `} 174 </style> 175 <div style={{ width: '100%', maxWidth: '800px', overflowY: 'auto', maxHeight: '100%' }}> 176 {showDreamSong && processedNodes.length > 0 ? ( 177 processedNodes.map((node, index) => renderNode(node, index)) 178 ) : circlePackingData ? ( 179 <DisplayContent 180 data={circlePackingData} 181 onCircleClick={(file) => window.electron.fileSystem.openFile(repoName, file)} 182 /> 183 ) : ( 184 <p>Loading...</p> 185 )} 186 </div> 187 </div> 188 <div style={{ 189 position: 'absolute', 190 top: 0, 191 left: 0, 192 width: '100%', 193 height: '100%', 194 background: 'radial-gradient(circle, rgba(0,0,0,0) 50%, rgba(0,0,0,0.7) 60%, rgba(0,0,0,1) 70%)', 195 pointerEvents: 'none', 196 borderRadius: '50%', 197 }} /> 198 <div 199 style={{ 200 position: 'absolute', 201 bottom: '10px', 202 left: '50%', 203 transform: 'translateX(-50%)', 204 opacity: 0, 205 transition: 'opacity 0.3s ease', 206 }} 207 className="flip-button-container" 208 > 209 <button 210 onClick={(e) => { 211 e.stopPropagation(); 212 onFlip(); 213 }} 214 style={{ 215 background: BLUE, 216 color: WHITE, 217 border: 'none', 218 borderRadius: '5px', 219 padding: '5px 10px', 220 cursor: 'pointer', 221 }} 222 > 223 Flip 224 </button> 225 </div> 226 {processedNodes.length > 0 && ( 227 <> 228 <button 229 onClick={(e) => { 230 e.stopPropagation(); 231 toggleView(); 232 }} 233 style={{ 234 position: 'absolute', 235 left: '10px', 236 top: '50%', 237 transform: 'translateY(-50%)', 238 background: 'rgba(0, 0, 0, 0.5)', 239 color: WHITE, 240 border: 'none', 241 borderRadius: '50%', 242 width: '30px', 243 height: '30px', 244 fontSize: '20px', 245 cursor: 'pointer', 246 display: 'flex', 247 justifyContent: 'center', 248 alignItems: 'center', 249 }} 250 > 251 ‹ 252 </button> 253 <button 254 onClick={(e) => { 255 e.stopPropagation(); 256 toggleView(); 257 }} 258 style={{ 259 position: 'absolute', 260 right: '10px', 261 top: '50%', 262 transform: 'translateY(-50%)', 263 background: 'rgba(0, 0, 0, 0.5)', 264 color: WHITE, 265 border: 'none', 266 borderRadius: '50%', 267 width: '30px', 268 height: '30px', 269 fontSize: '20px', 270 cursor: 'pointer', 271 display: 'flex', 272 justifyContent: 'center', 273 alignItems: 'center', 274 }} 275 > 276 › 277 </button> 278 </> 279 )} 280 <style> 281 {` 282 .dream-song:hover .flip-button-container { 283 opacity: 1; 284 } 285 `} 286 </style> 287 {contextMenu && ( 288 <FileContextMenu 289 x={contextMenu.x} 290 y={contextMenu.y} 291 file={contextMenu.file} 292 repoName={repoName} 293 onClose={handleCloseContextMenu} 294 onProcessFile={onFileRightClick} 295 /> 296 )} 297 </div> 298 ); 299 }; 300 301 export default React.memo(DreamSong);