Skip to content

Commit

Permalink
Merge pull request #44 from Joery-M/39-shared-indexeddb-timeline-saving
Browse files Browse the repository at this point in the history
39 shared indexeddb timeline saving
  • Loading branch information
Joery-M authored May 17, 2024
2 parents df56805 + 7d5439e commit e7c0691
Show file tree
Hide file tree
Showing 6 changed files with 161 additions and 38 deletions.
16 changes: 15 additions & 1 deletion packages/safelight/src/stores/currentProject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,21 @@ export class CurrentProject {
).default;
Storage.setStorage(new IndexedDbStorageController());
const SimpleProject = (await import('@safelight/shared/Project/SimpleProject')).default;
this.setProject(new SimpleProject());

const proj = new SimpleProject();

// TODO: Make configurable
proj.createTimeline({
framerate: 60,
width: 1920,
height: 1080,
name: 'Main'
});

this.setProject(proj);

await proj.Save();
this.setSessionProject(proj);

if (goToEditor) await this.toEditor();
}
Expand Down
12 changes: 6 additions & 6 deletions packages/shared/src/Project/SimpleProject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,12 @@ export default class SimpleProject

public media = shallowReactive<Media[]>([]);

public selectedTimelineIndex = ref(0);
public selectedTimeline = ref<string>();
public timelines = shallowReactive<SimpleTimeline[]>([]);
public timeline = computed(() => this.timelines.at(this.selectedTimelineIndex.value)!);
// public timeline = computed(() => this.timelines.find(this.selectedTimelineIndex.value)!);
public timeline = computed(() =>
this.timelines.find(({ id }) => id == this.selectedTimeline.value)
);

constructor() {
super();
Expand All @@ -48,10 +51,7 @@ export default class SimpleProject
}

public selectTimeline(timeline: SimpleTimeline) {
const timelineIndex = this.timelines.indexOf(timeline);
if (timelineIndex >= 0) {
this.selectedTimelineIndex.value = timelineIndex;
}
this.selectedTimeline.value = timeline.id;
}

