mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
chore: normalize line endings
This commit is contained in:
@@ -1,24 +1,24 @@
|
||||
import { create } from "zustand";
|
||||
import { DEFAULT_CANVAS_PRESETS } from "@/constants/project-constants";
|
||||
import type { TCanvasSize } from "@/lib/project/types";
|
||||
|
||||
interface EditorState {
|
||||
isInitializing: boolean;
|
||||
isPanelsReady: boolean;
|
||||
canvasPresets: TCanvasSize[];
|
||||
setInitializing: (loading: boolean) => void;
|
||||
setPanelsReady: (ready: boolean) => void;
|
||||
initializeApp: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const useEditorStore = create<EditorState>()((set) => ({
|
||||
isInitializing: true,
|
||||
isPanelsReady: false,
|
||||
canvasPresets: DEFAULT_CANVAS_PRESETS,
|
||||
setInitializing: (loading) => set({ isInitializing: loading }),
|
||||
setPanelsReady: (ready) => set({ isPanelsReady: ready }),
|
||||
initializeApp: async () => {
|
||||
set({ isInitializing: true, isPanelsReady: false });
|
||||
set({ isPanelsReady: true, isInitializing: false });
|
||||
},
|
||||
}));
|
||||
import { create } from "zustand";
|
||||
import { DEFAULT_CANVAS_PRESETS } from "@/constants/project-constants";
|
||||
import type { TCanvasSize } from "@/lib/project/types";
|
||||
|
||||
interface EditorState {
|
||||
isInitializing: boolean;
|
||||
isPanelsReady: boolean;
|
||||
canvasPresets: TCanvasSize[];
|
||||
setInitializing: (loading: boolean) => void;
|
||||
setPanelsReady: (ready: boolean) => void;
|
||||
initializeApp: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const useEditorStore = create<EditorState>()((set) => ({
|
||||
isInitializing: true,
|
||||
isPanelsReady: false,
|
||||
canvasPresets: DEFAULT_CANVAS_PRESETS,
|
||||
setInitializing: (loading) => set({ isInitializing: loading }),
|
||||
setPanelsReady: (ready) => set({ isPanelsReady: ready }),
|
||||
initializeApp: async () => {
|
||||
set({ isInitializing: true, isPanelsReady: false });
|
||||
set({ isPanelsReady: true, isInitializing: false });
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -1,347 +1,347 @@
|
||||
"use client";
|
||||
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
import type { TActionWithOptionalArgs } from "@/lib/actions";
|
||||
import { getDefaultShortcuts } from "@/lib/actions";
|
||||
import { isTypableDOMElement } from "@/utils/browser";
|
||||
import { isAppleDevice } from "@/utils/platform";
|
||||
import type { KeybindingConfig, ShortcutKey } from "@/lib/actions/keybinding";
|
||||
import { runMigrations, CURRENT_VERSION } from "./keybindings/migrations";
|
||||
|
||||
const defaultKeybindings: KeybindingConfig = getDefaultShortcuts();
|
||||
|
||||
export interface KeybindingConflict {
|
||||
key: ShortcutKey;
|
||||
existingAction: TActionWithOptionalArgs;
|
||||
newAction: TActionWithOptionalArgs;
|
||||
}
|
||||
|
||||
interface KeybindingsState {
|
||||
keybindings: KeybindingConfig;
|
||||
isCustomized: boolean;
|
||||
overlayDepth: number;
|
||||
openOverlayIds: string[];
|
||||
isLoadingProject: boolean;
|
||||
isRecording: boolean;
|
||||
|
||||
updateKeybinding: (key: ShortcutKey, action: TActionWithOptionalArgs) => void;
|
||||
removeKeybinding: (key: ShortcutKey) => void;
|
||||
resetToDefaults: () => void;
|
||||
importKeybindings: (config: KeybindingConfig) => void;
|
||||
exportKeybindings: () => KeybindingConfig;
|
||||
openOverlay: (overlayId: string, source: string) => void;
|
||||
closeOverlay: (overlayId: string, source: string) => void;
|
||||
setLoadingProject: (loading: boolean) => void;
|
||||
setIsRecording: (isRecording: boolean) => void;
|
||||
validateKeybinding: (
|
||||
key: ShortcutKey,
|
||||
action: TActionWithOptionalArgs,
|
||||
) => KeybindingConflict | null;
|
||||
getKeybindingsForAction: (action: TActionWithOptionalArgs) => ShortcutKey[];
|
||||
getKeybindingString: (ev: KeyboardEvent) => ShortcutKey | null;
|
||||
}
|
||||
|
||||
function isDOMElement(element: EventTarget | null): element is HTMLElement {
|
||||
return element instanceof HTMLElement;
|
||||
}
|
||||
|
||||
export const useKeybindingsStore = create<KeybindingsState>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
keybindings: { ...defaultKeybindings },
|
||||
isCustomized: false,
|
||||
overlayDepth: 0,
|
||||
openOverlayIds: [],
|
||||
isLoadingProject: false,
|
||||
isRecording: false,
|
||||
|
||||
openOverlay: (overlayId, source) =>
|
||||
set((s) => {
|
||||
const openOverlayIds = s.openOverlayIds.includes(overlayId)
|
||||
? s.openOverlayIds
|
||||
: [...s.openOverlayIds, overlayId];
|
||||
const nextOverlayDepth = openOverlayIds.length;
|
||||
// #region agent log
|
||||
fetch(
|
||||
"http://127.0.0.1:7245/ingest/669b22f8-172b-4e65-aa3f-1c702ede83f7",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Debug-Session-Id": "3997d9",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
sessionId: "3997d9",
|
||||
runId: "initial",
|
||||
hypothesisId: "H2",
|
||||
location: "keybindings-store.ts:openOverlay",
|
||||
message: "Overlay depth incremented",
|
||||
data: {
|
||||
source,
|
||||
overlayId,
|
||||
overlayDepth: s.overlayDepth,
|
||||
nextOverlayDepth,
|
||||
openOverlayIds,
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
}),
|
||||
},
|
||||
).catch(() => {});
|
||||
// #endregion
|
||||
return {
|
||||
openOverlayIds,
|
||||
overlayDepth: nextOverlayDepth,
|
||||
};
|
||||
}),
|
||||
closeOverlay: (overlayId, source) =>
|
||||
set((s) => {
|
||||
const openOverlayIds = s.openOverlayIds.filter(
|
||||
(id) => id !== overlayId,
|
||||
);
|
||||
const nextOverlayDepth = openOverlayIds.length;
|
||||
// #region agent log
|
||||
fetch(
|
||||
"http://127.0.0.1:7245/ingest/669b22f8-172b-4e65-aa3f-1c702ede83f7",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Debug-Session-Id": "3997d9",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
sessionId: "3997d9",
|
||||
runId: "initial",
|
||||
hypothesisId: "H2",
|
||||
location: "keybindings-store.ts:closeOverlay",
|
||||
message: "Overlay depth decremented",
|
||||
data: {
|
||||
source,
|
||||
overlayId,
|
||||
overlayDepth: s.overlayDepth,
|
||||
nextOverlayDepth,
|
||||
openOverlayIds,
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
}),
|
||||
},
|
||||
).catch(() => {});
|
||||
// #endregion
|
||||
return {
|
||||
openOverlayIds,
|
||||
overlayDepth: nextOverlayDepth,
|
||||
};
|
||||
}),
|
||||
setLoadingProject: (loading) => {
|
||||
// #region agent log
|
||||
fetch(
|
||||
"http://127.0.0.1:7245/ingest/669b22f8-172b-4e65-aa3f-1c702ede83f7",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Debug-Session-Id": "3997d9",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
sessionId: "3997d9",
|
||||
runId: "initial",
|
||||
hypothesisId: "H2",
|
||||
location: "keybindings-store.ts:setLoadingProject",
|
||||
message: "Loading gate updated",
|
||||
data: { loading },
|
||||
timestamp: Date.now(),
|
||||
}),
|
||||
},
|
||||
).catch(() => {});
|
||||
// #endregion
|
||||
set({ isLoadingProject: loading });
|
||||
},
|
||||
|
||||
updateKeybinding: (key: ShortcutKey, action: TActionWithOptionalArgs) => {
|
||||
set((state) => {
|
||||
const newKeybindings = { ...state.keybindings };
|
||||
newKeybindings[key] = action;
|
||||
|
||||
return {
|
||||
keybindings: newKeybindings,
|
||||
isCustomized: true,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
removeKeybinding: (key: ShortcutKey) => {
|
||||
set((state) => {
|
||||
const newKeybindings = { ...state.keybindings };
|
||||
delete newKeybindings[key];
|
||||
|
||||
return {
|
||||
keybindings: newKeybindings,
|
||||
isCustomized: true,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
resetToDefaults: () => {
|
||||
set({
|
||||
keybindings: { ...defaultKeybindings },
|
||||
isCustomized: false,
|
||||
});
|
||||
},
|
||||
|
||||
importKeybindings: (config: KeybindingConfig) => {
|
||||
for (const [key] of Object.entries(config)) {
|
||||
if (typeof key !== "string" || key.length === 0) {
|
||||
throw new Error(`Invalid key format: ${key}`);
|
||||
}
|
||||
}
|
||||
set({
|
||||
keybindings: { ...config },
|
||||
isCustomized: true,
|
||||
});
|
||||
},
|
||||
|
||||
exportKeybindings: () => {
|
||||
return get().keybindings;
|
||||
},
|
||||
|
||||
validateKeybinding: (
|
||||
key: ShortcutKey,
|
||||
action: TActionWithOptionalArgs,
|
||||
) => {
|
||||
const { keybindings } = get();
|
||||
const existingAction = keybindings[key];
|
||||
|
||||
if (existingAction && existingAction !== action) {
|
||||
return {
|
||||
key,
|
||||
existingAction,
|
||||
newAction: action,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
setIsRecording: (isRecording: boolean) => {
|
||||
// #region agent log
|
||||
fetch(
|
||||
"http://127.0.0.1:7245/ingest/669b22f8-172b-4e65-aa3f-1c702ede83f7",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Debug-Session-Id": "3997d9",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
sessionId: "3997d9",
|
||||
runId: "initial",
|
||||
hypothesisId: "H2",
|
||||
location: "keybindings-store.ts:setIsRecording",
|
||||
message: "Recording gate updated",
|
||||
data: { isRecording },
|
||||
timestamp: Date.now(),
|
||||
}),
|
||||
},
|
||||
).catch(() => {});
|
||||
// #endregion
|
||||
set({ isRecording });
|
||||
},
|
||||
|
||||
getKeybindingsForAction: (action: TActionWithOptionalArgs) => {
|
||||
const { keybindings } = get();
|
||||
return Object.keys(keybindings).filter(
|
||||
(key) => keybindings[key as ShortcutKey] === action,
|
||||
) as ShortcutKey[];
|
||||
},
|
||||
|
||||
getKeybindingString: (ev: KeyboardEvent) => {
|
||||
return generateKeybindingString(ev) as ShortcutKey | null;
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: "opencut-keybindings",
|
||||
version: CURRENT_VERSION,
|
||||
partialize: (state) => ({
|
||||
keybindings: state.keybindings,
|
||||
isCustomized: state.isCustomized,
|
||||
}),
|
||||
migrate: (persisted, version) =>
|
||||
runMigrations({ state: persisted, fromVersion: version }),
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
function generateKeybindingString(ev: KeyboardEvent): ShortcutKey | null {
|
||||
const target = ev.target;
|
||||
const modifierKey = getActiveModifier(ev);
|
||||
const key = getPressedKey(ev);
|
||||
if (!key) return null;
|
||||
|
||||
if (modifierKey) {
|
||||
if (
|
||||
modifierKey === "shift" &&
|
||||
isDOMElement(target) &&
|
||||
isTypableDOMElement({ element: target as HTMLElement })
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return `${modifierKey}+${key}` as ShortcutKey;
|
||||
}
|
||||
|
||||
if (
|
||||
isDOMElement(target) &&
|
||||
isTypableDOMElement({ element: target as HTMLElement })
|
||||
)
|
||||
return null;
|
||||
|
||||
return `${key}` as ShortcutKey;
|
||||
}
|
||||
|
||||
function getPressedKey(ev: KeyboardEvent): string | null {
|
||||
const key = (ev.key ?? "").toLowerCase();
|
||||
const code = ev.code ?? "";
|
||||
|
||||
if (code === "Space" || key === " " || key === "spacebar" || key === "space")
|
||||
return "space";
|
||||
|
||||
if (key.startsWith("arrow")) return key.slice(5);
|
||||
|
||||
if (key === "escape") return "escape";
|
||||
if (key === "tab") return "tab";
|
||||
if (key === "home") return "home";
|
||||
if (key === "end") return "end";
|
||||
if (key === "delete") return "delete";
|
||||
if (key === "backspace") return "backspace";
|
||||
|
||||
if (code.startsWith("Key")) {
|
||||
const letter = code.slice(3).toLowerCase();
|
||||
if (letter.length === 1 && letter >= "a" && letter <= "z") return letter;
|
||||
}
|
||||
|
||||
// Use physical key position for AZERTY and other non-QWERTY layouts
|
||||
if (code.startsWith("Digit")) {
|
||||
const digit = code.slice(5);
|
||||
if (digit.length === 1 && digit >= "0" && digit <= "9") return digit;
|
||||
}
|
||||
|
||||
const isDigit = key.length === 1 && key >= "0" && key <= "9";
|
||||
if (isDigit) return key;
|
||||
|
||||
if (key === "/" || key === "." || key === "enter") return key;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function getActiveModifier(ev: KeyboardEvent): string | null {
|
||||
const modifierKeys = {
|
||||
ctrl: isAppleDevice() ? ev.metaKey : ev.ctrlKey,
|
||||
alt: ev.altKey,
|
||||
shift: ev.shiftKey,
|
||||
};
|
||||
|
||||
const activeModifier = Object.keys(modifierKeys)
|
||||
.filter((key) => modifierKeys[key as keyof typeof modifierKeys])
|
||||
.join("+");
|
||||
|
||||
return activeModifier === "" ? null : activeModifier;
|
||||
}
|
||||
"use client";
|
||||
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
import type { TActionWithOptionalArgs } from "@/lib/actions";
|
||||
import { getDefaultShortcuts } from "@/lib/actions";
|
||||
import { isTypableDOMElement } from "@/utils/browser";
|
||||
import { isAppleDevice } from "@/utils/platform";
|
||||
import type { KeybindingConfig, ShortcutKey } from "@/lib/actions/keybinding";
|
||||
import { runMigrations, CURRENT_VERSION } from "./keybindings/migrations";
|
||||
|
||||
const defaultKeybindings: KeybindingConfig = getDefaultShortcuts();
|
||||
|
||||
export interface KeybindingConflict {
|
||||
key: ShortcutKey;
|
||||
existingAction: TActionWithOptionalArgs;
|
||||
newAction: TActionWithOptionalArgs;
|
||||
}
|
||||
|
||||
interface KeybindingsState {
|
||||
keybindings: KeybindingConfig;
|
||||
isCustomized: boolean;
|
||||
overlayDepth: number;
|
||||
openOverlayIds: string[];
|
||||
isLoadingProject: boolean;
|
||||
isRecording: boolean;
|
||||
|
||||
updateKeybinding: (key: ShortcutKey, action: TActionWithOptionalArgs) => void;
|
||||
removeKeybinding: (key: ShortcutKey) => void;
|
||||
resetToDefaults: () => void;
|
||||
importKeybindings: (config: KeybindingConfig) => void;
|
||||
exportKeybindings: () => KeybindingConfig;
|
||||
openOverlay: (overlayId: string, source: string) => void;
|
||||
closeOverlay: (overlayId: string, source: string) => void;
|
||||
setLoadingProject: (loading: boolean) => void;
|
||||
setIsRecording: (isRecording: boolean) => void;
|
||||
validateKeybinding: (
|
||||
key: ShortcutKey,
|
||||
action: TActionWithOptionalArgs,
|
||||
) => KeybindingConflict | null;
|
||||
getKeybindingsForAction: (action: TActionWithOptionalArgs) => ShortcutKey[];
|
||||
getKeybindingString: (ev: KeyboardEvent) => ShortcutKey | null;
|
||||
}
|
||||
|
||||
function isDOMElement(element: EventTarget | null): element is HTMLElement {
|
||||
return element instanceof HTMLElement;
|
||||
}
|
||||
|
||||
export const useKeybindingsStore = create<KeybindingsState>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
keybindings: { ...defaultKeybindings },
|
||||
isCustomized: false,
|
||||
overlayDepth: 0,
|
||||
openOverlayIds: [],
|
||||
isLoadingProject: false,
|
||||
isRecording: false,
|
||||
|
||||
openOverlay: (overlayId, source) =>
|
||||
set((s) => {
|
||||
const openOverlayIds = s.openOverlayIds.includes(overlayId)
|
||||
? s.openOverlayIds
|
||||
: [...s.openOverlayIds, overlayId];
|
||||
const nextOverlayDepth = openOverlayIds.length;
|
||||
// #region agent log
|
||||
fetch(
|
||||
"http://127.0.0.1:7245/ingest/669b22f8-172b-4e65-aa3f-1c702ede83f7",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Debug-Session-Id": "3997d9",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
sessionId: "3997d9",
|
||||
runId: "initial",
|
||||
hypothesisId: "H2",
|
||||
location: "keybindings-store.ts:openOverlay",
|
||||
message: "Overlay depth incremented",
|
||||
data: {
|
||||
source,
|
||||
overlayId,
|
||||
overlayDepth: s.overlayDepth,
|
||||
nextOverlayDepth,
|
||||
openOverlayIds,
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
}),
|
||||
},
|
||||
).catch(() => {});
|
||||
// #endregion
|
||||
return {
|
||||
openOverlayIds,
|
||||
overlayDepth: nextOverlayDepth,
|
||||
};
|
||||
}),
|
||||
closeOverlay: (overlayId, source) =>
|
||||
set((s) => {
|
||||
const openOverlayIds = s.openOverlayIds.filter(
|
||||
(id) => id !== overlayId,
|
||||
);
|
||||
const nextOverlayDepth = openOverlayIds.length;
|
||||
// #region agent log
|
||||
fetch(
|
||||
"http://127.0.0.1:7245/ingest/669b22f8-172b-4e65-aa3f-1c702ede83f7",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Debug-Session-Id": "3997d9",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
sessionId: "3997d9",
|
||||
runId: "initial",
|
||||
hypothesisId: "H2",
|
||||
location: "keybindings-store.ts:closeOverlay",
|
||||
message: "Overlay depth decremented",
|
||||
data: {
|
||||
source,
|
||||
overlayId,
|
||||
overlayDepth: s.overlayDepth,
|
||||
nextOverlayDepth,
|
||||
openOverlayIds,
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
}),
|
||||
},
|
||||
).catch(() => {});
|
||||
// #endregion
|
||||
return {
|
||||
openOverlayIds,
|
||||
overlayDepth: nextOverlayDepth,
|
||||
};
|
||||
}),
|
||||
setLoadingProject: (loading) => {
|
||||
// #region agent log
|
||||
fetch(
|
||||
"http://127.0.0.1:7245/ingest/669b22f8-172b-4e65-aa3f-1c702ede83f7",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Debug-Session-Id": "3997d9",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
sessionId: "3997d9",
|
||||
runId: "initial",
|
||||
hypothesisId: "H2",
|
||||
location: "keybindings-store.ts:setLoadingProject",
|
||||
message: "Loading gate updated",
|
||||
data: { loading },
|
||||
timestamp: Date.now(),
|
||||
}),
|
||||
},
|
||||
).catch(() => {});
|
||||
// #endregion
|
||||
set({ isLoadingProject: loading });
|
||||
},
|
||||
|
||||
updateKeybinding: (key: ShortcutKey, action: TActionWithOptionalArgs) => {
|
||||
set((state) => {
|
||||
const newKeybindings = { ...state.keybindings };
|
||||
newKeybindings[key] = action;
|
||||
|
||||
return {
|
||||
keybindings: newKeybindings,
|
||||
isCustomized: true,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
removeKeybinding: (key: ShortcutKey) => {
|
||||
set((state) => {
|
||||
const newKeybindings = { ...state.keybindings };
|
||||
delete newKeybindings[key];
|
||||
|
||||
return {
|
||||
keybindings: newKeybindings,
|
||||
isCustomized: true,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
resetToDefaults: () => {
|
||||
set({
|
||||
keybindings: { ...defaultKeybindings },
|
||||
isCustomized: false,
|
||||
});
|
||||
},
|
||||
|
||||
importKeybindings: (config: KeybindingConfig) => {
|
||||
for (const [key] of Object.entries(config)) {
|
||||
if (typeof key !== "string" || key.length === 0) {
|
||||
throw new Error(`Invalid key format: ${key}`);
|
||||
}
|
||||
}
|
||||
set({
|
||||
keybindings: { ...config },
|
||||
isCustomized: true,
|
||||
});
|
||||
},
|
||||
|
||||
exportKeybindings: () => {
|
||||
return get().keybindings;
|
||||
},
|
||||
|
||||
validateKeybinding: (
|
||||
key: ShortcutKey,
|
||||
action: TActionWithOptionalArgs,
|
||||
) => {
|
||||
const { keybindings } = get();
|
||||
const existingAction = keybindings[key];
|
||||
|
||||
if (existingAction && existingAction !== action) {
|
||||
return {
|
||||
key,
|
||||
existingAction,
|
||||
newAction: action,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
setIsRecording: (isRecording: boolean) => {
|
||||
// #region agent log
|
||||
fetch(
|
||||
"http://127.0.0.1:7245/ingest/669b22f8-172b-4e65-aa3f-1c702ede83f7",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Debug-Session-Id": "3997d9",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
sessionId: "3997d9",
|
||||
runId: "initial",
|
||||
hypothesisId: "H2",
|
||||
location: "keybindings-store.ts:setIsRecording",
|
||||
message: "Recording gate updated",
|
||||
data: { isRecording },
|
||||
timestamp: Date.now(),
|
||||
}),
|
||||
},
|
||||
).catch(() => {});
|
||||
// #endregion
|
||||
set({ isRecording });
|
||||
},
|
||||
|
||||
getKeybindingsForAction: (action: TActionWithOptionalArgs) => {
|
||||
const { keybindings } = get();
|
||||
return Object.keys(keybindings).filter(
|
||||
(key) => keybindings[key as ShortcutKey] === action,
|
||||
) as ShortcutKey[];
|
||||
},
|
||||
|
||||
getKeybindingString: (ev: KeyboardEvent) => {
|
||||
return generateKeybindingString(ev) as ShortcutKey | null;
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: "opencut-keybindings",
|
||||
version: CURRENT_VERSION,
|
||||
partialize: (state) => ({
|
||||
keybindings: state.keybindings,
|
||||
isCustomized: state.isCustomized,
|
||||
}),
|
||||
migrate: (persisted, version) =>
|
||||
runMigrations({ state: persisted, fromVersion: version }),
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
function generateKeybindingString(ev: KeyboardEvent): ShortcutKey | null {
|
||||
const target = ev.target;
|
||||
const modifierKey = getActiveModifier(ev);
|
||||
const key = getPressedKey(ev);
|
||||
if (!key) return null;
|
||||
|
||||
if (modifierKey) {
|
||||
if (
|
||||
modifierKey === "shift" &&
|
||||
isDOMElement(target) &&
|
||||
isTypableDOMElement({ element: target as HTMLElement })
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return `${modifierKey}+${key}` as ShortcutKey;
|
||||
}
|
||||
|
||||
if (
|
||||
isDOMElement(target) &&
|
||||
isTypableDOMElement({ element: target as HTMLElement })
|
||||
)
|
||||
return null;
|
||||
|
||||
return `${key}` as ShortcutKey;
|
||||
}
|
||||
|
||||
function getPressedKey(ev: KeyboardEvent): string | null {
|
||||
const key = (ev.key ?? "").toLowerCase();
|
||||
const code = ev.code ?? "";
|
||||
|
||||
if (code === "Space" || key === " " || key === "spacebar" || key === "space")
|
||||
return "space";
|
||||
|
||||
if (key.startsWith("arrow")) return key.slice(5);
|
||||
|
||||
if (key === "escape") return "escape";
|
||||
if (key === "tab") return "tab";
|
||||
if (key === "home") return "home";
|
||||
if (key === "end") return "end";
|
||||
if (key === "delete") return "delete";
|
||||
if (key === "backspace") return "backspace";
|
||||
|
||||
if (code.startsWith("Key")) {
|
||||
const letter = code.slice(3).toLowerCase();
|
||||
if (letter.length === 1 && letter >= "a" && letter <= "z") return letter;
|
||||
}
|
||||
|
||||
// Use physical key position for AZERTY and other non-QWERTY layouts
|
||||
if (code.startsWith("Digit")) {
|
||||
const digit = code.slice(5);
|
||||
if (digit.length === 1 && digit >= "0" && digit <= "9") return digit;
|
||||
}
|
||||
|
||||
const isDigit = key.length === 1 && key >= "0" && key <= "9";
|
||||
if (isDigit) return key;
|
||||
|
||||
if (key === "/" || key === "." || key === "enter") return key;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function getActiveModifier(ev: KeyboardEvent): string | null {
|
||||
const modifierKeys = {
|
||||
ctrl: isAppleDevice() ? ev.metaKey : ev.ctrlKey,
|
||||
alt: ev.altKey,
|
||||
shift: ev.shiftKey,
|
||||
};
|
||||
|
||||
const activeModifier = Object.keys(modifierKeys)
|
||||
.filter((key) => modifierKeys[key as keyof typeof modifierKeys])
|
||||
.join("+");
|
||||
|
||||
return activeModifier === "" ? null : activeModifier;
|
||||
}
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
import type { KeybindingConfig, ShortcutKey } from "@/lib/actions/keybinding";
|
||||
import type { TActionWithOptionalArgs } from "@/lib/actions";
|
||||
|
||||
interface V2State {
|
||||
keybindings: KeybindingConfig;
|
||||
isCustomized: boolean;
|
||||
}
|
||||
|
||||
export function v2ToV3({ state }: { state: unknown }): unknown {
|
||||
const v2 = state as V2State;
|
||||
|
||||
const renames: Record<string, string> = {
|
||||
"split-selected": "split",
|
||||
"split-selected-left": "split-left",
|
||||
"split-selected-right": "split-right",
|
||||
};
|
||||
|
||||
const migrated = { ...v2.keybindings };
|
||||
for (const [key, action] of Object.entries(migrated)) {
|
||||
if (action && renames[action]) {
|
||||
migrated[key as ShortcutKey] = renames[action] as TActionWithOptionalArgs;
|
||||
}
|
||||
}
|
||||
|
||||
return { ...v2, keybindings: migrated };
|
||||
}
|
||||
import type { KeybindingConfig, ShortcutKey } from "@/lib/actions/keybinding";
|
||||
import type { TActionWithOptionalArgs } from "@/lib/actions";
|
||||
|
||||
interface V2State {
|
||||
keybindings: KeybindingConfig;
|
||||
isCustomized: boolean;
|
||||
}
|
||||
|
||||
export function v2ToV3({ state }: { state: unknown }): unknown {
|
||||
const v2 = state as V2State;
|
||||
|
||||
const renames: Record<string, string> = {
|
||||
"split-selected": "split",
|
||||
"split-selected-left": "split-left",
|
||||
"split-selected-right": "split-right",
|
||||
};
|
||||
|
||||
const migrated = { ...v2.keybindings };
|
||||
for (const [key, action] of Object.entries(migrated)) {
|
||||
if (action && renames[action]) {
|
||||
migrated[key as ShortcutKey] = renames[action] as TActionWithOptionalArgs;
|
||||
}
|
||||
}
|
||||
|
||||
return { ...v2, keybindings: migrated };
|
||||
}
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
import type { TActionWithOptionalArgs } from "@/lib/actions";
|
||||
import type { ShortcutKey } from "@/lib/actions/keybinding";
|
||||
import type { KeybindingConfig } from "@/lib/actions/keybinding";
|
||||
|
||||
interface V3State {
|
||||
keybindings: KeybindingConfig;
|
||||
isCustomized: boolean;
|
||||
}
|
||||
|
||||
export function v3ToV4({ state }: { state: unknown }): unknown {
|
||||
const v3 = state as V3State;
|
||||
|
||||
const renames: Record<string, string> = {
|
||||
"paste-selected": "paste-copied",
|
||||
};
|
||||
|
||||
const migrated = { ...v3.keybindings };
|
||||
for (const [key, action] of Object.entries(migrated)) {
|
||||
if (action && renames[action]) {
|
||||
migrated[key as ShortcutKey] = renames[action] as TActionWithOptionalArgs;
|
||||
}
|
||||
}
|
||||
|
||||
return { ...v3, keybindings: migrated };
|
||||
}
|
||||
import type { TActionWithOptionalArgs } from "@/lib/actions";
|
||||
import type { ShortcutKey } from "@/lib/actions/keybinding";
|
||||
import type { KeybindingConfig } from "@/lib/actions/keybinding";
|
||||
|
||||
interface V3State {
|
||||
keybindings: KeybindingConfig;
|
||||
isCustomized: boolean;
|
||||
}
|
||||
|
||||
export function v3ToV4({ state }: { state: unknown }): unknown {
|
||||
const v3 = state as V3State;
|
||||
|
||||
const renames: Record<string, string> = {
|
||||
"paste-selected": "paste-copied",
|
||||
};
|
||||
|
||||
const migrated = { ...v3.keybindings };
|
||||
for (const [key, action] of Object.entries(migrated)) {
|
||||
if (action && renames[action]) {
|
||||
migrated[key as ShortcutKey] = renames[action] as TActionWithOptionalArgs;
|
||||
}
|
||||
}
|
||||
|
||||
return { ...v3, keybindings: migrated };
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import type { KeybindingConfig } from "@/lib/actions/keybinding";
|
||||
|
||||
interface V4State {
|
||||
keybindings: KeybindingConfig;
|
||||
isCustomized: boolean;
|
||||
}
|
||||
|
||||
export function v4ToV5({ state }: { state: unknown }): unknown {
|
||||
const v4 = state as V4State;
|
||||
const keybindings = { ...v4.keybindings };
|
||||
|
||||
if (!keybindings.escape) {
|
||||
keybindings.escape = "deselect-all";
|
||||
}
|
||||
|
||||
return { ...v4, keybindings };
|
||||
}
|
||||
import type { KeybindingConfig } from "@/lib/actions/keybinding";
|
||||
|
||||
interface V4State {
|
||||
keybindings: KeybindingConfig;
|
||||
isCustomized: boolean;
|
||||
}
|
||||
|
||||
export function v4ToV5({ state }: { state: unknown }): unknown {
|
||||
const v4 = state as V4State;
|
||||
const keybindings = { ...v4.keybindings };
|
||||
|
||||
if (!keybindings.escape) {
|
||||
keybindings.escape = "deselect-all";
|
||||
}
|
||||
|
||||
return { ...v4, keybindings };
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import type { KeybindingConfig } from "@/lib/actions/keybinding";
|
||||
|
||||
interface V5State {
|
||||
keybindings: KeybindingConfig;
|
||||
isCustomized: boolean;
|
||||
}
|
||||
|
||||
export function v5ToV6({ state }: { state: unknown }): unknown {
|
||||
const v5 = state as V5State;
|
||||
const keybindings = { ...v5.keybindings };
|
||||
|
||||
if (keybindings.escape === "deselect-all") {
|
||||
keybindings.escape = "cancel-interaction";
|
||||
}
|
||||
|
||||
return { ...v5, keybindings };
|
||||
}
|
||||
import type { KeybindingConfig } from "@/lib/actions/keybinding";
|
||||
|
||||
interface V5State {
|
||||
keybindings: KeybindingConfig;
|
||||
isCustomized: boolean;
|
||||
}
|
||||
|
||||
export function v5ToV6({ state }: { state: unknown }): unknown {
|
||||
const v5 = state as V5State;
|
||||
const keybindings = { ...v5.keybindings };
|
||||
|
||||
if (keybindings.escape === "deselect-all") {
|
||||
keybindings.escape = "cancel-interaction";
|
||||
}
|
||||
|
||||
return { ...v5, keybindings };
|
||||
}
|
||||
|
||||
@@ -1,93 +1,93 @@
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
import { PANEL_CONFIG } from "@/constants/editor-constants";
|
||||
|
||||
export interface PanelSizes {
|
||||
tools: number;
|
||||
preview: number;
|
||||
properties: number;
|
||||
mainContent: number;
|
||||
timeline: number;
|
||||
}
|
||||
|
||||
export type PanelId = keyof PanelSizes;
|
||||
|
||||
interface PanelState {
|
||||
panels: PanelSizes;
|
||||
setPanel: (panel: PanelId, size: number) => void;
|
||||
setPanels: (sizes: Partial<PanelSizes>) => void;
|
||||
resetPanels: () => void;
|
||||
}
|
||||
|
||||
export const usePanelStore = create<PanelState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
...PANEL_CONFIG,
|
||||
setPanel: (panel, size) =>
|
||||
set((state) => ({
|
||||
panels: {
|
||||
...state.panels,
|
||||
[panel]: size,
|
||||
},
|
||||
})),
|
||||
setPanels: (sizes) =>
|
||||
set((state) => ({
|
||||
panels: {
|
||||
...state.panels,
|
||||
...sizes,
|
||||
},
|
||||
})),
|
||||
resetPanels: () => set({ ...PANEL_CONFIG }),
|
||||
}),
|
||||
{
|
||||
name: "panel-sizes",
|
||||
version: 2,
|
||||
migrate: (persistedState) => {
|
||||
const state = persistedState as
|
||||
| {
|
||||
panels?: Partial<PanelSizes> | null;
|
||||
toolsPanel?: number;
|
||||
previewPanel?: number;
|
||||
propertiesPanel?: number;
|
||||
mainContent?: number;
|
||||
timeline?: number;
|
||||
tools?: number;
|
||||
preview?: number;
|
||||
properties?: number;
|
||||
}
|
||||
| undefined
|
||||
| null;
|
||||
|
||||
if (!state) return { panels: { ...PANEL_CONFIG.panels } };
|
||||
|
||||
if (state.panels && typeof state.panels === "object") {
|
||||
return {
|
||||
panels: {
|
||||
...PANEL_CONFIG.panels,
|
||||
...state.panels,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
panels: {
|
||||
tools: state.tools ?? state.toolsPanel ?? PANEL_CONFIG.panels.tools,
|
||||
preview:
|
||||
state.preview ??
|
||||
state.previewPanel ??
|
||||
PANEL_CONFIG.panels.preview,
|
||||
properties:
|
||||
state.properties ??
|
||||
state.propertiesPanel ??
|
||||
PANEL_CONFIG.panels.properties,
|
||||
mainContent: state.mainContent ?? PANEL_CONFIG.panels.mainContent,
|
||||
timeline: state.timeline ?? PANEL_CONFIG.panels.timeline,
|
||||
},
|
||||
};
|
||||
},
|
||||
partialize: (state) => ({
|
||||
panels: state.panels,
|
||||
}),
|
||||
},
|
||||
),
|
||||
);
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
import { PANEL_CONFIG } from "@/constants/editor-constants";
|
||||
|
||||
export interface PanelSizes {
|
||||
tools: number;
|
||||
preview: number;
|
||||
properties: number;
|
||||
mainContent: number;
|
||||
timeline: number;
|
||||
}
|
||||
|
||||
export type PanelId = keyof PanelSizes;
|
||||
|
||||
interface PanelState {
|
||||
panels: PanelSizes;
|
||||
setPanel: (panel: PanelId, size: number) => void;
|
||||
setPanels: (sizes: Partial<PanelSizes>) => void;
|
||||
resetPanels: () => void;
|
||||
}
|
||||
|
||||
export const usePanelStore = create<PanelState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
...PANEL_CONFIG,
|
||||
setPanel: (panel, size) =>
|
||||
set((state) => ({
|
||||
panels: {
|
||||
...state.panels,
|
||||
[panel]: size,
|
||||
},
|
||||
})),
|
||||
setPanels: (sizes) =>
|
||||
set((state) => ({
|
||||
panels: {
|
||||
...state.panels,
|
||||
...sizes,
|
||||
},
|
||||
})),
|
||||
resetPanels: () => set({ ...PANEL_CONFIG }),
|
||||
}),
|
||||
{
|
||||
name: "panel-sizes",
|
||||
version: 2,
|
||||
migrate: (persistedState) => {
|
||||
const state = persistedState as
|
||||
| {
|
||||
panels?: Partial<PanelSizes> | null;
|
||||
toolsPanel?: number;
|
||||
previewPanel?: number;
|
||||
propertiesPanel?: number;
|
||||
mainContent?: number;
|
||||
timeline?: number;
|
||||
tools?: number;
|
||||
preview?: number;
|
||||
properties?: number;
|
||||
}
|
||||
| undefined
|
||||
| null;
|
||||
|
||||
if (!state) return { panels: { ...PANEL_CONFIG.panels } };
|
||||
|
||||
if (state.panels && typeof state.panels === "object") {
|
||||
return {
|
||||
panels: {
|
||||
...PANEL_CONFIG.panels,
|
||||
...state.panels,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
panels: {
|
||||
tools: state.tools ?? state.toolsPanel ?? PANEL_CONFIG.panels.tools,
|
||||
preview:
|
||||
state.preview ??
|
||||
state.previewPanel ??
|
||||
PANEL_CONFIG.panels.preview,
|
||||
properties:
|
||||
state.properties ??
|
||||
state.propertiesPanel ??
|
||||
PANEL_CONFIG.panels.properties,
|
||||
mainContent: state.mainContent ?? PANEL_CONFIG.panels.mainContent,
|
||||
timeline: state.timeline ?? PANEL_CONFIG.panels.timeline,
|
||||
},
|
||||
};
|
||||
},
|
||||
partialize: (state) => ({
|
||||
panels: state.panels,
|
||||
}),
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,112 +1,112 @@
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
import { isGuideId, type GuideId } from "@/lib/guides";
|
||||
import { DEFAULT_GRID_CONFIG } from "@/constants/guide-constants";
|
||||
import type { GridConfig } from "@/lib/guides/types";
|
||||
|
||||
interface PreviewOverlaysState {
|
||||
bookmarks: boolean;
|
||||
}
|
||||
|
||||
interface PersistedPreviewState {
|
||||
activeGuide?: string | null;
|
||||
layoutGuide?: {
|
||||
platform?: string | null;
|
||||
};
|
||||
overlays?: PreviewOverlaysState;
|
||||
gridConfig?: GridConfig;
|
||||
}
|
||||
|
||||
interface PreviewState {
|
||||
activeGuide: GuideId | null;
|
||||
overlays: PreviewOverlaysState;
|
||||
gridConfig: GridConfig;
|
||||
toggleGuide: (guideId: GuideId) => void;
|
||||
setGridConfig: (config: Partial<GridConfig>) => void;
|
||||
setOverlayVisibility: ({
|
||||
overlay,
|
||||
isVisible,
|
||||
}: {
|
||||
overlay: keyof PreviewOverlaysState;
|
||||
isVisible: boolean;
|
||||
}) => void;
|
||||
toggleOverlayVisibility: ({
|
||||
overlay,
|
||||
}: {
|
||||
overlay: keyof PreviewOverlaysState;
|
||||
}) => void;
|
||||
}
|
||||
|
||||
const DEFAULT_PREVIEW_OVERLAYS: PreviewOverlaysState = {
|
||||
bookmarks: true,
|
||||
};
|
||||
|
||||
function getPersistedActiveGuide(
|
||||
state: PersistedPreviewState | undefined,
|
||||
): GuideId | null {
|
||||
const persistedGuide =
|
||||
state?.activeGuide ?? state?.layoutGuide?.platform ?? null;
|
||||
|
||||
if (typeof persistedGuide !== "string") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return isGuideId(persistedGuide) ? persistedGuide : null;
|
||||
}
|
||||
|
||||
export const usePreviewStore = create<PreviewState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
activeGuide: null,
|
||||
overlays: DEFAULT_PREVIEW_OVERLAYS,
|
||||
gridConfig: DEFAULT_GRID_CONFIG,
|
||||
toggleGuide: (guideId) => {
|
||||
set((state) => ({
|
||||
activeGuide: state.activeGuide === guideId ? null : guideId,
|
||||
}));
|
||||
},
|
||||
setGridConfig: (config) => {
|
||||
set((state) => ({
|
||||
gridConfig: { ...state.gridConfig, ...config },
|
||||
}));
|
||||
},
|
||||
setOverlayVisibility: ({ overlay, isVisible }) => {
|
||||
set((state) => ({
|
||||
overlays: {
|
||||
...state.overlays,
|
||||
[overlay]: isVisible,
|
||||
},
|
||||
}));
|
||||
},
|
||||
toggleOverlayVisibility: ({ overlay }) => {
|
||||
set((state) => ({
|
||||
overlays: {
|
||||
...state.overlays,
|
||||
[overlay]: !state.overlays[overlay],
|
||||
},
|
||||
}));
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: "preview-settings",
|
||||
version: 4,
|
||||
migrate: (persistedState) => {
|
||||
const state = persistedState as PersistedPreviewState | undefined;
|
||||
|
||||
return {
|
||||
activeGuide: getPersistedActiveGuide(state),
|
||||
overlays: state?.overlays ?? DEFAULT_PREVIEW_OVERLAYS,
|
||||
gridConfig: {
|
||||
rows: state?.gridConfig?.rows ?? DEFAULT_GRID_CONFIG.rows,
|
||||
cols: state?.gridConfig?.cols ?? DEFAULT_GRID_CONFIG.cols,
|
||||
},
|
||||
};
|
||||
},
|
||||
partialize: (state) => ({
|
||||
activeGuide: state.activeGuide,
|
||||
overlays: state.overlays,
|
||||
gridConfig: state.gridConfig,
|
||||
}),
|
||||
},
|
||||
),
|
||||
);
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
import { isGuideId, type GuideId } from "@/lib/guides";
|
||||
import { DEFAULT_GRID_CONFIG } from "@/constants/guide-constants";
|
||||
import type { GridConfig } from "@/lib/guides/types";
|
||||
|
||||
interface PreviewOverlaysState {
|
||||
bookmarks: boolean;
|
||||
}
|
||||
|
||||
interface PersistedPreviewState {
|
||||
activeGuide?: string | null;
|
||||
layoutGuide?: {
|
||||
platform?: string | null;
|
||||
};
|
||||
overlays?: PreviewOverlaysState;
|
||||
gridConfig?: GridConfig;
|
||||
}
|
||||
|
||||
interface PreviewState {
|
||||
activeGuide: GuideId | null;
|
||||
overlays: PreviewOverlaysState;
|
||||
gridConfig: GridConfig;
|
||||
toggleGuide: (guideId: GuideId) => void;
|
||||
setGridConfig: (config: Partial<GridConfig>) => void;
|
||||
setOverlayVisibility: ({
|
||||
overlay,
|
||||
isVisible,
|
||||
}: {
|
||||
overlay: keyof PreviewOverlaysState;
|
||||
isVisible: boolean;
|
||||
}) => void;
|
||||
toggleOverlayVisibility: ({
|
||||
overlay,
|
||||
}: {
|
||||
overlay: keyof PreviewOverlaysState;
|
||||
}) => void;
|
||||
}
|
||||
|
||||
const DEFAULT_PREVIEW_OVERLAYS: PreviewOverlaysState = {
|
||||
bookmarks: true,
|
||||
};
|
||||
|
||||
function getPersistedActiveGuide(
|
||||
state: PersistedPreviewState | undefined,
|
||||
): GuideId | null {
|
||||
const persistedGuide =
|
||||
state?.activeGuide ?? state?.layoutGuide?.platform ?? null;
|
||||
|
||||
if (typeof persistedGuide !== "string") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return isGuideId(persistedGuide) ? persistedGuide : null;
|
||||
}
|
||||
|
||||
export const usePreviewStore = create<PreviewState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
activeGuide: null,
|
||||
overlays: DEFAULT_PREVIEW_OVERLAYS,
|
||||
gridConfig: DEFAULT_GRID_CONFIG,
|
||||
toggleGuide: (guideId) => {
|
||||
set((state) => ({
|
||||
activeGuide: state.activeGuide === guideId ? null : guideId,
|
||||
}));
|
||||
},
|
||||
setGridConfig: (config) => {
|
||||
set((state) => ({
|
||||
gridConfig: { ...state.gridConfig, ...config },
|
||||
}));
|
||||
},
|
||||
setOverlayVisibility: ({ overlay, isVisible }) => {
|
||||
set((state) => ({
|
||||
overlays: {
|
||||
...state.overlays,
|
||||
[overlay]: isVisible,
|
||||
},
|
||||
}));
|
||||
},
|
||||
toggleOverlayVisibility: ({ overlay }) => {
|
||||
set((state) => ({
|
||||
overlays: {
|
||||
...state.overlays,
|
||||
[overlay]: !state.overlays[overlay],
|
||||
},
|
||||
}));
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: "preview-settings",
|
||||
version: 4,
|
||||
migrate: (persistedState) => {
|
||||
const state = persistedState as PersistedPreviewState | undefined;
|
||||
|
||||
return {
|
||||
activeGuide: getPersistedActiveGuide(state),
|
||||
overlays: state?.overlays ?? DEFAULT_PREVIEW_OVERLAYS,
|
||||
gridConfig: {
|
||||
rows: state?.gridConfig?.rows ?? DEFAULT_GRID_CONFIG.rows,
|
||||
cols: state?.gridConfig?.cols ?? DEFAULT_GRID_CONFIG.cols,
|
||||
},
|
||||
};
|
||||
},
|
||||
partialize: (state) => ({
|
||||
activeGuide: state.activeGuide,
|
||||
overlays: state.overlays,
|
||||
gridConfig: state.gridConfig,
|
||||
}),
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
+261
-261
@@ -1,261 +1,261 @@
|
||||
import { create } from "zustand";
|
||||
import type { SoundEffect, SavedSound } from "@/lib/sounds/types";
|
||||
import { storageService } from "@/services/storage/service";
|
||||
import { toast } from "sonner";
|
||||
import { EditorCore } from "@/core";
|
||||
import { buildLibraryAudioElement } from "@/lib/timeline/element-utils";
|
||||
|
||||
interface SoundsStore {
|
||||
topSoundEffects: SoundEffect[];
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
hasLoaded: boolean;
|
||||
showCommercialOnly: boolean;
|
||||
toggleCommercialFilter: () => void;
|
||||
searchQuery: string;
|
||||
searchResults: SoundEffect[];
|
||||
isSearching: boolean;
|
||||
searchError: string | null;
|
||||
lastSearchQuery: string;
|
||||
scrollPosition: number;
|
||||
currentPage: number;
|
||||
hasNextPage: boolean;
|
||||
totalCount: number;
|
||||
isLoadingMore: boolean;
|
||||
savedSounds: SavedSound[];
|
||||
isSavedSoundsLoaded: boolean;
|
||||
isLoadingSavedSounds: boolean;
|
||||
savedSoundsError: string | null;
|
||||
|
||||
addSoundToTimeline: ({ sound }: { sound: SoundEffect }) => Promise<boolean>;
|
||||
setTopSoundEffects: ({ sounds }: { sounds: SoundEffect[] }) => void;
|
||||
setLoading: ({ loading }: { loading: boolean }) => void;
|
||||
setError: ({ error }: { error: string | null }) => void;
|
||||
setHasLoaded: ({ loaded }: { loaded: boolean }) => void;
|
||||
setSearchQuery: ({ query }: { query: string }) => void;
|
||||
setSearchResults: ({ results }: { results: SoundEffect[] }) => void;
|
||||
setSearching: ({ searching }: { searching: boolean }) => void;
|
||||
setSearchError: ({ error }: { error: string | null }) => void;
|
||||
setLastSearchQuery: ({ query }: { query: string }) => void;
|
||||
setScrollPosition: ({ position }: { position: number }) => void;
|
||||
setCurrentPage: ({ page }: { page: number }) => void;
|
||||
setHasNextPage: ({ hasNext }: { hasNext: boolean }) => void;
|
||||
setTotalCount: ({ count }: { count: number }) => void;
|
||||
setLoadingMore: ({ loading }: { loading: boolean }) => void;
|
||||
appendSearchResults: ({ results }: { results: SoundEffect[] }) => void;
|
||||
appendTopSounds: ({ results }: { results: SoundEffect[] }) => void;
|
||||
resetPagination: () => void;
|
||||
loadSavedSounds: () => Promise<void>;
|
||||
saveSoundEffect: ({
|
||||
soundEffect,
|
||||
}: {
|
||||
soundEffect: SoundEffect;
|
||||
}) => Promise<void>;
|
||||
removeSavedSound: ({ soundId }: { soundId: number }) => Promise<void>;
|
||||
isSoundSaved: ({ soundId }: { soundId: number }) => boolean;
|
||||
toggleSavedSound: ({
|
||||
soundEffect,
|
||||
}: {
|
||||
soundEffect: SoundEffect;
|
||||
}) => Promise<void>;
|
||||
clearSavedSounds: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const useSoundsStore = create<SoundsStore>((set, get) => ({
|
||||
topSoundEffects: [],
|
||||
isLoading: false,
|
||||
error: null,
|
||||
hasLoaded: false,
|
||||
showCommercialOnly: true,
|
||||
|
||||
toggleCommercialFilter: () => {
|
||||
set((state) => ({ showCommercialOnly: !state.showCommercialOnly }));
|
||||
},
|
||||
|
||||
searchQuery: "",
|
||||
searchResults: [],
|
||||
isSearching: false,
|
||||
searchError: null,
|
||||
lastSearchQuery: "",
|
||||
scrollPosition: 0,
|
||||
currentPage: 1,
|
||||
hasNextPage: false,
|
||||
totalCount: 0,
|
||||
isLoadingMore: false,
|
||||
savedSounds: [],
|
||||
isSavedSoundsLoaded: false,
|
||||
isLoadingSavedSounds: false,
|
||||
savedSoundsError: null,
|
||||
|
||||
setTopSoundEffects: ({ sounds }) => set({ topSoundEffects: sounds }),
|
||||
setLoading: ({ loading }) => set({ isLoading: loading }),
|
||||
setError: ({ error }) => set({ error }),
|
||||
setHasLoaded: ({ loaded }) => set({ hasLoaded: loaded }),
|
||||
setSearchQuery: ({ query }) => set({ searchQuery: query }),
|
||||
setSearchResults: ({ results }) =>
|
||||
set({ searchResults: results, currentPage: 1 }),
|
||||
setSearching: ({ searching }) => set({ isSearching: searching }),
|
||||
setSearchError: ({ error }) => set({ searchError: error }),
|
||||
setLastSearchQuery: ({ query }) => set({ lastSearchQuery: query }),
|
||||
setScrollPosition: ({ position }) => set({ scrollPosition: position }),
|
||||
setCurrentPage: ({ page }) => set({ currentPage: page }),
|
||||
setHasNextPage: ({ hasNext }) => set({ hasNextPage: hasNext }),
|
||||
setTotalCount: ({ count }) => set({ totalCount: count }),
|
||||
setLoadingMore: ({ loading }) => set({ isLoadingMore: loading }),
|
||||
|
||||
appendSearchResults: ({ results }) =>
|
||||
set((state) => ({
|
||||
searchResults: [...state.searchResults, ...results],
|
||||
})),
|
||||
|
||||
appendTopSounds: ({ results }) =>
|
||||
set((state) => ({
|
||||
topSoundEffects: [...state.topSoundEffects, ...results],
|
||||
})),
|
||||
|
||||
resetPagination: () =>
|
||||
set({
|
||||
currentPage: 1,
|
||||
hasNextPage: false,
|
||||
totalCount: 0,
|
||||
isLoadingMore: false,
|
||||
}),
|
||||
|
||||
loadSavedSounds: async () => {
|
||||
if (get().isSavedSoundsLoaded) return;
|
||||
|
||||
try {
|
||||
set({ isLoadingSavedSounds: true, savedSoundsError: null });
|
||||
const savedSoundsData = await storageService.loadSavedSounds();
|
||||
set({
|
||||
savedSounds: savedSoundsData.sounds,
|
||||
isSavedSoundsLoaded: true,
|
||||
isLoadingSavedSounds: false,
|
||||
});
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : "Failed to load saved sounds";
|
||||
set({
|
||||
savedSoundsError: errorMessage,
|
||||
isLoadingSavedSounds: false,
|
||||
});
|
||||
console.error("Failed to load saved sounds:", error);
|
||||
}
|
||||
},
|
||||
|
||||
saveSoundEffect: async ({ soundEffect }) => {
|
||||
try {
|
||||
await storageService.saveSoundEffect({ soundEffect });
|
||||
|
||||
const savedSoundsData = await storageService.loadSavedSounds();
|
||||
set({ savedSounds: savedSoundsData.sounds });
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : "Failed to save sound";
|
||||
set({ savedSoundsError: errorMessage });
|
||||
toast.error("Failed to save sound");
|
||||
console.error("Failed to save sound:", error);
|
||||
}
|
||||
},
|
||||
|
||||
removeSavedSound: async ({ soundId }) => {
|
||||
try {
|
||||
await storageService.removeSavedSound({ soundId });
|
||||
|
||||
set((state) => ({
|
||||
savedSounds: state.savedSounds.filter((sound) => sound.id !== soundId),
|
||||
}));
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : "Failed to remove sound";
|
||||
set({ savedSoundsError: errorMessage });
|
||||
toast.error("Failed to remove sound");
|
||||
console.error("Failed to remove sound:", error);
|
||||
}
|
||||
},
|
||||
|
||||
isSoundSaved: ({ soundId }) => {
|
||||
const { savedSounds } = get();
|
||||
return savedSounds.some((sound) => sound.id === soundId);
|
||||
},
|
||||
|
||||
toggleSavedSound: async ({ soundEffect }) => {
|
||||
const { isSoundSaved, saveSoundEffect, removeSavedSound } = get();
|
||||
|
||||
if (isSoundSaved({ soundId: soundEffect.id })) {
|
||||
await removeSavedSound({ soundId: soundEffect.id });
|
||||
} else {
|
||||
await saveSoundEffect({ soundEffect });
|
||||
}
|
||||
},
|
||||
|
||||
clearSavedSounds: async () => {
|
||||
try {
|
||||
await storageService.clearSavedSounds();
|
||||
set({
|
||||
savedSounds: [],
|
||||
savedSoundsError: null,
|
||||
});
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : "Failed to clear saved sounds";
|
||||
set({ savedSoundsError: errorMessage });
|
||||
toast.error("Failed to clear saved sounds");
|
||||
console.error("Failed to clear saved sounds:", error);
|
||||
}
|
||||
},
|
||||
|
||||
addSoundToTimeline: async ({ sound }) => {
|
||||
const audioUrl = sound.previewUrl;
|
||||
if (!audioUrl) {
|
||||
toast.error("Sound file not available");
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const editor = EditorCore.getInstance();
|
||||
const currentTime = editor.playback.getCurrentTime();
|
||||
const tracks = editor.timeline.getTracks();
|
||||
|
||||
const response = await fetch(audioUrl);
|
||||
if (!response.ok)
|
||||
throw new Error(`Failed to download audio: ${response.statusText}`);
|
||||
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
const audioContext = new AudioContext();
|
||||
const buffer = await audioContext.decodeAudioData(arrayBuffer);
|
||||
|
||||
const audioTrack = tracks.find((t) => t.type === "audio");
|
||||
let trackId: string;
|
||||
|
||||
if (audioTrack) {
|
||||
trackId = audioTrack.id;
|
||||
} else {
|
||||
trackId = editor.timeline.addTrack({ type: "audio" });
|
||||
}
|
||||
|
||||
const element = buildLibraryAudioElement({
|
||||
sourceUrl: audioUrl,
|
||||
name: sound.name,
|
||||
duration: sound.duration,
|
||||
startTime: currentTime,
|
||||
buffer,
|
||||
});
|
||||
|
||||
editor.timeline.insertElement({
|
||||
placement: { mode: "explicit", trackId },
|
||||
element,
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Failed to add sound to timeline:", error);
|
||||
toast.error(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Failed to add sound to timeline",
|
||||
{ id: `sound-${sound.id}` },
|
||||
);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
}));
|
||||
import { create } from "zustand";
|
||||
import type { SoundEffect, SavedSound } from "@/lib/sounds/types";
|
||||
import { storageService } from "@/services/storage/service";
|
||||
import { toast } from "sonner";
|
||||
import { EditorCore } from "@/core";
|
||||
import { buildLibraryAudioElement } from "@/lib/timeline/element-utils";
|
||||
|
||||
interface SoundsStore {
|
||||
topSoundEffects: SoundEffect[];
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
hasLoaded: boolean;
|
||||
showCommercialOnly: boolean;
|
||||
toggleCommercialFilter: () => void;
|
||||
searchQuery: string;
|
||||
searchResults: SoundEffect[];
|
||||
isSearching: boolean;
|
||||
searchError: string | null;
|
||||
lastSearchQuery: string;
|
||||
scrollPosition: number;
|
||||
currentPage: number;
|
||||
hasNextPage: boolean;
|
||||
totalCount: number;
|
||||
isLoadingMore: boolean;
|
||||
savedSounds: SavedSound[];
|
||||
isSavedSoundsLoaded: boolean;
|
||||
isLoadingSavedSounds: boolean;
|
||||
savedSoundsError: string | null;
|
||||
|
||||
addSoundToTimeline: ({ sound }: { sound: SoundEffect }) => Promise<boolean>;
|
||||
setTopSoundEffects: ({ sounds }: { sounds: SoundEffect[] }) => void;
|
||||
setLoading: ({ loading }: { loading: boolean }) => void;
|
||||
setError: ({ error }: { error: string | null }) => void;
|
||||
setHasLoaded: ({ loaded }: { loaded: boolean }) => void;
|
||||
setSearchQuery: ({ query }: { query: string }) => void;
|
||||
setSearchResults: ({ results }: { results: SoundEffect[] }) => void;
|
||||
setSearching: ({ searching }: { searching: boolean }) => void;
|
||||
setSearchError: ({ error }: { error: string | null }) => void;
|
||||
setLastSearchQuery: ({ query }: { query: string }) => void;
|
||||
setScrollPosition: ({ position }: { position: number }) => void;
|
||||
setCurrentPage: ({ page }: { page: number }) => void;
|
||||
setHasNextPage: ({ hasNext }: { hasNext: boolean }) => void;
|
||||
setTotalCount: ({ count }: { count: number }) => void;
|
||||
setLoadingMore: ({ loading }: { loading: boolean }) => void;
|
||||
appendSearchResults: ({ results }: { results: SoundEffect[] }) => void;
|
||||
appendTopSounds: ({ results }: { results: SoundEffect[] }) => void;
|
||||
resetPagination: () => void;
|
||||
loadSavedSounds: () => Promise<void>;
|
||||
saveSoundEffect: ({
|
||||
soundEffect,
|
||||
}: {
|
||||
soundEffect: SoundEffect;
|
||||
}) => Promise<void>;
|
||||
removeSavedSound: ({ soundId }: { soundId: number }) => Promise<void>;
|
||||
isSoundSaved: ({ soundId }: { soundId: number }) => boolean;
|
||||
toggleSavedSound: ({
|
||||
soundEffect,
|
||||
}: {
|
||||
soundEffect: SoundEffect;
|
||||
}) => Promise<void>;
|
||||
clearSavedSounds: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const useSoundsStore = create<SoundsStore>((set, get) => ({
|
||||
topSoundEffects: [],
|
||||
isLoading: false,
|
||||
error: null,
|
||||
hasLoaded: false,
|
||||
showCommercialOnly: true,
|
||||
|
||||
toggleCommercialFilter: () => {
|
||||
set((state) => ({ showCommercialOnly: !state.showCommercialOnly }));
|
||||
},
|
||||
|
||||
searchQuery: "",
|
||||
searchResults: [],
|
||||
isSearching: false,
|
||||
searchError: null,
|
||||
lastSearchQuery: "",
|
||||
scrollPosition: 0,
|
||||
currentPage: 1,
|
||||
hasNextPage: false,
|
||||
totalCount: 0,
|
||||
isLoadingMore: false,
|
||||
savedSounds: [],
|
||||
isSavedSoundsLoaded: false,
|
||||
isLoadingSavedSounds: false,
|
||||
savedSoundsError: null,
|
||||
|
||||
setTopSoundEffects: ({ sounds }) => set({ topSoundEffects: sounds }),
|
||||
setLoading: ({ loading }) => set({ isLoading: loading }),
|
||||
setError: ({ error }) => set({ error }),
|
||||
setHasLoaded: ({ loaded }) => set({ hasLoaded: loaded }),
|
||||
setSearchQuery: ({ query }) => set({ searchQuery: query }),
|
||||
setSearchResults: ({ results }) =>
|
||||
set({ searchResults: results, currentPage: 1 }),
|
||||
setSearching: ({ searching }) => set({ isSearching: searching }),
|
||||
setSearchError: ({ error }) => set({ searchError: error }),
|
||||
setLastSearchQuery: ({ query }) => set({ lastSearchQuery: query }),
|
||||
setScrollPosition: ({ position }) => set({ scrollPosition: position }),
|
||||
setCurrentPage: ({ page }) => set({ currentPage: page }),
|
||||
setHasNextPage: ({ hasNext }) => set({ hasNextPage: hasNext }),
|
||||
setTotalCount: ({ count }) => set({ totalCount: count }),
|
||||
setLoadingMore: ({ loading }) => set({ isLoadingMore: loading }),
|
||||
|
||||
appendSearchResults: ({ results }) =>
|
||||
set((state) => ({
|
||||
searchResults: [...state.searchResults, ...results],
|
||||
})),
|
||||
|
||||
appendTopSounds: ({ results }) =>
|
||||
set((state) => ({
|
||||
topSoundEffects: [...state.topSoundEffects, ...results],
|
||||
})),
|
||||
|
||||
resetPagination: () =>
|
||||
set({
|
||||
currentPage: 1,
|
||||
hasNextPage: false,
|
||||
totalCount: 0,
|
||||
isLoadingMore: false,
|
||||
}),
|
||||
|
||||
loadSavedSounds: async () => {
|
||||
if (get().isSavedSoundsLoaded) return;
|
||||
|
||||
try {
|
||||
set({ isLoadingSavedSounds: true, savedSoundsError: null });
|
||||
const savedSoundsData = await storageService.loadSavedSounds();
|
||||
set({
|
||||
savedSounds: savedSoundsData.sounds,
|
||||
isSavedSoundsLoaded: true,
|
||||
isLoadingSavedSounds: false,
|
||||
});
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : "Failed to load saved sounds";
|
||||
set({
|
||||
savedSoundsError: errorMessage,
|
||||
isLoadingSavedSounds: false,
|
||||
});
|
||||
console.error("Failed to load saved sounds:", error);
|
||||
}
|
||||
},
|
||||
|
||||
saveSoundEffect: async ({ soundEffect }) => {
|
||||
try {
|
||||
await storageService.saveSoundEffect({ soundEffect });
|
||||
|
||||
const savedSoundsData = await storageService.loadSavedSounds();
|
||||
set({ savedSounds: savedSoundsData.sounds });
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : "Failed to save sound";
|
||||
set({ savedSoundsError: errorMessage });
|
||||
toast.error("Failed to save sound");
|
||||
console.error("Failed to save sound:", error);
|
||||
}
|
||||
},
|
||||
|
||||
removeSavedSound: async ({ soundId }) => {
|
||||
try {
|
||||
await storageService.removeSavedSound({ soundId });
|
||||
|
||||
set((state) => ({
|
||||
savedSounds: state.savedSounds.filter((sound) => sound.id !== soundId),
|
||||
}));
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : "Failed to remove sound";
|
||||
set({ savedSoundsError: errorMessage });
|
||||
toast.error("Failed to remove sound");
|
||||
console.error("Failed to remove sound:", error);
|
||||
}
|
||||
},
|
||||
|
||||
isSoundSaved: ({ soundId }) => {
|
||||
const { savedSounds } = get();
|
||||
return savedSounds.some((sound) => sound.id === soundId);
|
||||
},
|
||||
|
||||
toggleSavedSound: async ({ soundEffect }) => {
|
||||
const { isSoundSaved, saveSoundEffect, removeSavedSound } = get();
|
||||
|
||||
if (isSoundSaved({ soundId: soundEffect.id })) {
|
||||
await removeSavedSound({ soundId: soundEffect.id });
|
||||
} else {
|
||||
await saveSoundEffect({ soundEffect });
|
||||
}
|
||||
},
|
||||
|
||||
clearSavedSounds: async () => {
|
||||
try {
|
||||
await storageService.clearSavedSounds();
|
||||
set({
|
||||
savedSounds: [],
|
||||
savedSoundsError: null,
|
||||
});
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : "Failed to clear saved sounds";
|
||||
set({ savedSoundsError: errorMessage });
|
||||
toast.error("Failed to clear saved sounds");
|
||||
console.error("Failed to clear saved sounds:", error);
|
||||
}
|
||||
},
|
||||
|
||||
addSoundToTimeline: async ({ sound }) => {
|
||||
const audioUrl = sound.previewUrl;
|
||||
if (!audioUrl) {
|
||||
toast.error("Sound file not available");
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const editor = EditorCore.getInstance();
|
||||
const currentTime = editor.playback.getCurrentTime();
|
||||
const tracks = editor.timeline.getTracks();
|
||||
|
||||
const response = await fetch(audioUrl);
|
||||
if (!response.ok)
|
||||
throw new Error(`Failed to download audio: ${response.statusText}`);
|
||||
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
const audioContext = new AudioContext();
|
||||
const buffer = await audioContext.decodeAudioData(arrayBuffer);
|
||||
|
||||
const audioTrack = tracks.find((t) => t.type === "audio");
|
||||
let trackId: string;
|
||||
|
||||
if (audioTrack) {
|
||||
trackId = audioTrack.id;
|
||||
} else {
|
||||
trackId = editor.timeline.addTrack({ type: "audio" });
|
||||
}
|
||||
|
||||
const element = buildLibraryAudioElement({
|
||||
sourceUrl: audioUrl,
|
||||
name: sound.name,
|
||||
duration: sound.duration,
|
||||
startTime: currentTime,
|
||||
buffer,
|
||||
});
|
||||
|
||||
editor.timeline.insertElement({
|
||||
placement: { mode: "explicit", trackId },
|
||||
element,
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Failed to add sound to timeline:", error);
|
||||
toast.error(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Failed to add sound to timeline",
|
||||
{ id: `sound-${sound.id}` },
|
||||
);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -1,56 +1,56 @@
|
||||
/**
|
||||
* UI state for the timeline
|
||||
* For core logic, use EditorCore instead.
|
||||
*/
|
||||
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
import type { ClipboardItem } from "@/lib/timeline";
|
||||
|
||||
interface TimelineStore {
|
||||
snappingEnabled: boolean;
|
||||
toggleSnapping: () => void;
|
||||
rippleEditingEnabled: boolean;
|
||||
toggleRippleEditing: () => void;
|
||||
clipboard: {
|
||||
items: ClipboardItem[];
|
||||
} | null;
|
||||
setClipboard: (
|
||||
clipboard: {
|
||||
items: ClipboardItem[];
|
||||
} | null,
|
||||
) => void;
|
||||
}
|
||||
|
||||
export const useTimelineStore = create<TimelineStore>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
snappingEnabled: true,
|
||||
|
||||
toggleSnapping: () => {
|
||||
set((state) => ({ snappingEnabled: !state.snappingEnabled }));
|
||||
},
|
||||
|
||||
rippleEditingEnabled: false,
|
||||
|
||||
toggleRippleEditing: () => {
|
||||
set((state) => ({
|
||||
rippleEditingEnabled: !state.rippleEditingEnabled,
|
||||
}));
|
||||
},
|
||||
|
||||
clipboard: null,
|
||||
|
||||
setClipboard: (clipboard) => {
|
||||
set({ clipboard });
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: "timeline-store",
|
||||
partialize: (state) => ({
|
||||
snappingEnabled: state.snappingEnabled,
|
||||
rippleEditingEnabled: state.rippleEditingEnabled,
|
||||
}),
|
||||
},
|
||||
),
|
||||
);
|
||||
/**
|
||||
* UI state for the timeline
|
||||
* For core logic, use EditorCore instead.
|
||||
*/
|
||||
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
import type { ClipboardItem } from "@/lib/timeline";
|
||||
|
||||
interface TimelineStore {
|
||||
snappingEnabled: boolean;
|
||||
toggleSnapping: () => void;
|
||||
rippleEditingEnabled: boolean;
|
||||
toggleRippleEditing: () => void;
|
||||
clipboard: {
|
||||
items: ClipboardItem[];
|
||||
} | null;
|
||||
setClipboard: (
|
||||
clipboard: {
|
||||
items: ClipboardItem[];
|
||||
} | null,
|
||||
) => void;
|
||||
}
|
||||
|
||||
export const useTimelineStore = create<TimelineStore>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
snappingEnabled: true,
|
||||
|
||||
toggleSnapping: () => {
|
||||
set((state) => ({ snappingEnabled: !state.snappingEnabled }));
|
||||
},
|
||||
|
||||
rippleEditingEnabled: false,
|
||||
|
||||
toggleRippleEditing: () => {
|
||||
set((state) => ({
|
||||
rippleEditingEnabled: !state.rippleEditingEnabled,
|
||||
}));
|
||||
},
|
||||
|
||||
clipboard: null,
|
||||
|
||||
setClipboard: (clipboard) => {
|
||||
set({ clipboard });
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: "timeline-store",
|
||||
partialize: (state) => ({
|
||||
snappingEnabled: state.snappingEnabled,
|
||||
rippleEditingEnabled: state.rippleEditingEnabled,
|
||||
}),
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user