Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion .github/workflows/deploy-staging.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -103,10 +103,21 @@ jobs:
with:
platform: windows

- name: Compute Windows Store version
id: msix-version
shell: bash
run: |
SHORT="${{ needs.prepare.outputs.short_version }}"
MAJOR=$(echo "$SHORT" | cut -d. -f1)
MINOR=$(echo "$SHORT" | cut -d. -f2)
PATCH=$(echo "$SHORT" | cut -d. -f3)
BUILD=$(( PATCH * 1000 + ${{ needs.prepare.outputs.revision }} ))
echo "version=${MAJOR}.${MINOR}.${BUILD}.0" >> $GITHUB_OUTPUT

- name: Apply app version
uses: ./.github/actions/apply-version
with:
version: ${{ needs.prepare.outputs.version }}
version: ${{ steps.msix-version.outputs.version }}
staging: "true"

- name: Build MSIX bundle
Expand Down
15 changes: 11 additions & 4 deletions components/analytics/stats/CharactersStats.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,18 +63,25 @@ const barOpts = {

// ── Dialogue counting ─────────────────────────────────────────────────────────

function countDialoguePerCharacter(screenplay: any[]): Record<string, number> {
type ScreenplayNode = {
type?: string;
attrs?: Record<string, unknown>;
content?: ScreenplayNode[];
text?: string;
};

function countDialoguePerCharacter(screenplay: ScreenplayNode[]): Record<string, number> {
const counts: Record<string, number> = {};
let currentChar: string | null = null;

for (const node of screenplay) {
const nodeType: string = node.attrs?.["class"] ?? node.type ?? "";
const nodeType: string = (node.attrs?.["class"] as string) ?? node.type ?? "";

if (nodeType === ScreenplayElement.Character) {
// Extract plain text from the node's content array
const text = (node.content ?? [])
.flatMap((c: any) => c.content ?? [c])
.map((c: any) => c.text ?? "")
.flatMap((c) => c.content ?? [c])
.map((c) => c.text ?? "")
.join("")
.toUpperCase()
.replace(/\(.*?\)/g, "") // strip parentheticals like (V.O.)
Expand Down
3 changes: 1 addition & 2 deletions components/analytics/stats/LocationsStats.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -123,14 +123,13 @@ export default function LocationsStats() {
}
}

const totalUnique = topLocations.length; // distinct named locations in top 10
const totalAll = Object.keys(locationCount).filter((k) => k !== "UNKNOWN").length;

return { topLocations, overallSettings, totalAll };
}, [scenes]);

if (stats.totalAll === 0) {
return <p className={styles.empty}>No locations found. Scene headings like "INT. OFFICE - DAY" will appear here.</p>;
return <p className={styles.empty}>{'No locations found. Scene headings like "INT. OFFICE - DAY" will appear here.'}</p>;
}

// ── Bar chart — top locations ───────────────────────────────────────────────
Expand Down
20 changes: 11 additions & 9 deletions components/board/BoardCanvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,15 @@ const BoardCanvas = ({ isVisible }: { isVisible: boolean }) => {
const [isSnapping, setIsSnapping] = useState(true);
const [cardContextMenu, setCardContextMenu] = useState<CardContextMenuState | null>(null);
const [arrowContextMenu, setArrowContextMenu] = useState<ArrowContextMenuState | null>(null);
const [prevIsVisible, setPrevIsVisible] = useState(isVisible);
if (prevIsVisible !== isVisible) {
setPrevIsVisible(isVisible);
if (!isVisible) {
setIsSnapping(true);
if (cardContextMenu) setCardContextMenu(null);
if (arrowContextMenu) setArrowContextMenu(null);
}
}
const [isCameraReady, setIsCameraReady] = useState(false);
const [connectingFrom, setConnectingFrom] = useState<{ cardId: string; side: string } | null>(null);
const [connectingLine, setConnectingLine] = useState<{ x: number; y: number } | null>(null);
Expand Down Expand Up @@ -191,10 +200,7 @@ const BoardCanvas = ({ isVisible }: { isVisible: boolean }) => {

// Handle keyboard events for snapping
useEffect(() => {
if (!isVisible) {
setIsSnapping(true); // Reset snap state when hidden
return;
}
if (!isVisible) return;

const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Shift") {
Expand Down Expand Up @@ -222,11 +228,7 @@ const BoardCanvas = ({ isVisible }: { isVisible: boolean }) => {

// Close context menus on click anywhere
useEffect(() => {
if (!isVisible) {
if (cardContextMenu) setCardContextMenu(null);
if (arrowContextMenu) setArrowContextMenu(null);
return;
}
if (!isVisible) return;

const handleClick = () => {
if (cardContextMenu) setCardContextMenu(null);
Expand Down
10 changes: 8 additions & 2 deletions components/board/BoardCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,19 @@ const BoardCard = ({
const [isEditingTitle, setIsEditingTitle] = useState(false);
const [localTitle, setLocalTitle] = useState(card.title);
const [localDescription, setLocalDescription] = useState(card.description);
const [prevTitle, setPrevTitle] = useState(card.title);
const [prevDescription, setPrevDescription] = useState(card.description);
const dragOffset = useRef({ x: 0, y: 0 });
const resizeStart = useRef({ x: 0, y: 0, width: 0, height: 0 });

useEffect(() => {
if (prevTitle !== card.title) {
setPrevTitle(card.title);
setLocalTitle(card.title);
}
if (prevDescription !== card.description) {
setPrevDescription(card.description);
setLocalDescription(card.description);
}, [card.title, card.description]);
}

const snapToGrid = useCallback(
(value: number) => {
Expand Down
14 changes: 7 additions & 7 deletions components/dashboard/DashboardModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,13 @@ import { DashboardContext } from "@src/context/DashboardContext";
import { ProjectContext } from "@src/context/ProjectContext";
import { useCookieUser } from "@src/lib/utils/hooks";

import CloseSVG from "@public/images/close.svg";

import SidebarMenu, { MenuSection } from "./DashboardSidebar";
import ProjectSettings from "./project/ProjectSettings";
import CollaboratorsSettings from "./project/CollaboratorsSettings";

import styles from "./DashboardModal.module.css";
import ExportProject from "./project/ExportProject";
import { CreditCard, FileDown, Folder, Globe, Keyboard, Palette, PanelsTopLeft, User, Users } from "lucide-react";
import { CreditCard, FileDown, Folder, Globe, Keyboard, Palette, PanelsTopLeft, User, Users, X } from "lucide-react";
import { useTranslations } from "next-intl";
import KeybindsSettings from "./preferences/KeybindsSettings";
import AppearanceSettings from "./preferences/AppearanceSettings";
Expand Down Expand Up @@ -84,11 +82,13 @@ const DashboardModal = () => {
if (isSignedIn && activeTab === "Auth") {
setActiveTab("Profile");
}
}, [isInProject, isSignedIn, activeTab, setActiveTab]);
}, [isInProject, isSignedIn, activeTab, setActiveTab, ACCOUNT_MENU, PREFERENCES_MENU, PROJECT_MENU]);

useEffect(() => {
const [prevActiveTab, setPrevActiveTab] = useState(activeTab);
if (prevActiveTab !== activeTab) {
setPrevActiveTab(activeTab);
setDangerOpen(false);
}, [activeTab]);
}

useEffect(() => {
const handleEsc = (e: KeyboardEvent) => {
Expand All @@ -108,7 +108,7 @@ const DashboardModal = () => {
<div className={styles.content}>
<header className={styles.contentHeader}>
<h3>{t(`tabs.${activeTab}` as Parameters<typeof t>[0])}</h3>
<CloseSVG className={styles.close_btn} onClick={closeDashboard} />
<X className={styles.close_btn} onClick={closeDashboard} />
</header>

<div className={styles.scrollArea}>
Expand Down
17 changes: 7 additions & 10 deletions components/dashboard/preferences/KeybindsSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,19 +84,16 @@ const KeybindsSettings = () => {
const { settings, saveSettings } = useSettings();

const t = useTranslations("keybinds");
const [userKeybinds, setUserKeybinds] = useState<UserKeybindsMap>({});
const [userKeybinds, setUserKeybinds] = useState<UserKeybindsMap>(settings?.keybinds ?? {});
const [listeningFor, setListeningFor] = useState<string | null>(null);
const [tempCombo, setTempCombo] = useState<string | null>(null);
const [hasUpdatedKeybinds, setHasUpdatedKeybinds] = useState(false);
const tinykeysStopRef = useRef<(() => void) | null>(null);

useEffect(() => {
if (settings && settings.keybinds) {
setUserKeybinds(settings.keybinds);
} else {
setUserKeybinds({});
}
}, [settings]);
const [prevSettings, setPrevSettings] = useState(settings);
if (prevSettings !== settings) {
setPrevSettings(settings);
setUserKeybinds(settings?.keybinds ?? {});
}

useEffect(() => {
if (tinykeysStopRef.current) {
Expand Down Expand Up @@ -185,7 +182,7 @@ const KeybindsSettings = () => {
window.removeEventListener("keydown", onKeyDown);
window.removeEventListener("keydown", onCancel);
};
}, [listeningFor]);
}, [listeningFor, saveSettings, t]);

const startListening = (id: string) => {
setListeningFor(id);
Expand Down
10 changes: 5 additions & 5 deletions components/dashboard/preferences/LanguageSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,7 @@ const LanguageSettings = () => {

// Sync word list from Yjs map and observe live changes
useEffect(() => {
if (!dictMap) {
setCustomWords([]);
return;
}
if (!dictMap) return;

const sync = () => {
const words: string[] = [];
Expand All @@ -54,7 +51,10 @@ const LanguageSettings = () => {

sync();
dictMap.observe(sync);
return () => dictMap.unobserve(sync);
return () => {
dictMap.unobserve(sync);
setCustomWords([]);
};
}, [dictMap]);

const handleAddWord = useCallback(() => {
Expand Down
9 changes: 7 additions & 2 deletions components/dashboard/project/CollaboratorsSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import * as Roles from "@src/lib/utils/roles";
import { ApiResponse } from "@src/lib/utils/api-utils";
import { DashboardContext } from "@src/context/DashboardContext";
import { redirect } from "next/navigation";
import Link from "next/link";

const MAX_COLLABORATORS = 5;

Expand All @@ -34,7 +35,11 @@ const CollaboratorsSettings = () => {
const owner = collaborators.find((c) => c.role === ProjectRole.OWNER);
const otherMembers = collaborators.filter((c) => c.role !== ProjectRole.OWNER);

const result: { type: "MEMBER" | "INVITE" | "EMPTY"; data: any; key: string }[] = [];
type Slot =
| { type: "MEMBER"; data: Collaborator; key: string }
| { type: "INVITE"; data: ProjectInvite; key: string }
| { type: "EMPTY"; data: null; key: string };
const result: Slot[] = [];

if (owner) result.push({ type: "MEMBER", data: owner, key: `member-${owner.user.id}` });
otherMembers.forEach((m) => result.push({ type: "MEMBER", data: m, key: `member-${m.user.id}` }));
Expand Down Expand Up @@ -87,7 +92,7 @@ const CollaboratorsSettings = () => {
<div className={styles.proGateBanner}>
<Lock size={14} />
<span>{t("proRequired")}</span>
<a href="/?settings=Profile" className={styles.proGateLink}>{t("upgrade")}</a>
<Link href="/?settings=Profile" className={styles.proGateLink}>{t("upgrade")}</Link>
</div>
)}

Expand Down
6 changes: 3 additions & 3 deletions components/dashboard/project/ExportProject.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ const ExportProject = () => {
const authorEmail = user?.email || "Unknown";
const projectAuthor = membership?.project.author || localAuthor || undefined;

let baseOptions: BaseExportOptions = {
const baseOptions: BaseExportOptions = {
title: projectTitle,
author: authorEmail,
projectAuthor,
Expand Down Expand Up @@ -122,13 +122,13 @@ const ExportProject = () => {
editorElement: editor?.view?.dom,
titlePageElement: titlePageEditor?.view?.dom,
};
await adapter.export(ydoc, pdfOptions as any);
await adapter.export(ydoc, pdfOptions as BaseExportOptions);
} else if (format === ExportFormat.SCRIPTIO) {
const scriptioOptions: ScriptioExportOptions = {
...baseOptions,
readable: readableExport,
};
await adapter.export(ydoc, scriptioOptions as any);
await adapter.export(ydoc, scriptioOptions as BaseExportOptions);
} else {
await adapter.export(ydoc, baseOptions);
}
Expand Down
45 changes: 23 additions & 22 deletions components/dashboard/project/LayoutSettings.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { useContext, useEffect, useState, useCallback, useMemo } from "react";
import { useContext, useEffect, useState, useMemo } from "react";
import { useTranslations } from "next-intl";
import { ProjectContext } from "@src/context/ProjectContext";
import {
Expand Down Expand Up @@ -35,7 +35,15 @@ import Dropdown, { DropdownOption } from "@components/utils/Dropdown";
import form from "./../../utils/Form.module.css";
import sharedStyles from "./ProjectSettings.module.css";
import styles from "./LayoutSettings.module.css";
import optionCard from "./OptionCard.module.css";
const MARGIN_ELEMENTS = [
"scene",
"action",
"character",
"dialogue",
"parenthetical",
"transition",
"section",
] as const;

const LayoutSettings = () => {
const t = useTranslations("layout");
Expand All @@ -62,16 +70,6 @@ const LayoutSettings = () => {
setElementStyles,
} = context;

const MARGIN_ELEMENTS = [
"scene",
"action",
"character",
"dialogue",
"parenthetical",
"transition",
"section",
] as const;

// Strip wrapping parentheses for display
const stripParens = (s: string) => (s.startsWith("(") && s.endsWith(")") ? s.slice(1, -1) : s);

Expand Down Expand Up @@ -110,15 +108,18 @@ const LayoutSettings = () => {

// Sync when context changes externally
useEffect(() => {
setLocalFormat(pageFormat);
setLocalPageMargins(initialPageMargins);
setLocalDisplaySceneNumbers(displaySceneNumbers);
setLocalSceneNumberOnRight(sceneNumberOnRight);
setLocalHeadingSpacing(sceneHeadingSpacing);
setLocalContdLabel(stripParens(contdLabel));
setLocalMoreLabel(stripParens(moreLabel));
setLocalMargins(initialMargins);
setLocalStyles(initialStyles);
const sync = () => {
setLocalFormat(pageFormat);
setLocalPageMargins(initialPageMargins);
setLocalDisplaySceneNumbers(displaySceneNumbers);
setLocalSceneNumberOnRight(sceneNumberOnRight);
setLocalHeadingSpacing(sceneHeadingSpacing);
setLocalContdLabel(stripParens(contdLabel));
setLocalMoreLabel(stripParens(moreLabel));
setLocalMargins(initialMargins);
setLocalStyles(initialStyles);
};
sync();
}, [
pageFormat,
initialPageMargins,
Expand Down Expand Up @@ -237,7 +238,7 @@ const LayoutSettings = () => {
setLocalPageMargins((prev) => ({ ...prev, [side]: newValue }));
};

const updateLocalStyle = (e: React.MouseEvent, element: string, styleKey: keyof ElementStyle, value: any) => {
const updateLocalStyle = (e: React.MouseEvent, element: string, styleKey: keyof ElementStyle, value: ElementStyle[keyof ElementStyle]) => {
e.preventDefault();
e.stopPropagation();

Expand Down
Loading
Loading