summaryrefslogtreecommitdiff
path: root/apps/web/src/lib
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2025-09-01 16:17:06 +0200
committerMaze Winther <mazewinther@gmail.com>2025-09-01 16:17:06 +0200
commit350b55081cde3a4332c982ea5c53b7d359029f6b (patch)
treef63b66635d2a13a0a43c622a9bea25d9b9822960 /apps/web/src/lib
parent2b700b7474ffc728f5318c178244e035c93716b5 (diff)
downloadopencut-350b55081cde3a4332c982ea5c53b7d359029f6b.tar.gz
refactor(core): switch to scene-based architecture
Diffstat (limited to 'apps/web/src/lib')
-rw-r--r--apps/web/src/lib/fetch-github-stars.ts29
-rw-r--r--apps/web/src/lib/storage/storage-service.ts159
-rw-r--r--apps/web/src/lib/storage/types.ts20
3 files changed, 140 insertions, 68 deletions
diff --git a/apps/web/src/lib/fetch-github-stars.ts b/apps/web/src/lib/fetch-github-stars.ts
deleted file mode 100644
index 0372f25b..00000000
--- a/apps/web/src/lib/fetch-github-stars.ts
+++ /dev/null
@@ -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";
- }
-}
diff --git a/apps/web/src/lib/storage/storage-service.ts b/apps/web/src/lib/storage/storage-service.ts
index 3aadf883..80f10d3d 100644
--- a/apps/web/src/lib/storage/storage-service.ts
+++ b/apps/web/src/lib/storage/storage-service.ts
@@ -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);
diff --git a/apps/web/src/lib/storage/types.ts b/apps/web/src/lib/storage/types.ts
index 8f593867..e66239cd 100644
--- a/apps/web/src/lib/storage/types.ts
+++ b/apps/web/src/lib/storage/types.ts
@@ -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[];
};