@hangtime/grip-connect 0.6.0 → 0.6.1
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/README.md +2 -2
- package/deno.json +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +0 -1
- package/dist/interfaces/index.d.ts +8 -0
- package/dist/interfaces/index.js +1 -0
- package/dist/models/device/kilterboard.model.d.ts +4 -4
- package/dist/models/device/kilterboard.model.js +4 -4
- package/dist/models/device/progressor.model.d.ts +1 -0
- package/dist/models/device/progressor.model.js +2 -2
- package/dist/models/device.model.d.ts +4 -4
- package/dist/models/device.model.js +33 -30
- package/package.json +2 -2
- package/src/index.ts +10 -1
- package/src/interfaces/index.ts +15 -0
- package/src/models/device/kilterboard.model.ts +11 -11
- package/src/models/device/progressor.model.ts +2 -2
- package/src/models/device.model.ts +32 -30
package/README.md
CHANGED
|
@@ -102,10 +102,10 @@ document.querySelector("#motherboard").addEventListener("click", async () => {
|
|
|
102
102
|
// await motherboard.stop()
|
|
103
103
|
|
|
104
104
|
// Download data as CSV, JSON, or XML (default: CSV) format => timestamp, frame, battery, samples, masses
|
|
105
|
-
// motherboard.download('json')
|
|
105
|
+
// await motherboard.download('json')
|
|
106
106
|
|
|
107
107
|
// Optionally disconnect from device after we are done
|
|
108
|
-
motherboard.disconnect(
|
|
108
|
+
motherboard.disconnect()
|
|
109
109
|
},
|
|
110
110
|
(error) => {
|
|
111
111
|
// Optinal custom error handeling
|
package/deno.json
CHANGED
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type { IClimbro } from "./device/climbro.interface.js";
|
|
2
|
+
export type { IEntralpi } from "./device/entralpi.interface.js";
|
|
3
|
+
export type { IForceBoard } from "./device/forceboard.interface.js";
|
|
4
|
+
export type { IKilterBoard } from "./device/kilterboard.interface.js";
|
|
5
|
+
export type { IMotherboard } from "./device/motherboard.interface.js";
|
|
6
|
+
export type { ImySmartBoard } from "./device/mysmartboard.interface.js";
|
|
7
|
+
export type { IProgressor } from "./device/progressor.interface.js";
|
|
8
|
+
export type { IWHC06 } from "./device/wh-c06.interface.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -74,13 +74,13 @@ export declare class KilterBoard extends Device implements IKilterBoard {
|
|
|
74
74
|
/**
|
|
75
75
|
* Calculates the checksum for a byte array by summing up all bytes ot hre packet in a single-byte variable.
|
|
76
76
|
* @param data - The array of bytes to calculate the checksum for.
|
|
77
|
-
* @returns The calculated checksum value.
|
|
77
|
+
* @returns {number} The calculated checksum value.
|
|
78
78
|
*/
|
|
79
79
|
private checksum;
|
|
80
80
|
/**
|
|
81
81
|
* Wraps a byte array with header and footer bytes for transmission.
|
|
82
82
|
* @param data - The array of bytes to wrap.
|
|
83
|
-
* @returns The wrapped byte array.
|
|
83
|
+
* @returns {number[]} The wrapped byte array.
|
|
84
84
|
*/
|
|
85
85
|
private wrapBytes;
|
|
86
86
|
/**
|
|
@@ -88,7 +88,7 @@ export declare class KilterBoard extends Device implements IKilterBoard {
|
|
|
88
88
|
* The lowest 8 bits of the position get put in the first byte of the group.
|
|
89
89
|
* The highest 8 bits of the position get put in the second byte of the group.
|
|
90
90
|
* @param position - The position to encode.
|
|
91
|
-
* @returns The encoded byte array representing the position.
|
|
91
|
+
* @returns {number[]} The encoded byte array representing the position.
|
|
92
92
|
*/
|
|
93
93
|
private encodePosition;
|
|
94
94
|
/**
|
|
@@ -116,7 +116,7 @@ export declare class KilterBoard extends Device implements IKilterBoard {
|
|
|
116
116
|
* https://github.com/ramda/ramda/blob/master/source/splitEvery.js
|
|
117
117
|
* @param {Number} n
|
|
118
118
|
* @param {Array} list
|
|
119
|
-
* @return {Array}
|
|
119
|
+
* @return {Array<number[]>}
|
|
120
120
|
*/
|
|
121
121
|
private splitEvery;
|
|
122
122
|
/**
|
|
@@ -130,7 +130,7 @@ export class KilterBoard extends Device {
|
|
|
130
130
|
/**
|
|
131
131
|
* Calculates the checksum for a byte array by summing up all bytes ot hre packet in a single-byte variable.
|
|
132
132
|
* @param data - The array of bytes to calculate the checksum for.
|
|
133
|
-
* @returns The calculated checksum value.
|
|
133
|
+
* @returns {number} The calculated checksum value.
|
|
134
134
|
*/
|
|
135
135
|
checksum(data) {
|
|
136
136
|
let i = 0;
|
|
@@ -142,7 +142,7 @@ export class KilterBoard extends Device {
|
|
|
142
142
|
/**
|
|
143
143
|
* Wraps a byte array with header and footer bytes for transmission.
|
|
144
144
|
* @param data - The array of bytes to wrap.
|
|
145
|
-
* @returns The wrapped byte array.
|
|
145
|
+
* @returns {number[]} The wrapped byte array.
|
|
146
146
|
*/
|
|
147
147
|
wrapBytes(data) {
|
|
148
148
|
if (data.length > KilterBoard.messageBodyMaxLength) {
|
|
@@ -165,7 +165,7 @@ export class KilterBoard extends Device {
|
|
|
165
165
|
* The lowest 8 bits of the position get put in the first byte of the group.
|
|
166
166
|
* The highest 8 bits of the position get put in the second byte of the group.
|
|
167
167
|
* @param position - The position to encode.
|
|
168
|
-
* @returns The encoded byte array representing the position.
|
|
168
|
+
* @returns {number[]} The encoded byte array representing the position.
|
|
169
169
|
*/
|
|
170
170
|
encodePosition(position) {
|
|
171
171
|
const position1 = position & 255;
|
|
@@ -237,7 +237,7 @@ export class KilterBoard extends Device {
|
|
|
237
237
|
* https://github.com/ramda/ramda/blob/master/source/splitEvery.js
|
|
238
238
|
* @param {Number} n
|
|
239
239
|
* @param {Array} list
|
|
240
|
-
* @return {Array}
|
|
240
|
+
* @return {Array<number[]>}
|
|
241
241
|
*/
|
|
242
242
|
splitEvery(n, list) {
|
|
243
243
|
if (n <= 0) {
|
|
@@ -2,6 +2,7 @@ import { Device } from "../device.model.js";
|
|
|
2
2
|
import type { IProgressor } from "../../interfaces/device/progressor.interface.js";
|
|
3
3
|
/**
|
|
4
4
|
* Represents a Tindeq Progressor device.
|
|
5
|
+
* {@link https://tindeq.com}
|
|
5
6
|
*/
|
|
6
7
|
export declare class Progressor extends Device implements IProgressor {
|
|
7
8
|
constructor();
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { Device } from "../device.model.js";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
* {@link https://tindeq.com}
|
|
3
|
+
* Progressor responses
|
|
5
4
|
*/
|
|
6
5
|
var ProgressorResponses;
|
|
7
6
|
(function (ProgressorResponses) {
|
|
@@ -33,6 +32,7 @@ var ProgressorResponses;
|
|
|
33
32
|
})(ProgressorResponses || (ProgressorResponses = {}));
|
|
34
33
|
/**
|
|
35
34
|
* Represents a Tindeq Progressor device.
|
|
35
|
+
* {@link https://tindeq.com}
|
|
36
36
|
*/
|
|
37
37
|
export class Progressor extends Device {
|
|
38
38
|
constructor() {
|
|
@@ -257,13 +257,13 @@ export declare abstract class Device extends BaseModel implements IDevice {
|
|
|
257
257
|
* @param {('csv' | 'json' | 'xml')} [format='csv'] - The format in which to download the data.
|
|
258
258
|
* Defaults to 'csv'. Accepted values are 'csv', 'json', and 'xml'.
|
|
259
259
|
*
|
|
260
|
-
* @returns {void}
|
|
261
|
-
* @
|
|
260
|
+
* @returns {Promise<void>} Resolves when the data has been downloaded/written
|
|
261
|
+
* @public
|
|
262
262
|
*
|
|
263
263
|
* @example
|
|
264
|
-
* device.download('json');
|
|
264
|
+
* await device.download('json');
|
|
265
265
|
*/
|
|
266
|
-
download: (format?: "csv" | "json" | "xml") => void
|
|
266
|
+
download: (format?: "csv" | "json" | "xml") => Promise<void>;
|
|
267
267
|
/**
|
|
268
268
|
* Returns UUIDs of all services associated with the device.
|
|
269
269
|
* @returns {string[]} Array of service UUIDs.
|
|
@@ -372,54 +372,57 @@ export class Device extends BaseModel {
|
|
|
372
372
|
* @param {('csv' | 'json' | 'xml')} [format='csv'] - The format in which to download the data.
|
|
373
373
|
* Defaults to 'csv'. Accepted values are 'csv', 'json', and 'xml'.
|
|
374
374
|
*
|
|
375
|
-
* @returns {void}
|
|
376
|
-
* @
|
|
375
|
+
* @returns {Promise<void>} Resolves when the data has been downloaded/written
|
|
376
|
+
* @public
|
|
377
377
|
*
|
|
378
378
|
* @example
|
|
379
|
-
* device.download('json');
|
|
379
|
+
* await device.download('json');
|
|
380
380
|
*/
|
|
381
|
-
download = (format = "csv") => {
|
|
382
|
-
if (typeof document === "undefined" || typeof window === "undefined") {
|
|
383
|
-
console.warn("Download is not supported outside a browser environment.");
|
|
384
|
-
return;
|
|
385
|
-
}
|
|
381
|
+
download = async (format = "csv") => {
|
|
386
382
|
let content = "";
|
|
387
|
-
let mimeType = "";
|
|
388
|
-
let fileName = "";
|
|
389
383
|
if (format === "csv") {
|
|
390
384
|
content = this.downloadToCSV();
|
|
391
|
-
mimeType = "text/csv";
|
|
392
385
|
}
|
|
393
386
|
else if (format === "json") {
|
|
394
387
|
content = this.downloadToJSON();
|
|
395
|
-
mimeType = "application/json";
|
|
396
388
|
}
|
|
397
389
|
else if (format === "xml") {
|
|
398
390
|
content = this.downloadToXML();
|
|
399
|
-
mimeType = "application/xml";
|
|
400
391
|
}
|
|
401
392
|
const now = new Date();
|
|
402
393
|
// YYYY-MM-DD
|
|
403
394
|
const date = now.toISOString().split("T")[0];
|
|
404
395
|
// HH-MM-SS
|
|
405
396
|
const time = now.toTimeString().split(" ")[0].replace(/:/g, "-");
|
|
406
|
-
fileName = `data-export-${date}-${time}.${format}`;
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
397
|
+
const fileName = `data-export-${date}-${time}.${format}`;
|
|
398
|
+
if (typeof window !== "undefined" && typeof document !== "undefined") {
|
|
399
|
+
const mimeTypes = {
|
|
400
|
+
csv: "text/csv",
|
|
401
|
+
json: "application/json",
|
|
402
|
+
xml: "application/xml",
|
|
403
|
+
};
|
|
404
|
+
// Create a Blob object containing the data
|
|
405
|
+
const blob = new Blob([content], { type: mimeTypes[format] });
|
|
406
|
+
// Create a URL for the Blob
|
|
407
|
+
const url = globalThis.URL.createObjectURL(blob);
|
|
408
|
+
// Create a link element
|
|
409
|
+
const link = document.createElement("a");
|
|
410
|
+
// Set link attributes
|
|
411
|
+
link.href = url;
|
|
412
|
+
link.setAttribute("download", fileName);
|
|
413
|
+
// Append link to document body
|
|
414
|
+
document.body.appendChild(link);
|
|
415
|
+
// Programmatically click the link to trigger the download
|
|
416
|
+
link.click();
|
|
417
|
+
// Clean up: remove the link and revoke the URL
|
|
418
|
+
document.body.removeChild(link);
|
|
419
|
+
globalThis.URL.revokeObjectURL(url);
|
|
420
|
+
}
|
|
421
|
+
else {
|
|
422
|
+
const { writeFile } = await import("node:fs/promises");
|
|
423
|
+
await writeFile(fileName, content);
|
|
424
|
+
console.log(`File saved as ${fileName}`);
|
|
425
|
+
}
|
|
423
426
|
};
|
|
424
427
|
/**
|
|
425
428
|
* Returns UUIDs of all services associated with the device.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hangtime/grip-connect",
|
|
3
|
-
"version": "0.6.
|
|
4
|
-
"description": "Griptonite Motherboard, Tindeq Progressor, PitchSix Force Board, WHC-06, Entralpi, Climbro, mySmartBoard: Web Bluetooth API Force-Sensing strength analysis for climbers",
|
|
3
|
+
"version": "0.6.1",
|
|
4
|
+
"description": "Griptonite Motherboard, Tindeq Progressor, PitchSix Force Board, WHC-06, Entralpi, Climbro, mySmartBoard: Web + Node Bluetooth API Force-Sensing strength analysis for climbers",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type { IClimbro } from "./device/climbro.interface.js"
|
|
2
|
+
|
|
3
|
+
export type { IEntralpi } from "./device/entralpi.interface.js"
|
|
4
|
+
|
|
5
|
+
export type { IForceBoard } from "./device/forceboard.interface.js"
|
|
6
|
+
|
|
7
|
+
export type { IKilterBoard } from "./device/kilterboard.interface.js"
|
|
8
|
+
|
|
9
|
+
export type { IMotherboard } from "./device/motherboard.interface.js"
|
|
10
|
+
|
|
11
|
+
export type { ImySmartBoard } from "./device/mysmartboard.interface.js"
|
|
12
|
+
|
|
13
|
+
export type { IProgressor } from "./device/progressor.interface.js"
|
|
14
|
+
|
|
15
|
+
export type { IWHC06 } from "./device/wh-c06.interface.js"
|
|
@@ -136,9 +136,9 @@ export class KilterBoard extends Device implements IKilterBoard {
|
|
|
136
136
|
/**
|
|
137
137
|
* Calculates the checksum for a byte array by summing up all bytes ot hre packet in a single-byte variable.
|
|
138
138
|
* @param data - The array of bytes to calculate the checksum for.
|
|
139
|
-
* @returns The calculated checksum value.
|
|
139
|
+
* @returns {number} The calculated checksum value.
|
|
140
140
|
*/
|
|
141
|
-
private checksum(data: number[]) {
|
|
141
|
+
private checksum(data: number[]): number {
|
|
142
142
|
let i = 0
|
|
143
143
|
for (const value of data) {
|
|
144
144
|
i = (i + value) & 255
|
|
@@ -149,9 +149,9 @@ export class KilterBoard extends Device implements IKilterBoard {
|
|
|
149
149
|
/**
|
|
150
150
|
* Wraps a byte array with header and footer bytes for transmission.
|
|
151
151
|
* @param data - The array of bytes to wrap.
|
|
152
|
-
* @returns The wrapped byte array.
|
|
152
|
+
* @returns {number[]} The wrapped byte array.
|
|
153
153
|
*/
|
|
154
|
-
private wrapBytes(data: number[]) {
|
|
154
|
+
private wrapBytes(data: number[]): number[] {
|
|
155
155
|
if (data.length > KilterBoard.messageBodyMaxLength) {
|
|
156
156
|
return []
|
|
157
157
|
}
|
|
@@ -173,9 +173,9 @@ export class KilterBoard extends Device implements IKilterBoard {
|
|
|
173
173
|
* The lowest 8 bits of the position get put in the first byte of the group.
|
|
174
174
|
* The highest 8 bits of the position get put in the second byte of the group.
|
|
175
175
|
* @param position - The position to encode.
|
|
176
|
-
* @returns The encoded byte array representing the position.
|
|
176
|
+
* @returns {number[]} The encoded byte array representing the position.
|
|
177
177
|
*/
|
|
178
|
-
private encodePosition(position: number) {
|
|
178
|
+
private encodePosition(position: number): number[] {
|
|
179
179
|
const position1 = position & 255
|
|
180
180
|
const position2 = (position & 65280) >> 8
|
|
181
181
|
|
|
@@ -188,7 +188,7 @@ export class KilterBoard extends Device implements IKilterBoard {
|
|
|
188
188
|
* @param color - The color string in hexadecimal format (e.g., 'FFFFFF').
|
|
189
189
|
* @returns The encoded /compressed color value.
|
|
190
190
|
*/
|
|
191
|
-
private encodeColor(color: string) {
|
|
191
|
+
private encodeColor(color: string): number {
|
|
192
192
|
const substring = color.substring(0, 2)
|
|
193
193
|
const substring2 = color.substring(2, 4)
|
|
194
194
|
|
|
@@ -209,7 +209,7 @@ export class KilterBoard extends Device implements IKilterBoard {
|
|
|
209
209
|
* @param ledColor - The color of the LED in hexadecimal format (e.g., 'FFFFFF').
|
|
210
210
|
* @returns The encoded byte array representing the placement.
|
|
211
211
|
*/
|
|
212
|
-
private encodePlacement(position: number, ledColor: string) {
|
|
212
|
+
private encodePlacement(position: number, ledColor: string): number[] {
|
|
213
213
|
return [...this.encodePosition(position), this.encodeColor(ledColor)]
|
|
214
214
|
}
|
|
215
215
|
|
|
@@ -218,7 +218,7 @@ export class KilterBoard extends Device implements IKilterBoard {
|
|
|
218
218
|
* @param {{ position: number; role_id: number }[]} climbPlacementList - The list of climb placements containing position and role ID.
|
|
219
219
|
* @returns {number[]} The final byte array ready for transmission.
|
|
220
220
|
*/
|
|
221
|
-
private prepBytesV3(climbPlacementList: { position: number; role_id: number }[]) {
|
|
221
|
+
private prepBytesV3(climbPlacementList: { position: number; role_id: number }[]): number[] {
|
|
222
222
|
const resultArray: number[][] = []
|
|
223
223
|
let tempArray: number[] = [KilterBoardPacket.V3_MIDDLE]
|
|
224
224
|
|
|
@@ -257,9 +257,9 @@ export class KilterBoard extends Device implements IKilterBoard {
|
|
|
257
257
|
* https://github.com/ramda/ramda/blob/master/source/splitEvery.js
|
|
258
258
|
* @param {Number} n
|
|
259
259
|
* @param {Array} list
|
|
260
|
-
* @return {Array}
|
|
260
|
+
* @return {Array<number[]>}
|
|
261
261
|
*/
|
|
262
|
-
private splitEvery(n: number, list: number[]) {
|
|
262
|
+
private splitEvery(n: number, list: number[]): number[][] {
|
|
263
263
|
if (n <= 0) {
|
|
264
264
|
throw new Error("First argument to splitEvery must be a positive integer")
|
|
265
265
|
}
|
|
@@ -2,8 +2,7 @@ import { Device } from "../device.model.js"
|
|
|
2
2
|
import type { IProgressor } from "../../interfaces/device/progressor.interface.js"
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
* {@link https://tindeq.com}
|
|
5
|
+
* Progressor responses
|
|
7
6
|
*/
|
|
8
7
|
enum ProgressorResponses {
|
|
9
8
|
/**
|
|
@@ -39,6 +38,7 @@ enum ProgressorResponses {
|
|
|
39
38
|
|
|
40
39
|
/**
|
|
41
40
|
* Represents a Tindeq Progressor device.
|
|
41
|
+
* {@link https://tindeq.com}
|
|
42
42
|
*/
|
|
43
43
|
export class Progressor extends Device implements IProgressor {
|
|
44
44
|
constructor() {
|
|
@@ -420,30 +420,21 @@ export abstract class Device extends BaseModel implements IDevice {
|
|
|
420
420
|
* @param {('csv' | 'json' | 'xml')} [format='csv'] - The format in which to download the data.
|
|
421
421
|
* Defaults to 'csv'. Accepted values are 'csv', 'json', and 'xml'.
|
|
422
422
|
*
|
|
423
|
-
* @returns {void}
|
|
424
|
-
* @
|
|
423
|
+
* @returns {Promise<void>} Resolves when the data has been downloaded/written
|
|
424
|
+
* @public
|
|
425
425
|
*
|
|
426
426
|
* @example
|
|
427
|
-
* device.download('json');
|
|
427
|
+
* await device.download('json');
|
|
428
428
|
*/
|
|
429
|
-
download = (format: "csv" | "json" | "xml" = "csv"): void => {
|
|
430
|
-
if (typeof document === "undefined" || typeof window === "undefined") {
|
|
431
|
-
console.warn("Download is not supported outside a browser environment.")
|
|
432
|
-
return
|
|
433
|
-
}
|
|
429
|
+
download = async (format: "csv" | "json" | "xml" = "csv"): Promise<void> => {
|
|
434
430
|
let content = ""
|
|
435
|
-
let mimeType = ""
|
|
436
|
-
let fileName = ""
|
|
437
431
|
|
|
438
432
|
if (format === "csv") {
|
|
439
433
|
content = this.downloadToCSV()
|
|
440
|
-
mimeType = "text/csv"
|
|
441
434
|
} else if (format === "json") {
|
|
442
435
|
content = this.downloadToJSON()
|
|
443
|
-
mimeType = "application/json"
|
|
444
436
|
} else if (format === "xml") {
|
|
445
437
|
content = this.downloadToXML()
|
|
446
|
-
mimeType = "application/xml"
|
|
447
438
|
}
|
|
448
439
|
|
|
449
440
|
const now = new Date()
|
|
@@ -452,30 +443,41 @@ export abstract class Device extends BaseModel implements IDevice {
|
|
|
452
443
|
// HH-MM-SS
|
|
453
444
|
const time = now.toTimeString().split(" ")[0].replace(/:/g, "-")
|
|
454
445
|
|
|
455
|
-
fileName = `data-export-${date}-${time}.${format}`
|
|
446
|
+
const fileName = `data-export-${date}-${time}.${format}`
|
|
456
447
|
|
|
457
|
-
|
|
458
|
-
|
|
448
|
+
if (typeof window !== "undefined" && typeof document !== "undefined") {
|
|
449
|
+
const mimeTypes = {
|
|
450
|
+
csv: "text/csv",
|
|
451
|
+
json: "application/json",
|
|
452
|
+
xml: "application/xml",
|
|
453
|
+
}
|
|
459
454
|
|
|
460
|
-
|
|
461
|
-
|
|
455
|
+
// Create a Blob object containing the data
|
|
456
|
+
const blob = new Blob([content], { type: mimeTypes[format] })
|
|
457
|
+
// Create a URL for the Blob
|
|
458
|
+
const url = globalThis.URL.createObjectURL(blob)
|
|
462
459
|
|
|
463
|
-
|
|
464
|
-
|
|
460
|
+
// Create a link element
|
|
461
|
+
const link = document.createElement("a")
|
|
465
462
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
463
|
+
// Set link attributes
|
|
464
|
+
link.href = url
|
|
465
|
+
link.setAttribute("download", fileName)
|
|
469
466
|
|
|
470
|
-
|
|
471
|
-
|
|
467
|
+
// Append link to document body
|
|
468
|
+
document.body.appendChild(link)
|
|
472
469
|
|
|
473
|
-
|
|
474
|
-
|
|
470
|
+
// Programmatically click the link to trigger the download
|
|
471
|
+
link.click()
|
|
475
472
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
473
|
+
// Clean up: remove the link and revoke the URL
|
|
474
|
+
document.body.removeChild(link)
|
|
475
|
+
globalThis.URL.revokeObjectURL(url)
|
|
476
|
+
} else {
|
|
477
|
+
const { writeFile } = await import("node:fs/promises")
|
|
478
|
+
await writeFile(fileName, content)
|
|
479
|
+
console.log(`File saved as ${fileName}`)
|
|
480
|
+
}
|
|
479
481
|
}
|
|
480
482
|
|
|
481
483
|
/**
|