Skip to content
This repository was archived by the owner on Feb 6, 2024. It is now read-only.
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
6 changes: 6 additions & 0 deletions studio/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions studio/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
"@stencil/store": "^1.3.0",
"@types/socket.io-client": "^1.4.34",
"@types/uuid": "^8.3.0",
"@types/wicg-native-file-system": "^2020.6.0",
"autoprefixer": "^9.8.6",
"husky": "^4.3.0",
"prettier": "2.2.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@ import remoteStore from '../../../../../stores/remote.store';
import deckStore from '../../../../../stores/deck.store';
import userStore from '../../../../../stores/user.store';
import shareStore from '../../../../../stores/share.store';
import errorStore from '../../../../../stores/error.store';

import {MoreAction} from '../../../../../utils/editor/more-action';

import {BackupOfflineService} from '../../../../../services/editor/backup/backup.offline.service';

@Component({
tag: 'app-actions-deck',
shadow: false,
Expand Down Expand Up @@ -226,6 +229,14 @@ export class AppActionsDeck {
}
}

private async backupOfflineData() {
try {
await BackupOfflineService.getInstance().backup();
} catch (err) {
errorStore.state.error = `Something went wrong. ${err}.`;
}
}

render() {
return (
<ion-toolbar>
Expand Down Expand Up @@ -300,6 +311,8 @@ export class AppActionsDeck {
{offlineStore.state.offline ? <ion-label aria-hidden="true">Go online</ion-label> : <ion-label aria-hidden="true">Go offline</ion-label>}
</button>

{this.renderBackup()}

<app-action-help class="wider-devices"></app-action-help>

<button
Expand Down Expand Up @@ -334,4 +347,23 @@ export class AppActionsDeck {
</button>
);
}

private renderBackup() {
if (!offlineStore.state.offline) {
return undefined;
}

return (
<button
onMouseDown={($event) => $event.stopPropagation()}
onTouchStart={($event) => $event.stopPropagation()}
aria-label="Backup"
onClick={() => this.backupOfflineData()}
color="primary"
class="wider-devices ion-activatable">
<ion-ripple-effect></ion-ripple-effect>
<ion-icon aria-hidden="true" src="/assets/icons/ionicons/download.svg"></ion-icon> <ion-label aria-hidden="true">Backup</ion-label>
</button>
);
}
}
168 changes: 168 additions & 0 deletions studio/src/app/services/editor/backup/backup.offline.service.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import firebase from 'firebase/app';

import {get} from 'idb-keyval';

import deckStore from '../../../stores/deck.store';

import {Deck, DeckAttributes} from '../../../models/data/deck';
import {Slide} from '../../../models/data/slide';

import {FirestoreUtils} from '../../../utils/editor/firestore.utils';

interface DeckBackupData {
name: string;

attributes?: DeckAttributes;
background?: string;
header?: string;
footer?: string;

owner_id: string;

slides?: Slide[];

api_id?: string;

created_at?: firebase.firestore.Timestamp;
updated_at?: firebase.firestore.Timestamp;
}

interface DeckBackup {
id: string;
data: DeckBackupData;
}

export class BackupOfflineService {
private static instance: BackupOfflineService;

static getInstance(): BackupOfflineService {
if (!BackupOfflineService.instance) {
BackupOfflineService.instance = new BackupOfflineService();
}
return BackupOfflineService.instance;
}

async backup() {
if (!deckStore.state.deck || !deckStore.state.deck.id || !deckStore.state.deck.data) {
throw new Error('No deck found');
}

const slides: Slide[] = await this.getSlides(deckStore.state.deck);

// We select what we want to backup and add the slides (not their id) in these data
const backupDeckData: DeckBackupData = FirestoreUtils.filterDelete(this.prepareDeckBackupData(slides), true);

await this.save({
id: deckStore.state.deck.id,
data: backupDeckData,
});
}

private prepareDeckBackupData(slides: Slide[]): DeckBackupData {
return {
name: deckStore.state.deck.data.name,

attributes: deckStore.state.deck.data.attributes,
background: deckStore.state.deck.data.background,
header: deckStore.state.deck.data.header,
footer: deckStore.state.deck.data.footer,

owner_id: deckStore.state.deck.data.owner_id,

slides,

api_id: deckStore.state.deck.data.api_id,

created_at: deckStore.state.deck.data.created_at,
updated_at: deckStore.state.deck.data.updated_at,
};
}

private async getSlides(deck: Deck): Promise<Slide[]> {
if (!deck.data.slides || deck.data.slides.length <= 0) {
return [];
}

try {
const promises: Promise<Slide>[] = [];

for (let i: number = 0; i < deck.data.slides.length; i++) {
const slideId: string = deck.data.slides[i];

promises.push(get(`/decks/${deck.id}/slides/${slideId}`));
}

if (!promises || promises.length <= 0) {
return [];
}

const slides: Slide[] = await Promise.all(promises);

return slides;
} catch (err) {
throw new Error('Error while fetching slides');
}
}

private save(deck: DeckBackup): Promise<void> {
if ('showSaveFilePicker' in window) {
return this.exportNativeFileSystem(deck);
}

return this.exportDownload(deck);
}

private async exportNativeFileSystem(deck: DeckBackup) {
const fileHandle: FileSystemFileHandle = await this.getNewFileHandle();

if (!fileHandle) {
throw new Error('Cannot access filesystem');
}

await this.writeFile(fileHandle, JSON.stringify(deck));
}

private async getNewFileHandle(): Promise<FileSystemFileHandle> {
const opts: SaveFilePickerOptions = {
types: [
{
description: 'JSON Files',
accept: {
'application/json': ['.json'],
},
},
],
};

return showSaveFilePicker(opts);
}

private async writeFile(fileHandle: FileSystemFileHandle, contents: string | BufferSource | Blob) {
// Create a writer (request permission if necessary).
const writer = await fileHandle.createWritable();
// Write the full length of the contents
await writer.write(contents);
// Close the file and write the contents to disk
await writer.close();
}

private async exportDownload(deck: DeckBackup) {
const a: HTMLAnchorElement = document.createElement('a');
a.style.display = 'none';
document.body.appendChild(a);

const blob: Blob = new Blob([JSON.stringify(deck)], {type: 'octet/stream'});
const url: string = window.URL.createObjectURL(blob);

a.href = url;
a.download = `${deck.data.name}.json`;

a.click();

window.URL.revokeObjectURL(url);

if (a && a.parentElement) {
a.parentElement.removeChild(a);
}
}
}
6 changes: 5 additions & 1 deletion studio/src/assets/assets.json
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,11 @@
{"src": "/icons/word-cloud.svg", "ariaLabel": "Word Cloud"},
{"src": "/icons/ionicons/color-wand.svg", "ariaLabel": "Transform element"},
{"src": "/icons/ionicons/chevron-down.svg", "ariaLabel": "Chevron down"},
{"src": "/icons/ionicons/chevron-back.svg", "ariaLabel": "Chevron back"},
{"src": "/icons/ionicons/chevron-forward.svg", "ariaLabel": "Chevron forward"},
{"src": "/icons/ionicons/settings.svg", "ariaLabel": "Settings"},
{"src": "/icons/text.svg", "ariaLabel": "Text"}
{"src": "/icons/text.svg", "ariaLabel": "Text"},
{"src": "/icons/ionicons/play.svg", "ariaLabel": "Present"},
{"src": "/icons/ionicons/download.svg", "ariaLabel": "Backup"}
]
}
1 change: 1 addition & 0 deletions studio/src/assets/icons/ionicons/download.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.