refactor(core): switch to scene-based architecture

This commit is contained in:
Maze Winther
2025-09-01 16:17:06 +02:00
parent 2b700b7474
commit 350b55081c
15 changed files with 793 additions and 164 deletions
-29
View File
@@ -1,29 +0,0 @@
export async function getStars(): Promise<string> {
try {
const res = await fetch(
"https://api.github.com/repos/OpenCut-app/OpenCut",
{
next: { revalidate: 3600 },
}
);
if (!res.ok) {
throw new Error(`GitHub API error: ${res.status} ${res.statusText}`);
}
const data = (await res.json()) as { stargazers_count: number };
const count = data.stargazers_count;
if (typeof count !== "number") {
throw new Error("Invalid stargazers_count from GitHub API");
}
if (count >= 1_000_000)
return (count / 1_000_000).toFixed(1).replace(/\.0$/, "") + "M";
if (count >= 1000)
return (count / 1000).toFixed(1).replace(/\.0$/, "") + "k";
return count.toString();
} catch (error) {
console.error("Failed to fetch GitHub stars:", error);
return "1.5k";
}
}
+122 -37
View File
@@ -6,6 +6,7 @@ import {
MediaFileData,
StorageConfig,
SerializedProject,
SerializedScene,
TimelineData,
} from "./types";
import { TimelineTrack } from "@/types/timeline";
@@ -39,7 +40,7 @@ class StorageService {
}
// Helper to get project-specific media adapters
private getProjectMediaAdapters(projectId: string) {
private getProjectMediaAdapters({ projectId }: { projectId: string }) {
const mediaMetadataAdapter = new IndexedDBAdapter<MediaFileData>(
`${this.config.mediaDb}-${projectId}`,
"media-metadata",
@@ -52,23 +53,43 @@ class StorageService {
}
// Helper to get project-specific timeline adapter
private getProjectTimelineAdapter(projectId: string) {
private getProjectTimelineAdapter({
projectId,
sceneId,
}: {
projectId: string;
sceneId?: string;
}) {
const dbName = sceneId
? `${this.config.timelineDb}-${projectId}-${sceneId}`
: `${this.config.timelineDb}-${projectId}`;
return new IndexedDBAdapter<TimelineData>(
`${this.config.timelineDb}-${projectId}`,
dbName,
"timeline",
this.config.version
);
}
// Project operations
async saveProject(project: TProject): Promise<void> {
async saveProject({ project }: { project: TProject }): Promise<void> {
// Convert TProject to serializable format
const serializedScenes: SerializedScene[] = project.scenes.map((scene) => ({
id: scene.id,
name: scene.name,
isMain: scene.isMain,
createdAt: scene.createdAt.toISOString(),
updatedAt: scene.updatedAt.toISOString(),
}));
const serializedProject: SerializedProject = {
id: project.id,
name: project.name,
thumbnail: project.thumbnail,
createdAt: project.createdAt.toISOString(),
updatedAt: project.updatedAt.toISOString(),
scenes: serializedScenes,
currentSceneId: project.currentSceneId,
backgroundColor: project.backgroundColor,
backgroundType: project.backgroundType,
blurIntensity: project.blurIntensity,
@@ -81,11 +102,21 @@ class StorageService {
await this.projectsAdapter.set(project.id, serializedProject);
}
async loadProject(id: string): Promise<TProject | null> {
async loadProject({ id }: { id: string }): Promise<TProject | null> {
const serializedProject = await this.projectsAdapter.get(id);
if (!serializedProject) return null;
// Now convert serialized scenes back to Scene objects
const scenes =
serializedProject.scenes?.map((scene) => ({
id: scene.id,
name: scene.name,
isMain: scene.isMain,
createdAt: new Date(scene.createdAt),
updatedAt: new Date(scene.updatedAt),
})) || [];
// Convert back to TProject format
return {
id: serializedProject.id,
@@ -93,6 +124,8 @@ class StorageService {
thumbnail: serializedProject.thumbnail,
createdAt: new Date(serializedProject.createdAt),
updatedAt: new Date(serializedProject.updatedAt),
scenes,
currentSceneId: serializedProject.currentSceneId || "",
backgroundColor: serializedProject.backgroundColor,
backgroundType: serializedProject.backgroundType,
blurIntensity: serializedProject.blurIntensity,
@@ -108,7 +141,7 @@ class StorageService {
const projects: TProject[] = [];
for (const id of projectIds) {
const project = await this.loadProject(id);
const project = await this.loadProject({ id });
if (project) {
projects.push(project);
}
@@ -120,14 +153,20 @@ class StorageService {
);
}
async deleteProject(id: string): Promise<void> {
async deleteProject({ id }: { id: string }): Promise<void> {
await this.projectsAdapter.remove(id);
}
// Media operations
async saveMediaFile(projectId: string, mediaItem: MediaFile): Promise<void> {
async saveMediaFile({
projectId,
mediaItem,
}: {
projectId: string;
mediaItem: MediaFile;
}): Promise<void> {
const { mediaMetadataAdapter, mediaFilesAdapter } =
this.getProjectMediaAdapters(projectId);
this.getProjectMediaAdapters({ projectId });
// Save file to project-specific OPFS
await mediaFilesAdapter.set(mediaItem.id, mediaItem.file);
@@ -148,12 +187,15 @@ class StorageService {
await mediaMetadataAdapter.set(mediaItem.id, metadata);
}
async loadMediaFile(
projectId: string,
id: string
): Promise<MediaFile | null> {
async loadMediaFile({
projectId,
id,
}: {
projectId: string;
id: string;
}): Promise<MediaFile | null> {
const { mediaMetadataAdapter, mediaFilesAdapter } =
this.getProjectMediaAdapters(projectId);
this.getProjectMediaAdapters({ projectId });
const [file, metadata] = await Promise.all([
mediaFilesAdapter.get(id),
@@ -192,14 +234,20 @@ class StorageService {
};
}
async loadAllMediaFiles(projectId: string): Promise<MediaFile[]> {
const { mediaMetadataAdapter } = this.getProjectMediaAdapters(projectId);
async loadAllMediaFiles({
projectId,
}: {
projectId: string;
}): Promise<MediaFile[]> {
const { mediaMetadataAdapter } = this.getProjectMediaAdapters({
projectId,
});
const mediaIds = await mediaMetadataAdapter.list();
const mediaItems: MediaFile[] = [];
for (const id of mediaIds) {
const item = await this.loadMediaFile(projectId, id);
const item = await this.loadMediaFile({ projectId, id });
if (item) {
mediaItems.push(item);
}
@@ -208,9 +256,15 @@ class StorageService {
return mediaItems;
}
async deleteMediaFile(projectId: string, id: string): Promise<void> {
async deleteMediaFile({
projectId,
id,
}: {
projectId: string;
id: string;
}): Promise<void> {
const { mediaMetadataAdapter, mediaFilesAdapter } =
this.getProjectMediaAdapters(projectId);
this.getProjectMediaAdapters({ projectId });
await Promise.all([
mediaFilesAdapter.remove(id),
@@ -218,9 +272,13 @@ class StorageService {
]);
}
async deleteProjectMedia(projectId: string): Promise<void> {
async deleteProjectMedia({
projectId,
}: {
projectId: string;
}): Promise<void> {
const { mediaMetadataAdapter, mediaFilesAdapter } =
this.getProjectMediaAdapters(projectId);
this.getProjectMediaAdapters({ projectId });
await Promise.all([
mediaMetadataAdapter.clear(),
@@ -228,12 +286,20 @@ class StorageService {
]);
}
// Timeline operations - now project-specific
async saveTimeline(
projectId: string,
tracks: TimelineTrack[]
): Promise<void> {
const timelineAdapter = this.getProjectTimelineAdapter(projectId);
// Timeline operations - supports both legacy and scene-based storage
async saveTimeline({
projectId,
tracks,
sceneId,
}: {
projectId: string;
tracks: TimelineTrack[];
sceneId?: string;
}): Promise<void> {
const timelineAdapter = this.getProjectTimelineAdapter({
projectId,
sceneId,
});
const timelineData: TimelineData = {
tracks,
lastModified: new Date().toISOString(),
@@ -241,14 +307,27 @@ class StorageService {
await timelineAdapter.set("timeline", timelineData);
}
async loadTimeline(projectId: string): Promise<TimelineTrack[] | null> {
const timelineAdapter = this.getProjectTimelineAdapter(projectId);
async loadTimeline({
projectId,
sceneId,
}: {
projectId: string;
sceneId?: string;
}): Promise<TimelineTrack[] | null> {
const timelineAdapter = this.getProjectTimelineAdapter({
projectId,
sceneId,
});
const timelineData = await timelineAdapter.get("timeline");
return timelineData ? timelineData.tracks : null;
}
async deleteProjectTimeline(projectId: string): Promise<void> {
const timelineAdapter = this.getProjectTimelineAdapter(projectId);
async deleteProjectTimeline({
projectId,
}: {
projectId: string;
}): Promise<void> {
const timelineAdapter = this.getProjectTimelineAdapter({ projectId });
await timelineAdapter.remove("timeline");
}
@@ -274,12 +353,14 @@ class StorageService {
};
}
async getProjectStorageInfo(projectId: string): Promise<{
async getProjectStorageInfo({ projectId }: { projectId: string }): Promise<{
mediaItems: number;
hasTimeline: boolean;
}> {
const { mediaMetadataAdapter } = this.getProjectMediaAdapters(projectId);
const timelineAdapter = this.getProjectTimelineAdapter(projectId);
const { mediaMetadataAdapter } = this.getProjectMediaAdapters({
projectId,
});
const timelineAdapter = this.getProjectTimelineAdapter({ projectId });
const [mediaIds, timelineData] = await Promise.all([
mediaMetadataAdapter.list(),
@@ -307,7 +388,11 @@ class StorageService {
}
}
async saveSoundEffect(soundEffect: SoundEffect): Promise<void> {
async saveSoundEffect({
soundEffect,
}: {
soundEffect: SoundEffect;
}): Promise<void> {
try {
const currentData = await this.loadSavedSounds();
@@ -340,7 +425,7 @@ class StorageService {
}
}
async removeSavedSound(soundId: number): Promise<void> {
async removeSavedSound({ soundId }: { soundId: number }): Promise<void> {
try {
const currentData = await this.loadSavedSounds();
@@ -356,7 +441,7 @@ class StorageService {
}
}
async isSoundSaved(soundId: number): Promise<boolean> {
async isSoundSaved({ soundId }: { soundId: number }): Promise<boolean> {
try {
const currentData = await this.loadSavedSounds();
return currentData.sounds.some((sound) => sound.id === soundId);
+18 -2
View File
@@ -1,4 +1,4 @@
import { TProject } from "@/types/project";
import { TProject, Scene } from "@/types/project";
import { TimelineTrack } from "@/types/timeline";
export interface StorageAdapter<T> {
@@ -23,11 +23,23 @@ export interface MediaFileData {
// File will be stored separately in OPFS
}
// Legacy timeline data, kept for backward compatibility
export interface TimelineData {
tracks: TimelineTrack[];
lastModified: string;
}
export interface SceneTimelineData {
sceneId: string;
tracks: TimelineTrack[];
lastModified: string;
}
export type SerializedScene = Omit<Scene, "createdAt" | "updatedAt"> & {
createdAt: string;
updatedAt: string;
};
export interface StorageConfig {
projectsDb: string;
mediaDb: string;
@@ -37,9 +49,13 @@ export interface StorageConfig {
}
// Helper type for serialization - converts Date objects to strings
export type SerializedProject = Omit<TProject, "createdAt" | "updatedAt"> & {
export type SerializedProject = Omit<
TProject,
"createdAt" | "updatedAt" | "scenes"
> & {
createdAt: string;
updatedAt: string;
scenes: SerializedScene[];
bookmarks?: number[];
};