Skip to content

Commit 0ee1f50

Browse files
committed
refactor: updated B64Service and FileService to prefer FileReader API
1 parent d8952e5 commit 0ee1f50

9 files changed

Lines changed: 168 additions & 88 deletions

File tree

packages/studio-web/src/app/b64.service.ts

Lines changed: 39 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,21 @@
1-
import { forkJoin, Observable, BehaviorSubject } from "rxjs";
2-
import { switchMap } from "rxjs/operators";
1+
import { forkJoin, Observable, BehaviorSubject, lastValueFrom } from "rxjs";
2+
import { map, switchMap } from "rxjs/operators";
33

44
import { HttpClient } from "@angular/common/http";
5-
import { Injectable } from "@angular/core";
5+
import { inject, Injectable } from "@angular/core";
66

77
import { FileService } from "./file.service";
88

99
@Injectable({
1010
providedIn: "root",
1111
})
1212
export class B64Service {
13-
JS_BUNDLE_URL = "assets/bundle.js";
14-
FONTS_BUNDLE_URL = "assets/fonts.b64.css";
15-
/**
16-
* Creates an instance of B64Service, a service for B64 encoding assets.
17-
* @param {HttpClient} http - The HttpClient service for making HTTP requests.
18-
* @param {FileService} fileService - The FileService for handling file operations.
19-
*/
20-
jsAndFontsBundle$ = new BehaviorSubject<[string, string] | null>(null);
21-
constructor(
22-
private http: HttpClient,
23-
private fileService: FileService,
24-
) {
25-
this.getBundle$().subscribe((bundle) => {
26-
this.jsAndFontsBundle$.next([
27-
this.indent(bundle[0], 6), //apply the indentation to the js bundle
28-
this.indent(bundle[1], 6),
29-
]);
30-
});
31-
}
13+
private readonly JS_BUNDLE_URL = "assets/bundle.js";
14+
private readonly FONTS_BUNDLE_URL = "assets/fonts.b64.css";
15+
private http = inject(HttpClient);
16+
private fileService = inject(FileService);
17+
private cachedBundle: Promise<[string, string]> | null = null;
18+
3219
getBundle$(): Observable<[string, string]> {
3320
return forkJoin([
3421
this.http
@@ -38,7 +25,19 @@ export class B64Service {
3825
this.http
3926
.get(this.FONTS_BUNDLE_URL, { responseType: "blob" })
4027
.pipe(switchMap((blob: Blob) => this.fileService.readFile$(blob))),
41-
]);
28+
]).pipe(
29+
map((bundle) => [this.indent(bundle[0], 6), this.indent(bundle[1], 6)]),
30+
);
31+
}
32+
33+
// A promise implementation of getBundle()$. Additionally, the promise is
34+
// cached and reused on subsequent bundle requests.
35+
getBundle(): Promise<[string, string]> {
36+
if (!this.cachedBundle) {
37+
this.cachedBundle = lastValueFrom(this.getBundle$());
38+
}
39+
40+
return this.cachedBundle;
4241
}
4342
utf8_to_b64(str: string) {
4443
// See https://stackoverflow.com/questions/30106476/using-javascripts-atob-to-decode-base64-doesnt-properly-decode-utf-8-strings
@@ -59,24 +58,32 @@ export class B64Service {
5958
);
6059
}
6160

