/ src / components / DreamSong.jsx
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              &#8249;
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              &#8250;
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);