Skip to content

Commit 4566f32

Browse files
authored
feat: added UI for embedding custom CSS and font to a read-along
Merge pull request #391 from ReadAlongs/dev.del/feat-studio-add-css
2 parents 59e5fb9 + 6293099 commit 4566f32

31 files changed

Lines changed: 1636 additions & 43 deletions

packages/studio-web/src/app/app.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { PrivacyDialog } from "./app.component";
2020
import { ErrorPageComponent } from "./error-page/error-page.component";
2121
import { EditorComponent } from "./editor/editor.component";
2222
import { SharedModule } from "./shared/shared.module";
23+
import { WcStylingComponent } from "./shared/wc-styling/wc-styling.component";
2324

2425
defineCustomElements();
2526

@@ -34,6 +35,7 @@ defineCustomElements();
3435
ErrorPageComponent,
3536
EditorComponent,
3637
// ShepherdComponent
38+
WcStylingComponent,
3739
],
3840
imports: [
3941
BrowserModule,

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
[(ngModel)]="studioService.slots.title"
3939
[ngStyle]="{
4040
'width.ch': studioService.slots.title.length,
41-
'min-width.ch': 20
41+
'min-width.ch': 20,
4242
}"
4343
style="border: none"
4444
placeholder="Enter your title here"
@@ -50,7 +50,7 @@
5050
[(ngModel)]="studioService.slots.subtitle"
5151
[ngStyle]="{
5252
'width.ch': studioService.slots.subtitle.length,
53-
'min-width.ch': 20
53+
'min-width.ch': 20,
5454
}"
5555
style="border: none"
5656
placeholder="Enter your subtitle here"
@@ -60,6 +60,5 @@
6060
</read-along>
6161
</div>
6262
</div>
63-
<div class="row"></div>
6463
</div>
6564
</section>

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

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { StudioService } from "../studio/studio.service";
88
import { DownloadService } from "../shared/download/download.service";
99
import { SupportedOutputs } from "../ras.service";
1010
import { ToastrService } from "ngx-toastr";
11-
11+
import { WcStylingService } from "../shared/wc-styling/wc-styling.service";
1212
@Component({
1313
selector: "app-demo",
1414
templateUrl: "./demo.component.html",
@@ -24,13 +24,20 @@ export class DemoComponent implements OnDestroy, OnInit {
2424
public studioService: StudioService,
2525
private downloadService: DownloadService,
2626
private toastr: ToastrService,
27+
private wcStylingService: WcStylingService,
2728
) {
2829
// If we do more languages, this should be a lookup table
2930
if ($localize.locale == "fr") {
3031
this.language = "fra";
3132
} else if ($localize.locale == "es") {
3233
this.language = "spa";
3334
}
35+
this.wcStylingService.$wcStyleInput.subscribe((css) =>
36+
this.updateWCStyle(css),
37+
);
38+
this.wcStylingService.$wcStyleFonts.subscribe((font) =>
39+
this.addWCCustomFont(font),
40+
);
3441
}
3542

3643
ngOnInit(): void {}
@@ -47,6 +54,8 @@ export class DemoComponent implements OnDestroy, OnInit {
4754
this.studioService.b64Inputs$.value[1],
4855
this.studioService.slots,
4956
this.readalong,
57+
"Studio",
58+
this.wcStylingService,
5059
);
5160
} else {
5261
this.toastr.error($localize`Download failed.`, $localize`Sorry!`, {
@@ -72,4 +81,12 @@ export class DemoComponent implements OnDestroy, OnInit {
7281
);
7382
}
7483
}
84+
async updateWCStyle($event: string) {
85+
this.readalong?.setCss(
86+
`data:text/css;base64,${this.b64Service.utf8_to_b64($event ?? "")}`,
87+
);
88+
}
89+
async addWCCustomFont($font: string) {
90+
this.readalong?.addCustomFont($font);
91+
}
7592
}

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

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,36 @@ <h2 i18n="Title for upload box" class="title">
5353
</div>
5454
</div>
5555
</div>
56-
<div class="container">
57-
<div class="row justify-content-center">
58-
<div class="col">
59-
<div id="readalongContainer" #readalongContainer></div>
60-
</div>
56+
<!--div class="fluid-container">
57+
<div class="row flex-nowrap">
58+
<div class="col col-lg-7">
59+
60+
</div>
61+
<div class="col-12 bg-fade col-lg-5 pt-lg-5 d-lg-flex flex-row" *ngIf="rasFileIsLoaded">
62+
63+
64+
</div>
65+
</div></div-->
66+
<div
67+
class="d-flex justify-content-center flex-column flex-xl-row flex-xl-nowrap bg-light mt-3 full-height"
68+
>
69+
<div id="readalongContainer" #readalongContainer></div>
70+
<div
71+
[class.d-none]="!rasFileIsLoaded"
72+
[class.d-lg-flex]="rasFileIsLoaded"
73+
class="flex-row"
74+
>
75+
<div
76+
class="m-0 p-0 handle me-xl-2 d-none d-xl-block"
77+
#handle
78+
draggable="true"
79+
id="handle"
80+
></div>
81+
<app-wc-styling #styleWindow id="styleWindow"></app-wc-styling>
6182
</div>
83+
</div>
6284

85+
<div class="container">
6386
<div class="row justify-content-center mt-5 mb-5">
6487
<div class="col" id="audioToolbar">
6588
<mat-toolbar *ngIf="editorService.audioControl$.valid">

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

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,36 @@
66

77
.row
88
--bs-gutter-x: 0
9+
10+
#readalongContainer
11+
margin: 0 auto
12+
13+
#styleWindow
14+
justify-content: start
15+
margin: 0 auto
16+
17+
@media screen and (min-width:1200px) and (max-width:1500px)
18+
.full-height
19+
max-height: 95vmin
20+
21+
@media screen and (min-width:1200px)
22+
div > #handle
23+
position: relative
24+
width: 13px
25+
height: 100%
26+
background-color: rgba(0,0,0,0.7)
27+
border: 3px solid #eee
28+
border-width: 0 5px
29+
margin-right: 1em
30+
margin-top: 1em
31+
cursor: col-resize
32+
z-index: 10
33+
display: block
34+
#styleWindow
35+
margin: 0
36+
max-height: 95vh
37+
overflow-y: auto
38+
39+
app-wc-styling:has(#style-section.collapsed)
40+
width: 100px
41+
overflow: hidden !important

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

Lines changed: 122 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import WaveSurfer from "wavesurfer.js";
22

3-
import { takeUntil, Subject, take } from "rxjs";
3+
import { takeUntil, Subject, take, fromEvent, debounceTime } from "rxjs";
44
import {
55
AfterViewInit,
66
Component,
@@ -31,6 +31,8 @@ import { DownloadService } from "../shared/download/download.service";
3131
import { SupportedOutputs } from "../ras.service";
3232
import { ToastrService } from "ngx-toastr";
3333
import { validateFileType } from "../utils/utils";
34+
import { WcStylingService } from "../shared/wc-styling/wc-styling.service";
35+
import { WcStylingComponent } from "../shared/wc-styling/wc-styling.component";
3436
@Component({
3537
selector: "app-editor",
3638
templateUrl: "./editor.component.html",
@@ -41,7 +43,8 @@ export class EditorComponent implements OnDestroy, OnInit, AfterViewInit {
4143
@ViewChild("wavesurferContainer") wavesurferContainer!: ElementRef;
4244
wavesurfer: WaveSurfer;
4345
@ViewChild("readalongContainer") readalongContainerElement: ElementRef;
44-
46+
@ViewChild("handle") handleElement!: ElementRef;
47+
@ViewChild("styleWindow") styleElement!: WcStylingComponent;
4548
readalong: Components.ReadAlong;
4649

4750
language: "eng" | "fra" | "spa" = "eng";
@@ -51,14 +54,31 @@ export class EditorComponent implements OnDestroy, OnInit, AfterViewInit {
5154
htmlUploadAccepts = ".html";
5255

5356
unsubscribe$ = new Subject<void>();
57+
rasFileIsLoaded = false;
5458
constructor(
5559
public b64Service: B64Service,
5660
private fileService: FileService,
5761
public shepherdService: ShepherdService,
5862
public editorService: EditorService,
5963
private toastr: ToastrService,
6064
private downloadService: DownloadService,
61-
) {}
65+
private wcStylingService: WcStylingService,
66+
) {
67+
this.wcStylingService.$wcStyleInput.subscribe((css) =>
68+
this.updateWCStyle(css),
69+
);
70+
this.wcStylingService.$wcStyleFonts.subscribe((font) =>
71+
this.addWCCustomFont(font),
72+
);
73+
fromEvent(window, "resize")
74+
.pipe(debounceTime(100), takeUntil(this.unsubscribe$)) // wait for 1 second after the last resize event
75+
.subscribe(() => {
76+
// When the window is resized, we want to reset the style window size
77+
// so that it does not get squeezed too small
78+
console.log("[DEBUG] window resized");
79+
this.resetStyleWindowSize();
80+
});
81+
}
6282

6383
async ngAfterViewInit(): Promise<void> {
6484
this.wavesurfer = WaveSurfer.create({
@@ -121,6 +141,40 @@ export class EditorComponent implements OnDestroy, OnInit, AfterViewInit {
121141
if (window.location.hash.endsWith("startTour=yes")) {
122142
this.startTour();
123143
}
144+
if (this.handleElement) {
145+
fromEvent(this.handleElement.nativeElement, "dragend")
146+
.pipe(takeUntil(this.unsubscribe$))
147+
.subscribe((event) => {
148+
const ev = event as DragEvent;
149+
console.log("[DEBUG] dragged");
150+
if (this.styleElement.collapsed$.getValue()) {
151+
this.resetStyleWindowSize();
152+
return;
153+
}
154+
if (ev.x < 600) {
155+
return;
156+
} // do not let the read along be squeezed past 600px width
157+
if (window.innerWidth - ev.x < 400) return; // do not let the style window be squeezed past 600px width)
158+
// When the handle is dragged, we want to resize the readalong and style containers
159+
const styleEle = this.styleElement?.styleSection
160+
.nativeElement as HTMLElement;
161+
const readAlong = this.readalongContainerElement
162+
?.nativeElement as HTMLElement;
163+
if (styleEle?.style) {
164+
styleEle.style.width = `calc(100vw - ${ev.x + 50}px)`;
165+
}
166+
167+
if (readAlong?.style) {
168+
readAlong.style.width = `${ev.x}px`;
169+
}
170+
});
171+
} else {
172+
this.resetStyleWindowSize();
173+
}
174+
this.styleElement.collapsed$.subscribe((collapsed) => {
175+
// When the style element is collapsed, we want to reset the style window size
176+
this.resetStyleWindowSize();
177+
});
124178
}
125179

126180
ngOnInit(): void {}
@@ -138,8 +192,10 @@ export class EditorComponent implements OnDestroy, OnInit, AfterViewInit {
138192
this.readalong,
139193
this.editorService.slots,
140194
this.editorService.audioB64Control$.value,
195+
this.wcStylingService,
141196
);
142197
}
198+
this.rasFileIsLoaded = false;
143199
}
144200

145201
download(download_type: SupportedOutputs) {
@@ -154,6 +210,7 @@ export class EditorComponent implements OnDestroy, OnInit, AfterViewInit {
154210
this.editorService.slots,
155211
this.readalong,
156212
"Editor", //from
213+
this.wcStylingService,
157214
);
158215
} else {
159216
this.toastr.error($localize`Download failed.`, $localize`Sorry!`, {
@@ -216,10 +273,14 @@ export class EditorComponent implements OnDestroy, OnInit, AfterViewInit {
216273
}
217274

218275
async loadRasFile(file: File | Blob) {
276+
//reset css
277+
this.wcStylingService.$wcStyleInput.next("");
278+
this.wcStylingService.$wcStyleFonts.next("");
219279
const text = await file.text();
220280
const readalong = await this.parseReadalong(text);
221281
this.loadAudioIntoWavesurferElement();
222282
this.renderReadalong(readalong);
283+
this.rasFileIsLoaded = true;
223284
}
224285

225286
async renderReadalong(readalongBody: string | undefined) {
@@ -259,6 +320,12 @@ export class EditorComponent implements OnDestroy, OnInit, AfterViewInit {
259320
// Make Editable
260321
rasElement.setAttribute("mode", "EDIT");
261322
this.readalong = rasElement;
323+
//set custom fonts
324+
if (this.wcStylingService.$wcStyleFonts.getValue().length) {
325+
this.readalong.addCustomFont(
326+
this.wcStylingService.$wcStyleFonts.getValue(),
327+
);
328+
}
262329
const currentWord$ = await this.readalong.getCurrentWord();
263330
const alignments = await this.readalong.getAlignments();
264331
// Subscribe to the current word of the readalong and center the wavesurfer element on it
@@ -359,6 +426,34 @@ export class EditorComponent implements OnDestroy, OnInit, AfterViewInit {
359426
this.createSegments(this.editorService.rasControl$.value);
360427
}
361428

429+
// stylesheet linked
430+
431+
const css = element.getAttribute("css-url");
432+
433+
if (css !== null && css.length > 0) {
434+
if (css.startsWith("data:text/css;base64,")) {
435+
this.wcStylingService.$wcStyleInput.next(
436+
this.b64Service.b64_to_utf8(css.substring(css.indexOf(",") + 1)),
437+
);
438+
} else {
439+
const reply = await fetch(css);
440+
// Did that work? Great!
441+
if (reply.ok) {
442+
reply.text().then((cssText) => {
443+
this.wcStylingService.$wcStyleInput.next(cssText);
444+
});
445+
}
446+
}
447+
} else {
448+
this.wcStylingService.$wcStyleInput.next("");
449+
}
450+
//check for custom fonts
451+
const customFont = readalong.querySelector("#ra-wc-custom-font");
452+
if (customFont !== null) {
453+
this.wcStylingService.$wcStyleFonts.next(customFont.innerHTML);
454+
} else {
455+
this.wcStylingService.$wcStyleFonts.next("");
456+
}
362457
return readalong.querySelector("body")?.innerHTML;
363458
}
364459

@@ -460,4 +555,28 @@ export class EditorComponent implements OnDestroy, OnInit, AfterViewInit {
460555
]);
461556
this.shepherdService.start();
462557
}
558+
async updateWCStyle($event: string) {
559+
this.readalong?.setCss(
560+
`data:text/css;base64,${this.b64Service.utf8_to_b64($event ?? "")}`,
561+
);
562+
}
563+
async addWCCustomFont($font: string) {
564+
this.readalong?.addCustomFont($font);
565+
}
566+
resetStyleWindowSize() {
567+
const styleEle = this.styleElement?.styleSection
568+
.nativeElement as HTMLElement;
569+
const readAlong = this.readalongContainerElement
570+
?.nativeElement as HTMLElement;
571+
572+
if (window.innerWidth > 1199) {
573+
styleEle.style.width = this.styleElement.collapsed$.value
574+
? `65vh`
575+
: "calc(30vw - 50px)";
576+
readAlong.style.width = `70vw`;
577+
} else {
578+
styleEle.style.width = `95vw`;
579+
readAlong.style.width = `95vw`;
580+
}
581+
}
463582
}

0 commit comments

Comments
 (0)