Checkpoint
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="css/roboto.css">
|
||||
<title>T3k License</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Roboto,Arial, sans-serif;
|
||||
margin: 32px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
h6 {
|
||||
font-size: 1.2em;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
p {
|
||||
font-size: 1em;
|
||||
max-width: 600px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h6>T3K License</h6>
|
||||
<p>Users may download and use the data file in software and publish the resulting outputs without royalties or restrictions. However, they may not upload, republish, or distribute the data file without the author's permission.</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -19,7 +19,6 @@
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
|
||||
@@ -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