Sfoglia il codice sorgente

fix: keyboard shortcuts, compare layout, annotation visibility, stroke/arrow size

- Arrow keys: ±1 frame (stepFrame), Shift+Arrow: ±1s seek
- Compare mode: comment panel hidden, comments moved below each video
- rVFC closure bug: update visibleAnnotationsRef AFTER redrawAnnotationsRef
  assignment so compare player's annotations always use latest array
- Stroke lineWidth: 3 → 4.5 (1.5×), arrow headLen: 16 → 24
- Avatar: add xs size for inline compare comment lists
Claude Dev 1 mese fa
parent
commit
588d8d906e

+ 79 - 9
src/app/review/[assetId]/page.tsx

@@ -591,8 +591,8 @@ export default function ReviewPage() {
           {/* ── Side-by-side compare layout ───────────────────────── */}
           {compareMode ? (
             <div className="flex gap-2 w-full">
-              {/* Main video */}
-              <div className="flex-1 min-w-0">
+              {/* Main video + its comments */}
+              <div className="flex-1 min-w-0 flex flex-col gap-0">
                 <div className="text-xs mb-1 px-1 truncate" style={{ color: 'rgba(255,255,255,0.5)' }}>
                   {asset.title}
                 </div>
@@ -617,11 +617,47 @@ export default function ReviewPage() {
                   externalCurrentTime={currentTime}
                   externalPlaying={playing}
                 />
+                {/* Comments below main video */}
+                <div className="mt-2 rounded-xl overflow-hidden" style={{ background: 'rgba(10,11,20,0.80)', border: '1px solid rgba(255,255,255,0.06)' }}>
+                  <div className="px-3 py-2 flex items-center gap-2" style={{ borderBottom: '1px solid rgba(255,255,255,0.06)' }}>
+                    <span className="text-xs font-medium" style={{ color: 'var(--text)' }}>
+                      Comments
+                    </span>
+                    <span className="text-xs px-1.5 py-0.5 rounded-full" style={{ background: 'rgba(255,255,255,0.06)', color: 'var(--text-muted)' }}>
+                      {visibleComments.length}
+                    </span>
+                    <span className="font-mono text-[11px] ml-auto" style={{ color: '#818CF8' }}>
+                      {formatTimecode(currentTime, fps, asset?.duration ?? 0)}
+                    </span>
+                  </div>
+                  <div className="max-h-32 overflow-y-auto">
+                    {visibleComments.length === 0 ? (
+                      <p className="text-xs text-center py-4" style={{ color: 'var(--text-muted)' }}>No comments</p>
+                    ) : (
+                      visibleComments.slice(0, 5).map(comment => (
+                        <div key={comment.id} className="px-3 py-2 flex items-start gap-2" style={{ borderBottom: '1px solid rgba(255,255,255,0.04)' }}>
+                          <Avatar name={comment.user?.name ?? 'U'} size="xs" />
+                          <div className="flex-1 min-w-0">
+                            <div className="flex items-center gap-1.5 mb-0.5">
+                              <span className="text-[11px] font-medium" style={{ color: 'var(--text)' }}>{comment.user?.name ?? 'Unknown'}</span>
+                              {comment.timestamp != null && (
+                                <span className="text-[10px] font-mono px-1 rounded" style={{ background: 'rgba(99,102,241,0.10)', color: '#818CF8' }}>
+                                  {formatTimecode(comment.timestamp, fps, asset?.duration ?? 0)}
+                                </span>
+                              )}
+                            </div>
+                            <p className="text-[11px] truncate" style={{ color: 'var(--text-muted)' }}>{comment.content}</p>
+                          </div>
+                        </div>
+                      ))
+                    )}
+                  </div>
+                </div>
               </div>
 
-              {/* Compare video — only show when durations match */}
+              {/* Compare video + its comments — only show when durations match */}
               {compareAsset && !compareMismatch && (
-                <div className="flex-1 min-w-0">
+                <div className="flex-1 min-w-0 flex flex-col gap-0">
                   <div className="text-xs mb-1 px-1 truncate" style={{ color: 'rgba(255,255,255,0.5)' }}>
                     {compareAsset.title}
                   </div>
@@ -645,6 +681,39 @@ export default function ReviewPage() {
                     externalCurrentTime={currentTime}
                     externalPlaying={playing}
                   />
+                  {/* Comments below compare video */}
+                  <div className="mt-2 rounded-xl overflow-hidden" style={{ background: 'rgba(10,11,20,0.80)', border: '1px solid rgba(255,255,255,0.06)' }}>
+                    <div className="px-3 py-2 flex items-center gap-2" style={{ borderBottom: '1px solid rgba(255,255,255,0.06)' }}>
+                      <span className="text-xs font-medium" style={{ color: 'var(--text)' }}>
+                        Comments
+                      </span>
+                      <span className="text-xs px-1.5 py-0.5 rounded-full" style={{ background: 'rgba(255,255,255,0.06)', color: 'var(--text-muted)' }}>
+                        {compareVisibleComments.length}
+                      </span>
+                    </div>
+                    <div className="max-h-32 overflow-y-auto">
+                      {compareVisibleComments.length === 0 ? (
+                        <p className="text-xs text-center py-4" style={{ color: 'var(--text-muted)' }}>No comments</p>
+                      ) : (
+                        compareVisibleComments.slice(0, 5).map(comment => (
+                          <div key={comment.id} className="px-3 py-2 flex items-start gap-2" style={{ borderBottom: '1px solid rgba(255,255,255,0.04)' }}>
+                            <Avatar name={comment.user?.name ?? 'U'} size="xs" />
+                            <div className="flex-1 min-w-0">
+                              <div className="flex items-center gap-1.5 mb-0.5">
+                                <span className="text-[11px] font-medium" style={{ color: 'var(--text)' }}>{comment.user?.name ?? 'Unknown'}</span>
+                                {comment.timestamp != null && (
+                                  <span className="text-[10px] font-mono px-1 rounded" style={{ background: 'rgba(99,102,241,0.10)', color: '#818CF8' }}>
+                                    {formatTimecode(comment.timestamp, fps, asset?.duration ?? 0)}
+                                  </span>
+                                )}
+                              </div>
+                              <p className="text-[11px] truncate" style={{ color: 'var(--text-muted)' }}>{comment.content}</p>
+                            </div>
+                          </div>
+                        ))
+                      )}
+                    </div>
+                  </div>
                 </div>
               )}
             </div>
@@ -758,8 +827,7 @@ export default function ReviewPage() {
           {!compareMode && (
           <div className="flex flex-wrap gap-3 text-xs shrink-0 hidden sm:flex" style={{ color: 'var(--text-subtle)' }}>
             <span><kbd className="px-1.5 py-0.5 rounded text-[10px]" style={{ background: 'rgba(255,255,255,0.06)' }}>Space</kbd> play/pause</span>
-            <span><kbd className="px-1.5 py-0.5 rounded text-[10px]" style={{ background: 'rgba(255,255,255,0.06)' }}>←</kbd><kbd className="px-1.5 py-0.5 rounded text-[10px] ml-0.5" style={{ background: 'rgba(255,255,255,0.06)' }}>→</kbd> seek ±5s</span>
-            <span><kbd className="px-1.5 py-0.5 rounded text-[10px]" style={{ background: 'rgba(255,255,255,0.06)' }}>U</kbd><kbd className="px-1.5 py-0.5 rounded text-[10px] ml-0.5" style={{ background: 'rgba(255,255,255,0.06)' }}>I</kbd> frame</span>
+            <span><kbd className="px-1.5 py-0.5 rounded text-[10px]" style={{ background: 'rgba(255,255,255,0.06)' }}>←</kbd><kbd className="px-1.5 py-0.5 rounded text-[10px] ml-0.5" style={{ background: 'rgba(255,255,255,0.06)' }}>→</kbd> ±1 frame <kbd className="px-1.5 py-0.5 rounded text-[10px]" style={{ background: 'rgba(255,255,255,0.06)' }}>⇧←</kbd><kbd className="px-1.5 py-0.5 rounded text-[10px] ml-0.5" style={{ background: 'rgba(255,255,255,0.06)' }}>⇧→</kbd> ±1s</span>
             <span><kbd className="px-1.5 py-0.5 rounded text-[10px]" style={{ background: 'rgba(255,255,255,0.06)' }}>C</kbd> draw mode</span>
             <span><kbd className="px-1.5 py-0.5 rounded text-[10px]" style={{ background: 'rgba(255,255,255,0.06)' }}>Esc</kbd> exit draw</span>
             <span className="font-mono text-[11px]">{formatTimecode(currentTime, fps, asset?.duration ?? 0)}</span>
@@ -767,12 +835,13 @@ export default function ReviewPage() {
           )}
         </div>
 
-        {/* Resize handle — only shown in landscape */}
-        {!isPortrait && (
+        {/* Resize handle — only shown in landscape, hidden in compare mode */}
+        {!isPortrait && !compareMode && (
           <div className="resize-handle" onMouseDown={handleResizeStart} style={{ width: '4px' }} />
         )}
 
-        {/* ── Comment panel ─────────────────────────────────── */}
+        {/* ── Comment panel — hidden in compare mode (comments are below each video) ── */}
+        {!compareMode && (
         <div
           ref={panelRef}
           className="flex flex-col overflow-hidden shrink-0"
@@ -969,6 +1038,7 @@ export default function ReviewPage() {
             </form>
           </div>
         </div>
+        )}
       </div>
     </div>
   );

+ 2 - 1
src/components/ui/avatar.tsx

@@ -3,12 +3,13 @@ import React from 'react';
 interface AvatarProps {
   name: string;
   src?: string | null;
-  size?: 'sm' | 'md' | 'lg';
+  size?: 'xs' | 'sm' | 'md' | 'lg';
   className?: string;
   style?: React.CSSProperties;
 }
 
 const sizes: Record<string, string> = {
+  xs: 'w-5 h-5 text-[9px]',
   sm: 'w-6 h-6 text-[10px]',
   md: 'w-8 h-8 text-xs',
   lg: 'w-10 h-10 text-sm',

+ 2 - 2
src/components/video-player/AnnotationCanvas.tsx

@@ -20,7 +20,7 @@ export function drawShape(ctx: CanvasRenderingContext2D, ann: AnnotationData) {
   ctx.save();
   ctx.strokeStyle = ann.color;
   ctx.fillStyle = ann.color;
-  ctx.lineWidth = 3;
+  ctx.lineWidth = 4.5; // 1.5× base 3
   ctx.lineCap = 'round';
   ctx.lineJoin = 'round';
 
@@ -37,7 +37,7 @@ export function drawShape(ctx: CanvasRenderingContext2D, ann: AnnotationData) {
     const sx = x1 * ctx.canvas.width, sy = y1 * ctx.canvas.height;
     const ex = x2 * ctx.canvas.width, ey = y2 * ctx.canvas.height;
     const angle = Math.atan2(ey - sy, ex - sx);
-    const headLen = 16;
+    const headLen = 24; // 1.5× original 16
     ctx.beginPath();
     ctx.moveTo(sx, sy); ctx.lineTo(ex, ey); ctx.stroke();
     ctx.beginPath();

+ 13 - 5
src/components/video-player/VideoPlayer.tsx

@@ -112,8 +112,10 @@ export function VideoPlayer({
   const visibleAnnotationsRef = useRef(visibleAnnotations);
   const drawModeRef = useRef(drawMode);
   fpsRef.current = fps;
-  visibleAnnotationsRef.current = visibleAnnotations;
   drawModeRef.current = drawMode;
+  // Update annotation ref AFTER redrawAnnotationsRef so the closure inside
+  // redrawAnnotations always sees the latest array (not the previous render's stale value).
+  visibleAnnotationsRef.current = visibleAnnotations;
 
   // ── Frame-step debounce ──────────────────────────────────────────────────
   // Prevent rapid keypresses from piling up seeks before they complete.
@@ -258,13 +260,19 @@ export function VideoPlayer({
       const video = videoRef.current;
       if (!video || e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return;
       if (e.code === 'Space') { e.preventDefault(); video.paused ? video.play() : video.pause(); }
-      if (e.code === 'ArrowLeft') { e.preventDefault(); video.currentTime = Math.max(0, video.currentTime - 5); }
-      if (e.code === 'ArrowRight') { e.preventDefault(); video.currentTime = Math.min(duration, video.currentTime + 5); }
+      if (e.code === 'ArrowLeft') {
+        e.preventDefault();
+        if (e.shiftKey) { video.currentTime = Math.max(0, video.currentTime - 1); }
+        else { stepFrame(-1); }
+      }
+      if (e.code === 'ArrowRight') {
+        e.preventDefault();
+        if (e.shiftKey) { video.currentTime = Math.min(duration, video.currentTime + 1); }
+        else { stepFrame(1); }
+      }
       if (e.code === 'KeyC') { e.preventDefault(); onDrawModeChange(!drawMode); }
       if (e.code === 'KeyF') { e.preventDefault(); toggleFullscreen(); }
       if (e.code === 'KeyM') { e.preventDefault(); toggleMute(); }
-      if (e.code === 'KeyU') { e.preventDefault(); stepFrame(-1); }
-      if (e.code === 'KeyI') { e.preventDefault(); stepFrame(1); }
       if (e.code === 'Escape' && drawMode) { e.preventDefault(); onDrawModeChange(false); }
     };
     window.addEventListener('keydown', handleKey);