Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

10 sl UI autosaving projects #23

Merged
merged 6 commits into from
Apr 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading