mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
feat: masks, properties refactor, shaders, storage migrations, and more
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { create } from "zustand";
|
||||
import { DEFAULT_CANVAS_PRESETS } from "@/constants/project-constants";
|
||||
import type { TCanvasSize } from "@/types/project";
|
||||
import type { TCanvasSize } from "@/lib/project/types";
|
||||
|
||||
interface EditorState {
|
||||
isInitializing: boolean;
|
||||
|
||||
@@ -6,10 +6,10 @@ 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 "@/types/keybinding";
|
||||
import type { KeybindingConfig, ShortcutKey } from "@/lib/actions/keybinding";
|
||||
import { runMigrations, CURRENT_VERSION } from "./keybindings/migrations";
|
||||
|
||||
export const defaultKeybindings: KeybindingConfig = getDefaultShortcuts();
|
||||
const defaultKeybindings: KeybindingConfig = getDefaultShortcuts();
|
||||
|
||||
export interface KeybindingConflict {
|
||||
key: ShortcutKey;
|
||||
@@ -20,7 +20,9 @@ export interface KeybindingConflict {
|
||||
interface KeybindingsState {
|
||||
keybindings: KeybindingConfig;
|
||||
isCustomized: boolean;
|
||||
keybindingsEnabled: boolean;
|
||||
overlayDepth: number;
|
||||
openOverlayIds: string[];
|
||||
isLoadingProject: boolean;
|
||||
isRecording: boolean;
|
||||
|
||||
updateKeybinding: (key: ShortcutKey, action: TActionWithOptionalArgs) => void;
|
||||
@@ -28,8 +30,9 @@ interface KeybindingsState {
|
||||
resetToDefaults: () => void;
|
||||
importKeybindings: (config: KeybindingConfig) => void;
|
||||
exportKeybindings: () => KeybindingConfig;
|
||||
enableKeybindings: () => void;
|
||||
disableKeybindings: () => void;
|
||||
openOverlay: (overlayId: string, source: string) => void;
|
||||
closeOverlay: (overlayId: string, source: string) => void;
|
||||
setLoadingProject: (loading: boolean) => void;
|
||||
setIsRecording: (isRecording: boolean) => void;
|
||||
validateKeybinding: (
|
||||
key: ShortcutKey,
|
||||
@@ -48,9 +51,112 @@ export const useKeybindingsStore = create<KeybindingsState>()(
|
||||
(set, get) => ({
|
||||
keybindings: { ...defaultKeybindings },
|
||||
isCustomized: false,
|
||||
keybindingsEnabled: true,
|
||||
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 };
|
||||
@@ -82,17 +188,9 @@ export const useKeybindingsStore = create<KeybindingsState>()(
|
||||
});
|
||||
},
|
||||
|
||||
enableKeybindings: () => {
|
||||
set({ keybindingsEnabled: true });
|
||||
},
|
||||
|
||||
disableKeybindings: () => {
|
||||
set({ keybindingsEnabled: false });
|
||||
},
|
||||
|
||||
importKeybindings: (config: KeybindingConfig) => {
|
||||
for (const [key] of Object.entries(config)) {
|
||||
if (typeof key !== "string" || key.length === 0) {
|
||||
importKeybindings: (config: KeybindingConfig) => {
|
||||
for (const [key] of Object.entries(config)) {
|
||||
if (typeof key !== "string" || key.length === 0) {
|
||||
throw new Error(`Invalid key format: ${key}`);
|
||||
}
|
||||
}
|
||||
@@ -124,6 +222,27 @@ export const useKeybindingsStore = create<KeybindingsState>()(
|
||||
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 });
|
||||
},
|
||||
|
||||
@@ -169,7 +288,10 @@ function generateKeybindingString(ev: KeyboardEvent): ShortcutKey | null {
|
||||
return `${modifierKey}+${key}` as ShortcutKey;
|
||||
}
|
||||
|
||||
if (isDOMElement(target) && isTypableDOMElement({ element: target as HTMLElement }))
|
||||
if (
|
||||
isDOMElement(target) &&
|
||||
isTypableDOMElement({ element: target as HTMLElement })
|
||||
)
|
||||
return null;
|
||||
|
||||
return `${key}` as ShortcutKey;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { v2ToV3 } from "./v2-to-v3";
|
||||
import { v3ToV4 } from "./v3-to-v4";
|
||||
import { v4ToV5 } from "./v4-to-v5";
|
||||
import { v5ToV6 } from "./v5-to-v6";
|
||||
|
||||
type MigrationFn = ({ state }: { state: unknown }) => unknown;
|
||||
|
||||
@@ -8,9 +9,10 @@ const migrations: Record<number, MigrationFn> = {
|
||||
2: v2ToV3,
|
||||
3: v3ToV4,
|
||||
4: v4ToV5,
|
||||
5: v5ToV6,
|
||||
};
|
||||
|
||||
export const CURRENT_VERSION = 5;
|
||||
export const CURRENT_VERSION = 6;
|
||||
|
||||
export function runMigrations({
|
||||
state,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { KeybindingConfig, ShortcutKey } from "@/types/keybinding";
|
||||
import type { KeybindingConfig, ShortcutKey } from "@/lib/actions/keybinding";
|
||||
import type { TActionWithOptionalArgs } from "@/lib/actions";
|
||||
|
||||
interface V2State {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { TActionWithOptionalArgs } from "@/lib/actions";
|
||||
import type { ShortcutKey } from "@/types/keybinding";
|
||||
import type { KeybindingConfig } from "@/types/keybinding";
|
||||
import type { ShortcutKey } from "@/lib/actions/keybinding";
|
||||
import type { KeybindingConfig } from "@/lib/actions/keybinding";
|
||||
|
||||
interface V3State {
|
||||
keybindings: KeybindingConfig;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { KeybindingConfig } from "@/types/keybinding";
|
||||
import type { KeybindingConfig } from "@/lib/actions/keybinding";
|
||||
|
||||
interface V4State {
|
||||
keybindings: KeybindingConfig;
|
||||
|
||||
@@ -0,0 +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 };
|
||||
}
|
||||
@@ -1,20 +1,28 @@
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
import type { TPlatformLayout } from "@/types/editor";
|
||||
|
||||
interface LayoutGuideSettings {
|
||||
platform: TPlatformLayout | null;
|
||||
}
|
||||
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 {
|
||||
layoutGuide: LayoutGuideSettings;
|
||||
activeGuide: GuideId | null;
|
||||
overlays: PreviewOverlaysState;
|
||||
setLayoutGuide: (settings: Partial<LayoutGuideSettings>) => void;
|
||||
toggleLayoutGuide: (platform: TPlatformLayout) => void;
|
||||
gridConfig: GridConfig;
|
||||
toggleGuide: (guideId: GuideId) => void;
|
||||
setGridConfig: (config: Partial<GridConfig>) => void;
|
||||
setOverlayVisibility: ({
|
||||
overlay,
|
||||
isVisible,
|
||||
@@ -33,24 +41,33 @@ 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) => ({
|
||||
layoutGuide: { platform: null },
|
||||
activeGuide: null,
|
||||
overlays: DEFAULT_PREVIEW_OVERLAYS,
|
||||
setLayoutGuide: (settings) => {
|
||||
gridConfig: DEFAULT_GRID_CONFIG,
|
||||
toggleGuide: (guideId) => {
|
||||
set((state) => ({
|
||||
layoutGuide: {
|
||||
...state.layoutGuide,
|
||||
...settings,
|
||||
},
|
||||
activeGuide: state.activeGuide === guideId ? null : guideId,
|
||||
}));
|
||||
},
|
||||
toggleLayoutGuide: (platform) => {
|
||||
setGridConfig: (config) => {
|
||||
set((state) => ({
|
||||
layoutGuide: {
|
||||
platform: state.layoutGuide.platform === platform ? null : platform,
|
||||
},
|
||||
gridConfig: { ...state.gridConfig, ...config },
|
||||
}));
|
||||
},
|
||||
setOverlayVisibility: ({ overlay, isVisible }) => {
|
||||
@@ -72,22 +89,23 @@ export const usePreviewStore = create<PreviewState>()(
|
||||
}),
|
||||
{
|
||||
name: "preview-settings",
|
||||
version: 2,
|
||||
version: 4,
|
||||
migrate: (persistedState) => {
|
||||
const state = persistedState as
|
||||
| {
|
||||
layoutGuide?: LayoutGuideSettings;
|
||||
overlays?: PreviewOverlaysState;
|
||||
}
|
||||
| undefined;
|
||||
const state = persistedState as PersistedPreviewState | undefined;
|
||||
|
||||
return {
|
||||
layoutGuide: state?.layoutGuide ?? { platform: null },
|
||||
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) => ({
|
||||
layoutGuide: state.layoutGuide,
|
||||
activeGuide: state.activeGuide,
|
||||
overlays: state.overlays,
|
||||
gridConfig: state.gridConfig,
|
||||
}),
|
||||
},
|
||||
),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { create } from "zustand";
|
||||
import type { SoundEffect, SavedSound } from "@/types/sounds";
|
||||
import type { SoundEffect, SavedSound } from "@/lib/sounds/types";
|
||||
import { storageService } from "@/services/storage/service";
|
||||
import { toast } from "sonner";
|
||||
import { EditorCore } from "@/core";
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
import { EditorCore } from "@/core";
|
||||
import { searchStickers as searchStickersFromProviders } from "@/lib/stickers";
|
||||
import type { StickerSearchResult } from "@/lib/stickers";
|
||||
import { buildStickerElement } from "@/lib/timeline/element-utils";
|
||||
import {
|
||||
browseAll,
|
||||
browseCategory,
|
||||
searchAll,
|
||||
searchStickers as searchStickersFromProviders,
|
||||
} from "@/lib/stickers";
|
||||
import type { StickerBrowseResult, StickerSearchResult } from "@/lib/stickers";
|
||||
import { STICKER_CATEGORIES } from "@/constants/sticker-constants";
|
||||
import type { StickerCategory } from "@/types/stickers";
|
||||
import type { StickerCategory } from "@/lib/stickers/types";
|
||||
import { registerDefaultStickerProviders } from "@/lib/stickers/providers";
|
||||
import { hasProvider } from "@/lib/stickers/registry";
|
||||
import { stickersRegistry } from "@/lib/stickers/registry";
|
||||
import { parseStickerId } from "@/lib/stickers/sticker-id";
|
||||
|
||||
const MAX_RECENT_STICKERS = 50;
|
||||
let browseRequestVersion = 0;
|
||||
|
||||
function isValidStickerId(value: unknown): value is string {
|
||||
if (typeof value !== "string") {
|
||||
@@ -19,7 +23,7 @@ function isValidStickerId(value: unknown): value is string {
|
||||
|
||||
try {
|
||||
const parsed = parseStickerId({ stickerId: value });
|
||||
return hasProvider({ providerId: parsed.providerId });
|
||||
return stickersRegistry.has(parsed.providerId);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
@@ -60,20 +64,15 @@ interface StickersStore {
|
||||
selectedCategory: StickerCategory;
|
||||
viewMode: ViewMode;
|
||||
searchResults: StickerSearchResult | null;
|
||||
browseContent: StickerBrowseResult | null;
|
||||
recentStickers: string[];
|
||||
isSearching: boolean;
|
||||
addingSticker: string | null;
|
||||
isBrowsing: boolean;
|
||||
|
||||
setSearchQuery: ({ query }: { query: string }) => void;
|
||||
setSelectedCategory: ({ category }: { category: StickerCategory }) => void;
|
||||
searchStickers: ({ query }: { query: string }) => Promise<void>;
|
||||
addStickerToTimeline: ({
|
||||
stickerId,
|
||||
name,
|
||||
}: {
|
||||
stickerId: string;
|
||||
name?: string;
|
||||
}) => void;
|
||||
browseStickers: () => Promise<void>;
|
||||
addToRecentStickers: ({ stickerId }: { stickerId: string }) => void;
|
||||
clearRecentStickers: () => void;
|
||||
}
|
||||
@@ -86,22 +85,34 @@ export const useStickersStore = create<StickersStore>()(
|
||||
viewMode: "browse",
|
||||
|
||||
searchResults: null,
|
||||
browseContent: null,
|
||||
recentStickers: [],
|
||||
|
||||
isSearching: false,
|
||||
addingSticker: null,
|
||||
isBrowsing: false,
|
||||
|
||||
setSearchQuery: ({ query }) => set({ searchQuery: query }),
|
||||
|
||||
setSelectedCategory: ({ category }) =>
|
||||
set({
|
||||
selectedCategory: category in STICKER_CATEGORIES ? category : "all",
|
||||
viewMode: "browse",
|
||||
}),
|
||||
setSelectedCategory: ({ category }) => {
|
||||
set({
|
||||
selectedCategory: category in STICKER_CATEGORIES ? category : "all",
|
||||
browseContent: null,
|
||||
});
|
||||
|
||||
const query = get().searchQuery.trim();
|
||||
if (query) {
|
||||
void get().searchStickers({ query });
|
||||
return;
|
||||
}
|
||||
|
||||
void get().browseStickers();
|
||||
},
|
||||
|
||||
searchStickers: async ({ query }: { query: string }) => {
|
||||
if (!query.trim()) {
|
||||
const trimmedQuery = query.trim();
|
||||
if (!trimmedQuery) {
|
||||
set({ searchResults: null, viewMode: "browse" });
|
||||
await get().browseStickers();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -111,12 +122,17 @@ export const useStickersStore = create<StickersStore>()(
|
||||
|
||||
set({ isSearching: true, viewMode: "search" });
|
||||
try {
|
||||
const results = await searchStickersFromProviders({
|
||||
query,
|
||||
category: selectedCategory,
|
||||
limit: 100,
|
||||
});
|
||||
set({ searchResults: results });
|
||||
if (selectedCategory === "all") {
|
||||
const browseContent = await searchAll({ query: trimmedQuery });
|
||||
set({ browseContent, searchResults: null });
|
||||
} else {
|
||||
const results = await searchStickersFromProviders({
|
||||
query: trimmedQuery,
|
||||
category: selectedCategory,
|
||||
limit: 100,
|
||||
});
|
||||
set({ searchResults: results });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Search failed:", error);
|
||||
set({ searchResults: null });
|
||||
@@ -125,67 +141,70 @@ export const useStickersStore = create<StickersStore>()(
|
||||
}
|
||||
},
|
||||
|
||||
addStickerToTimeline: ({
|
||||
stickerId,
|
||||
name,
|
||||
}: {
|
||||
stickerId: string;
|
||||
name?: string;
|
||||
}) => {
|
||||
set({ addingSticker: stickerId });
|
||||
try {
|
||||
const editor = EditorCore.getInstance();
|
||||
const currentTime = editor.playback.getCurrentTime();
|
||||
const tracks = editor.timeline.getTracks();
|
||||
browseStickers: async () => {
|
||||
const version = ++browseRequestVersion;
|
||||
const category = get().selectedCategory;
|
||||
const selectedCategory =
|
||||
category in STICKER_CATEGORIES ? category : "all";
|
||||
|
||||
const stickerTrack = tracks.find((t) => t.type === "sticker");
|
||||
let trackId: string;
|
||||
set({ isBrowsing: true, viewMode: "browse" });
|
||||
try {
|
||||
const browseContent =
|
||||
selectedCategory === "all"
|
||||
? await browseAll({
|
||||
recentStickers: get().recentStickers,
|
||||
})
|
||||
: await browseCategory({
|
||||
category: selectedCategory,
|
||||
});
|
||||
|
||||
if (stickerTrack) {
|
||||
trackId = stickerTrack.id;
|
||||
} else {
|
||||
trackId = editor.timeline.addTrack({ type: "sticker" });
|
||||
}
|
||||
if (version !== browseRequestVersion) return;
|
||||
set({ browseContent });
|
||||
} catch (error) {
|
||||
if (version !== browseRequestVersion) return;
|
||||
console.error("Browse failed:", error);
|
||||
set({ browseContent: null });
|
||||
} finally {
|
||||
if (version === browseRequestVersion) {
|
||||
set({ isBrowsing: false });
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
const element = buildStickerElement({
|
||||
stickerId,
|
||||
name,
|
||||
startTime: currentTime,
|
||||
});
|
||||
editor.timeline.insertElement({
|
||||
placement: { mode: "explicit", trackId },
|
||||
element,
|
||||
});
|
||||
addToRecentStickers: ({ stickerId }: { stickerId: string }) => {
|
||||
const sanitizedStickerIds = sanitizeRecentStickers({
|
||||
recentStickers: [stickerId],
|
||||
});
|
||||
if (sanitizedStickerIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
get().addToRecentStickers({ stickerId });
|
||||
} finally {
|
||||
set({ addingSticker: null });
|
||||
set((state) => {
|
||||
const recent = [
|
||||
sanitizedStickerIds[0],
|
||||
...state.recentStickers.filter((s) => s !== sanitizedStickerIds[0]),
|
||||
];
|
||||
return {
|
||||
recentStickers: recent.slice(0, MAX_RECENT_STICKERS),
|
||||
};
|
||||
});
|
||||
|
||||
if (get().viewMode === "browse" && get().selectedCategory === "all") {
|
||||
void get().browseStickers();
|
||||
}
|
||||
},
|
||||
|
||||
clearRecentStickers: () => {
|
||||
set({ recentStickers: [] });
|
||||
|
||||
if (get().viewMode === "browse" && get().selectedCategory === "all") {
|
||||
void get().browseStickers();
|
||||
}
|
||||
},
|
||||
|
||||
addToRecentStickers: ({ stickerId }: { stickerId: string }) => {
|
||||
const sanitizedStickerIds = sanitizeRecentStickers({
|
||||
recentStickers: [stickerId],
|
||||
});
|
||||
if (sanitizedStickerIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
set((state) => {
|
||||
const recent = [
|
||||
sanitizedStickerIds[0],
|
||||
...state.recentStickers.filter((s) => s !== sanitizedStickerIds[0]),
|
||||
];
|
||||
return {
|
||||
recentStickers: recent.slice(0, MAX_RECENT_STICKERS),
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
clearRecentStickers: () => set({ recentStickers: [] }),
|
||||
}),
|
||||
{
|
||||
name: "stickers-settings",
|
||||
version: 1,
|
||||
migrate: (persistedState) => {
|
||||
if (
|
||||
typeof persistedState === "object" &&
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
import type { ClipboardItem } from "@/types/timeline";
|
||||
import type { ClipboardItem } from "@/lib/timeline";
|
||||
|
||||
interface TimelineStore {
|
||||
snappingEnabled: boolean;
|
||||
|
||||
Reference in New Issue
Block a user