2817c7e815
Deploy / deploy (push) Successful in 3m7s
Points were normalised to the whole player box, but the video is drawn object-contain inside it, so any change in the player's shape (window or panel resize, fullscreen, phone rotation, the annotation toolbar opening) moved the picture under the marks. The canvas now sits exactly over the displayed picture (same letterbox measurement as AspectOverlay), so points are fractions of the image and new saves are tagged version "2.0". Legacy "1.0" annotations are mapped onto the picture at load time from their stored canvasWidth/canvasHeight and the video's intrinsic size; no data migration. The save action now accepts both versions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
328 lines
8.9 KiB
TypeScript
328 lines
8.9 KiB
TypeScript
import { Role, ApprovalStatus, ReviewStatus, ShotStatus, ShotApprovalStatus, ShotPriority, ProjectStatus, TaskStatus, TaskType } from "@prisma/client";
|
|
|
|
// Re-export Prisma enums for convenience
|
|
export { Role, ApprovalStatus, ReviewStatus, ShotStatus, ShotApprovalStatus, ShotPriority, ProjectStatus, TaskStatus, TaskType };
|
|
|
|
// ── Annotation Types ─────────────────────────────────────────────────────────
|
|
|
|
export type AnnotationTool = "freehand" | "arrow" | "rectangle" | "circle";
|
|
|
|
export interface AnnotationPoint {
|
|
x: number; // normalized 0-1 — v2.0: of the picture; v1.0: of the player box
|
|
y: number; // normalized 0-1 — v2.0: of the picture; v1.0: of the player box
|
|
}
|
|
|
|
export interface AnnotationShape {
|
|
id: string;
|
|
tool: AnnotationTool;
|
|
/** For freehand: all path points. For others: [startPoint, endPoint] */
|
|
points: AnnotationPoint[];
|
|
color: string;
|
|
strokeWidth: number;
|
|
frameNumber: number;
|
|
}
|
|
|
|
export interface AnnotationDrawingData {
|
|
shapes: AnnotationShape[];
|
|
canvasWidth: number;
|
|
canvasHeight: number;
|
|
/**
|
|
* "2.0": points are relative to the displayed picture, so they stay locked to
|
|
* the image at any player size. "1.0" (legacy): relative to the whole player
|
|
* box (picture + letterbox bars) that was canvasWidth x canvasHeight.
|
|
*/
|
|
version: "1.0" | "2.0";
|
|
}
|
|
|
|
// ── Review Player State ──────────────────────────────────────────────────────
|
|
|
|
export interface ReviewPlayerState {
|
|
isPlaying: boolean;
|
|
currentFrame: number;
|
|
currentTime: number;
|
|
duration: number;
|
|
playbackRate: number;
|
|
volume: number;
|
|
isMuted: boolean;
|
|
isFullscreen: boolean;
|
|
isAnnotating: boolean;
|
|
selectedTool: AnnotationTool;
|
|
selectedColor: string;
|
|
strokeWidth: number;
|
|
showAnnotations: boolean;
|
|
fps: number;
|
|
totalFrames: number;
|
|
}
|
|
|
|
// ── Comment Types ────────────────────────────────────────────────────────────
|
|
|
|
export interface CommentWithReplies {
|
|
id: string;
|
|
versionId: string;
|
|
authorId: string;
|
|
frameNumber: number;
|
|
timestamp: number;
|
|
text: string;
|
|
isResolved: boolean;
|
|
createdAt: Date;
|
|
updatedAt: Date;
|
|
author: {
|
|
id: string;
|
|
name: string | null;
|
|
email: string;
|
|
image: string | null;
|
|
role: Role;
|
|
} | null;
|
|
replies: CommentReplyWithAuthor[];
|
|
annotations: AnnotationSummary[];
|
|
}
|
|
|
|
export interface CommentReplyWithAuthor {
|
|
id: string;
|
|
commentId: string;
|
|
authorId: string;
|
|
text: string;
|
|
createdAt: Date;
|
|
author: {
|
|
id: string;
|
|
name: string | null;
|
|
email: string;
|
|
image: string | null;
|
|
role: Role;
|
|
} | null;
|
|
}
|
|
|
|
export interface AnnotationSummary {
|
|
id: string;
|
|
frameNumber: number;
|
|
drawingData: unknown;
|
|
color: string;
|
|
isVisible: boolean;
|
|
authorId: string;
|
|
}
|
|
|
|
// ── Version Types ────────────────────────────────────────────────────────────
|
|
|
|
export interface VersionWithDetails {
|
|
id: string;
|
|
versionNumber: number;
|
|
shotId: string | null;
|
|
taskId: string | null;
|
|
artistId: string | null;
|
|
isClientVisible: boolean;
|
|
sharedAt: Date | null;
|
|
sharedById: string | null;
|
|
fileUrl: string;
|
|
fileName: string;
|
|
fileSize: bigint | null;
|
|
mimeType: string | null;
|
|
thumbnailUrl: string | null;
|
|
posterUrl: string | null;
|
|
proxyUrl: string | null;
|
|
fps: number;
|
|
duration: number | null;
|
|
frameCount: number | null;
|
|
width: number | null;
|
|
height: number | null;
|
|
notes: string | null;
|
|
approvalStatus: ApprovalStatus;
|
|
reviewStatus: ReviewStatus;
|
|
isLatest: boolean;
|
|
createdAt: Date;
|
|
updatedAt: Date;
|
|
artist: {
|
|
id: string;
|
|
name: string | null;
|
|
email: string;
|
|
image: string | null;
|
|
} | null;
|
|
_count?: {
|
|
comments: number;
|
|
};
|
|
approvals?: {
|
|
id: string;
|
|
status: string;
|
|
notes: string | null;
|
|
createdAt: Date;
|
|
user: { id: string; name: string | null; image: string | null };
|
|
}[];
|
|
}
|
|
|
|
// ── Shot Types ───────────────────────────────────────────────────────────────
|
|
|
|
export interface ShotWithDetails {
|
|
id: string;
|
|
shotCode: string;
|
|
/** Client-supplied naming alias — slates/overlays/delivery only, never internal paths. */
|
|
clientCode: string | null;
|
|
scene: string;
|
|
episode: string | null;
|
|
shotNumber: number;
|
|
sequence: string | null;
|
|
description: string | null;
|
|
notes: string | null;
|
|
status: ShotStatus;
|
|
priority: ShotPriority;
|
|
artistId: string | null;
|
|
projectId: string;
|
|
fps: number;
|
|
frameStart: number | null;
|
|
frameEnd: number | null;
|
|
dueDate: Date | null;
|
|
thumbnailUrl: string | null;
|
|
generatedThumbnailUrl: string | null;
|
|
generatedThumbnailFrom: string | null;
|
|
originalFootageUrl: string | null;
|
|
originalFootageKey: string | null;
|
|
shotGroupId: string | null;
|
|
shotApprovalStatus: ShotApprovalStatus;
|
|
sharedWithClient: boolean;
|
|
// EDL / pull CSV fields
|
|
sourceClip: string | null;
|
|
timecodeStart: string | null;
|
|
timecodeEnd: string | null;
|
|
clipDuration: string | null;
|
|
exrOutput: string | null;
|
|
// Sequence / picture lock timecodes
|
|
seqTimecodeStart: string | null;
|
|
seqTimecodeEnd: string | null;
|
|
// High-res deliverable
|
|
highResFilename: string | null;
|
|
// Shot-level version tracking
|
|
shotVersion: string;
|
|
// Netflix slate fields
|
|
slateType: string | null;
|
|
slateDescription: string | null;
|
|
slateVfxScope: string | null;
|
|
slateSubmissionNote: string | null;
|
|
shotGroup: { id: string; name: string } | null;
|
|
footagePlates: {
|
|
id: string;
|
|
label: string;
|
|
fileUrl: string;
|
|
fileKey: string;
|
|
fileName: string;
|
|
fileSize: bigint | string | null;
|
|
sortOrder: number;
|
|
createdAt: Date;
|
|
}[];
|
|
createdAt: Date;
|
|
updatedAt: Date;
|
|
artist: {
|
|
id: string;
|
|
name: string | null;
|
|
email: string;
|
|
image: string | null;
|
|
} | null;
|
|
versions: VersionWithDetails[];
|
|
_count?: {
|
|
versions: number;
|
|
};
|
|
}
|
|
|
|
// ── Task Types ───────────────────────────────────────────────────────────────
|
|
|
|
export interface TaskWithDetails {
|
|
id: string;
|
|
title: string;
|
|
description: string | null;
|
|
type: TaskType;
|
|
status: TaskStatus;
|
|
priority: ShotPriority;
|
|
dueDate: Date | null;
|
|
estimatedHours: number | null;
|
|
sortOrder: number;
|
|
shotId: string | null;
|
|
assetId: string | null;
|
|
assignedArtistId: string | null;
|
|
createdById: string;
|
|
projectId: string;
|
|
createdAt: Date;
|
|
updatedAt: Date;
|
|
assignedArtist: {
|
|
id: string;
|
|
name: string | null;
|
|
email: string;
|
|
image: string | null;
|
|
} | null;
|
|
createdBy: {
|
|
id: string;
|
|
name: string | null;
|
|
email: string;
|
|
};
|
|
_count?: {
|
|
versions: number;
|
|
};
|
|
}
|
|
|
|
export interface AssetWithDetails {
|
|
id: string;
|
|
projectId: string;
|
|
assetCode: string;
|
|
name: string;
|
|
description: string | null;
|
|
status: ShotStatus;
|
|
priority: ShotPriority;
|
|
leadId: string | null;
|
|
dueDate: Date | null;
|
|
createdAt: Date;
|
|
updatedAt: Date;
|
|
lead: {
|
|
id: string;
|
|
name: string | null;
|
|
email: string;
|
|
image: string | null;
|
|
} | null;
|
|
tasks: TaskWithDetails[];
|
|
_count?: {
|
|
tasks: number;
|
|
};
|
|
}
|
|
|
|
// ── Dashboard Types ──────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Dashboard headline numbers. Every one is derived from pipeline state and
|
|
* scoped to live productions — see lib/dashboard/ops.ts. The previous shape
|
|
* carried task due/overdue counts, which read a column nothing writes.
|
|
*/
|
|
export interface DashboardStats {
|
|
blocked: number;
|
|
inQc: number;
|
|
readyToDeliver: number;
|
|
awaitingClientReview: number;
|
|
activeProjects: number;
|
|
}
|
|
|
|
// ── Notification Badge ────────────────────────────────────────────────────────
|
|
|
|
export interface NotificationItem {
|
|
id: string;
|
|
type: string;
|
|
title: string;
|
|
message: string;
|
|
data: Record<string, unknown> | null;
|
|
isRead: boolean;
|
|
createdAt: Date;
|
|
}
|
|
|
|
// ── Next-Auth session extension ──────────────────────────────────────────────
|
|
|
|
declare module "next-auth" {
|
|
interface Session {
|
|
user: {
|
|
id: string;
|
|
role: Role;
|
|
mustChangePassword: boolean;
|
|
name?: string | null;
|
|
email?: string | null;
|
|
image?: string | null;
|
|
};
|
|
}
|
|
|
|
interface User {
|
|
role: Role;
|
|
mustChangePassword?: boolean;
|
|
}
|
|
}
|