public createTimeline(config: SimpleTimelineConfig, selectWhenCreated = true): SimpleTimeline {
Expand Down
134 changes: 112 additions & 22 deletions packages/shared/src/Storage/IndexedDbStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import BaseStorageController from '../base/Storage';
import type BaseTimeline from '../base/Timeline';
import Media from '../Media/Media';
import SimpleProject from '../Project/SimpleProject';
import type SimpleTimeline from '../Timeline/SimpleTimeline';
import SimpleTimeline from '../Timeline/SimpleTimeline';
import AudioTimelineItem from '../TimelineItem/AudioTimelineItem';
import VideoTimelineItem from '../TimelineItem/VideoTimelineItem';
import { SafelightIndexedDB } from './db';

export default class IndexedDbStorageController extends BaseStorageController {
Expand All @@ -19,24 +21,28 @@ export default class IndexedDbStorageController extends BaseStorageController {

private db = new SafelightIndexedDB();

async SaveProject(project: BaseProject | StoredProject): Promise<SaveResults> {
async SaveProject(project: BaseProject, includeTimelines = true): Promise<SaveResults> {
const existingProject = await this.db.project.get({ id: project.id });

const storableProject: StoredProject =
'updated' in project
? project
: {
id: project.id,
name: project.name.value,
type: project.type,
media: project.media.map((m) => m.id).filter((id) => id !== undefined),
timelines: project.timelines.map((m) => m.id),
updated: DateTime.now().toISO(),
created: existingProject?.created ?? DateTime.now().toISO()
};
const storableProject: StoredProject = {
id: project.id,
name: project.name.value,
type: project.type,
media: project.media.map((m) => m.id).filter((id) => id !== undefined),
timelines: project.timelines.map((m) => m.id),
activeTimeline: project.timeline.value?.id,
updated: DateTime.now().toISO(),
created: existingProject?.created ?? DateTime.now().toISO()
};

try {
await this.db.project.put(storableProject, project.id);

if (includeTimelines) {
const proms = project.timelines.map((timeline) => this.SaveTimeline(timeline));
await Promise.allSettled(proms);
}

return 'Success';
} catch (error: any) {
return error.toString();
Expand Down Expand Up @@ -69,11 +75,15 @@ export default class IndexedDbStorageController extends BaseStorageController {
);
if (timeline) {
proj.timelines.push(timeline);
if (timeline.id == project.activeTimeline) {
proj.selectTimeline(timeline);
}
}
});

// Load all timelines and media
await Promise.allSettled([...timelineFetches, ...mediaFetches]);
await Promise.allSettled(mediaFetches);
await Promise.allSettled(timelineFetches);
proj.name.value = project.name;
return proj;
} else {
Expand Down Expand Up @@ -189,14 +199,94 @@ export default class IndexedDbStorageController extends BaseStorageController {
return media;
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
async SaveTimeline(_timeline: BaseTimeline): Promise<SaveResults> {
throw new Error('Method not implemented.');
SaveTimeline(timeline: SimpleTimeline): Promise<SaveResults> {
return new Promise<SaveResults>(async (resolve) => {
await this.db.timelineItem.bulkAdd(
Array.from(timeline.items.values()).map((item) => ({
duration: item.isAudio() || item.isVideo() ? item.duration.value : undefined,
end: item.end.value,
start: item.start.value,
type: item.type,
media: item.hasMedia() ? item.media.value?.id : undefined,
id: item.id,
name: item.name.value,
layer: item.layer.value
}))
);

this.db.timeline
.put({
id: timeline.id,
name: timeline.name.value,
type: timeline.type,
items: timeline.isSimpleTimeline()
? Array.from(timeline.items.values()).map(({ id }) => id)
: [],
framerate: timeline.framerate.value,
height: timeline.height.value,
width: timeline.width.value
})
.catch((err) => {
resolve('Error');
console.error(err);
})
.then(() => {
resolve('Success');
});
});
}
LoadTimeline<Timeline extends BaseTimeline = BaseTimeline>(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_timelineId: string
async LoadTimeline<Timeline extends BaseTimeline = BaseTimeline>(
timelineId: string
): Promise<Timeline | undefined> {
throw new Error('Method not implemented.');
const storedTimeline = await this.db.timeline.get(timelineId);
if (!storedTimeline) return;

if (storedTimeline.type == 'Simple') {
const timeline = new SimpleTimeline({
framerate: storedTimeline.framerate,
height: storedTimeline.height,
width: storedTimeline.width,
name: storedTimeline.name
});

timeline.id = storedTimeline.id;

const TLitems = await this.db.timelineItem.bulkGet(storedTimeline.items);

const promises = TLitems.map(async (stored) => {
if (!stored) return;

if (stored.type == 'Audio') {
const item = new AudioTimelineItem();
item.id = stored.id;
item.duration.value = stored.duration!;
item.end.value = stored.end;
item.layer.value = stored.layer;
item.name.value = stored.name;
if (stored.media) {
item.media.value = await this.LoadMedia(stored.media);
}

timeline.items.add(item);
} else if (stored.type == 'Video') {
const item = new VideoTimelineItem();

item.id = stored.id;
item.duration.value = stored.duration!;
item.end.value = stored.end;
item.layer.value = stored.layer;
item.name.value = stored.name;
if (stored.media) {
item.media.value = await this.LoadMedia(stored.media);
}

timeline.items.add(item);
}
});

await Promise.allSettled(promises);

return timeline as unknown as Timeline;
}
}
}
13 changes: 11 additions & 2 deletions packages/shared/src/Storage/db.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import Dexie, { type Table } from 'dexie';
import type { StoredMedia, StoredProject } from '../base/Storage';
import type {
StoredMedia,
StoredProject,
StoredSimpleTimeline,
StoredSimpleTimelineItem
} from '../base/Storage';

export class SafelightIndexedDB extends Dexie {
media!: Table<StoredMedia, string>;
project!: Table<StoredProject, string>;
timeline!: Table<StoredSimpleTimeline, string>;
timelineItem!: Table<StoredSimpleTimelineItem, string>;

constructor() {
// Only during tests
Expand All @@ -17,7 +24,9 @@ export class SafelightIndexedDB extends Dexie {

this.version(1).stores({
media: 'id, name, contentHash',
project: 'id, name, type'
project: 'id, name, type',
timeline: 'id, name',
timelineItem: 'id'
});
}
}
8 changes: 4 additions & 4 deletions packages/shared/src/base/Project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { Subject } from 'rxjs';
import { isReactive, isRef, ref, type ComputedRef, type Ref, type ShallowReactive } from 'vue';
import type Media from '../Media/Media';
import type SimpleProject from '../Project/SimpleProject';
import type SimpleTimeline from '../Timeline/SimpleTimeline';
import type { SaveResults } from './Storage';
import type BaseTimeline from './Timeline';

export default abstract class BaseProject {
public abstract id: string;
Expand All @@ -12,9 +12,9 @@ export default abstract class BaseProject {

public abstract media: ShallowReactive<Media[]>;

public abstract selectedTimelineIndex: Ref<number>;
public abstract timelines: ShallowReactive<BaseTimeline[]>;
public abstract timeline: ComputedRef<BaseTimeline>;
public abstract selectedTimeline: Ref<string | undefined>;
public abstract timelines: ShallowReactive<SimpleTimeline[]>;
public abstract timeline: ComputedRef<SimpleTimeline | undefined>;

/**
* Triggered when this class has been changed
Expand Down
16 changes: 13 additions & 3 deletions packages/shared/src/base/Storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type IndexedDbStorageController from '../Storage/IndexedDbStorage';
import type { default as BaseProject, ProjectType } from './Project';
import type BaseTimeline from './Timeline';
import type { TimelineItemType } from './TimelineItem';
import type { TimelineType } from './Timeline';

export default abstract class BaseStorageController {
public abstract type: StorageControllerType;
Expand Down Expand Up @@ -94,24 +95,33 @@ export interface StoredProject {
* Array of timeline id's
*/
timelines: string[];
activeTimeline?: string;
created: string;
updated: string;
}

export interface StoredTimeline {
export interface StoredSimpleTimeline {
id: string;
name: string;
type: TimelineType;
width: number;
height: number;
framerate: number;
items: string[];
}
// TODO Add all necessary properties
export interface StoredTimelineItem {
export interface StoredSimpleTimelineItem {
id: string;
name: string;
type: TimelineItemType;
/**
* The ID of a stored media item.
*
* Media has to be included in the stored project's media list to be used.
*/
media?: string;
start?: number;
start: number;
end: number;
layer: number;
duration?: number;
}

0 comments on commit e7c0691

Please sign in to comment.