From abda7aa9e5d7194c56e37a188e4e60575aa2ccb2 Mon Sep 17 00:00:00 2001 From: Aidan Pine Date: Wed, 10 Jul 2024 14:48:37 -0700 Subject: [PATCH 01/12] feat: add navbar navigation --- .../studio-web/src/app/app.component.html | 33 ++++++++++++- .../studio-web/src/app/app.component.sass | 6 +++ packages/studio-web/src/app/app.component.ts | 49 +++++++++++++++++-- packages/studio-web/src/app/app.module.ts | 3 +- .../src/app/{studio => }/privacy-dialog.html | 0 .../src/app/studio/studio.component.html | 12 +---- .../src/app/studio/studio.component.spec.ts | 9 ---- .../src/app/studio/studio.component.ts | 34 +------------ 8 files changed, 88 insertions(+), 58 deletions(-) rename packages/studio-web/src/app/{studio => }/privacy-dialog.html (100%) diff --git a/packages/studio-web/src/app/app.component.html b/packages/studio-web/src/app/app.component.html index 9d6757f5..74476173 100644 --- a/packages/studio-web/src/app/app.component.html +++ b/packages/studio-web/src/app/app.component.html @@ -1,2 +1,33 @@ + + + ReadAlong Studio + + + + -
@readalongs/studio-web version: {{ version }}
+ diff --git a/packages/studio-web/src/app/app.component.sass b/packages/studio-web/src/app/app.component.sass index 64ec5a06..d5af050b 100644 --- a/packages/studio-web/src/app/app.component.sass +++ b/packages/studio-web/src/app/app.component.sass @@ -7,3 +7,9 @@ align-items: center justify-content: space-between align-content:space-around + +.nav__button + align-items: right + +.nav-spacer + flex: 1 1 auto \ No newline at end of file diff --git a/packages/studio-web/src/app/app.component.ts b/packages/studio-web/src/app/app.component.ts index 701f2ca2..a2cc1fe7 100644 --- a/packages/studio-web/src/app/app.component.ts +++ b/packages/studio-web/src/app/app.component.ts @@ -1,7 +1,8 @@ -import { Subject } from "rxjs"; - +import { Subject, takeUntil } from "rxjs"; +import { MatDialogRef, MatDialog } from "@angular/material/dialog"; import { Component, OnDestroy, OnInit } from "@angular/core"; import { environment } from "../environments/environment"; +import { Router } from "@angular/router"; @Component({ selector: "app-root", templateUrl: "./app.component.html", @@ -10,8 +11,25 @@ import { environment } from "../environments/environment"; export class AppComponent implements OnDestroy, OnInit { unsubscribe$ = new Subject(); version = environment.packageJson.singleFileBundleVersion; - constructor() {} - ngOnInit(): void {} + currentURL = "/"; + constructor( + private dialog: MatDialog, + public router: Router, + ) {} + ngOnInit(): void { + this.router.events.pipe(takeUntil(this.unsubscribe$)).subscribe((event) => { + if (event.type === 1) { + this.currentURL = event.url; + } + }); + } + + openPrivacyDialog(): void { + this.dialog.open(PrivacyDialog, { + width: "50vw", + maxHeight: "90vh", + }); + } ngOnDestroy(): void { this.unsubscribe$.next(); @@ -20,3 +38,26 @@ export class AppComponent implements OnDestroy, OnInit { ngAfterViewInit() {} } + +@Component({ + selector: "privacy-dialog", + templateUrl: "privacy-dialog.html", +}) +export class PrivacyDialog { + analyticsExcluded = + window.localStorage.getItem("plausible_ignore") === "true"; + constructor(public dialogRef: MatDialogRef) {} + ngOnInit() { + this.dialogRef.updateSize("100%"); + } + + toggleAnalytics() { + if (this.analyticsExcluded) { + window.localStorage.removeItem("plausible_ignore"); + } else { + window.localStorage.setItem("plausible_ignore", "true"); + } + this.analyticsExcluded = + window.localStorage.getItem("plausible_ignore") === "true"; + } +} diff --git a/packages/studio-web/src/app/app.module.ts b/packages/studio-web/src/app/app.module.ts index 11677665..8e291425 100644 --- a/packages/studio-web/src/app/app.module.ts +++ b/packages/studio-web/src/app/app.module.ts @@ -15,7 +15,8 @@ import { UploadComponent } from "./upload/upload.component"; import { TextFormatDialogComponent } from "./text-format-dialog/text-format-dialog.component"; import { NgxRAWebComponentModule } from "@readalongs/ngx-web-component"; import { defineCustomElements } from "@readalongs/web-component/loader"; -import { StudioComponent, PrivacyDialog } from "./studio/studio.component"; +import { StudioComponent } from "./studio/studio.component"; +import { PrivacyDialog } from "./app.component"; import { ErrorPageComponent } from "./error-page/error-page.component"; import { EditorComponent } from "./editor/editor.component"; import { SharedModule } from "./shared/shared.module"; diff --git a/packages/studio-web/src/app/studio/privacy-dialog.html b/packages/studio-web/src/app/privacy-dialog.html similarity index 100% rename from packages/studio-web/src/app/studio/privacy-dialog.html rename to packages/studio-web/src/app/privacy-dialog.html diff --git a/packages/studio-web/src/app/studio/studio.component.html b/packages/studio-web/src/app/studio/studio.component.html index f10e0455..9d867e7f 100644 --- a/packages/studio-web/src/app/studio/studio.component.html +++ b/packages/studio-web/src/app/studio/studio.component.html @@ -11,9 +11,6 @@
-

- Welcome to ReadAlong Studio -

This is a tool to help you make your own interactive 'readalong' that highlights words as they are spoken. Have a look at @@ -28,13 +25,8 @@

to get a better idea of what these are.

- To get started making your own, read our - privacy and data sovereignty policy, then take a tour by clicking on the tour button below, and follow - the steps below. + To get started making your own, click on the tour button below, and + follow the steps below.

diff --git a/packages/studio-web/src/app/studio/studio.component.spec.ts b/packages/studio-web/src/app/studio/studio.component.spec.ts index 8f88fa7a..0bf8d97f 100644 --- a/packages/studio-web/src/app/studio/studio.component.spec.ts +++ b/packages/studio-web/src/app/studio/studio.component.spec.ts @@ -46,13 +46,4 @@ describe("StudioComponent", () => { const app = fixture.componentInstance; expect(app.title).toEqual("readalong-studio"); }); - - it("should have an h1", () => { - const fixture = TestBed.createComponent(StudioComponent); - fixture.detectChanges(); - const compiled = fixture.nativeElement as HTMLElement; - expect(compiled.querySelector("h1")?.textContent).toContain( - "Welcome to ReadAlong Studio", - ); - }); }); diff --git a/packages/studio-web/src/app/studio/studio.component.ts b/packages/studio-web/src/app/studio/studio.component.ts index 0be8e5ee..253b5b71 100644 --- a/packages/studio-web/src/app/studio/studio.component.ts +++ b/packages/studio-web/src/app/studio/studio.component.ts @@ -5,12 +5,11 @@ import { Segment } from "soundswallower"; import { Component, OnDestroy, OnInit, ViewChild } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; import { FormGroup } from "@angular/forms"; -import { MatDialog, MatDialogRef } from "@angular/material/dialog"; +import { MatDialog } from "@angular/material/dialog"; import { Meta } from "@angular/platform-browser"; import { MatStepper } from "@angular/material/stepper"; import { Title } from "@angular/platform-browser"; -import { B64Service } from "../b64.service"; import { createAlignedXML, SoundswallowerService, @@ -60,7 +59,6 @@ export class StudioComponent implements OnDestroy, OnInit { private titleService: Title, private router: Router, private fileService: FileService, - private dialog: MatDialog, private meta: Meta, public shepherdService: ShepherdService, private ssjsService: SoundswallowerService, @@ -277,13 +275,6 @@ export class StudioComponent implements OnDestroy, OnInit { this.shepherdService.start(); } - openPrivacyDialog(): void { - this.dialog.open(PrivacyDialog, { - width: "50vw", - maxHeight: "90vh", - }); - } - formChanged(formGroup: FormGroup) { this.firstFormGroup = formGroup; } @@ -303,26 +294,3 @@ export class StudioComponent implements OnDestroy, OnInit { } } } - -@Component({ - selector: "privacy-dialog", - templateUrl: "privacy-dialog.html", -}) -export class PrivacyDialog { - analyticsExcluded = - window.localStorage.getItem("plausible_ignore") === "true"; - constructor(public dialogRef: MatDialogRef) {} - ngOnInit() { - this.dialogRef.updateSize("100%"); - } - - toggleAnalytics() { - if (this.analyticsExcluded) { - window.localStorage.removeItem("plausible_ignore"); - } else { - window.localStorage.setItem("plausible_ignore", "true"); - } - this.analyticsExcluded = - window.localStorage.getItem("plausible_ignore") === "true"; - } -} From 988129b6c447a6d6ae12bac3606b09034920eda9 Mon Sep 17 00:00:00 2001 From: Aidan Pine Date: Wed, 17 Jul 2024 13:42:45 -0700 Subject: [PATCH 02/12] style: make style a little more consistent across editor and studio --- .../src/app/editor/editor.component.html | 29 +++++---- .../src/app/studio/studio.component.html | 62 ++++++++++--------- 2 files changed, 48 insertions(+), 43 deletions(-) diff --git a/packages/studio-web/src/app/editor/editor.component.html b/packages/studio-web/src/app/editor/editor.component.html index d937aaa9..9954f28f 100644 --- a/packages/studio-web/src/app/editor/editor.component.html +++ b/packages/studio-web/src/app/editor/editor.component.html @@ -1,16 +1,4 @@ -
-
- -
-
+
@@ -18,6 +6,21 @@

Welcome to the ReadAlong Studio Editor

+

+ This is a tool to help you edit your existing 'readalongs'. To get + started, click on the tour button below, and follow the steps. +

+
+ +
-
-

- This is a tool to help you make your own interactive 'readalong' - that highlights words as they are spoken. Have a look at - - launch - this example in East Cree - to get a better idea of what these are. -

-

- To get started making your own, click on the tour button below, and - follow the steps below. -

-
-
- -
+
+
+

+ This is a tool to help you make your own interactive 'readalong' + that highlights words as they are spoken. Have a look at + + launch + this example in East Cree + to get a better idea of what these are. +

+

+ To get started making your own, click on the tour button below, + and follow the steps. +

+
+ +
From e561f108e249f883087dec571c976b2bd7084931 Mon Sep 17 00:00:00 2001 From: Aidan Pine Date: Wed, 17 Jul 2024 16:22:37 -0700 Subject: [PATCH 03/12] fix(editor): return standard xml instead of html fixes https://github.com/ReadAlongs/Studio-Web/issues/325 --- .../src/app/editor/editor.component.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/studio-web/src/app/editor/editor.component.ts b/packages/studio-web/src/app/editor/editor.component.ts index 158ad475..3c2538ef 100644 --- a/packages/studio-web/src/app/editor/editor.component.ts +++ b/packages/studio-web/src/app/editor/editor.component.ts @@ -157,9 +157,25 @@ export class EditorComponent implements OnDestroy, OnInit, AfterViewInit { async parseReadalong(text: string): Promise { const parser = new DOMParser(); const readalong = parser.parseFromString(text, "text/html"); - this.rasControl$.setValue(readalong); const element = readalong.querySelector("read-along"); if (element === null) return null; + + // Store the element as parsed XML + // Create missing body element + const body = document.createElement("body"); + body.id = "t0b0"; + const textNode = element.children[0]; + if (textNode) { + while (textNode.hasChildNodes()) { + // @ts-ignore + body.appendChild(textNode.firstChild); + } + textNode.appendChild(body); + } + const serializer = new XMLSerializer(); + const xmlString = serializer.serializeToString(element); + this.rasControl$.setValue(parser.parseFromString(xmlString, "text/xml")); // re-parse as XML + // Oh, there's an audio file, okay, try to load it const audio = element.getAttribute("audio"); if (audio !== null) { From b4f9c14e6768eb603eb50b25dd861d482c3f6160 Mon Sep 17 00:00:00 2001 From: Aidan Pine Date: Thu, 18 Jul 2024 09:27:23 -0700 Subject: [PATCH 04/12] fix(studio): put dialog in center --- packages/studio-web/src/app/app.component.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/studio-web/src/app/app.component.ts b/packages/studio-web/src/app/app.component.ts index a2cc1fe7..daffe2b6 100644 --- a/packages/studio-web/src/app/app.component.ts +++ b/packages/studio-web/src/app/app.component.ts @@ -27,7 +27,8 @@ export class AppComponent implements OnDestroy, OnInit { openPrivacyDialog(): void { this.dialog.open(PrivacyDialog, { width: "50vw", - maxHeight: "90vh", + maxWidth: "50vw", // maxWidth is required to force material to use justify-content: flex-start + minWidth: "50vw", }); } From fd589b34e4c1a79c8764f110572657e4f1ff6c08 Mon Sep 17 00:00:00 2001 From: Aidan Pine Date: Thu, 18 Jul 2024 12:21:01 -0700 Subject: [PATCH 05/12] fix(i18n): add translations --- packages/studio-web/src/i18n/messages.es.json | 18 +++++++++--------- packages/studio-web/src/i18n/messages.fr.json | 18 +++++++++--------- packages/studio-web/src/i18n/messages.json | 18 +++++++++--------- 3 files changed, 27 insertions(+), 27 deletions(-) diff --git a/packages/studio-web/src/i18n/messages.es.json b/packages/studio-web/src/i18n/messages.es.json index 2402f4dd..bbcb002e 100644 --- a/packages/studio-web/src/i18n/messages.es.json +++ b/packages/studio-web/src/i18n/messages.es.json @@ -1,11 +1,12 @@ { "locale": "es", "translations": { + "8299719445551358262": "Studio de ReadAlong", "2148054857885504777": " ¡Felicidades! ¡Aquí está su ReadAlong! ", "5701618810648052610": "Título", "1137319519199859335": "Subtítulo", - "8366946611697584032": " ¡Siga el tour! ", "1743078436798702107": " Bienvenidos al editor del Studio de ReadAlong ", + "8366946611697584032": " ¡Siga el tour! ", "3903003521039334232": " Seleccione un documento HTML offline de ReadAlong. ", "5192585697967841844": " Para obtener este documento, debería haber creado un ReadAlong utilizando el {$START_LINK}{$START_TAG_MAT_ICON}launch{$CLOSE_TAG_MAT_ICON} Studio{$CLOSE_LINK} y luego haber seleccionado «HTML offline» como formato. O puede utilizar el archivo HTML offline incluido en el formato de descarga Archivos web comprimidos. ", "2136422915588832930": "Barra de herramientas de audio", @@ -17,6 +18,12 @@ "7504412437394032605": " ¡Uy! ¡Parece que está extraviado! No se supone que usted vea esta página. Por favor regrese a la página de inicio haciendo clic aquí debajo. ", "8793650218766282421": " ¡Lléveme al Inicio! ", "6818717224121299397": "Ah, el fichero no se puede acceder. Intente otra vez más tarde.", + "8439955599488894226": "Política de privacidad", + "9145384756401372637": " Hemos creado esta herramienta con el objetivo fundamental de respetar su privacidad y la soberanía de sus datos. El audio que usted use en este sitio {$START_BOLD_TEXT}no es cargado a ninguna otra parte{$CLOSE_BOLD_TEXT}. Se queda en su computadora. El texto que usted use para este ReadAlong se cargará a un servidor mediante una conexión encriptada para pre-procesarlo. Su texto no se guarda en el servidor ni se usará para otros propósitos. Hacer uso de este sitio significa que usted está de acuerdo con que sus datos (audio + texto) se utilicen de la manera aquí indicada.", + "3354727320770003649": " Casi todos los sitios web que usted visita recogen información y datos sobre sus usuarios—en este caso, usted—utilizando herramientas de Web Analytics (análisis de la web) y esto conlleva a ciertas preocupaciones sobre la privacidad del usuario. Nosotros hemos optado por utilizar {$START_LINK}Plausible Analytics{$CLOSE_LINK}, lo que acarrea un costo para nosotros pero al mismo tiempo garantiza que la información recopilada en su visita al sitio no sea vendida y respeta su privacidad. Nuestro equipo utiliza esta información para determinar cuántas personas acceden al sitio, qué tipo de equipo utilizan para acceder y otros indicadores que utilizamos para mejorar el sitio. Para ver una lista completa de la información que recogemos, por favor ver {$START_LINK_1}la política de información de Plausible{$CLOSE_LINK}. Para un ejemplo de los datos que vemos en un lenguaje menos técnico, puede visitar este {$START_LINK_2}sitio de prueba{$CLOSE_LINK}. En cualquier caso, usted puede optar por no brindar ninguna información al sitio si hace clic en el botón «No aceptar» aquí debajo (siempre tiene la posibilidad de cambiar esta opción luego si usted lo desea).", + "8754999798797911202": " Aceptar Analytics ", + "5348316094035024059": " No aceptar Analytics ", + "479660647798030606": " Estoy de acuerdo ", "8259766897258591399": "Formato de descarga", "5625881490589550893": "HTML offline", "1380336453423081030": "Archivos web comprimidos", @@ -80,16 +87,9 @@ "9150310094905104116": "Esto representa visualmente el alineamiento audio-texto de su ReadAlong. Haga clic en la forma de onda para escuchar una palabra. Mueve las barras en los bordes de una palabra para ajustar su alineamiento. Luego haga clic en el «play» botón del ReadAlong para observar los resultados de sus ajustes en el resaltado de texto de su ReadAlong.", "444446810820436138": "Corregir errores de ortografía", "2773189193481434399": "Puede corregir errores de ortografía haciendo clic en una palabra y editándola.", - "8439955599488894226": "Política de privacidad", - "9145384756401372637": " Hemos creado esta herramienta con el objetivo fundamental de respetar su privacidad y la soberanía de sus datos. El audio que usted use en este sitio {$START_BOLD_TEXT}no es cargado a ninguna otra parte{$CLOSE_BOLD_TEXT}. Se queda en su computadora. El texto que usted use para este ReadAlong se cargará a un servidor mediante una conexión encriptada para pre-procesarlo. Su texto no se guarda en el servidor ni se usará para otros propósitos. Hacer uso de este sitio significa que usted está de acuerdo con que sus datos (audio + texto) se utilicen de la manera aquí indicada.", - "3354727320770003649": " Casi todos los sitios web que usted visita recogen información y datos sobre sus usuarios—en este caso, usted—utilizando herramientas de Web Analytics (análisis de la web) y esto conlleva a ciertas preocupaciones sobre la privacidad del usuario. Nosotros hemos optado por utilizar {$START_LINK}Plausible Analytics{$CLOSE_LINK}, lo que acarrea un costo para nosotros pero al mismo tiempo garantiza que la información recopilada en su visita al sitio no sea vendida y respeta su privacidad. Nuestro equipo utiliza esta información para determinar cuántas personas acceden al sitio, qué tipo de equipo utilizan para acceder y otros indicadores que utilizamos para mejorar el sitio. Para ver una lista completa de la información que recogemos, por favor ver {$START_LINK_1}la política de información de Plausible{$CLOSE_LINK}. Para un ejemplo de los datos que vemos en un lenguaje menos técnico, puede visitar este {$START_LINK_2}sitio de prueba{$CLOSE_LINK}. En cualquier caso, usted puede optar por no brindar ninguna información al sitio si hace clic en el botón «No aceptar» aquí debajo (siempre tiene la posibilidad de cambiar esta opción luego si usted lo desea).", - "8754999798797911202": " Aceptar Analytics ", - "5348316094035024059": " No aceptar Analytics ", - "479660647798030606": " Estoy de acuerdo ", "8428348909593474745": "Paso 1", - "7458890725604973091": "Bienvenido al Studio de ReadAlong", "3614618598824071164": " Esta es una herramienta diseñada para ayudarlo a crear su propio 'readalong' que resalta las palabras a medida que se pronuncian. Puede ver {$START_LINK}{$START_TAG_MAT_ICON}launch{$CLOSE_TAG_MAT_ICON} este ejemplo en el idioma cree oriental{$CLOSE_LINK} para tener una mejor idea de qué es un 'readalong'. ", - "11480612528390167": "Para empezar a crear su propio 'readalong', lea nuestra {$START_LINK}política de privacidad y soberanía de los datos{$CLOSE_LINK} y luego siga el tour del sitio haciendo clic en el botón «Siga el tour» y siguiendo los pasos descritos aquí debajo. ", + "5224223351795929492": "Para empezar a crear su propio 'readalong' siga el tour del sitio haciendo clic en el botón «Siga el tour» y siguiendo los pasos descritos aquí debajo.", "3943314737845757694": "Paso 2", "1021386634200142621": "Studio de ReadAlong para Narraciones Interactivas", "5448899278320615037": "Cree sus propias historias interactivas que resaltan las palabras a medida que se pronuncian y que se pueden ver offline.", diff --git a/packages/studio-web/src/i18n/messages.fr.json b/packages/studio-web/src/i18n/messages.fr.json index 06eec436..ab0b05f3 100644 --- a/packages/studio-web/src/i18n/messages.fr.json +++ b/packages/studio-web/src/i18n/messages.fr.json @@ -1,11 +1,12 @@ { "locale": "fr", "translations": { + "8299719445551358262": "Studio ReadAlong", "2148054857885504777": " Félicitations! Voici votre ReadAlong! ", "5701618810648052610": "Titre", "1137319519199859335": "Sous-titre", - "8366946611697584032": " Visite guidée ", "1743078436798702107": " Bienvenue à l'éditeur du Studio ReadAlong ", + "8366946611697584032": " Visite guidée ", "3903003521039334232": " Choisissez un fichier HTML ReadAlong. ", "5192585697967841844": " Pour obtenir ce fichier, vous devez avoir créé un ReadAlong en utilisant le {$START_LINK}{$START_TAG_MAT_ICON}launch{$CLOSE_TAG_MAT_ICON} Studio{$CLOSE_LINK}, puis sélectionné « Fichier HTML hors réseau » comme format. Ou, vous pouvez utiliser le fichier HTML hors réseau (« Offline-HTML ») inclus dans le format de Fichiers Web zippés. ", "2136422915588832930": "Barre d'outils audio", @@ -17,6 +18,12 @@ "7504412437394032605": " Oops! Vous semblez perdus! You n'étiez pas supposé voir cette page. Prière de retourner à la page d'accueil en clique ci-dessous. ", "8793650218766282421": " Ramenez-moi à la maison! ", "6818717224121299397": "Hum, le fichier ne peut pas être téléchargé. Prière de réessayer plus tard.", + "8439955599488894226": "Politique de vie privée", + "9145384756401372637": " Cet outil a été conçu avec le but principal de respecter votre vie privée et la souveraineté de vos données. L'audio que vous rentrez sur ce site {$START_BOLD_TEXT}ne sera jamais téléversé{$CLOSE_BOLD_TEXT} mais restera sur votre ordinateur. Le texte que vous utilisez pour ce ReadAlong sera transféré à un serveur par une connection chiffrée pour accomplir le traitement nécessaire, mais ne sera ni sauvegardé ni utilisé à d'autres fins. Votre utilisation de ce site indique votre accord avec cette utilisation de vos données.\n", + "3354727320770003649": " Presque tous les sites web que vous visitez collectent des données d'utilisation à l'aide d'outils d'analytique Web, ce qui peut s'accompagner de divers probèmes de confidentialité. Nous avons choisi d'utiliser {$START_LINK}Plausible Analytics{$CLOSE_LINK}, ce qui entraîne des frais pour nous mais garantit que les données recueillies sur votre visite ici ne sont pas vendues et respectent votre vie privée. Nous utilisons ces données pour déterminer le nombre de personnes qui accèdent au site, les types appareils qu'elles utilisent et d'autres mesures utilisées pour apporter des améliorations au site. Pour une liste complète de ce qui est collecté, veuillez consulter la {$START_LINK_1}polique de données de Plausible{$CLOSE_LINK}. Pour un exemple plus simple du type de données que nous voyons, vous pouvez consulter ce {$START_LINK_2}site de démonstration{$CLOSE_LINK}. Dans tous les cas, vous pouvez désactiver l'analytique en cliquant sur le bouton ci-dessous (vous pourrez toujours la réactiver plus tard). ", + "8754999798797911202": " Ré-activer l'analytique Web ", + "5348316094035024059": " Désactiver l'analytique Web ", + "479660647798030606": " D'accord ", "8259766897258591399": "Format du téléchargement", "5625881490589550893": "Fichier HTML hors réseau", "1380336453423081030": "Fichiers Web zippés", @@ -80,16 +87,9 @@ "9150310094905104116": "Ceci représente visuellement l'alignement audio-texte de votre ReadAlong. Cliquez sur la forme d'onde pour écouter un mot. Déplacez les barres aux bordures d'un mot pour ajuster son alignement. Clickez ensuite sur le bouton de lecture du ReadAlong pour constater le résultat de vos ajustements sur le surlignage de votre ReadAlong.", "444446810820436138": "Corriger les fautes d'orthographe", "2773189193481434399": "Pour corriger une faute d'orthographe, cliquez sur un mot et modifiez-le.", - "8439955599488894226": "Politique de vie privée", - "9145384756401372637": " Cet outil a été conçu avec le but principal de respecter votre vie privée et la souveraineté de vos données. L'audio que vous rentrez sur ce site {$START_BOLD_TEXT}ne sera jamais téléversé{$CLOSE_BOLD_TEXT} mais restera sur votre ordinateur. Le texte que vous utilisez pour ce ReadAlong sera transféré à un serveur par une connection chiffrée pour accomplir le traitement nécessaire, mais ne sera ni sauvegardé ni utilisé à d'autres fins. Votre utilisation de ce site indique votre accord avec cette utilisation de vos données.\n", - "3354727320770003649": " Presque tous les sites web que vous visitez collectent des données d'utilisation à l'aide d'outils d'analytique Web, ce qui peut s'accompagner de divers probèmes de confidentialité. Nous avons choisi d'utiliser {$START_LINK}Plausible Analytics{$CLOSE_LINK}, ce qui entraîne des frais pour nous mais garantit que les données recueillies sur votre visite ici ne sont pas vendues et respectent votre vie privée. Nous utilisons ces données pour déterminer le nombre de personnes qui accèdent au site, les types appareils qu'elles utilisent et d'autres mesures utilisées pour apporter des améliorations au site. Pour une liste complète de ce qui est collecté, veuillez consulter la {$START_LINK_1}polique de données de Plausible{$CLOSE_LINK}. Pour un exemple plus simple du type de données que nous voyons, vous pouvez consulter ce {$START_LINK_2}site de démonstration{$CLOSE_LINK}. Dans tous les cas, vous pouvez désactiver l'analytique en cliquant sur le bouton ci-dessous (vous pourrez toujours la réactiver plus tard). ", - "8754999798797911202": " Ré-activer l'analytique Web ", - "5348316094035024059": " Désactiver l'analytique Web ", - "479660647798030606": " D'accord ", "8428348909593474745": "Étape 1", - "7458890725604973091": " Bienvenue au Studio ReadAlong ", "3614618598824071164": " Cet outil vous aidera à créer une page interactive de lecture accompagnée, sur laquelle les mots sont surlignés lorsqu'ils sont lus à voix haute. Jetez un coup d'œil à {$START_LINK}{$START_TAG_MAT_ICON}launch{$CLOSE_TAG_MAT_ICON} cet exemple en cri de l'Est{$CLOSE_LINK} pour mieux comprendre le concept. ", - "11480612528390167": " Avant de commencer à en faire vous-même, veuillez lire notre {$START_LINK}politique de vie privée et de souveraineté des données{$CLOSE_LINK}, faites une visite guidée en cliquant sur le bouton « Visite guidée » puis suivez les étapes ci-dessous. ", + "5224223351795929492": "Avant de commencer à en faire vous-même faites une visite guidée en cliquant sur le bouton « Visite guidée » puis suivez les étapes ci-dessous.", "3943314737845757694": "Étape 2", "1021386634200142621": "Studio ReadAlong pour contes interactifs", "5448899278320615037": "Créer vos propres contes interactifs multimédias, accessibles hors connexion, qui surlignent les mots en les lisant à voix haute.", diff --git a/packages/studio-web/src/i18n/messages.json b/packages/studio-web/src/i18n/messages.json index a8ce928a..a326133b 100644 --- a/packages/studio-web/src/i18n/messages.json +++ b/packages/studio-web/src/i18n/messages.json @@ -1,11 +1,12 @@ { "locale": "en", "translations": { + "8299719445551358262": " ReadAlong Studio ", "2148054857885504777": " Congratulations! Here's your ReadAlong! ", "5701618810648052610": "Title", "1137319519199859335": "Subtitle", - "8366946611697584032": " Take the tour! ", "1743078436798702107": " Welcome to the ReadAlong Studio Editor ", + "8366946611697584032": " Take the tour! ", "3903003521039334232": " Select an offline HTML ReadAlong file. ", "5192585697967841844": " To get this file, you should have created a ReadAlong using the {$START_LINK}{$START_TAG_MAT_ICON}launch{$CLOSE_TAG_MAT_ICON} Studio{$CLOSE_LINK}, and then selected \"Offline HTML\" as the output. Or, you can use the Offline-HTML file included in the Web Bundle download format. ", "2136422915588832930": "Audio Toolbar", @@ -17,6 +18,12 @@ "7504412437394032605": " Whoops! Looks like you're lost! You weren't supposed to see this page. Please go back to the home page by clicking below. ", "8793650218766282421": " Take me home! ", "6818717224121299397": "Hmm, the file is unreachable. Please try again later.", + "8439955599488894226": "Privacy Policy", + "9145384756401372637": " We have built this tool with the top goal of respecting your privacy and data sovereignty. The audio that you use on this site {$START_BOLD_TEXT}does not get uploaded anywhere{$CLOSE_BOLD_TEXT}. It will stay on your computer. The text you use for this Read Along will be uploaded over an encrypted connection to a server for preprocessing. The text is not saved and is not used for any other purpose. Using this site means that you agree for your data to be used in this way.\n", + "3354727320770003649": " Almost every website you visit will collect data about you using Web Analytics tools which can come with a variety of privacy concerns. We have opted to use {$START_LINK}Plausible Analytics{$CLOSE_LINK}, which incurs a cost for us but ensures that the data gathered about your visit here is not sold and respects your privacy. We use this data to determine how many people access the site, which devices they use, and other metrics used to make improvements to the site. For a full list of what is collected, please view {$START_LINK_1}Plausible's data policy{$CLOSE_LINK}. For a less jargon-y example of the kind of data we see, you can visit this {$START_LINK_2}demonstration site here{$CLOSE_LINK}. In any case, you can opt out entirely by clicking the \"Opt Out\" button below (you can always opt back in later).\n", + "8754999798797911202": " Opt in to Analytics ", + "5348316094035024059": " Opt out of Analytics ", + "479660647798030606": " I agree ", "8259766897258591399": "Output Format", "5625881490589550893": "Offline HTML", "1380336453423081030": "Web Bundle", @@ -80,16 +87,9 @@ "9150310094905104116": "This is a visual representation of the audio-to-text alignment in your read-along. You can click on the wave form of a word to hear it, and drag the bars at the word boundaries to adjust its alignment. Please click on the play button above to see how adjusting the word boundaries affects the highlighting of your readalong.", "444446810820436138": "Fix Spelling Errors", "2773189193481434399": "You can also fix spelling errors by clicking on a word and editing it.", - "8439955599488894226": "Privacy Policy", - "9145384756401372637": " We have built this tool with the top goal of respecting your privacy and data sovereignty. The audio that you use on this site {$START_BOLD_TEXT}does not get uploaded anywhere{$CLOSE_BOLD_TEXT}. It will stay on your computer. The text you use for this Read Along will be uploaded over an encrypted connection to a server for preprocessing. The text is not saved and is not used for any other purpose. Using this site means that you agree for your data to be used in this way.\n", - "3354727320770003649": " Almost every website you visit will collect data about you using Web Analytics tools which can come with a variety of privacy concerns. We have opted to use {$START_LINK}Plausible Analytics{$CLOSE_LINK}, which incurs a cost for us but ensures that the data gathered about your visit here is not sold and respects your privacy. We use this data to determine how many people access the site, which devices they use, and other metrics used to make improvements to the site. For a full list of what is collected, please view {$START_LINK_1}Plausible's data policy{$CLOSE_LINK}. For a less jargon-y example of the kind of data we see, you can visit this {$START_LINK_2}demonstration site here{$CLOSE_LINK}. In any case, you can opt out entirely by clicking the \"Opt Out\" button below (you can always opt back in later).\n", - "8754999798797911202": " Opt in to Analytics ", - "5348316094035024059": " Opt out of Analytics ", - "479660647798030606": " I agree ", "8428348909593474745": "Step 1", - "7458890725604973091": " Welcome to ReadAlong Studio ", "3614618598824071164": " This is a tool to help you make your own interactive 'readalong' that highlights words as they are spoken. Have a look at {$START_LINK}{$START_TAG_MAT_ICON}launch{$CLOSE_TAG_MAT_ICON} this example in East Cree{$CLOSE_LINK} to get a better idea of what these are. ", - "11480612528390167": " To get started making your own, read our {$START_LINK}privacy and data sovereignty policy{$CLOSE_LINK}, then take a tour by clicking on the tour button below, and follow the steps below. ", + "5224223351795929492": " To get started making your own, click on the tour button below, and follow the steps. ", "3943314737845757694": "Step 2", "1021386634200142621": "ReadAlong-Studio for Interactive Storytelling", "5448899278320615037": "Create your own offline compatible interactive multimedia stories that highlight words as they are spoken.", From aa3f438570cb7b1c5653afeed356bb95ae045f02 Mon Sep 17 00:00:00 2001 From: Aidan Pine Date: Fri, 19 Jul 2024 12:11:52 -0700 Subject: [PATCH 06/12] refactor: move editor logic to service --- .../src/app/editor/editor.component.html | 12 ++-- .../src/app/editor/editor.component.ts | 59 ++++++++----------- .../src/app/editor/editor.service.spec.ts | 16 +++++ .../src/app/editor/editor.service.ts | 22 +++++++ 4 files changed, 71 insertions(+), 38 deletions(-) create mode 100644 packages/studio-web/src/app/editor/editor.service.spec.ts create mode 100644 packages/studio-web/src/app/editor/editor.service.ts diff --git a/packages/studio-web/src/app/editor/editor.component.html b/packages/studio-web/src/app/editor/editor.component.html index 9954f28f..17f1de23 100644 --- a/packages/studio-web/src/app/editor/editor.component.html +++ b/packages/studio-web/src/app/editor/editor.component.html @@ -25,14 +25,16 @@

@@ -70,7 +72,7 @@

- + Audio Toolbar diff --git a/packages/studio-web/src/app/editor/editor.component.ts b/packages/studio-web/src/app/editor/editor.component.ts index 3c2538ef..29b2ef76 100644 --- a/packages/studio-web/src/app/editor/editor.component.ts +++ b/packages/studio-web/src/app/editor/editor.component.ts @@ -1,6 +1,6 @@ import WaveSurfer from "wavesurfer.js"; -import { FormBuilder, FormControl, Validators } from "@angular/forms"; -import { BehaviorSubject, takeUntil, Subject, combineLatest, take } from "rxjs"; + +import { takeUntil, Subject, take } from "rxjs"; import { AfterViewInit, Component, @@ -9,11 +9,9 @@ import { OnInit, ViewChild, } from "@angular/core"; -import SegmentsPlugin, { Segment } from "./segments"; -import { ReadAlongSlots } from "../ras.service"; +import SegmentsPlugin from "./segments"; import { Alignment, Components } from "@readalongs/web-component/loader"; import { B64Service } from "../b64.service"; -import { ToastrService } from "ngx-toastr"; import { FileService } from "../file.service"; import { readalong_editor_intro, @@ -28,6 +26,7 @@ import { readalong_editor_fix_text, } from "../shepherd.steps"; import { ShepherdService } from "../shepherd.service"; +import { EditorService } from "./editor.service"; @Component({ selector: "app-editor", templateUrl: "./editor.component.html", @@ -37,29 +36,19 @@ export class EditorComponent implements OnDestroy, OnInit, AfterViewInit { @ViewChild("wavesurferContainer") wavesurferContainer!: ElementRef; wavesurfer: WaveSurfer; @ViewChild("readalongContainer") readalongContainerElement: ElementRef; - audioControl$ = new FormControl(null, Validators.required); - rasControl$ = new FormControl(null, Validators.required); + readalong: Components.ReadAlong; - slots: ReadAlongSlots = { - title: "Title", - subtitle: "Subtitle", - }; + language: "eng" | "fra" | "spa" = "eng"; - audioB64Control$ = new FormControl(null, Validators.required); - public uploadFormGroup = this._formBuilder.group({ - audio: this.audioControl$, - ras: this.rasControl$, - audioB64: this.audioB64Control$, - }); + unsubscribe$ = new Subject(); constructor( - private _formBuilder: FormBuilder, public b64Service: B64Service, private fileService: FileService, - private toastr: ToastrService, public shepherdService: ShepherdService, + public editorService: EditorService, ) { - this.audioControl$.valueChanges + this.editorService.audioControl$.valueChanges .pipe(takeUntil(this.unsubscribe$)) .subscribe((audioFile) => { // If an audio file is loaded, then load the blob to wave surfer and clear any segments @@ -70,7 +59,7 @@ export class EditorComponent implements OnDestroy, OnInit, AfterViewInit { .readFileAsData$(audioFile) .pipe(take(1)) .subscribe((audiob64) => { - this.audioB64Control$.setValue(audiob64); + this.editorService.audioB64Control$.setValue(audiob64); }); } }); @@ -106,8 +95,8 @@ export class EditorComponent implements OnDestroy, OnInit, AfterViewInit { changedSegment.innerText = segment.data.text; } // Update XML text - if (this.rasControl$.value) { - changedSegment = this.rasControl$.value.getElementById( + if (this.editorService.rasControl$.value) { + changedSegment = this.editorService.rasControl$.value.getElementById( segment.data.id, ); if (changedSegment) { @@ -125,10 +114,11 @@ export class EditorComponent implements OnDestroy, OnInit, AfterViewInit { const new_al: Alignment = {}; new_al[segment.data.id] = [start_ms, dur_ms]; // Update XML alignments (uses seconds) - if (this.rasControl$.value) { - let changedSegment = this.rasControl$.value.getElementById( - segment.data.id, - ); + if (this.editorService.rasControl$.value) { + let changedSegment = + this.editorService.rasControl$.value.getElementById( + segment.data.id, + ); if (changedSegment) { changedSegment.setAttribute("time", segment.start); changedSegment.setAttribute("dur", dur.toString()); @@ -174,7 +164,9 @@ export class EditorComponent implements OnDestroy, OnInit, AfterViewInit { } const serializer = new XMLSerializer(); const xmlString = serializer.serializeToString(element); - this.rasControl$.setValue(parser.parseFromString(xmlString, "text/xml")); // re-parse as XML + this.editorService.rasControl$.setValue( + parser.parseFromString(xmlString, "text/xml"), + ); // re-parse as XML // Oh, there's an audio file, okay, try to load it const audio = element.getAttribute("audio"); @@ -183,7 +175,7 @@ export class EditorComponent implements OnDestroy, OnInit, AfterViewInit { // Did that work? Great! if (reply.ok) { const blob = await reply.blob(); - this.audioControl$.setValue( + this.editorService.audioControl$.setValue( new File([blob], "test-audio.webm", { type: "audio/webm" }), ); } @@ -215,21 +207,22 @@ export class EditorComponent implements OnDestroy, OnInit, AfterViewInit { "span[slot='read-along-subheader']", ); if (titleSlot) { - this.slots.title = titleSlot.innerText; + this.editorService.slots.title = titleSlot.innerText; titleSlot.setAttribute("contenteditable", true); // Because we're just loading this from the single-file HTML, it's cumbersome to // use Angular event input event listeners like we do in the demo titleSlot.addEventListener( "input", - (ev: any) => (this.slots.title = ev.target?.innerHTML), + (ev: any) => (this.editorService.slots.title = ev.target?.innerHTML), ); } if (subtitleSlot) { - this.slots.subtitle = subtitleSlot.innerText; + this.editorService.slots.subtitle = subtitleSlot.innerText; subtitleSlot.setAttribute("contenteditable", true); subtitleSlot.addEventListener( "input", - (ev: any) => (this.slots.subtitle = ev.target?.innerHTML), + (ev: any) => + (this.editorService.slots.subtitle = ev.target?.innerHTML), ); } // Make Editable diff --git a/packages/studio-web/src/app/editor/editor.service.spec.ts b/packages/studio-web/src/app/editor/editor.service.spec.ts new file mode 100644 index 00000000..71f4b1ba --- /dev/null +++ b/packages/studio-web/src/app/editor/editor.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from "@angular/core/testing"; + +import { EditorService } from "./editor.service"; + +describe("EditorService", () => { + let service: EditorService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(EditorService); + }); + + it("should be created", () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/packages/studio-web/src/app/editor/editor.service.ts b/packages/studio-web/src/app/editor/editor.service.ts new file mode 100644 index 00000000..a180b1dc --- /dev/null +++ b/packages/studio-web/src/app/editor/editor.service.ts @@ -0,0 +1,22 @@ +import { Injectable } from "@angular/core"; +import { FormBuilder, FormControl, Validators } from "@angular/forms"; +import { ReadAlongSlots } from "../ras.service"; + +@Injectable({ + providedIn: "root", +}) +export class EditorService { + audioControl$ = new FormControl(null, Validators.required); + rasControl$ = new FormControl(null, Validators.required); + audioB64Control$ = new FormControl(null, Validators.required); + slots: ReadAlongSlots = { + title: "Title", + subtitle: "Subtitle", + }; + uploadFormGroup = this._formBuilder.group({ + audio: this.audioControl$, + ras: this.rasControl$, + audioB64: this.audioB64Control$, + }); + constructor(private _formBuilder: FormBuilder) {} +} From ebb7d707fb2642c2ce026b1a4431adfee3251f59 Mon Sep 17 00:00:00 2001 From: Aidan Pine Date: Fri, 19 Jul 2024 16:18:22 -0700 Subject: [PATCH 07/12] feat: preserve state in editor --- .../src/app/editor/editor.component.ts | 222 +++++++++++------- .../src/app/editor/editor.service.ts | 1 + 2 files changed, 133 insertions(+), 90 deletions(-) diff --git a/packages/studio-web/src/app/editor/editor.component.ts b/packages/studio-web/src/app/editor/editor.component.ts index 29b2ef76..f8fb54ca 100644 --- a/packages/studio-web/src/app/editor/editor.component.ts +++ b/packages/studio-web/src/app/editor/editor.component.ts @@ -27,6 +27,7 @@ import { } from "../shepherd.steps"; import { ShepherdService } from "../shepherd.service"; import { EditorService } from "./editor.service"; +import { DownloadService } from "../shared/download/download.service"; @Component({ selector: "app-editor", templateUrl: "./editor.component.html", @@ -47,23 +48,8 @@ export class EditorComponent implements OnDestroy, OnInit, AfterViewInit { private fileService: FileService, public shepherdService: ShepherdService, public editorService: EditorService, - ) { - this.editorService.audioControl$.valueChanges - .pipe(takeUntil(this.unsubscribe$)) - .subscribe((audioFile) => { - // If an audio file is loaded, then load the blob to wave surfer and clear any segments - if (audioFile) { - this.wavesurfer.loadBlob(audioFile); - this.wavesurfer.clearSegments(); - this.fileService - .readFileAsData$(audioFile) - .pipe(take(1)) - .subscribe((audiob64) => { - this.editorService.audioB64Control$.setValue(audiob64); - }); - } - }); - } + private downloadService: DownloadService, + ) {} async ngAfterViewInit(): Promise { this.wavesurfer = WaveSurfer.create({ @@ -80,29 +66,22 @@ export class EditorComponent implements OnDestroy, OnInit, AfterViewInit { height: 200, minPxPerSec: 300, // FIXME: uncertain about this }); + this.loadAudioIntoWavesurferElement(); + + // reload the temporary saved blob from the service + if (this.editorService.temporaryBlob) { + this.onRasFileSelected({ + target: { files: [this.editorService.temporaryBlob] }, + }); + } + this.wavesurfer.on("segment-updated", async (segment, e) => { // Each time a segment is updated we have to update it in both the demo ReadAlong // as well as the XML. This is faster than just editing the XML and asking the // ReadAlong to re-render, which would create all the new audio sprites again etc // when we are just editing a single element at a time. if (e.action == "contentEdited") { - // Update Demo text - let readalongContainerElement = - await this.readalong.getReadAlongElement(); - let changedSegment = - readalongContainerElement.shadowRoot?.getElementById(segment.data.id); - if (changedSegment) { - changedSegment.innerText = segment.data.text; - } - // Update XML text - if (this.editorService.rasControl$.value) { - changedSegment = this.editorService.rasControl$.value.getElementById( - segment.data.id, - ); - if (changedSegment) { - changedSegment.innerText = segment.data.text; - } - } + this.setReadAlongText(segment.data.id, segment.data.text); } if (e.action == "resize") { // Update Demo Alignments (uses milliseconds) @@ -132,23 +111,130 @@ export class EditorComponent implements OnDestroy, OnInit, AfterViewInit { segment.play(); }); } + ngOnInit(): void {} - ngOnDestroy(): void { - this.unsubscribe$.next(); - this.unsubscribe$.complete(); + async ngOnDestroy() { + // Save translations, images and all other edits to a temporary blob before destroying component + // We just re-use the download service method here for simplicity and reload from this when + // navigating back to the editor + if ( + this.editorService.rasControl$.value && + this.editorService.audioB64Control$.value + ) { + this.editorService.temporaryBlob = + await this.downloadService.createSingleFileBlob( + this.editorService.rasControl$.value, + this.readalong, + this.editorService.slots, + this.editorService.audioB64Control$.value, + ); + } + } + + async setReadAlongText(id: string, text: string) { + // Update Demo text + let readalongContainerElement = await this.readalong.getReadAlongElement(); + let changedSegment = + readalongContainerElement.shadowRoot?.getElementById(id); + if (changedSegment) { + changedSegment.innerText = text; + } + // Update XML text + if (this.editorService.rasControl$.value) { + changedSegment = this.editorService.rasControl$.value.getElementById(id); + if (changedSegment) { + changedSegment.innerText = text; + } + } + } + + loadAudioIntoWavesurferElement() { + if (this.editorService.audioControl$.value) { + this.wavesurfer.loadBlob(this.editorService.audioControl$.value); + this.wavesurfer.clearSegments(); + this.fileService + .readFileAsData$(this.editorService.audioControl$.value) + .pipe(take(1)) + .subscribe((audiob64) => { + this.editorService.audioB64Control$.setValue(audiob64); + }); + } + if (this.editorService.rasControl$.value) { + this.createSegments(this.editorService.rasControl$.value); + } } async onRasFileSelected(event: any) { let file: File = event.target.files[0]; const text = await file.text(); - await this.parseReadalong(text); + const readalong = await this.parseReadalong(text); + this.loadAudioIntoWavesurferElement(); + this.renderReadalong(readalong); + } + + async renderReadalong(readalongBody: string | undefined) { + if (readalongBody) { + this.readalongContainerElement.nativeElement.innerHTML = readalongBody; + const rasElement = + this.readalongContainerElement.nativeElement.querySelector( + "read-along", + ); + // Get Title and Subtitle Slots + let titleSlot = rasElement.querySelector( + "span[slot='read-along-header']", + ); + let subtitleSlot = rasElement.querySelector( + "span[slot='read-along-subheader']", + ); + + if (titleSlot) { + if (this.editorService.slots.title) { + titleSlot.innerText = this.editorService.slots.title; + } + // this.editorService.slots.title = titleSlot.innerText; + titleSlot.setAttribute("contenteditable", true); + // Because we're just loading this from the single-file HTML, it's cumbersome to + // use Angular event input event listeners like we do in the demo + titleSlot.addEventListener( + "input", + (ev: any) => (this.editorService.slots.title = ev.target?.innerHTML), + ); + } + if (subtitleSlot) { + if (this.editorService.slots.subtitle) { + subtitleSlot.innerText = this.editorService.slots.subtitle; + } + subtitleSlot.setAttribute("contenteditable", true); + subtitleSlot.addEventListener( + "input", + (ev: any) => + (this.editorService.slots.subtitle = ev.target?.innerHTML), + ); + } + // Make Editable + rasElement.setAttribute("mode", "EDIT"); + this.readalong = rasElement; + const currentWord$ = await this.readalong.getCurrentWord(); + const alignments = await this.readalong.getAlignments(); + // Subscribe to the current word of the readalong and center the wavesurfer element on it + currentWord$.pipe(takeUntil(this.unsubscribe$)).subscribe((word) => { + if (word) { + this.wavesurfer.seekAndCenter( + alignments[word][0] / 1000 / this.wavesurfer.getDuration(), + ); + } + }); + } } - async parseReadalong(text: string): Promise { + async parseReadalong(text: string): Promise { const parser = new DOMParser(); const readalong = parser.parseFromString(text, "text/html"); const element = readalong.querySelector("read-along"); - if (element === null) return null; + + if (element === undefined || element === null) { + return undefined; + } // Store the element as parsed XML // Create missing body element @@ -170,6 +256,7 @@ export class EditorComponent implements OnDestroy, OnInit, AfterViewInit { // Oh, there's an audio file, okay, try to load it const audio = element.getAttribute("audio"); + if (audio !== null) { const reply = await fetch(audio); // Did that work? Great! @@ -183,7 +270,9 @@ export class EditorComponent implements OnDestroy, OnInit, AfterViewInit { // Is read-along linked (including data URI) or embedded? const href = element.getAttribute("href"); if (href === null) { - this.createSegments(element); + if (this.editorService.rasControl$.value) { + this.createSegments(this.editorService.rasControl$.value); + } } else { const reply = await fetch(href); if (reply.ok) { @@ -192,57 +281,10 @@ export class EditorComponent implements OnDestroy, OnInit, AfterViewInit { this.parseReadalong(text2); } } - let readalongBody = readalong.querySelector("body")?.innerHTML; - if (readalongBody) { - this.readalongContainerElement.nativeElement.innerHTML = readalongBody; - const rasElement = - this.readalongContainerElement.nativeElement.querySelector( - "read-along", - ); - // Get Title and Subtitle Slots - let titleSlot = rasElement.querySelector( - "span[slot='read-along-header']", - ); - let subtitleSlot = rasElement.querySelector( - "span[slot='read-along-subheader']", - ); - if (titleSlot) { - this.editorService.slots.title = titleSlot.innerText; - titleSlot.setAttribute("contenteditable", true); - // Because we're just loading this from the single-file HTML, it's cumbersome to - // use Angular event input event listeners like we do in the demo - titleSlot.addEventListener( - "input", - (ev: any) => (this.editorService.slots.title = ev.target?.innerHTML), - ); - } - if (subtitleSlot) { - this.editorService.slots.subtitle = subtitleSlot.innerText; - subtitleSlot.setAttribute("contenteditable", true); - subtitleSlot.addEventListener( - "input", - (ev: any) => - (this.editorService.slots.subtitle = ev.target?.innerHTML), - ); - } - // Make Editable - rasElement.setAttribute("mode", "EDIT"); - this.readalong = rasElement; - const currentWord$ = await this.readalong.getCurrentWord(); - const alignments = await this.readalong.getAlignments(); - // Subscribe to the current word of the readalong and center the wavesurfer element on it - currentWord$.pipe(takeUntil(this.unsubscribe$)).subscribe((word) => { - if (word) { - this.wavesurfer.seekAndCenter( - alignments[word][0] / 1000 / this.wavesurfer.getDuration(), - ); - } - }); - } - return readalong; + return readalong.querySelector("body")?.innerHTML; } - createSegments(element: Element) { + createSegments(element: Document) { this.wavesurfer.clearSegments(); for (const w of Array.from(element.querySelectorAll("w[id]"))) { const wordText = w.textContent; diff --git a/packages/studio-web/src/app/editor/editor.service.ts b/packages/studio-web/src/app/editor/editor.service.ts index a180b1dc..8932d8a2 100644 --- a/packages/studio-web/src/app/editor/editor.service.ts +++ b/packages/studio-web/src/app/editor/editor.service.ts @@ -18,5 +18,6 @@ export class EditorService { ras: this.rasControl$, audioB64: this.audioB64Control$, }); + temporaryBlob: Blob | undefined = undefined; constructor(private _formBuilder: FormBuilder) {} } From e42a83fa9d390999e9c274b09aa5a3ba8272fccf Mon Sep 17 00:00:00 2001 From: Aidan Pine Date: Fri, 19 Jul 2024 16:59:31 -0700 Subject: [PATCH 08/12] refactor: move studio logic into service --- .../src/app/demo/demo.component.html | 114 ++++++++-------- .../studio-web/src/app/demo/demo.component.ts | 17 +-- .../src/app/studio/studio.component.html | 8 +- .../src/app/studio/studio.component.ts | 38 +++--- .../src/app/studio/studio.service.spec.ts | 16 +++ .../src/app/studio/studio.service.ts | 51 +++++++ .../src/app/upload/upload.component.html | 59 +++++--- .../src/app/upload/upload.component.ts | 127 ++++++++---------- 8 files changed, 247 insertions(+), 183 deletions(-) create mode 100644 packages/studio-web/src/app/studio/studio.service.spec.ts create mode 100644 packages/studio-web/src/app/studio/studio.service.ts diff --git a/packages/studio-web/src/app/demo/demo.component.html b/packages/studio-web/src/app/demo/demo.component.html index 4cdb469f..b3018042 100644 --- a/packages/studio-web/src/app/demo/demo.component.html +++ b/packages/studio-web/src/app/demo/demo.component.html @@ -1,60 +1,66 @@
- -
-
-
-

- Congratulations! Here's your ReadAlong! -

-
-
- + +
+
+
+

+ Congratulations! Here's your ReadAlong! +

+
+
+ +
-
-
+
-
- - - - +
+ + + + +
+
-
-
- +
diff --git a/packages/studio-web/src/app/demo/demo.component.ts b/packages/studio-web/src/app/demo/demo.component.ts index 03ec811a..5d4493e6 100644 --- a/packages/studio-web/src/app/demo/demo.component.ts +++ b/packages/studio-web/src/app/demo/demo.component.ts @@ -1,10 +1,10 @@ -import { Observable, Subject } from "rxjs"; +import { Subject } from "rxjs"; -import { Component, Input, OnDestroy, OnInit, ViewChild } from "@angular/core"; +import { Component, OnDestroy, OnInit, ViewChild } from "@angular/core"; import { Components } from "@readalongs/web-component/loader"; import { B64Service } from "../b64.service"; -import { ReadAlongSlots } from "../ras.service"; +import { StudioService } from "../studio/studio.service"; @Component({ selector: "app-demo", @@ -12,17 +12,14 @@ import { ReadAlongSlots } from "../ras.service"; styleUrls: ["./demo.component.sass"], }) export class DemoComponent implements OnDestroy, OnInit { - @Input() b64Inputs: [string, Document]; - @Input() render$: Observable; @ViewChild("readalong") readalong!: Components.ReadAlong; - slots: ReadAlongSlots = { - title: $localize`Title`, - subtitle: $localize`Subtitle`, - }; language: "eng" | "fra" | "spa" = "eng"; unsubscribe$ = new Subject(); - constructor(public b64Service: B64Service) { + constructor( + public b64Service: B64Service, + public studioService: StudioService, + ) { // If we do more languages, this should be a lookup table if ($localize.locale == "fr") { this.language = "fra"; diff --git a/packages/studio-web/src/app/studio/studio.component.html b/packages/studio-web/src/app/studio/studio.component.html index c4ed1530..6d051784 100644 --- a/packages/studio-web/src/app/studio/studio.component.html +++ b/packages/studio-web/src/app/studio/studio.component.html @@ -4,7 +4,7 @@ (selectionChange)="selectionChange($event)" > @@ -54,10 +54,6 @@ --> - + diff --git a/packages/studio-web/src/app/studio/studio.component.ts b/packages/studio-web/src/app/studio/studio.component.ts index 253b5b71..872ac80e 100644 --- a/packages/studio-web/src/app/studio/studio.component.ts +++ b/packages/studio-web/src/app/studio/studio.component.ts @@ -39,6 +39,8 @@ import { DemoComponent } from "../demo/demo.component"; import { UploadComponent } from "../upload/upload.component"; import { StepperSelectionEvent } from "@angular/cdk/stepper"; import { HttpErrorResponse } from "@angular/common/http"; +import { DownloadService } from "../shared/download/download.service"; +import { StudioService } from "./studio.service"; @Component({ selector: "studio-component", @@ -46,10 +48,7 @@ import { HttpErrorResponse } from "@angular/common/http"; styleUrls: ["./studio.component.sass"], }) export class StudioComponent implements OnDestroy, OnInit { - firstFormGroup: any; title = "readalong-studio"; - b64Inputs$ = new Subject<[string, Document]>(); - render$ = new BehaviorSubject(false); @ViewChild("upload", { static: false }) upload?: UploadComponent; @ViewChild("demo", { static: false }) demo?: DemoComponent; @ViewChild("stepper") private stepper: MatStepper; @@ -57,6 +56,8 @@ export class StudioComponent implements OnDestroy, OnInit { private route: ActivatedRoute; constructor( private titleService: Title, + private downloadService: DownloadService, + public studioService: StudioService, private router: Router, private fileService: FileService, private meta: Meta, @@ -64,6 +65,7 @@ export class StudioComponent implements OnDestroy, OnInit { private ssjsService: SoundswallowerService, ) {} ngOnInit(): void { + console.log(this.studioService.langMode$.value); // Set Meta Tags for search engines and social media // We don't have to set charset or viewport for example since Angular already adds them this.titleService.setTitle( @@ -134,9 +136,9 @@ export class StudioComponent implements OnDestroy, OnInit { selectionChange(event: StepperSelectionEvent) { if (event.selectedIndex === 0) { - this.render$.next(false); + this.studioService.render$.next(false); } else if (event.selectedIndex === 1) { - this.render$.next(true); + this.studioService.render$.next(true); } } @@ -144,9 +146,9 @@ export class StudioComponent implements OnDestroy, OnInit { formIsDirty() { return ( - this.upload?.audioControl$.value !== null || - this.upload?.textControl$.value !== null || - this.upload?.$textInput + this.studioService.audioControl$.value !== null || + this.studioService.textControl$.value !== null || + this.studioService.$textInput ); } @@ -162,24 +164,24 @@ export class StudioComponent implements OnDestroy, OnInit { text_file_step["when"] = { show: () => { if (this.upload) { - this.upload.inputMethod.text = "upload"; + this.studioService.inputMethod.text = "upload"; } }, hide: () => { if (this.upload) { - this.upload.inputMethod.text = "edit"; + this.studioService.inputMethod.text = "edit"; } }, }; audio_file_step["when"] = { show: () => { if (this.upload) { - this.upload.inputMethod.audio = "upload"; + this.studioService.inputMethod.audio = "upload"; } }, hide: () => { if (this.upload) { - this.upload.inputMethod.audio = "mic"; + this.studioService.inputMethod.audio = "mic"; } }, }; @@ -198,9 +200,9 @@ export class StudioComponent implements OnDestroy, OnInit { .pipe(takeUntil(this.unsubscribe$)) .subscribe((audioFile) => { if (!(audioFile instanceof HttpErrorResponse) && this.upload) { - this.upload.$textInput.next("Hello world!"); - this.upload.inputMethod.text = "edit"; - this.upload.audioControl$.setValue(audioFile); + this.studioService.$textInput.next("Hello world!"); + this.studioService.inputMethod.text = "edit"; + this.studioService.audioControl$.setValue(audioFile); this.upload?.nextStep(); this.stepper.animationDone.pipe(take(1)).subscribe(() => { // We can only attach to the shadow dom once it's been created, so unfortunately we need to define the steps like this. @@ -275,10 +277,6 @@ export class StudioComponent implements OnDestroy, OnInit { this.shepherdService.start(); } - formChanged(formGroup: FormGroup) { - this.firstFormGroup = formGroup; - } - stepChange(event: any[]) { if (event[0] === "aligned") { const aligned_xml = createAlignedXML(event[2], event[3] as Segment); @@ -288,7 +286,7 @@ export class StudioComponent implements OnDestroy, OnInit { ]) .pipe(takeUntil(this.unsubscribe$)) .subscribe((x: any) => { - this.b64Inputs$.next(x); + this.studioService.b64Inputs$.next(x); this.stepper.next(); }); } diff --git a/packages/studio-web/src/app/studio/studio.service.spec.ts b/packages/studio-web/src/app/studio/studio.service.spec.ts new file mode 100644 index 00000000..24f8e7ec --- /dev/null +++ b/packages/studio-web/src/app/studio/studio.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from "@angular/core/testing"; + +import { StudioService } from "./studio.service"; + +describe("StudioService", () => { + let service: StudioService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(StudioService); + }); + + it("should be created", () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/packages/studio-web/src/app/studio/studio.service.ts b/packages/studio-web/src/app/studio/studio.service.ts new file mode 100644 index 00000000..121a15c5 --- /dev/null +++ b/packages/studio-web/src/app/studio/studio.service.ts @@ -0,0 +1,51 @@ +import { Injectable } from "@angular/core"; +import { ReadAlongSlots } from "../ras.service"; +import { Subject, BehaviorSubject } from "rxjs"; +import { FormBuilder, FormControl, Validators } from "@angular/forms"; + +export enum langMode { + generic = "generic", + specific = "specific", +} + +@Injectable({ + providedIn: "root", +}) +export class StudioService { + slots: ReadAlongSlots = { + title: "Title", + subtitle: "Subtitle", + }; + temporaryBlob: Blob | undefined = undefined; + b64Inputs$ = new Subject<[string, Document]>(); + render$ = new BehaviorSubject(false); + langMode$ = new BehaviorSubject(langMode.generic); + langControl$ = new FormControl( + { value: "und", disabled: this.langMode$.value !== "specific" }, + Validators.required, + ); + textControl$ = new FormControl(null, Validators.required); + audioControl$ = new FormControl( + null, + Validators.required, + ); + $textInput = new BehaviorSubject(""); + public uploadFormGroup = this._formBuilder.group({ + lang: this.langControl$, + text: this.textControl$, + audio: this.audioControl$, + }); + inputMethod = { + audio: "mic", + text: "edit", + }; + constructor(private _formBuilder: FormBuilder) { + this.langMode$.subscribe((chosenLangMode) => { + if (chosenLangMode === langMode.generic) { + this.langControl$.disable(); + } else { + this.langControl$.enable(); + } + }); + } +} diff --git a/packages/studio-web/src/app/upload/upload.component.html b/packages/studio-web/src/app/upload/upload.component.html index 4ebe0ac1..6d17492b 100644 --- a/packages/studio-web/src/app/upload/upload.component.html +++ b/packages/studio-web/src/app/upload/upload.component.html @@ -21,7 +21,7 @@

Text

(change)="toggleTextInput($event)" name="inputMethod" aria-label="Input Method" - [value]="inputMethod.text" + [value]="studioService.inputMethod.text" > WriteText

-
+
@@ -58,7 +58,7 @@

Text

-
+

@@ -78,7 +78,7 @@

Text