Skip to content

Commit

Permalink
Merge pull request #23 from Joery-M/10-sl-ui-autosaving-projects
Browse files Browse the repository at this point in the history
10 sl UI autosaving projects
  • Loading branch information
Joery-M authored Apr 23, 2024
2 parents dcda463 + c21096a commit 67ab0eb
Show file tree
Hide file tree
Showing 14 changed files with 176 additions and 68 deletions.
3 changes: 2 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"vue/no-v-for-template-key": "off",
"vue/no-v-model-argument": "off",
"no-async-promise-executor": "off",
"no-undef": "off"
"no-undef": "off",
"no-dupe-class-members": "off"
}
}
11 changes: 8 additions & 3 deletions packages/safelight/src/components/Editor/Library/Library.vue
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
</template>

<script setup lang="ts">
import { ProjectFeatures } from '@safelight/shared/base/Project';
import Media from '@safelight/shared/Media/Media';
import fuzzysearch from 'fuzzysearch';
import MimeMatcher from 'mime-matcher';
Expand All @@ -102,7 +103,11 @@ import InputGroupAddon from 'primevue/inputgroupaddon';
useDropZone(document.body, {
onDrop(files) {
files?.forEach(CurrentProject.loadFile);
files?.forEach((file) => {
if (CurrentProject.project.value?.hasFeature(ProjectFeatures.media)) {
CurrentProject.project.value.loadFile(file);
}
});
},
dataTypes(types) {
return !types.some((val) => {
Expand All @@ -120,8 +125,8 @@ fileDialog.onChange((fileList) => {
for (let i = 0; i < fileList.length; i++) {
const item = fileList.item(i);
if (item) {
CurrentProject.loadFile(item);
if (item && CurrentProject.project.value?.hasFeature(ProjectFeatures.media)) {
CurrentProject.project.value.loadFile(item);
}
}
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
</template>
<script setup lang="ts">
import { PhTrash, type PhDotsThreeVertical } from '@phosphor-icons/vue';
import { ProjectFeatures } from '@safelight/shared/base/Project';
import type Media from '@safelight/shared/Media/Media';
import type Menu from 'primevue/menu';
import type { MenuItem } from 'primevue/menuitem';
Expand All @@ -97,7 +98,7 @@ const menuItems = ref<MenuItem[]>([
const hasItemInTimeline = computed(
() =>
CurrentProject.project.value &&
CurrentProject.project.value.isSimpleProject() &&
CurrentProject.project.value.hasFeature(ProjectFeatures.media) &&
CurrentProject.project.value.usesMedia(props.item)
);
Expand Down
6 changes: 0 additions & 6 deletions packages/safelight/src/injections.ts

This file was deleted.

44 changes: 6 additions & 38 deletions packages/safelight/src/stores/currentProject.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
/* eslint-disable no-dupe-class-members */
import { router } from '@/main';
import type BaseProject from '@safelight/shared/base/Project';
import type { ProjectType } from '@safelight/shared/base/Project';
import { ProjectFeatures, type ProjectType } from '@safelight/shared/base/Project';
import BaseStorageController, { Storage, type StoredProject } from '@safelight/shared/base/Storage';
import MediaManager from '@safelight/shared/Storage/MediaManager';
import { DateTime } from 'luxon';

export class CurrentProject {
Expand Down Expand Up @@ -87,45 +86,14 @@ export class CurrentProject {
sessionStorage.removeItem('project');
}

// Might want to move this to BaseProject or SimpleProject
public static loadFile(file: File) {
return new Promise<void>((resolve) => {
const storingProcessing = useObservable(MediaManager.StoreMedia(file));
watch(storingProcessing, (s) => {
console.log(s?.type, s?.hashProgress);
});

watch(storingProcessing, async () => {
if (storingProcessing.value && storingProcessing.value.type == 'done') {
const existingMedia = this.project.value!.media.some(
(m) => m.id == storingProcessing.value!.id
);

if (!existingMedia) {
const media = await Storage.getStorage().LoadMedia(
storingProcessing.value.id!
);

if (media) {
this.project.value!.media.push(media);
this.save();
}
}

resolve();
}
});
});
}

public static async save() {
if (this.project.value) await Storage.getStorage().SaveProject(this.project.value);
}

public static async beforeExit(clearSession = true) {
if (this.project.value) {
this.project.value.destroy$.next();
this.project.value.destroy$.complete();
if (clearSession) this.clearSessionProject();
await this.save();
if (this.project.value.hasFeature(ProjectFeatures.saving)) {
await this.project.value.Save();
}
this.project.value = undefined;
}
}
Expand Down
10 changes: 9 additions & 1 deletion packages/safelight/src/views/Editor/Editor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
@change="
(newName) => {
CurrentProject.project.value!.name.value = newName;
CurrentProject.save();
}
"
/>
Expand Down Expand Up @@ -135,6 +134,15 @@ function showNoProjectDialog() {
function clearSelection() {
document.getSelection()?.removeAllRanges();
}
watch(
CurrentProject.project,
(project) => {
if (project && CurrentProject.isLoaded.value) {
project.onDeepChange.next();
}
},
{ deep: true }
);
onBeforeUnmount(async () => {
if (CurrentProject.isLoaded.value) {
Expand Down
3 changes: 1 addition & 2 deletions packages/safelight/tsconfig.app.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@
"src/**/*.ts",
"src/**/*.vue",
"src/**/*.json",
"tsconfig.worker.json",
"types/injections.ts"
"tsconfig.worker.json"
],
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {
Expand Down
1 change: 1 addition & 0 deletions packages/shared/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
},
"dependencies": {
"@vueuse/core": "^10.9.0",
"@vueuse/rxjs": "^10.9.0",
"@vueuse/shared": "^10.9.0",
"dexie": "^3.2.7",
"ffprobe-wasm": "^0.3.1",
Expand Down
69 changes: 66 additions & 3 deletions packages/shared/src/Project/SimpleProject.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,48 @@
import { useObservable } from '@vueuse/rxjs';
import { debounceTime, takeUntil } from 'rxjs';
import { v4 as uuidv4 } from 'uuid';
import { computed, ref, shallowReactive } from 'vue';
import BaseProject, { type ProjectType } from '../base/Project';
import { computed, ref, shallowReactive, watch } from 'vue';
import BaseProject, {
type ProjectFeatureMedia,
type ProjectFeatureSaving,
type ProjectType
} from '../base/Project';
import { Storage } from '../base/Storage';
import Media from '../Media/Media';
import MediaManager from '../Storage/MediaManager';
import SimpleTimeline, { type SimpleTimelineConfig } from '../Timeline/SimpleTimeline';

export default class SimpleProject extends BaseProject {
export default class SimpleProject
extends BaseProject
implements ProjectFeatureSaving, ProjectFeatureMedia
{
public id = uuidv4();
public type: ProjectType = 'Simple';
public isSaving = ref(false);

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

public selectedTimelineIndex = ref(0);
public timelines = shallowReactive<SimpleTimeline[]>([]);
public timeline = computed(() => this.timelines.at(this.selectedTimelineIndex.value)!);

constructor() {
super();

this.onDeepChange.pipe(takeUntil(this.destroy$), debounceTime(1000)).subscribe(() => {
this.Save();
});
}

public async Save() {
if (this.isSaving.value) return 'Cancelled';

this.isSaving.value = true;
const res = await Storage.getStorage().SaveProject(this);
this.isSaving.value = false;
return res;
}

public usesMedia(media: Media) {
return this.timelines.some((timeline) => timeline.usesMedia(media));
}
Expand All @@ -36,4 +65,38 @@ export default class SimpleProject extends BaseProject {

return timeline;
}

public loadFile(file: File) {
return new Promise<boolean>((resolve) => {
const storingProcessing = useObservable(
MediaManager.StoreMedia(file).pipe(takeUntil(this.destroy$))
);
watch(storingProcessing, (s) => {
console.log(s?.type, s?.hashProgress);
});

watch(storingProcessing, async () => {
if (storingProcessing.value && storingProcessing.value.type == 'done') {
const existingMedia = this.media.some(
(m) => m.id == storingProcessing.value!.id
);

if (!existingMedia) {
const media = await Storage.getStorage().LoadMedia(
storingProcessing.value.id!
);

if (media) {
this.media.push(media);

await this.Save();
resolve(true);
}
}

resolve(false);
}
});
});
}
}
4 changes: 2 additions & 2 deletions packages/shared/src/Storage/MediaManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Observable } from 'rxjs';
import { v4 as uuidv4 } from 'uuid';
import { Storage } from '../base/Storage';
import { generateMediaThumbnail } from '../helpers/Video/GenerateMediaThumbnail';
import { getVideoInfo } from '../helpers/Video/GetVideoInfo';
import { getFileInfo } from '../helpers/Files/GetFileInfo';
import {
MediaType,
type AudioTrackInfo,
Expand Down Expand Up @@ -69,7 +69,7 @@ export default class MediaManager {
type: 'fileInfo',
hashProgress: 1
});
const fileInfo = await getVideoInfo(file);
const fileInfo = await getFileInfo(file);

// Get thumbnail
subscriber.next({
Expand Down
50 changes: 49 additions & 1 deletion packages/shared/src/base/Project.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { ref, type ComputedRef, type Ref, type ShallowReactive } from 'vue';
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 { SaveResults } from './Storage';
import type BaseTimeline from './Timeline';

export default abstract class BaseProject {
Expand All @@ -14,8 +16,54 @@ export default abstract class BaseProject {
public abstract timelines: ShallowReactive<BaseTimeline[]>;
public abstract timeline: ComputedRef<BaseTimeline>;

/**
* Triggered when this class has been changed
*/
public onDeepChange = new Subject<void>();

public destroy$ = new Subject<void>();

isBaseProject = (): this is BaseProject => this.type == 'Base';
isSimpleProject = (): this is SimpleProject => this.type == 'Simple';

hasFeature(feature: ProjectFeatures.saving): this is this & ProjectFeatureSaving;
hasFeature(feature: ProjectFeatures.media): this is this & ProjectFeatureMedia;
hasFeature(feature: ProjectFeatures) {
switch (feature) {
case ProjectFeatures.saving:
return (
'Save' in this &&
typeof this.Save === 'function' &&
'isSaving' in this &&
isRef(this.isSaving)
);
case ProjectFeatures.media:
return (
'usesMedia' in this &&
typeof this.usesMedia === 'function' &&
'media' in this &&
isReactive(this.media)
);

default:
return false;
}
}
}

export type ProjectType = 'Base' | 'Simple';

export enum ProjectFeatures {
saving,
media
}

export interface ProjectFeatureSaving {
isSaving: Ref<boolean>;
Save(): Promise<SaveResults>;
}
export interface ProjectFeatureMedia {
media: ShallowReactive<Media[]>;
usesMedia(media: Media): boolean;
loadFile(file: File): Promise<boolean>;
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
import MediaInfoFactory, { type MediaInfoType, type ReadChunkFunc } from 'mediainfo.js';
import MediaInfoWasmUrl from 'mediainfo.js/MediaInfoModule.wasm?url';
import MimeMatcher from 'mime-matcher';

export async function getVideoInfo(file: File) {
export async function getFileInfo(file: File) {
return new Promise<MediaInfoType>((resolve, reject) => {
const isVideo = new MimeMatcher('video/*').match(file.type);

if (!isVideo) reject('File is not a video');

const readChunk: ReadChunkFunc = (chunkSize, offset) =>
new Promise((resolve, reject) => {
const reader = new FileReader();
Expand All @@ -25,10 +20,12 @@ export async function getVideoInfo(file: File) {
locateFile(_url, _scriptDirectory) {
return MediaInfoWasmUrl;
}
}).then(async (mediaInfo) => {
const data = await mediaInfo.analyzeData(() => file.size, readChunk);
})
.then(async (mediaInfo) => {
const data = await mediaInfo.analyzeData(() => file.size, readChunk);

resolve(data);
});
resolve(data);
})
.catch(reject);
});
}
20 changes: 20 additions & 0 deletions packages/shared/test/Storage/indexedDB/Project.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,23 @@ test('Retrieve project', async () => {

expect(loadedProject?.id).toEqual(project.id);
});

test('Update project', async () => {
const storage = new IndexedDbStorageController();

const project = new SimpleProject();
project.name.value = 'Name 1';

await storage.SaveProject(project);

project.name.value = 'Name 2';

Storage.setStorage(storage);

const res = await project.Save();
expect(res).toEqual('Success');

const loadedProject = await storage.LoadProject(project.id);
expect(loadedProject).toBeDefined();
expect(loadedProject?.name.value).toBe('Name 2');
});
Loading

0 comments on commit 67ab0eb

Please sign in to comment.