Checkpoint
This commit is contained in:
@@ -52,6 +52,7 @@ import PresetSelector from './PresetSelector';
|
||||
import SettingsDialog from './SettingsDialog';
|
||||
import AboutDialog from './AboutDialog';
|
||||
import BankDialog from './BankDialog';
|
||||
import {Tone3000DownloadStaus as Tone3000DownloadStatus} from './Tone3000Dialog';
|
||||
|
||||
import { PiPedalModelFactory, PiPedalModel, State, ZoomedControlInfo, wantsReloadingScreen } from './PiPedalModel';
|
||||
import ZoomedUiControl from './ZoomedUiControl'
|
||||
@@ -1099,7 +1100,9 @@ export
|
||||
</DialogActions>
|
||||
</DialogEx>
|
||||
|
||||
|
||||
{/* Status of Tone3000 download */}
|
||||
<Tone3000DownloadStatus zindex={2000}/>
|
||||
{/* Fatal error mask */}
|
||||
<Modal open={this.state.displayState === State.Error}
|
||||
aria-label="fatal-error"
|
||||
aria-describedby="aria-error-text"
|
||||
|
||||
@@ -211,6 +211,7 @@ export default withStyles(
|
||||
constructor(props: FilePropertyDialogProps) {
|
||||
super(props);
|
||||
|
||||
this.handleTone3000DownloadComplete = this.handleTone3000DownloadComplete.bind(this);
|
||||
|
||||
this.model = PiPedalModelFactory.getInstance();
|
||||
|
||||
@@ -560,15 +561,30 @@ export default withStyles(
|
||||
|
||||
private requestScroll: boolean = false;
|
||||
|
||||
handleTone3000DownloadComplete(resultPath: string) {
|
||||
if (resultPath === "") {
|
||||
// unknown state. Just refresh anyway.
|
||||
this.requestFiles(this.state.navDirectory);
|
||||
return;
|
||||
}
|
||||
if (resultPath.startsWith(this.state.navDirectory)) {
|
||||
this.setState({
|
||||
selectedFile: resultPath,
|
||||
});
|
||||
this.requestFiles(this.state.navDirectory);
|
||||
}
|
||||
}
|
||||
componentDidMount() {
|
||||
super.componentDidMount();
|
||||
this.mounted = true;
|
||||
this.requestFiles(this.state.navDirectory)
|
||||
this.model.onTone3000DownloadCompleteEvent.addEventHandler(this.handleTone3000DownloadComplete);
|
||||
this.requestScroll = true;
|
||||
}
|
||||
componentWillUnmount() {
|
||||
this.stopAutoScroll();
|
||||
this.cancelProgressTimeout();
|
||||
this.model.onTone3000DownloadCompleteEvent.removeEventHandler(this.handleTone3000DownloadComplete);
|
||||
|
||||
super.componentWillUnmount();
|
||||
this.mounted = false;
|
||||
|
||||
@@ -47,6 +47,7 @@ import AudioFileMetadata from './AudioFileMetadata';
|
||||
import { pathFileName } from './FileUtils';
|
||||
import { AlsaSequencerConfiguration, AlsaSequencerPortSelection } from './AlsaSequencer';
|
||||
import { getDefaultModGuiPreference } from './ModGuiHost';
|
||||
import Tone3000DownloadProgress from './Tone3000DownloadProgress';
|
||||
|
||||
export enum State {
|
||||
Loading,
|
||||
@@ -505,6 +506,8 @@ export class PiPedalModel //implements PiPedalModel
|
||||
hasWifiDevice: ObservableProperty<boolean> = new ObservableProperty<boolean>(false);
|
||||
onSnapshotModified: ObservableEvent<SnapshotModifiedEvent> = new ObservableEvent<SnapshotModifiedEvent>();
|
||||
|
||||
onTone3000DownloadCompleteEvent: ObservableEvent<string> = new ObservableEvent<string>();
|
||||
|
||||
ui_plugins: ObservableProperty<UiPlugin[]>
|
||||
= new ObservableProperty<UiPlugin[]>([]);
|
||||
state: ObservableProperty<State> = new ObservableProperty<State>(State.Loading);
|
||||
@@ -550,6 +553,9 @@ export class PiPedalModel //implements PiPedalModel
|
||||
);
|
||||
zoomedUiControl: ObservableProperty<ZoomedControlInfo | undefined> = new ObservableProperty<ZoomedControlInfo | undefined>(undefined);
|
||||
|
||||
tone3000Downloading : ObservableProperty<boolean> = new ObservableProperty<boolean>(false);
|
||||
tone3000DownloadProgress : ObservableProperty<Tone3000DownloadProgress | null> = new ObservableProperty<Tone3000DownloadProgress | null>(null);
|
||||
|
||||
uiPluginsByUri: Map<string, UiPlugin> = new Map<string, UiPlugin>();
|
||||
|
||||
private tone3000DownloadHandler: Tone3000DownloadHandler | null = null;
|
||||
@@ -671,6 +677,18 @@ export class PiPedalModel //implements PiPedalModel
|
||||
this.showAlert(getErrorMessage(error));
|
||||
}
|
||||
}
|
||||
cancelTone3000Download(): void {
|
||||
let downloadProgress = this.tone3000DownloadProgress.get();
|
||||
if (downloadProgress === null) {
|
||||
return;
|
||||
}
|
||||
if (downloadProgress.handle !== -1) {
|
||||
if (!this.webSocket) {
|
||||
return;
|
||||
}
|
||||
this.webSocket.send("cancelTone3000Download",downloadProgress.handle);
|
||||
}
|
||||
}
|
||||
|
||||
showTone3000DownloadDialog(
|
||||
downloadPath: string,
|
||||
@@ -923,6 +941,22 @@ export class PiPedalModel //implements PiPedalModel
|
||||
let hasWifi = body as boolean;
|
||||
this.hasWifiDevice.set(hasWifi);
|
||||
}
|
||||
else if (message === "onTone3000DownloadStarted") {
|
||||
let { handle, title } = body as { handle: number, title: string };
|
||||
this.onTone3000DownloadStarted(handle, title);
|
||||
}
|
||||
else if (message === "onTone3000DownloadProgress") {
|
||||
// body is DownloadProgress
|
||||
this.onTone3000DownloadProgress(body);
|
||||
}
|
||||
else if (message === "onTone3000DownloadComplete") {
|
||||
let resultPath = body as string;
|
||||
this.onTone3000DownloadComplete(resultPath);
|
||||
}
|
||||
else if (message === "onTone3000DownloadError") {
|
||||
let { handle, errorMessage } = body as { handle: number, errorMessage: string };
|
||||
this.onTone3000DownloadError(handle, errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1022,6 +1056,32 @@ export class PiPedalModel //implements PiPedalModel
|
||||
// this.webSocket?.reconnect(); // let the server do it for us.
|
||||
|
||||
}
|
||||
|
||||
// Tone3000 download notification handlers (stub implementations)
|
||||
private onTone3000DownloadStarted(handle: number, title: string): void {
|
||||
this.tone3000Downloading.set(true);
|
||||
}
|
||||
|
||||
private onTone3000DownloadProgress(progress: Tone3000DownloadProgress): void {
|
||||
this.tone3000Downloading.set(true);
|
||||
this.tone3000DownloadProgress.set(progress);
|
||||
}
|
||||
|
||||
private onTone3000DownloadComplete(resultPath: string): void {
|
||||
this.tone3000Downloading.set(false);
|
||||
this.onTone3000DownloadCompleteEvent.fire(resultPath);
|
||||
}
|
||||
|
||||
private onTone3000DownloadError(handle: number, errorMessage: string): void {
|
||||
console.error(`Tone3000 download error: handle=${handle}, error=${errorMessage}`);
|
||||
this.tone3000Downloading.set(false);
|
||||
this.onTone3000DownloadCompleteEvent.fire("");
|
||||
|
||||
setTimeout(() => {
|
||||
this.showAlert(errorMessage);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
setError(message: string): void {
|
||||
this.errorMessage.set(message);
|
||||
this.setState(State.Error);
|
||||
@@ -1084,6 +1144,9 @@ export class PiPedalModel //implements PiPedalModel
|
||||
this.vuSubscriptions = [];
|
||||
this.monitorPatchPropertyListeners = [];
|
||||
|
||||
this.tone3000Downloading.set(false);
|
||||
this.tone3000DownloadProgress.set(null);
|
||||
|
||||
if (this.isAndroidHosted()) {
|
||||
// if unexpected, go back to the device browser immediately.
|
||||
if (this.reconnectReason === ReconnectReason.Disconnected) {
|
||||
|
||||
@@ -30,8 +30,11 @@
|
||||
}
|
||||
|
||||
.text-info-dialog-content #user-content-cc_img {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin: 1px;
|
||||
opacity: 0.6;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.text-info-dialog-content #user-content-tone3000_thumbnail {
|
||||
width: 130px;
|
||||
|
||||
@@ -33,6 +33,15 @@ import rehypeSanitize, {defaultSchema} from 'rehype-sanitize';
|
||||
import rehypeExternalLinks from 'rehype-external-links'
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import { PiPedalError } from './PiPedalError';
|
||||
|
||||
// Extend the default schema to allow target and rel attributes on anchor tags
|
||||
const extendedSchema = {
|
||||
...defaultSchema,
|
||||
attributes: {
|
||||
...defaultSchema.attributes,
|
||||
a: [...(defaultSchema.attributes?.a || []), 'target', 'rel']
|
||||
}
|
||||
};
|
||||
import DialogEx from './DialogEx';
|
||||
import ResizeResponsiveComponent from './ResizeResponsiveComponent';
|
||||
|
||||
@@ -175,9 +184,9 @@ const TextInfoDialog = class extends ResizeResponsiveComponent<TextInfoDialogPro
|
||||
<ReactMarkdown rehypePlugins={
|
||||
[
|
||||
[remarkGfm],
|
||||
[rehypeExternalLinks, {target: '_blank'}],
|
||||
rehypeRaw,
|
||||
[rehypeSanitize, defaultSchema]
|
||||
[rehypeSanitize, extendedSchema],
|
||||
[rehypeExternalLinks, {target: '_blank'}]
|
||||
]}>
|
||||
{this.state.textFileContent}
|
||||
</ReactMarkdown>
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { JSX } from "@emotion/react/jsx-dev-runtime";
|
||||
import { PiPedalModel, getErrorMessage } from "./PiPedalModel";
|
||||
import { PiPedalModel, State, getErrorMessage } from "./PiPedalModel";
|
||||
import DialogEx from "./DialogEx";
|
||||
import DialogContent from "@mui/material/DialogContent";
|
||||
import DialogAction from "@mui/material/DialogActions";
|
||||
import { Button, Typography } from "@mui/material";
|
||||
import { useEffect, useState } from "react";
|
||||
import LinearProgress from "@mui/material/LinearProgress";
|
||||
import Tone3000DownloadProgress from "./Tone3000DownloadProgress";
|
||||
import Dialog from "@mui/material/Dialog";
|
||||
import DialogContent from "@mui/material/DialogContent";
|
||||
import DialogTitle from "@mui/material/DialogTitle";
|
||||
import DialogActions from "@mui/material/DialogActions";
|
||||
|
||||
|
||||
|
||||
export type OnTone3000DownloadHandler = (tone3000DownloadUrl: string) => void;
|
||||
@@ -24,7 +28,7 @@ export class Tone3000DownloadHandler {
|
||||
|
||||
this.handleTone3000DownloadComplete();
|
||||
|
||||
await this.model.downloadModelsFromTone3000(tone3000DownloadUrl,this.downloadPath);
|
||||
await this.model.downloadModelsFromTone3000(tone3000DownloadUrl, this.downloadPath);
|
||||
return;
|
||||
} catch (e) {
|
||||
this.handleTone3000DownloadError(getErrorMessage(e));
|
||||
@@ -57,7 +61,7 @@ export class Tone3000DownloadHandler {
|
||||
|
||||
private popupWindow: Window | null = null;
|
||||
|
||||
private appId: string = "pipedal_app";
|
||||
private appId: string = "pipedal_app3";
|
||||
private redirectUrl(): string {
|
||||
let hostname = window.location.hostname;
|
||||
let port = window.location.port;
|
||||
@@ -114,22 +118,26 @@ export class Tone3000DownloadHandler {
|
||||
this.stopTone3000DialogMonitor();
|
||||
}
|
||||
|
||||
private tone3000DialogInterval: number | undefined = undefined;
|
||||
private tone3000DialogTimeout: number | undefined = undefined;
|
||||
|
||||
private stopTone3000DialogMonitor() {
|
||||
if (this.tone3000DialogInterval !== undefined) {
|
||||
clearInterval(this.tone3000DialogInterval);
|
||||
this.tone3000DialogInterval = undefined;
|
||||
if (this.tone3000DialogTimeout !== undefined) {
|
||||
clearTimeout(this.tone3000DialogTimeout);
|
||||
this.tone3000DialogTimeout = undefined;
|
||||
}
|
||||
}
|
||||
private startTone3000DialogMonitor() {
|
||||
this.tone3000DialogInterval = setInterval(() => {
|
||||
private startTone3000DialogMonitor(delay: number = 2000) {
|
||||
this.stopTone3000DialogMonitor();
|
||||
this.tone3000DialogTimeout = window.setTimeout(() => {
|
||||
if (this.popupWindow == null || this.popupWindow.closed) {
|
||||
this.stopTone3000DialogMonitor();
|
||||
this.popupWindow = null;
|
||||
this.handleTone3000DialogClosed();
|
||||
this.tone3000DialogTimeout = undefined;
|
||||
} else {
|
||||
this.startTone3000DialogMonitor(500);
|
||||
}
|
||||
}, 500);
|
||||
}, delay);
|
||||
}
|
||||
private downloadPath: string = "";
|
||||
private onClosedCallback: (() => void) | undefined = undefined;
|
||||
@@ -151,13 +159,7 @@ export class Tone3000DownloadHandler {
|
||||
this.onDownloadStartedCallback = onDownloadStarted;
|
||||
this.onErrorCallback = onError;
|
||||
|
||||
fetch(TONE3000_PING_URL, { cache: "no-cache" })
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
this.handleTone3000DownloadError(`Unable to connect to Tone3000: ${response.status}${response.statusText}`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// online
|
||||
let popupWidth = Math.floor(window.innerWidth * 0.8);
|
||||
let popupHeight = Math.floor(window.innerHeight * 0.8);
|
||||
@@ -171,19 +173,28 @@ export class Tone3000DownloadHandler {
|
||||
"popup",
|
||||
`innerWidth=${popupWidth},innerHeight=${popupHeight},scrollbars=yes`
|
||||
);
|
||||
this.startTone3000DialogMonitor();
|
||||
|
||||
if (!this.popupWindow) {
|
||||
console.error("Failed to open Tone3000 dialog popup window.");
|
||||
this.handleTone3000DownloadError("Cannot open popup window.");
|
||||
return;
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
this.startTone3000DialogMonitor();
|
||||
fetch(TONE3000_PING_URL, { method: "HEAD", cache: "no-cache" }).then(response => {
|
||||
if (!response.ok) {
|
||||
// offline
|
||||
this.handleTone3000DownloadError("Unable to connect to Tone3000 servers. An internet connection is required.");
|
||||
}
|
||||
}).catch(error => {
|
||||
// offline
|
||||
this.handleTone3000DownloadError("Not online. Unable to connect to Tone3000 servers.");
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
// offline
|
||||
this.handleTone3000DownloadError("Not online. Unable to connect to Tone3000 servers.");
|
||||
return;
|
||||
});
|
||||
};
|
||||
|
||||
}
|
||||
public closeTone3000Dialog(): void {
|
||||
@@ -226,8 +237,15 @@ export function Tone3000DownloadDialog(props: Tone3000DownloadDialogProps): JSX.
|
||||
onDownloadComplete();
|
||||
}
|
||||
);
|
||||
let onStateChanged = (state: State) => {
|
||||
if (state !== State.Ready) {
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
model.state.addOnChangedHandler(onStateChanged);
|
||||
return () => {
|
||||
model.closeTone3000DownloadDialog();
|
||||
model.state.removeOnChangedHandler(onStateChanged);
|
||||
};
|
||||
});
|
||||
return (
|
||||
@@ -239,23 +257,80 @@ export function Tone3000DownloadDialog(props: Tone3000DownloadDialogProps): JSX.
|
||||
onEnterKey={() => { }}
|
||||
>
|
||||
<DialogContent>
|
||||
<Typography variant="body2">Downloading from Tone3000...</Typography>
|
||||
<Typography style={{minWidth: 300, maxWidth: 300}} variant="body2">Downloading from Tone3000...</Typography>
|
||||
{downloading &&
|
||||
<div style={{ display: "flex", justifyContent: "center", alignItems: "center" }}>
|
||||
<LinearProgress style={{ marginTop: 16, flexGrow: 1 }} />
|
||||
</div>
|
||||
}
|
||||
</DialogContent>
|
||||
<DialogAction>
|
||||
<DialogActions >
|
||||
{!downloading && (
|
||||
<Button
|
||||
<Button variant="dialogSecondary"
|
||||
onClick={() => {
|
||||
onClose();
|
||||
}}>
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
</DialogAction>
|
||||
</DialogActions>
|
||||
</DialogEx>
|
||||
);
|
||||
}
|
||||
|
||||
export function Tone3000DownloadStaus(
|
||||
props: {
|
||||
zindex: number;
|
||||
}): JSX.Element {
|
||||
const model = PiPedalModel.getInstance();
|
||||
const [downloading, setDownloading] = useState<boolean>(false);
|
||||
const [progress, setProgress] = useState<Tone3000DownloadProgress | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let onDownloadingChanged = (value: boolean) => {
|
||||
setDownloading(value);
|
||||
}
|
||||
model.tone3000Downloading.addOnChangedHandler(onDownloadingChanged);
|
||||
|
||||
let onProgressChanged = (value: Tone3000DownloadProgress | null) => {
|
||||
setProgress(value);
|
||||
}
|
||||
model.tone3000DownloadProgress.addOnChangedHandler(onProgressChanged);
|
||||
|
||||
return () => {
|
||||
model.tone3000Downloading.removeOnChangedHandler(onDownloadingChanged);
|
||||
model.tone3000DownloadProgress.removeOnChangedHandler(onProgressChanged);
|
||||
}
|
||||
})
|
||||
let open = downloading && progress !== null && progress.title.length > 0;
|
||||
if (model.state.get() !== State.Ready) {
|
||||
open = false;
|
||||
}
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={() => { /* Do nothing */ }}
|
||||
>
|
||||
<DialogTitle>Downloading from Tone3000</DialogTitle>
|
||||
<DialogContent>
|
||||
<Typography noWrap variant="body2">{progress?.title ?? "\u00A0"}</Typography>
|
||||
<div style={{ display: "flex", justifyContent: "center", alignItems: "center" }}>
|
||||
<LinearProgress
|
||||
style={{ marginTop: 16, flexGrow: 1 }}
|
||||
variant={(progress?.total ?? 0) === 0 ? "indeterminate" : "determinate"}
|
||||
value={(progress && progress.total !== 0) ? progress.progress / progress.total * 100 : 0}
|
||||
/>
|
||||
</div>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button variant="dialogSecondary"
|
||||
onClick={() => {
|
||||
model.cancelTone3000Download();
|
||||
}}
|
||||
>Cancel</Button>
|
||||
</DialogActions>
|
||||
|
||||
</Dialog>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Robin E. R. Davies
|
||||
* All rights reserved.
|
||||
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
|
||||
export default interface Tone3000DownloadProgress {
|
||||
handle: number;
|
||||
title: string;
|
||||
progress: number;
|
||||
total: number;
|
||||
};
|
||||
Reference in New Issue
Block a user