62-
xmlToB64(xml: Document) {
61+
xmlToB64(xml: Document): string {
6362
return this.utf8_to_b64(
6463
new XMLSerializer()
6564
.serializeToString(xml)
6665
.replace("?><read", "?>\n<read"),
6766
);
6867
}
6968

70-
blobToB64(blob: any) {
71-
return new Promise((resolve, _) => {
72-
const reader = new FileReader();
73-
// @ts-ignore
74-
reader.onloadend = () => resolve(reader.result);
75-
reader.readAsDataURL(blob);
76-
});
69+
// rasToDataURL creates a data URL containing the contents of the RAS document.
70+
rasToDataURL(xml: Document): Promise<string> {
71+
const xmlText = new XMLSerializer()
72+
.serializeToString(xml)
73+
.replace("?><read", "?>\n<read");
74+
75+
return this.fileService.readFileAsDataURL(
76+
xmlText,
77+
"application/readalong+xml",
78+
);
79+
}
80+
81+
// blobToDataURL encodes the blob to a data URL.
82+
blobToDataURL(blob: Blob): Promise<string> {
83+
return this.fileService.readFileAsDataURL(blob);
7784
}
7885

79-
indent(str: string, level: number) {
86+
private indent(str: string, level: number) {
8087
const indent = " ".repeat(level);
8188

8289
return str

packages/studio-web/src/app/demo/demo.component.html

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,7 @@
2727
#readalong
2828
*ngIf="studioService.render$ | async"
2929
mode="EDIT"
30-
href="data:application/readalong+xml;base64,{{
31-
b64Service.xmlToB64(b64Inputs[1])
32-
}}"
30+
href="{{ rasAsDataURL() }}"
3331
audio="{{ b64Inputs[0] }}"
3432
class="hydrated"
3533
>

packages/studio-web/src/app/demo/demo.component.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Component, OnDestroy, OnInit, ViewChild } from "@angular/core";
1+
import { Component, OnDestroy, OnInit, signal, ViewChild } from "@angular/core";
22
import { Components } from "@readalongs/web-component/loader";
33

44
import { B64Service } from "../b64.service";
@@ -16,7 +16,7 @@ import { ToastrService } from "ngx-toastr";
1616
export class DemoComponent implements OnDestroy, OnInit {
1717
@ViewChild("readalong") readalong!: Components.ReadAlong;
1818
language: "eng" | "fra" | "spa" = "eng";
19-
19+
protected rasAsDataURL = signal<string>("");
2020
constructor(
2121
public b64Service: B64Service,
2222
public studioService: StudioService,
@@ -29,6 +29,12 @@ export class DemoComponent implements OnDestroy, OnInit {
2929
} else if ($localize.locale == "es") {
3030
this.language = "spa";
3131
}
32+
33+
this.studioService.b64Inputs$.subscribe(async (b64Input) => {
34+
if (b64Input[1]) {
35+
this.rasAsDataURL.set(await this.b64Service.rasToDataURL(b64Input[1]));
36+
}
37+
});
3238
}
3339

3440
ngOnInit(): void {}

packages/studio-web/src/app/editor/editor.component.ts

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,7 @@ export class EditorComponent implements OnDestroy, AfterViewInit {
269269
this.wavesurfer.loadBlob(this.editorService.audioControl$.value);
270270
this.wavesurfer.clearSegments();
271271
this.fileService
272-
.readFileAsData$(this.editorService.audioControl$.value)
272+
.readFileAsDataURL$(this.editorService.audioControl$.value)
273273
.pipe(take(1))
274274
.subscribe((audiob64) => {
275275
this.editorService.audioB64Control$.setValue(audiob64);
@@ -461,18 +461,12 @@ export class EditorComponent implements OnDestroy, AfterViewInit {
461461
const css = element.getAttribute("css-url");
462462

463463
if (css !== null && css.length > 0) {
464-
if (css.startsWith("data:text/css;base64,")) {
465-
this.wcStylingService.$wcStyleInput.next(
466-
this.b64Service.b64_to_utf8(css.substring(css.indexOf(",") + 1)),
467-
);
468-
} else {
469-
const reply = await fetch(css);
470-
// Did that work? Great!
471-
if (reply.ok) {
472-
reply.text().then((cssText) => {
473-
this.wcStylingService.$wcStyleInput.next(cssText);
474-
});
475-
}
464+
const reply = await fetch(css);
465+
// Did that work? Great!
466+
if (reply.ok) {
467+
reply.text().then((cssText) => {
468+
this.wcStylingService.$wcStyleInput.next(cssText);
469+
});
476470
}
477471
} else {
478472
this.wcStylingService.$wcStyleInput.next("");
@@ -578,9 +572,11 @@ export class EditorComponent implements OnDestroy, AfterViewInit {
578572
this.shepherdService.start();
579573
}
580574
async updateWCStyle($event: string) {
581-
this.readalong?.setCss(
582-
`data:text/css;base64,${this.b64Service.utf8_to_b64($event ?? "")}`,
575+
const css = await this.fileService.readFileAsDataURL(
576+
$event ?? "",
577+
"text/css",
583578
);
579+
this.readalong?.setCss(css);
584580
}
585581
async addWCCustomFont($font: string) {
586582
this.readalong?.addCustomFont($font);

packages/studio-web/src/app/file.service.spec.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,33 @@ describe("FileService", () => {
2525
it("should be created", () => {
2626
expect(service).toBeTruthy();
2727
});
28+
29+
it("should read a text file from a promise", async () => {
30+
const got = await service.readFile("this is a test");
31+
expect(got).toBe("this is a test");
32+
});
33+
34+
it("should create a data URL from a promise", async () => {
35+
const got = await service.readFileAsDataURL("this is a test");
36+
expect(got.startsWith("data:text/plain;base64")).toBeTrue();
37+
38+
// reverse the readFileAsDataURL operation.
39+
const reversed = await fetch(got).then((resp) => resp.text());
40+
expect(reversed).toBe("this is a test");
41+
});
42+
43+
it("should turn utf8 to b64 and back", async () => {
44+
const testUTF8 = `󳬏ۓ脶򍫐䏷򻱚1󔊣򧰗J鷍ˑ⨘󝳗ʳꟴ󆋔=є򻥼Ӳ򦿴¦槩7}摠꾀𴮣۝م𬷊
45+
,^⒆!טФ񤨷Յe󫱷ъ"Ҁ*=ߋ󻅷񌍖_ᾀ\ꡝ񲁿g"՝MU񔡆ЀL
46+
劆֒񦘰ˑ{坋𹔸lǼc&񓱬񊄸Ӽ:󌈅=̹ɽ渭觙􈯶൰ȣ¡圤𹷟򱄢揋ﺝ􊃻^
47+
E̶򱩀򑪟eٌϐ򔜮霗燨综*􍪻񭚴oꕃ𷴨2ҽT񺆥uR혙񗊭:򉪼񙏺ۤƓ騡
48+
񱺡􌡩PȌ񸛍񩍢􅓴冖㌃ۄ𦄜7Ž⇆*zѱ澁nަvۭË́듍JҢ䆹M󩗟繶`;
49+
50+
const got = await service
51+
.readFileAsDataURL(testUTF8)
52+
.then((dataURL) => fetch(dataURL))
53+
.then((resp) => resp.text());
54+
55+
expect(got).toEqual(testUTF8);
56+
});
2857
});

packages/studio-web/src/app/file.service.ts

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
1-
import { Observable, catchError, from, map, of, take } from "rxjs";
1+
import {
2+
Observable,
3+
Subscriber,
4+
catchError,
5+
from,
6+
lastValueFrom,
7+
map,
8+
of,
9+
take,
10+
} from "rxjs";
211
import { HttpClient, HttpErrorResponse } from "@angular/common/http";
312
import { Injectable } from "@angular/core";
413
import { ToastrService } from "ngx-toastr";
@@ -17,14 +26,14 @@ export class FileService {
1726
file: File,
1827
sampleRate: number,
1928
): Observable<AudioBuffer> {
20-
var audioCtx = new AudioContext({ sampleRate });
21-
var audioFile = file.arrayBuffer().then((buffer: any) => {
29+
const audioCtx = new AudioContext({ sampleRate });
30+
const audioFile = file.arrayBuffer().then((buffer: any) => {
2231
return audioCtx.decodeAudioData(buffer);
2332
});
2433
return from(audioFile);
2534
}
2635

27-
returnFileFromPath$ = (url: string, responseType: string = "blob") => {
36+
returnFileFromPath$(url: string, responseType: string = "blob") {
2837
const httpOptions: Object = { responseType };
2938
return this.http.get<any>(url, httpOptions).pipe(
3039
catchError((err: HttpErrorResponse) => {
@@ -42,26 +51,57 @@ export class FileService {
4251
}),
4352
take(1),
4453
);
45-
};
54+
}
55+
56+
readFile$(
57+
blob: Blob | File | string,
58+
type: string = "text/plain",
59+
): Observable<string> {
60+
if (typeof blob === "string") {
61+
blob = new Blob([blob], { type: type });
62+
}
4663

47-
readFile$(blob: Blob | File): Observable<string> {
4864
const reader = new FileReader();
49-
return Observable.create((obs: any) => {
65+
return new Observable((obs: any) => {
5066
reader.onerror = (err) => obs.error(err);
5167
reader.onabort = (err) => obs.error(err);
5268
reader.onload = () => obs.next(reader.result);
5369
reader.onloadend = () => obs.complete();
5470
reader.readAsText(blob);
5571
});
5672
}
57-
readFileAsData$(blob: Blob | File): Observable<any> {
73+
74+
// A promise based implementation of readFile$.
75+
readFile(
76+
blob: Blob | File | string,
77+
type: string = "text/plain",
78+
): Promise<string> {
79+
return lastValueFrom(this.readFile$(blob, type));
80+
}
81+
82+
readFileAsDataURL$(
83+
blob: Blob | File | string,
84+
type: string = "text/plain",
85+
): Observable<string> {
86+
if (typeof blob === "string") {
87+
blob = new Blob([blob], { type: type });
88+
}
89+
5890
const reader = new FileReader();
59-
return Observable.create((obs: any) => {
91+
return new Observable((obs: any) => {
6092
reader.onerror = (err) => obs.error(err);
6193
reader.onabort = (err) => obs.error(err);
6294
reader.onload = () => obs.next(reader.result);
6395
reader.onloadend = () => obs.complete();
6496
reader.readAsDataURL(blob);
6597
});
6698
}
99+
100+
// A promise based implementation of readFileAsDataURL$.
101+
readFileAsDataURL(
102+
blob: Blob | File | string,
103+
type: string = "text/plain",
104+
): Promise<string> {
105+
return lastValueFrom(this.readFileAsDataURL$(blob, type));
106+
}
67107
}

0 commit comments

Comments
 (0)