@timmo001/groundwork-mobile 0.14.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timmo001/groundwork-mobile",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "description": "The page side of Groundwork's Android shell: the app config, the typed bridge to the shell, and a fallback to the PWA outside it",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -33,8 +33,8 @@
33
33
  "format:check": "prettier --check ."
34
34
  },
35
35
  "dependencies": {
36
- "@timmo001/groundwork-pwa": "0.14.0",
37
- "@timmo001/groundwork-tokens": "0.14.0",
36
+ "@timmo001/groundwork-pwa": "0.15.0",
37
+ "@timmo001/groundwork-tokens": "0.15.0",
38
38
  "effect": "4.1.2"
39
39
  },
40
40
  "devDependencies": {
package/src/bridge.ts CHANGED
@@ -181,6 +181,8 @@ export const mobileRequests = {
181
181
  pinShortcut: Schema.Boolean,
182
182
  /** Removes shortcuts, greying out pinned ones. */
183
183
  disableShortcuts: Schema.Boolean,
184
+ /** Saves a file the page made to Downloads, from `{ name, type, data }` in base64. */
185
+ saveFile: Schema.Boolean,
184
186
  } as const;
185
187
 
186
188
  /** The shell's messages, each with the schema its data is read with. */
package/src/config.ts CHANGED
@@ -12,6 +12,8 @@ export interface MobileLabels {
12
12
  readonly ongoing?: string;
13
13
  /** Defaults to `Download finished`. */
14
14
  readonly downloadFinished?: string;
15
+ /** Shown once a file the page made is saved. Defaults to `Saved to Downloads`. */
16
+ readonly fileSaved?: string;
15
17
  }
16
18
 
17
19
  export type ResolvedMobileLabels = Required<MobileLabels>;
@@ -104,6 +106,7 @@ export const resolveConfig = (
104
106
  notifications: "Notifications",
105
107
  ongoing: "Ongoing",
106
108
  downloadFinished: "Download finished",
109
+ fileSaved: "Saved to Downloads",
107
110
  ...config.labels,
108
111
  },
109
112
  });
package/src/view.ts CHANGED
@@ -168,8 +168,53 @@ export interface Mobile {
168
168
  ids: readonly string[],
169
169
  message?: string,
170
170
  ) => Promise<void>;
171
+ /**
172
+ * Saves a file the page made, such as an export: to the phone's Downloads
173
+ * in the shell, through the browser's download outside it.
174
+ */
175
+ readonly saveFile: (file: SavedFile) => Promise<void>;
176
+ }
177
+
178
+ /** A file for `saveFile`: its name, its type and what's in it. */
179
+ export interface SavedFile {
180
+ readonly name: string;
181
+ readonly type: string;
182
+ readonly contents: string | Blob;
171
183
  }
172
184
 
185
+ const blobOf = ({ type, contents }: SavedFile) =>
186
+ contents instanceof Blob ? contents : new Blob([contents], { type });
187
+
188
+ /** Saves a file through the browser's download. */
189
+ const downloadFile = (file: SavedFile) => {
190
+ const url = URL.createObjectURL(blobOf(file));
191
+ const link = document.createElement("a");
192
+
193
+ link.href = url;
194
+ link.download = file.name;
195
+ link.click();
196
+ URL.revokeObjectURL(url);
197
+ };
198
+
199
+ const decodeText = Schema.decodeUnknownOption(Schema.String);
200
+
201
+ /** A blob's bytes in base64, for the bridge's JSON. */
202
+ const base64Of = (blob: Blob) =>
203
+ new Promise<string>((resolve, reject) => {
204
+ const reader = new FileReader();
205
+
206
+ // readAsDataURL gives data:<type>;base64,<data>
207
+ reader.addEventListener("load", () => {
208
+ const url = Option.getOrElse(decodeText(reader.result), () => "");
209
+
210
+ resolve(url.split(",")[1] ?? "");
211
+ });
212
+ reader.addEventListener("error", () =>
213
+ reject(reader.error ?? new Error("Couldn't read the file")),
214
+ );
215
+ reader.readAsDataURL(blob);
216
+ });
217
+
173
218
  interface Waiting {
174
219
  readonly resolve: (value: Schema.Json) => void;
175
220
  readonly reject: (error: Error) => void;
@@ -270,6 +315,7 @@ const connectFloor = (
270
315
  setShortcuts: async () => 0,
271
316
  pinShortcut: async () => false,
272
317
  disableShortcuts: async () => {},
318
+ saveFile: async (file) => downloadFile(file),
273
319
  };
274
320
  };
275
321
 
@@ -438,5 +484,14 @@ export const connectMobile = async (
438
484
 
439
485
  await call("disableShortcuts", mobileRequests.disableShortcuts, params);
440
486
  },
487
+ saveFile: async (file) => {
488
+ const blob = blobOf(file);
489
+
490
+ await call("saveFile", mobileRequests.saveFile, {
491
+ name: file.name,
492
+ type: blob.type || file.type,
493
+ data: await base64Of(blob),
494
+ });
495
+ },
441
496
  };
442
497
  };