Skip to content
This repository was archived by the owner on Feb 6, 2024. It is now read-only.

Commit 0a6c05c

Browse files
Merge pull request #1021 from deckgo/backup-offline
feat: backup offline data
2 parents a728228 + 6efaa75 commit 0a6c05c

File tree

6 files changed

+213
-1
lines changed

6 files changed

+213
-1
lines changed

studio/package-lock.json

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

studio/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
"@stencil/store": "^1.3.0",
6565
"@types/socket.io-client": "^1.4.34",
6666
"@types/uuid": "^8.3.0",
67+
"@types/wicg-native-file-system": "^2020.6.0",
6768
"autoprefixer": "^9.8.6",
6869
"husky": "^4.3.0",
6970
"prettier": "2.2.0",

studio/src/app/components/editor/actions/deck/app-actions-deck/app-actions-deck.tsx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,12 @@ import remoteStore from '../../../../../stores/remote.store';
88
import deckStore from '../../../../../stores/deck.store';
99
import userStore from '../../../../../stores/user.store';
1010
import shareStore from '../../../../../stores/share.store';
11+
import errorStore from '../../../../../stores/error.store';
1112

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

15+
import {BackupOfflineService} from '../../../../../services/editor/backup/backup.offline.service';
16+
1417
@Component({
1518
tag: 'app-actions-deck',
1619
shadow: false,
@@ -226,6 +229,14 @@ export class AppActionsDeck {
226229
}
227230
}
228231

232+
private async backupOfflineData() {
233+
try {
234+
await BackupOfflineService.getInstance().backup();
235+
} catch (err) {
236+
errorStore.state.error = `Something went wrong. ${err}.`;
237+
}
238+
}
239+
229240
render() {
230241
return (
231242
<ion-toolbar>
@@ -300,6 +311,8 @@ export class AppActionsDeck {
300311
{offlineStore.state.offline ? <ion-label aria-hidden="true">Go online</ion-label> : <ion-label aria-hidden="true">Go offline</ion-label>}
301312
</button>
302313

314+
{this.renderBackup()}
315+
303316
<app-action-help class="wider-devices"></app-action-help>
304317

305318
<button
@@ -334,4 +347,23 @@ export class AppActionsDeck {
334347
</button>
335348
);
336349
}
350+
351+
private renderBackup() {
352+
if (!offlineStore.state.offline) {
353+
return undefined;
354+
}
355+
356+
return (
357+
<button
358+
onMouseDown={($event) => $event.stopPropagation()}
359+
onTouchStart={($event) => $event.stopPropagation()}
360+
aria-label="Backup"
361+
onClick={() => this.backupOfflineData()}
362+
color="primary"
363+
class="wider-devices ion-activatable">
364+
<ion-ripple-effect></ion-ripple-effect>
365+
<ion-icon aria-hidden="true" src="/assets/icons/ionicons/download.svg"></ion-icon> <ion-label aria-hidden="true">Backup</ion-label>
366+
</button>
367+
);
368+
}
337369
}
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
import firebase from 'firebase/app';
2+
3+
import {get} from 'idb-keyval';
4+
5+
import deckStore from '../../../stores/deck.store';
6+
7+
import {Deck, DeckAttributes} from '../../../models/data/deck';
8+
import {Slide} from '../../../models/data/slide';
9+
10+
import {FirestoreUtils} from '../../../utils/editor/firestore.utils';
11+
12+
interface DeckBackupData {
13+
name: string;
14+
15+
attributes?: DeckAttributes;
16+
background?: string;
17+
header?: string;
18+
footer?: string;
19+
20+
owner_id: string;
21+
22+
slides?: Slide[];
23+
24+
api_id?: string;
25+
26+
created_at?: firebase.firestore.Timestamp;
27+
updated_at?: firebase.firestore.Timestamp;
28+
}
29+
30+
interface DeckBackup {
31+
id: string;
32+
data: DeckBackupData;
33+
}
34+
35+
export class BackupOfflineService {
36+
private static instance: BackupOfflineService;
37+
38+
static getInstance(): BackupOfflineService {
39+
if (!BackupOfflineService.instance) {
40+
BackupOfflineService.instance = new BackupOfflineService();
41+
}
42+
return BackupOfflineService.instance;
43+
}
44+
45+
async backup() {
46+
if (!deckStore.state.deck || !deckStore.state.deck.id || !deckStore.state.deck.data) {
47+
throw new Error('No deck found');
48+
}
49+
50+
const slides: Slide[] = await this.getSlides(deckStore.state.deck);
51+
52+
// We select what we want to backup and add the slides (not their id) in these data
53+
const backupDeckData: DeckBackupData = FirestoreUtils.filterDelete(this.prepareDeckBackupData(slides), true);
54+
55+
await this.save({
56+
id: deckStore.state.deck.id,
57+
data: backupDeckData,
58+
});
59+
}
60+
61+
private prepareDeckBackupData(slides: Slide[]): DeckBackupData {
62+
return {
63+
name: deckStore.state.deck.data.name,
64+
65+
attributes: deckStore.state.deck.data.attributes,
66+
background: deckStore.state.deck.data.background,
67+
header: deckStore.state.deck.data.header,
68+
footer: deckStore.state.deck.data.footer,
69+
70+
owner_id: deckStore.state.deck.data.owner_id,
71+
72+
slides,
73+
74+
api_id: deckStore.state.deck.data.api_id,
75+
76+
created_at: deckStore.state.deck.data.created_at,
77+
updated_at: deckStore.state.deck.data.updated_at,
78+
};
79+
}
80+
81+
private async getSlides(deck: Deck): Promise<Slide[]> {
82+
if (!deck.data.slides || deck.data.slides.length <= 0) {
83+
return [];
84+
}
85+
86+
try {
87+
const promises: Promise<Slide>[] = [];
88+
89+
for (let i: number = 0; i < deck.data.slides.length; i++) {
90+
const slideId: string = deck.data.slides[i];
91+
92+
promises.push(get(`/decks/${deck.id}/slides/${slideId}`));
93+
}
94+
95+
if (!promises || promises.length <= 0) {
96+
return [];
97+
}
98+
99+
const slides: Slide[] = await Promise.all(promises);
100+
101+
return slides;
102+
} catch (err) {
103+
throw new Error('Error while fetching slides');
104+
}
105+
}
106+
107+
private save(deck: DeckBackup): Promise<void> {
108+
if ('showSaveFilePicker' in window) {
109+
return this.exportNativeFileSystem(deck);
110+
}
111+
112+
return this.exportDownload(deck);
113+
}
114+
115+
private async exportNativeFileSystem(deck: DeckBackup) {
116+
const fileHandle: FileSystemFileHandle = await this.getNewFileHandle();
117+
118+
if (!fileHandle) {
119+
throw new Error('Cannot access filesystem');
120+
}
121+
122+
await this.writeFile(fileHandle, JSON.stringify(deck));
123+
}
124+
125+
private async getNewFileHandle(): Promise<FileSystemFileHandle> {
126+
const opts: SaveFilePickerOptions = {
127+
types: [
128+
{
129+
description: 'JSON Files',
130+
accept: {
131+
'application/json': ['.json'],
132+
},
133+
},
134+
],
135+
};
136+
137+
return showSaveFilePicker(opts);
138+
}
139+
140+
private async writeFile(fileHandle: FileSystemFileHandle, contents: string | BufferSource | Blob) {
141+
// Create a writer (request permission if necessary).
142+
const writer = await fileHandle.createWritable();
143+
// Write the full length of the contents
144+
await writer.write(contents);
145+
// Close the file and write the contents to disk
146+
await writer.close();
147+
}
148+
149+
private async exportDownload(deck: DeckBackup) {
150+
const a: HTMLAnchorElement = document.createElement('a');
151+
a.style.display = 'none';
152+
document.body.appendChild(a);
153+
154+
const blob: Blob = new Blob([JSON.stringify(deck)], {type: 'octet/stream'});
155+
const url: string = window.URL.createObjectURL(blob);
156+
157+
a.href = url;
158+
a.download = `${deck.data.name}.json`;
159+
160+
a.click();
161+
162+
window.URL.revokeObjectURL(url);
163+
164+
if (a && a.parentElement) {
165+
a.parentElement.removeChild(a);
166+
}
167+
}
168+
}

studio/src/assets/assets.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,11 @@
237237
{"src": "/icons/word-cloud.svg", "ariaLabel": "Word Cloud"},
238238
{"src": "/icons/ionicons/color-wand.svg", "ariaLabel": "Transform element"},
239239
{"src": "/icons/ionicons/chevron-down.svg", "ariaLabel": "Chevron down"},
240+
{"src": "/icons/ionicons/chevron-back.svg", "ariaLabel": "Chevron back"},
241+
{"src": "/icons/ionicons/chevron-forward.svg", "ariaLabel": "Chevron forward"},
240242
{"src": "/icons/ionicons/settings.svg", "ariaLabel": "Settings"},
241-
{"src": "/icons/text.svg", "ariaLabel": "Text"}
243+
{"src": "/icons/text.svg", "ariaLabel": "Text"},
244+
{"src": "/icons/ionicons/play.svg", "ariaLabel": "Present"},
245+
{"src": "/icons/ionicons/download.svg", "ariaLabel": "Backup"}
242246
]
243247
}
Lines changed: 1 addition & 0 deletions
Loading

0 commit comments

Comments
 (0)