initial support for pwa mode

This commit is contained in:
2023-11-10 09:53:47 +01:00
parent 35c07a0707
commit ff3eb8fa92
7 changed files with 2608 additions and 188 deletions

View File

@ -12,7 +12,8 @@
"@tauri-apps/cli": "^1.4.0", "@tauri-apps/cli": "^1.4.0",
"@types/audioworklet": "^0.0.49", "@types/audioworklet": "^0.0.49",
"typescript": "^5.2.2", "typescript": "^5.2.2",
"vite": "^4.4.5" "vite": "^4.4.5",
"vite-plugin-pwa": "^0.16.7"
}, },
"dependencies": { "dependencies": {
"@codemirror/lang-javascript": "^6.1.9", "@codemirror/lang-javascript": "^6.1.9",

View File

@ -171,6 +171,7 @@ export class UserAPI {
this.app.interface.error_line.innerHTML = errorMessage; this.app.interface.error_line.innerHTML = errorMessage;
this.app.interface.error_line.style.color = "color-red-800"; this.app.interface.error_line.style.color = "color-red-800";
this.app.interface.error_line.classList.remove("hidden"); this.app.interface.error_line.classList.remove("hidden");
// @ts-ignore
this.errorTimeoutID = setTimeout( this.errorTimeoutID = setTimeout(
() => this.app.interface.error_line.classList.add("hidden"), () => this.app.interface.error_line.classList.add("hidden"),
2000 2000
@ -184,6 +185,7 @@ export class UserAPI {
this.app.interface.error_line.innerHTML = message as string; this.app.interface.error_line.innerHTML = message as string;
this.app.interface.error_line.style.color = "white"; this.app.interface.error_line.style.color = "white";
this.app.interface.error_line.classList.remove("hidden"); this.app.interface.error_line.classList.remove("hidden");
// @ts-ignore
this.printTimeoutID = setTimeout( this.printTimeoutID = setTimeout(
() => this.app.interface.error_line.classList.add("hidden"), () => this.app.interface.error_line.classList.add("hidden"),
4000 4000

View File

@ -81,6 +81,7 @@ export const blinkScript = (
_drawBlinker(shiftAmount); _drawBlinker(shiftAmount);
// Save timeout ID for later clearing // Save timeout ID for later clearing
// @ts-ignore
app.blinkTimeouts[shiftAmount] = setTimeout(() => { app.blinkTimeouts[shiftAmount] = setTimeout(() => {
_clearBlinker(shiftAmount); _clearBlinker(shiftAmount);
// Clear the canvas before drawing new blinkers // Clear the canvas before drawing new blinkers

View File

@ -578,8 +578,7 @@ export class MidiConnection {
if (typeof output === "number") { if (typeof output === "number") {
if (output < 0 || output >= this.midiOutputs.length) { if (output < 0 || output >= this.midiOutputs.length) {
console.error( console.error(
`Invalid MIDI output index. Index must be in the range 0-${ `Invalid MIDI output index. Index must be in the range 0-${this.midiOutputs.length - 1
this.midiOutputs.length - 1
}.` }.`
); );
return this.currentOutputIndex; return this.currentOutputIndex;
@ -608,8 +607,7 @@ export class MidiConnection {
if (typeof input === "number") { if (typeof input === "number") {
if (input < 0 || input >= this.midiInputs.length) { if (input < 0 || input >= this.midiInputs.length) {
console.error( console.error(
`Invalid MIDI input index. Index must be in the range 0-${ `Invalid MIDI input index. Index must be in the range 0-${this.midiInputs.length - 1
this.midiInputs.length - 1
}.` }.`
); );
return -1; return -1;
@ -676,6 +674,7 @@ export class MidiConnection {
delete this.scheduledNotes[noteNumber]; delete this.scheduledNotes[noteNumber];
}, (duration - 0.02) * 1000); }, (duration - 0.02) * 1000);
// @ts-ignore
this.scheduledNotes[noteNumber] = timeoutId; this.scheduledNotes[noteNumber] = timeoutId;
} else { } else {
console.error("MIDI output not available."); console.error("MIDI output not available.");

View File

@ -1,229 +1,230 @@
import { noteNameToMidi } from "zifferjs"; import { noteNameToMidi } from "zifferjs";
import { type UserAPI } from "./API"; import { type UserAPI } from "./API";
import { Player } from "./classes/ZPlayer"; import { Player } from "./classes/ZPlayer";
export {}; export { };
// Extend String prototype // Extend String prototype
declare global { declare global {
interface String { interface String {
speak(): void; speak(): void;
rate(speed: number): string; rate(speed: number): string;
pitch(pitch: number): string; pitch(pitch: number): string;
volume(volume: number): string; volume(volume: number): string;
voice(voice: number): string; voice(voice: number): string;
lang(language: string): string; lang(language: string): string;
options(): SpeechOptions; options(): SpeechOptions;
z(): Player; z(): Player;
z0(): Player; z0(): Player;
z1(): Player; z1(): Player;
z2(): Player; z2(): Player;
z3(): Player; z3(): Player;
z4(): Player; z4(): Player;
z5(): Player; z5(): Player;
z6(): Player; z6(): Player;
z7(): Player; z7(): Player;
z8(): Player; z8(): Player;
z9(): Player; z9(): Player;
z10(): Player; z10(): Player;
z11(): Player; z11(): Player;
z12(): Player; z12(): Player;
z13(): Player; z13(): Player;
z14(): Player; z14(): Player;
z15(): Player; z15(): Player;
z16(): Player; z16(): Player;
note(): number; note(): number;
} }
} }
const isJsonString = (str: string):boolean => { const isJsonString = (str: string): boolean => {
return str[0] === '{' && str[str.length - 1] === '}' return str[0] === '{' && str[str.length - 1] === '}'
} }
const stringObject = (str: string, params: object) => { const stringObject = (str: string, params: object) => {
if(isJsonString(str)) { if (isJsonString(str)) {
const obj = JSON.parse(str); const obj = JSON.parse(str);
return JSON.stringify({...obj, ...params}); return JSON.stringify({ ...obj, ...params });
} else { } else {
return JSON.stringify({...params, text: str}); return JSON.stringify({ ...params, text: str });
} }
} }
export const makeStringExtensions = (api: UserAPI) => { export const makeStringExtensions = (api: UserAPI) => {
String.prototype.speak = function () { String.prototype.speak = function() {
const options = JSON.parse(this.valueOf()); const options = JSON.parse(this.valueOf());
new Speaker({ ...options, text: options.text }).speak().then(() => { new Speaker({ ...options, text: options.text }).speak().then(() => {
// Done // Done
}).catch((e) => { }).catch((e) => {
console.log("Error speaking:", e); console.log("Error speaking:", e);
}); });
}; };
String.prototype.rate = function (speed: number) { String.prototype.rate = function(speed: number) {
return stringObject(this.valueOf(), {rate: speed}); return stringObject(this.valueOf(), { rate: speed });
}; };
String.prototype.pitch = function (pitch: number) { String.prototype.pitch = function(pitch: number) {
return stringObject(this.valueOf(), {pitch: pitch}); return stringObject(this.valueOf(), { pitch: pitch });
}; };
String.prototype.lang = function (language: string) { String.prototype.lang = function(language: string) {
return stringObject(this.valueOf(),{lang: language}); return stringObject(this.valueOf(), { lang: language });
}; };
String.prototype.volume = function (volume: number) { String.prototype.volume = function(volume: number) {
return stringObject(this.valueOf(), {volume: volume}); return stringObject(this.valueOf(), { volume: volume });
}; };
String.prototype.voice = function (voice: number) { String.prototype.voice = function(voice: number) {
return stringObject(this.valueOf(), {voice: voice}); return stringObject(this.valueOf(), { voice: voice });
}; };
String.prototype.z = function (options: {[key: string]: any} = {}) { String.prototype.z = function(options: { [key: string]: any } = {}) {
return api.z(this.valueOf(), options); return api.z(this.valueOf(), options);
}; };
String.prototype.z0 = function (options: {[key: string]: any} = {}) { String.prototype.z0 = function(options: { [key: string]: any } = {}) {
return api.z0(this.valueOf(), options); return api.z0(this.valueOf(), options);
}; };
String.prototype.z1 = function (options: {[key: string]: any} = {}) { String.prototype.z1 = function(options: { [key: string]: any } = {}) {
return api.z1(this.valueOf(), options); return api.z1(this.valueOf(), options);
}; };
String.prototype.z2 = function (options: {[key: string]: any} = {}) { String.prototype.z2 = function(options: { [key: string]: any } = {}) {
return api.z2(this.valueOf(), options); return api.z2(this.valueOf(), options);
}; };
String.prototype.z3 = function (options: {[key: string]: any} = {}) { String.prototype.z3 = function(options: { [key: string]: any } = {}) {
return api.z3(this.valueOf(), options); return api.z3(this.valueOf(), options);
}; };
String.prototype.z4 = function (options: {[key: string]: any} = {}) { String.prototype.z4 = function(options: { [key: string]: any } = {}) {
return api.z4(this.valueOf(), options); return api.z4(this.valueOf(), options);
}; };
String.prototype.z5 = function (options: {[key: string]: any} = {}) { String.prototype.z5 = function(options: { [key: string]: any } = {}) {
return api.z5(this.valueOf(), options); return api.z5(this.valueOf(), options);
}; };
String.prototype.z6 = function (options: {[key: string]: any} = {}) { String.prototype.z6 = function(options: { [key: string]: any } = {}) {
return api.z6(this.valueOf(), options); return api.z6(this.valueOf(), options);
}; };
String.prototype.z7 = function (options: {[key: string]: any} = {}) { String.prototype.z7 = function(options: { [key: string]: any } = {}) {
return api.z7(this.valueOf(), options); return api.z7(this.valueOf(), options);
}; };
String.prototype.z8 = function (options: {[key: string]: any} = {}) { String.prototype.z8 = function(options: { [key: string]: any } = {}) {
return api.z8(this.valueOf(), options); return api.z8(this.valueOf(), options);
}; };
String.prototype.z9 = function (options: {[key: string]: any} = {}) { String.prototype.z9 = function(options: { [key: string]: any } = {}) {
return api.z9(this.valueOf(), options); return api.z9(this.valueOf(), options);
}; };
String.prototype.z10 = function (options: {[key: string]: any} = {}) { String.prototype.z10 = function(options: { [key: string]: any } = {}) {
return api.z10(this.valueOf(), options); return api.z10(this.valueOf(), options);
}; };
String.prototype.z11 = function (options: {[key: string]: any} = {}) { String.prototype.z11 = function(options: { [key: string]: any } = {}) {
return api.z11(this.valueOf(), options); return api.z11(this.valueOf(), options);
}; };
String.prototype.z12 = function (options: {[key: string]: any} = {}) { String.prototype.z12 = function(options: { [key: string]: any } = {}) {
return api.z12(this.valueOf(), options); return api.z12(this.valueOf(), options);
}; };
String.prototype.z13 = function (options: {[key: string]: any} = {}) { String.prototype.z13 = function(options: { [key: string]: any } = {}) {
return api.z13(this.valueOf(), options); return api.z13(this.valueOf(), options);
}; };
String.prototype.z14 = function (options: {[key: string]: any} = {}) { String.prototype.z14 = function(options: { [key: string]: any } = {}) {
return api.z14(this.valueOf(), options); return api.z14(this.valueOf(), options);
}; };
String.prototype.z15 = function (options: {[key: string]: any} = {}) { String.prototype.z15 = function(options: { [key: string]: any } = {}) {
return api.z15(this.valueOf(), options); return api.z15(this.valueOf(), options);
}; };
String.prototype.z16 = function (options: {[key: string]: any} = {}) { String.prototype.z16 = function(options: { [key: string]: any } = {}) {
return api.z16(this.valueOf(), options); return api.z16(this.valueOf(), options);
}; };
String.prototype.note = function () { String.prototype.note = function() {
try { try {
return parseInt(this.valueOf()); return parseInt(this.valueOf());
} catch (e) { } catch (e) {
return noteNameToMidi(this.valueOf()); return noteNameToMidi(this.valueOf());
} }
}; };
} }
type SpeechOptions = { type SpeechOptions = {
text?: string; text?: string;
rate?: number; rate?: number;
pitch?: number; pitch?: number;
volume?: number; volume?: number;
voice?: number; voice?: number;
lang?: string; lang?: string;
} }
let speakerTimeout: number; let speakerTimeout: number;
export class Speaker { export class Speaker {
constructor( constructor(
public options: SpeechOptions public options: SpeechOptions
) {} ) { }
speak = () => { speak = () => {
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
if (this.options.text) { if (this.options.text) {
const synth = window.speechSynthesis; const synth = window.speechSynthesis;
if(synth.speaking) synth.cancel(); if (synth.speaking) synth.cancel();
const utterance = new SpeechSynthesisUtterance(this.options.text); const utterance = new SpeechSynthesisUtterance(this.options.text);
utterance.rate = this.options.rate || 1; utterance.rate = this.options.rate || 1;
utterance.pitch = this.options.pitch || 1; utterance.pitch = this.options.pitch || 1;
utterance.volume = this.options.volume || 1; utterance.volume = this.options.volume || 1;
if (this.options.voice) { if (this.options.voice) {
utterance.voice = synth.getVoices()[this.options.voice]; utterance.voice = synth.getVoices()[this.options.voice];
} }
if(this.options.lang) { if (this.options.lang) {
// Check if language has country code // Check if language has country code
if (this.options.lang.length === 2) { if (this.options.lang.length === 2) {
utterance.lang = `${this.options.lang}-${this.options.lang.toUpperCase()}` utterance.lang = `${this.options.lang}-${this.options.lang.toUpperCase()}`
} else if (this.options.lang.length === 5) { } else if (this.options.lang.length === 5) {
utterance.lang = this.options.lang; utterance.lang = this.options.lang;
} else { } else {
// Fallback to en us // Fallback to en us
utterance.lang = 'en-US'; utterance.lang = 'en-US';
} }
}
utterance.onend = () => {
resolve();
};
utterance.onerror = (error) => {
reject(error);
};
if(synth.speaking) {
// Cancel again?
synth.cancel();
// Set timeout
if(speakerTimeout) clearTimeout(speakerTimeout);
speakerTimeout = setTimeout(() => {
synth.speak(utterance);
}, 200);
} else {
synth.speak(utterance);
}
} else {
reject("No text provided");
} }
}); utterance.onend = () => {
} resolve();
};
utterance.onerror = (error) => {
reject(error);
};
if (synth.speaking) {
// Cancel again?
synth.cancel();
// Set timeout
if (speakerTimeout) clearTimeout(speakerTimeout);
// @ts-ignore
speakerTimeout = setTimeout(() => {
synth.speak(utterance);
}, 200);
} else {
synth.speak(utterance);
}
} else {
reject("No text provided");
}
});
}
} }

View File

@ -1,9 +1,14 @@
import { defineConfig } from "vite"; import { defineConfig } from "vite";
import { VitePWA } from 'vite-plugin-pwa';
// import * as mdPlugin from 'vite-plugin-markdown'; // import * as mdPlugin from 'vite-plugin-markdown';
export default defineConfig(({ command, mode, ssrBuild }) => { export default defineConfig(({ command, mode, ssrBuild }) => {
if (command === "serve") { if (command === "serve") {
return { return {
plugins: [
VitePWA({ registerType: 'autoUpdate' })
],
assetsInclude: ["**/*.md"], assetsInclude: ["**/*.md"],
server: { server: {
port: 8000, port: 8000,
@ -12,6 +17,9 @@ export default defineConfig(({ command, mode, ssrBuild }) => {
}; };
} else { } else {
return { return {
plugins: [
VitePWA({ registerType: 'autoUpdate' })
],
chunkSizeWarningLimit: 1600 * 2, chunkSizeWarningLimit: 1600 * 2,
build: { build: {
outDir: "dist", outDir: "dist",

2416
yarn.lock

File diff suppressed because it is too large Load Diff