Complete except for throttling bug.
This commit is contained in:
@@ -1039,6 +1039,7 @@ export
|
||||
defaultName={this.model_.banks.get().getSelectedEntryName()}
|
||||
title="Bank Name"
|
||||
acceptActionName={this.state.renameBankDialogOpen ? "Rename" : "Save as"}
|
||||
useSafeFilenames={false}
|
||||
onClose={() => {
|
||||
this.setState({
|
||||
renameBankDialogOpen: false,
|
||||
@@ -1100,7 +1101,7 @@ export
|
||||
</DialogActions>
|
||||
</DialogEx>
|
||||
|
||||
{/* Status of Tone3000 download */}
|
||||
{/* Status of TONE3000 download */}
|
||||
<Tone3000DownloadStatus zindex={2000}/>
|
||||
{/* Fatal error mask */}
|
||||
<Modal open={this.state.displayState === State.Error}
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
|
||||
import { pathConcat, pathParentDirectory, pathFileName } from "./FileUtils";
|
||||
import { PiPedalModel } from "./PiPedalModel";
|
||||
import { safeFilenameDecode } from "./SafeFilename";
|
||||
|
||||
export class ThumbnailType {
|
||||
static Unknown = 0;
|
||||
@@ -38,72 +39,6 @@ function getVersionSuffix(filePath: string): string | null {
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
export function safeFilenameDecode(filename: string): string {
|
||||
let originalFilename = filename;
|
||||
let pos = filename.indexOf('%');
|
||||
if (pos < 0) {
|
||||
return filename;
|
||||
}
|
||||
let result = "";
|
||||
while (true) {
|
||||
if (pos < 0) {
|
||||
result += filename;
|
||||
return result;
|
||||
}
|
||||
result += filename.substring(0, pos);
|
||||
if (pos+3 >= filename.length) {
|
||||
// invalid encoding. just use the original
|
||||
return originalFilename;
|
||||
}
|
||||
let hex = filename.charAt(pos+1) + filename.charAt(pos+2);
|
||||
let byte = parseInt(hex, 16);
|
||||
|
||||
// Handle UTF-8 to UTF-16 conversion
|
||||
if (byte < 0x80) {
|
||||
// Single-byte character (ASCII)
|
||||
result += String.fromCharCode(byte);
|
||||
filename = filename.substring(pos+3);
|
||||
} else if ((byte & 0xE0) === 0xC0) {
|
||||
// 2-byte sequence
|
||||
if (pos+6 > filename.length || filename.charAt(pos+3) !== '%') {
|
||||
return originalFilename; // invalid encoding
|
||||
}
|
||||
let byte2 = parseInt(filename.charAt(pos+4) + filename.charAt(pos+5), 16);
|
||||
let codePoint = ((byte & 0x1F) << 6) | (byte2 & 0x3F);
|
||||
result += String.fromCharCode(codePoint);
|
||||
filename = filename.substring(pos+6);
|
||||
} else if ((byte & 0xF0) === 0xE0) {
|
||||
// 3-byte sequence
|
||||
if (pos+9 > filename.length || filename.charAt(pos+3) !== '%' || filename.charAt(pos+6) !== '%') {
|
||||
return originalFilename; // invalid encoding
|
||||
}
|
||||
let byte2 = parseInt(filename.charAt(pos+4) + filename.charAt(pos+5), 16);
|
||||
let byte3 = parseInt(filename.charAt(pos+7) + filename.charAt(pos+8), 16);
|
||||
let codePoint = ((byte & 0x0F) << 12) | ((byte2 & 0x3F) << 6) | (byte3 & 0x3F);
|
||||
result += String.fromCharCode(codePoint);
|
||||
filename = filename.substring(pos+9);
|
||||
} else if ((byte & 0xF8) === 0xF0) {
|
||||
// 4-byte sequence (surrogate pair needed)
|
||||
if (pos+12 > filename.length || filename.charAt(pos+3) !== '%' || filename.charAt(pos+6) !== '%' || filename.charAt(pos+9) !== '%') {
|
||||
return originalFilename; // invalid encoding
|
||||
}
|
||||
let byte2 = parseInt(filename.charAt(pos+4) + filename.charAt(pos+5), 16);
|
||||
let byte3 = parseInt(filename.charAt(pos+7) + filename.charAt(pos+8), 16);
|
||||
let byte4 = parseInt(filename.charAt(pos+10) + filename.charAt(pos+11), 16);
|
||||
let codePoint = ((byte & 0x07) << 18) | ((byte2 & 0x3F) << 12) | ((byte3 & 0x3F) << 6) | (byte4 & 0x3F);
|
||||
// Convert to UTF-16 surrogate pair
|
||||
codePoint -= 0x10000;
|
||||
result += String.fromCharCode(0xD800 + (codePoint >> 10));
|
||||
result += String.fromCharCode(0xDC00 + (codePoint & 0x3FF));
|
||||
filename = filename.substring(pos+12);
|
||||
} else {
|
||||
// Invalid UTF-8 start byte
|
||||
return originalFilename;
|
||||
}
|
||||
pos = filename.indexOf('%');
|
||||
}
|
||||
}
|
||||
|
||||
export function getTrackTitle(pathname: string, metadata: AudioFileMetadata | null | undefined): string {
|
||||
if (!metadata) {
|
||||
return safeFilenameDecode(pathFileName(pathname));
|
||||
|
||||
@@ -469,6 +469,7 @@ const BankDialog = withStyles(
|
||||
title={this.state.filenameSaveAs ? "Save Bank As" : "Bank Name"}
|
||||
acceptActionName={this.state.filenameSaveAs ? "SAVE AS" : "RENAME"}
|
||||
onClose={() => { this.setState({ filenameDialogOpen: false }) }}
|
||||
useSafeFilenames={false}
|
||||
onOk={(text: string) => {
|
||||
if (this.state.filenameSaveAs) {
|
||||
this.handleSaveAsOk(text);
|
||||
|
||||
@@ -34,7 +34,7 @@ import ButtonBase from '@mui/material/ButtonBase'
|
||||
import MoreHorizIcon from '@mui/icons-material/MoreHoriz';
|
||||
import { PedalboardItem } from './Pedalboard';
|
||||
import { isDarkMode } from './DarkMode';
|
||||
import {safeFilenameDecode} from './AudioFileMetadata';
|
||||
import {safeFilenameDecode} from './SafeFilename';
|
||||
|
||||
|
||||
import { withStyles } from "tss-react/mui";
|
||||
|
||||
@@ -67,11 +67,17 @@ import OkCancelDialog from './OkCancelDialog';
|
||||
import HomeIcon from '@mui/icons-material/Home';
|
||||
import FilePropertyDirectorySelectDialog from './FilePropertyDirectorySelectDialog';
|
||||
import { getAlbumArtUri, getTrackTitle } from './AudioFileMetadata';
|
||||
import { Tone3000DownloadDialog } from './Tone3000Dialog';
|
||||
import Tone3000DownloadType from './Tone3000DownloadType';
|
||||
|
||||
|
||||
const ToobNamModelFileUrl = "http://two-play.com/plugins/toob-nam#modelFile";
|
||||
const ToobMlModelFileUrl = "http://two-play.com/plugins/toob-ml#modelFile";
|
||||
const ToobCabIrFileUrls = [
|
||||
"http://two-play.com/plugins/toob-cab-ir#impulseFile",
|
||||
"http://two-play.com/plugins/toob-cab-ir#impulseFile2",
|
||||
"http://two-play.com/plugins/toob-cab-ir#impulseFile3",
|
||||
];
|
||||
|
||||
|
||||
const AUTOSCROLL_TICK_DELAY = 30;
|
||||
const AUTOSCROLL_THRESHOLD = 48;
|
||||
@@ -171,7 +177,6 @@ export interface FilePropertyDialogState {
|
||||
previousSelection: string;
|
||||
multiSelect: boolean,
|
||||
selectedFiles: string[],
|
||||
openTone3000Dialog: boolean,
|
||||
openTone3000Help: boolean,
|
||||
openGuitarMlHelp: boolean,
|
||||
|
||||
@@ -249,7 +254,6 @@ export default withStyles(
|
||||
previousSelection: this.props.selectedFile,
|
||||
multiSelect: false,
|
||||
selectedFiles: [],
|
||||
openTone3000Dialog: false,
|
||||
openTone3000Help: false,
|
||||
openGuitarMlHelp: false
|
||||
};
|
||||
@@ -1086,7 +1090,21 @@ export default withStyles(
|
||||
handleTone3000Dialog(e: React.MouseEvent<HTMLButtonElement>) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.setState({ openTone3000Dialog: true });
|
||||
// Popup launch MUST be done from javascript, not react to avoid default popup blockers in browers.
|
||||
// The dialog just provides a blocker dialog to cancel the popup if the TONE3000 Select APIs escape.
|
||||
|
||||
let downloadType: Tone3000DownloadType = Tone3000DownloadType.Nam;
|
||||
if (this.props.fileProperty.patchProperty === ToobNamModelFileUrl) {
|
||||
downloadType = Tone3000DownloadType.Nam;
|
||||
} else if (ToobCabIrFileUrls.includes(this.props.fileProperty.patchProperty))
|
||||
{
|
||||
downloadType = Tone3000DownloadType.CabIr;
|
||||
} else {
|
||||
throw new Error("Unsupported file property for Tone3000 dialog");
|
||||
}
|
||||
this.model.showTone3000DownloadPopup(
|
||||
downloadType,
|
||||
this.state.currentDirectory);
|
||||
}
|
||||
|
||||
|
||||
@@ -1110,6 +1128,7 @@ export default withStyles(
|
||||
render() {
|
||||
const isTracksDirectory = this.isTracksDirectory();
|
||||
const isToobNamModelFile = this.props.fileProperty.patchProperty === ToobNamModelFileUrl;
|
||||
const isToobCabIrFile = ToobCabIrFileUrls.includes(this.props.fileProperty.patchProperty);
|
||||
const isToobMLModelFile = this.props.fileProperty.patchProperty === ToobMlModelFileUrl;
|
||||
|
||||
const classes = withStyles.getClasses(this.props);
|
||||
@@ -1532,7 +1551,7 @@ export default withStyles(
|
||||
<>
|
||||
<DialogActions style={{ justifyContent: "stretch", width: "100%" }}>
|
||||
<div style={{ display: "flex", flexFlow: "column nowrap", width: "100%", alignItems: "stretch", }}>
|
||||
{isToobNamModelFile && (
|
||||
{(isToobNamModelFile || isToobCabIrFile) && (
|
||||
<div style={{
|
||||
display: "flex", flexFlow: "row nowrap", justifyContent: "center",
|
||||
alignItems: "center", width: "100%"
|
||||
@@ -1540,7 +1559,12 @@ export default withStyles(
|
||||
<Button
|
||||
onClick={(e) => { this.handleTone3000Dialog(e); }}
|
||||
>
|
||||
Download model files from Tone3000
|
||||
{
|
||||
isToobNamModelFile
|
||||
? "Download model files from TONE3000"
|
||||
: "Download I/R files from TONE3000"
|
||||
}
|
||||
|
||||
</Button>
|
||||
<IconButtonEx tooltip="Help"
|
||||
onClick={(e) => { this.handleTone3000Help(e); }} aria-label="help" edge="end" color="inherit" style={{ opacity: 0.6, marginLeft: 8 }}
|
||||
@@ -1711,6 +1735,7 @@ export default withStyles(
|
||||
onClose={() => { this.setState({ newFolderDialogOpen: false }); }}
|
||||
title="New folder"
|
||||
acceptActionName="OK"
|
||||
useSafeFilenames={true}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1721,6 +1746,7 @@ export default withStyles(
|
||||
onOk={(newName) => { this.setState({ renameDialogOpen: false }); this.onExecuteRename(newName) }}
|
||||
onClose={() => { this.setState({ renameDialogOpen: false }); }}
|
||||
acceptActionName="OK"
|
||||
useSafeFilenames={true}
|
||||
/>
|
||||
|
||||
)
|
||||
@@ -1733,6 +1759,7 @@ export default withStyles(
|
||||
open={this.state.moveDialogOpen || this.state.copyDialogOpen}
|
||||
dialogTitle={this.state.moveDialogOpen ? "Move to" : "Copy Link to"}
|
||||
uiFileProperty={this.props.fileProperty}
|
||||
useSafeFilenames={true}
|
||||
defaultPath={this.getDefaultPath()}
|
||||
selectedFile={this.state.selectedFile}
|
||||
excludeDirectory={
|
||||
@@ -1755,20 +1782,11 @@ export default withStyles(
|
||||
)
|
||||
)
|
||||
}
|
||||
{this.state.openTone3000Dialog && (
|
||||
<Tone3000DownloadDialog
|
||||
onClose={() => this.setState({ openTone3000Dialog: false })}
|
||||
downloadPath={this.state.currentDirectory}
|
||||
onDownloadComplete={() => {
|
||||
this.setState({ openTone3000Dialog: false });
|
||||
this.requestFiles(this.state.navDirectory);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{this.state.openTone3000Help && (
|
||||
<Tone3000HelpDialog
|
||||
open={this.state.openTone3000Help}
|
||||
onClose={() => this.setState({ openTone3000Help: false })}
|
||||
downloadType={isToobNamModelFile ? Tone3000DownloadType.Nam : Tone3000DownloadType.CabIr}
|
||||
/>
|
||||
)}
|
||||
{this.state.openGuitarMlHelp && (
|
||||
|
||||
@@ -35,6 +35,7 @@ import FolderIcon from '@mui/icons-material/Folder';
|
||||
import HomeIcon from '@mui/icons-material/Home';
|
||||
import { isDarkMode } from './DarkMode';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import {safeFilenameDecode} from './SafeFilename';
|
||||
|
||||
|
||||
|
||||
@@ -126,6 +127,7 @@ export interface FilePropertyDirectorySelectDialogProps {
|
||||
defaultPath: string,
|
||||
excludeDirectory: string,
|
||||
selectedFile: string,
|
||||
useSafeFilenames: boolean,
|
||||
onOk: (path: string) => void,
|
||||
onClose: () => void
|
||||
};
|
||||
@@ -260,6 +262,11 @@ export default class FilePropertyDirectorySelectDialog extends ResizeResponsiveC
|
||||
selectBg = selected ? "rgba(255,255,255,0.10)" : "rgba(0,0,0,0.0)";
|
||||
}
|
||||
let showToggle = directoryTree.canExpand() && directoryTree.path.length !== 0;
|
||||
let name = directoryTree.name === "" ? "Home" : directoryTree.name;
|
||||
if (this.props.useSafeFilenames) {
|
||||
name = safeFilenameDecode(name);
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={directoryTree.path} style={{flexGrow: 1}}>
|
||||
<div style={{ width: "100%", display: "flex", flexFlow: "row", flexWrap: "nowrap", justifyContent: "flex-start", alignItems: "center" }}>
|
||||
@@ -292,7 +299,7 @@ export default class FilePropertyDirectorySelectDialog extends ResizeResponsiveC
|
||||
)
|
||||
}
|
||||
|
||||
<Typography noWrap style={{ marginLeft: 8, marginTop: 8, marginBottom: 8, textAlign: "left", flexGrow: 1, flexShrink: 1 }} variant="body2">{directoryTree.name === "" ? "Home" : directoryTree.name}</Typography>
|
||||
<Typography noWrap style={{ marginLeft: 8, marginTop: 8, marginBottom: 8, textAlign: "left", flexGrow: 1, flexShrink: 1 }} variant="body2">{name}</Typography>
|
||||
</div>
|
||||
</ButtonBase>
|
||||
</div>
|
||||
|
||||
@@ -60,6 +60,9 @@ export class ObservableProperty<VALUE_TYPE> {
|
||||
|
||||
set(value: VALUE_TYPE) : void
|
||||
{
|
||||
if (this._value_type === value) {
|
||||
return;
|
||||
}
|
||||
this._value_type = value;
|
||||
|
||||
let t = this._on_changed_handlers; // take an copy in case removes happen while iteratiing.
|
||||
|
||||
@@ -28,6 +28,7 @@ import PluginClass from './PluginClass';
|
||||
import ScreenOrientation from './ScreenOrientation';
|
||||
import PiPedalSocket, { PiPedalMessageHeader } from './PiPedalSocket';
|
||||
import {Tone3000DownloadHandler} from './Tone3000Dialog';
|
||||
import Tone3000DownloadType from './Tone3000DownloadType';
|
||||
import { nullCast } from './Utility'
|
||||
import { JackConfiguration, JackChannelSelection } from './Jack';
|
||||
import { BankIndex } from './Banks';
|
||||
@@ -660,9 +661,23 @@ export class PiPedalModel //implements PiPedalModel
|
||||
|
||||
}
|
||||
|
||||
async pingTone3000Server() : Promise<boolean> {
|
||||
if (this.webSocket === undefined) {
|
||||
return false;
|
||||
|
||||
}
|
||||
try {
|
||||
let result = await this.webSocket.request<boolean>("pingTone3000Server", {});
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.log("Error pinging TONE3000 Server on PiPedal server: " + getErrorMessage(error));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
async downloadModelsFromTone3000(
|
||||
tone3000DownloadUrl: string,
|
||||
downloadPath: string
|
||||
downloadPath: string,
|
||||
downloadType: Tone3000DownloadType
|
||||
): Promise<void> {
|
||||
try {
|
||||
if (!this.webSocket) {
|
||||
@@ -671,50 +686,59 @@ export class PiPedalModel //implements PiPedalModel
|
||||
this.webSocket.send("downloadModelsFromTone3000",
|
||||
{
|
||||
downloadPath: downloadPath,
|
||||
tone3000Url: tone3000DownloadUrl
|
||||
tone3000Url: tone3000DownloadUrl,
|
||||
downloadType: downloadType
|
||||
});
|
||||
} catch (error) {
|
||||
this.showAlert(getErrorMessage(error));
|
||||
}
|
||||
}
|
||||
cancelTone3000Download(): void {
|
||||
private cancelTone3000Download(): void {
|
||||
let downloadProgress = this.tone3000DownloadProgress.get();
|
||||
if (downloadProgress === null) {
|
||||
return;
|
||||
}
|
||||
if (downloadProgress.handle !== -1) {
|
||||
if (!this.webSocket) {
|
||||
return;
|
||||
if (downloadProgress !== null) {
|
||||
if (downloadProgress.handle !== -1) {
|
||||
if (!this.webSocket) {
|
||||
this.tone3000DownloadProgress.set(null);
|
||||
this.tone3000Downloading.set(false);
|
||||
return;
|
||||
}
|
||||
this.webSocket.send("cancelTone3000Download",downloadProgress.handle);
|
||||
}
|
||||
this.webSocket.send("cancelTone3000Download",downloadProgress.handle);
|
||||
}
|
||||
}
|
||||
|
||||
showTone3000DownloadDialog(
|
||||
downloadPath: string,
|
||||
onClosed: () => void,
|
||||
onDownloadStarted: () => void,
|
||||
onDownloadComplete: () => void
|
||||
|
||||
showTone3000DownloadPopup(
|
||||
downloadType: Tone3000DownloadType,
|
||||
downloadPath: string
|
||||
): void {
|
||||
if (this.tone3000DownloadHandler === null) {
|
||||
this.tone3000DownloadHandler = new Tone3000DownloadHandler(this);
|
||||
}
|
||||
this.tone3000DownloadHandler.launchTone3000Dialog(
|
||||
this.tone3000DownloadHandler.launchTone3000Popup(
|
||||
downloadType,
|
||||
downloadPath,
|
||||
onClosed,
|
||||
(errorMessage: string) => {
|
||||
this.showAlert(errorMessage);
|
||||
onClosed();
|
||||
},
|
||||
onDownloadStarted,
|
||||
onDownloadComplete
|
||||
);
|
||||
|
||||
};
|
||||
closeTone3000DownloadDialog(): void {
|
||||
|
||||
showTone3000DownloadStatus(): void {
|
||||
this.tone3000Downloading.set(true);
|
||||
this.tone3000DownloadProgress.set(null);
|
||||
console.log("Showing TONE3000 download status popup");
|
||||
}
|
||||
hideTone3000DownloadStatus(): void {
|
||||
this.tone3000Downloading.set(false);
|
||||
this.tone3000DownloadProgress.set(null);
|
||||
console.log("Hiding TONE3000 download status popup");
|
||||
}
|
||||
|
||||
closeTone3000DownloadPopup(): void {
|
||||
this.cancelTone3000Download();
|
||||
this.tone3000Downloading.set(false);
|
||||
this.tone3000DownloadProgress.set(null);
|
||||
|
||||
if (this.tone3000DownloadHandler) {
|
||||
this.tone3000DownloadHandler.closeTone3000Dialog();
|
||||
this.tone3000DownloadHandler.closeTone3000Popup();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1057,7 +1081,7 @@ export class PiPedalModel //implements PiPedalModel
|
||||
|
||||
}
|
||||
|
||||
// Tone3000 download notification handlers (stub implementations)
|
||||
// TONE3000 download notification handlers (stub implementations)
|
||||
private onTone3000DownloadStarted(handle: number, title: string): void {
|
||||
this.tone3000Downloading.set(true);
|
||||
}
|
||||
@@ -1069,12 +1093,14 @@ export class PiPedalModel //implements PiPedalModel
|
||||
|
||||
private onTone3000DownloadComplete(resultPath: string): void {
|
||||
this.tone3000Downloading.set(false);
|
||||
this.tone3000DownloadProgress.set(null);
|
||||
this.onTone3000DownloadCompleteEvent.fire(resultPath);
|
||||
}
|
||||
|
||||
private onTone3000DownloadError(handle: number, errorMessage: string): void {
|
||||
console.error(`Tone3000 download error: handle=${handle}, error=${errorMessage}`);
|
||||
console.error(`TONE3000 download error: handle=${handle}, error=${errorMessage}`);
|
||||
this.tone3000Downloading.set(false);
|
||||
this.tone3000DownloadProgress.set(null);
|
||||
this.onTone3000DownloadCompleteEvent.fire("");
|
||||
|
||||
setTimeout(() => {
|
||||
@@ -1090,9 +1116,12 @@ export class PiPedalModel //implements PiPedalModel
|
||||
return "var/" + url;
|
||||
}
|
||||
|
||||
setState(state: State) {
|
||||
private setState(state: State) {
|
||||
if (this.state.get() !== state) {
|
||||
this.state.set(state);
|
||||
if (state === State.Error) {
|
||||
this.closeTone3000DownloadPopup();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1397,7 +1426,7 @@ export class PiPedalModel //implements PiPedalModel
|
||||
m = message.toString();
|
||||
}
|
||||
this.errorMessage.set(m);
|
||||
this.state.set(State.Error);
|
||||
this.setState(State.Error);
|
||||
|
||||
}
|
||||
|
||||
@@ -1458,6 +1487,7 @@ export class PiPedalModel //implements PiPedalModel
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
initialize(): void {
|
||||
this.setError("");
|
||||
this.setState(State.Loading);
|
||||
@@ -1465,7 +1495,7 @@ export class PiPedalModel //implements PiPedalModel
|
||||
this.requestConfig()
|
||||
.then((succeeded) => {
|
||||
if (succeeded) {
|
||||
this.state.set(State.Ready);
|
||||
this.setState(State.Ready);
|
||||
if (this.androidHost) {
|
||||
this.hostVersion = new HostVersion(this.androidHost.getHostVersion());
|
||||
if (!this.hostVersion.lessThan(1, 1, 16)) {
|
||||
@@ -2857,29 +2887,6 @@ export class PiPedalModel //implements PiPedalModel
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
|
||||
async uploadTone3000File(url: string) : Promise<boolean> {
|
||||
try {
|
||||
let response = await fetch(
|
||||
url,
|
||||
{
|
||||
method: "GET",
|
||||
}
|
||||
);
|
||||
|
||||
let json = await response.json();
|
||||
|
||||
if (json.success === true) {
|
||||
return true;
|
||||
}
|
||||
if (json.errorMessage !== undefined)
|
||||
{
|
||||
throw new Error(json.errorMessage);
|
||||
}
|
||||
throw new Error("Invalid server response.");
|
||||
} catch (error) {
|
||||
throw new Error("Upload to PiPedal server failed. " + getErrorMessage(error));
|
||||
}
|
||||
}
|
||||
uploadUserFile(uploadPage: string, file: File, contentType: string = "application/octet-stream", abortController?: AbortController): Promise<string> {
|
||||
let result = new Promise<string>((resolve, reject) => {
|
||||
try {
|
||||
|
||||
@@ -338,6 +338,7 @@ const PluginPresetSelector =
|
||||
title="Rename"
|
||||
defaultName={this.state.renameDialogDefaultName}
|
||||
acceptActionName={this.state.renameDialogActionName}
|
||||
useSafeFilenames={false}
|
||||
onClose={() => this.handleRenameDialogClose()}
|
||||
onOk={(name: string) => this.handleRenameDialogOk(name)} />
|
||||
</div>
|
||||
|
||||
@@ -416,6 +416,7 @@ const PluginPresetsDialog = withStyles(
|
||||
title="Rename"
|
||||
open={this.state.renameOpen}
|
||||
defaultName={this.getSelectedName()}
|
||||
useSafeFilenames={false}
|
||||
acceptActionName={"Rename"}
|
||||
onClose={() => { this.setState({ renameOpen: false }) }}
|
||||
onOk={(text: string) => {
|
||||
|
||||
@@ -526,6 +526,7 @@ const PresetDialog = withStyles(
|
||||
title="Rename"
|
||||
open={this.state.renameOpen}
|
||||
defaultName={this.getSelectedName()}
|
||||
useSafeFilenames={false}
|
||||
acceptActionName={"Rename"}
|
||||
onClose={() => { this.setState({ renameOpen: false }) }}
|
||||
onOk={(text: string) => {
|
||||
|
||||
@@ -467,6 +467,7 @@ const PresetSelector =
|
||||
title={this.state.renameDialogTitle}
|
||||
defaultName={this.state.renameDialogDefaultName}
|
||||
acceptActionName={this.state.renameDialogActionName}
|
||||
useSafeFilenames={false}
|
||||
onClose={() => this.handleRenameDialogClose()}
|
||||
onOk={(name: string) => this.handleRenameDialogOk(name)} />
|
||||
<UploadPresetDialog
|
||||
|
||||
@@ -28,10 +28,12 @@ import ResizeResponsiveComponent from './ResizeResponsiveComponent';
|
||||
import {PiPedalModel,PiPedalModelFactory} from './PiPedalModel';
|
||||
//import TextFieldEx from './TextFieldEx';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import {safeFilenameEncode, safeFilenameDecode} from "./SafeFilename";
|
||||
|
||||
|
||||
export interface RenameDialogProps {
|
||||
open: boolean,
|
||||
useSafeFilenames: boolean,
|
||||
defaultName: string,
|
||||
acceptActionName: string,
|
||||
title: string,
|
||||
@@ -95,6 +97,12 @@ export default class RenameDialog extends ResizeResponsiveComponent<RenameDialog
|
||||
let props = this.props;
|
||||
let { open, defaultName, acceptActionName, onClose, onOk } = props;
|
||||
|
||||
let name=defaultName;
|
||||
if (props.useSafeFilenames)
|
||||
{
|
||||
name = safeFilenameDecode(name);
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
onClose();
|
||||
};
|
||||
@@ -102,13 +110,19 @@ export default class RenameDialog extends ResizeResponsiveComponent<RenameDialog
|
||||
const handleOk = () => {
|
||||
let text = nullCast(this.refText.current).value;
|
||||
text = text.trim();
|
||||
try {
|
||||
this.checkForIllegalCharacters(text);
|
||||
} catch (e:any)
|
||||
if (props.useSafeFilenames)
|
||||
{
|
||||
let model:PiPedalModel = PiPedalModelFactory.getInstance();
|
||||
model.showAlert(e.toString());
|
||||
return;
|
||||
text = safeFilenameEncode(text);
|
||||
}
|
||||
else {
|
||||
try {
|
||||
this.checkForIllegalCharacters(text);
|
||||
} catch (e:any)
|
||||
{
|
||||
let model:PiPedalModel = PiPedalModelFactory.getInstance();
|
||||
model.showAlert(e.toString());
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (text.length === 0 && props.allowEmpty !== true) return;
|
||||
onOk(text);
|
||||
@@ -154,7 +168,7 @@ export default class RenameDialog extends ResizeResponsiveComponent<RenameDialog
|
||||
type="text"
|
||||
label={props.label ?? "Name"}
|
||||
fullWidth
|
||||
defaultValue={defaultName}
|
||||
defaultValue={name}
|
||||
inputRef={this.refText}
|
||||
/>
|
||||
</DialogContent>
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
export function safeFilenameDecode(filename: string): string {
|
||||
let originalFilename = filename;
|
||||
let pos = filename.indexOf('%');
|
||||
if (pos < 0) {
|
||||
return filename;
|
||||
}
|
||||
let result = "";
|
||||
while (true) {
|
||||
if (pos < 0) {
|
||||
result += filename;
|
||||
return result;
|
||||
}
|
||||
result += filename.substring(0, pos);
|
||||
if (pos+3 >= filename.length) {
|
||||
// invalid encoding. just use the original
|
||||
return originalFilename;
|
||||
}
|
||||
let hex = filename.charAt(pos+1) + filename.charAt(pos+2);
|
||||
let byte = parseInt(hex, 16);
|
||||
|
||||
// Handle UTF-8 to UTF-16 conversion
|
||||
if (byte < 0x80) {
|
||||
// Single-byte character (ASCII)
|
||||
result += String.fromCharCode(byte);
|
||||
filename = filename.substring(pos+3);
|
||||
} else if ((byte & 0xE0) === 0xC0) {
|
||||
// 2-byte sequence
|
||||
if (pos+6 > filename.length || filename.charAt(pos+3) !== '%') {
|
||||
return originalFilename; // invalid encoding
|
||||
}
|
||||
let byte2 = parseInt(filename.charAt(pos+4) + filename.charAt(pos+5), 16);
|
||||
let codePoint = ((byte & 0x1F) << 6) | (byte2 & 0x3F);
|
||||
result += String.fromCharCode(codePoint);
|
||||
filename = filename.substring(pos+6);
|
||||
} else if ((byte & 0xF0) === 0xE0) {
|
||||
// 3-byte sequence
|
||||
if (pos+9 > filename.length || filename.charAt(pos+3) !== '%' || filename.charAt(pos+6) !== '%') {
|
||||
return originalFilename; // invalid encoding
|
||||
}
|
||||
let byte2 = parseInt(filename.charAt(pos+4) + filename.charAt(pos+5), 16);
|
||||
let byte3 = parseInt(filename.charAt(pos+7) + filename.charAt(pos+8), 16);
|
||||
let codePoint = ((byte & 0x0F) << 12) | ((byte2 & 0x3F) << 6) | (byte3 & 0x3F);
|
||||
result += String.fromCharCode(codePoint);
|
||||
filename = filename.substring(pos+9);
|
||||
} else if ((byte & 0xF8) === 0xF0) {
|
||||
// 4-byte sequence (surrogate pair needed)
|
||||
if (pos+12 > filename.length || filename.charAt(pos+3) !== '%' || filename.charAt(pos+6) !== '%' || filename.charAt(pos+9) !== '%') {
|
||||
return originalFilename; // invalid encoding
|
||||
}
|
||||
let byte2 = parseInt(filename.charAt(pos+4) + filename.charAt(pos+5), 16);
|
||||
let byte3 = parseInt(filename.charAt(pos+7) + filename.charAt(pos+8), 16);
|
||||
let byte4 = parseInt(filename.charAt(pos+10) + filename.charAt(pos+11), 16);
|
||||
let codePoint = ((byte & 0x07) << 18) | ((byte2 & 0x3F) << 12) | ((byte3 & 0x3F) << 6) | (byte4 & 0x3F);
|
||||
// Convert to UTF-16 surrogate pair
|
||||
codePoint -= 0x10000;
|
||||
result += String.fromCharCode(0xD800 + (codePoint >> 10));
|
||||
result += String.fromCharCode(0xDC00 + (codePoint & 0x3FF));
|
||||
filename = filename.substring(pos+12);
|
||||
} else {
|
||||
// Invalid UTF-8 start byte
|
||||
return originalFilename;
|
||||
}
|
||||
pos = filename.indexOf('%');
|
||||
}
|
||||
}
|
||||
|
||||
export function safeFilenameEncode(filename: string): string {
|
||||
let result = "";
|
||||
|
||||
for (let i = 0; i < filename.length; i++) {
|
||||
let charCode = filename.charCodeAt(i);
|
||||
|
||||
// Handle UTF-16 surrogate pairs
|
||||
if (charCode >= 0xD800 && charCode <= 0xDBFF) {
|
||||
// High surrogate - must be followed by low surrogate
|
||||
if (i + 1 < filename.length) {
|
||||
let lowSurrogate = filename.charCodeAt(i + 1);
|
||||
if (lowSurrogate >= 0xDC00 && lowSurrogate <= 0xDFFF) {
|
||||
// Combine surrogates to get the code point
|
||||
let codePoint = 0x10000 + ((charCode & 0x3FF) << 10) + (lowSurrogate & 0x3FF);
|
||||
|
||||
// Encode as 4-byte UTF-8
|
||||
let byte1 = 0xF0 | ((codePoint >> 18) & 0x07);
|
||||
let byte2 = 0x80 | ((codePoint >> 12) & 0x3F);
|
||||
let byte3 = 0x80 | ((codePoint >> 6) & 0x3F);
|
||||
let byte4 = 0x80 | (codePoint & 0x3F);
|
||||
|
||||
result += '%' + byte1.toString(16).padStart(2, '0');
|
||||
result += '%' + byte2.toString(16).padStart(2, '0');
|
||||
result += '%' + byte3.toString(16).padStart(2, '0');
|
||||
result += '%' + byte4.toString(16).padStart(2, '0');
|
||||
|
||||
i++; // Skip the low surrogate
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// Invalid surrogate pair - encode as-is (shouldn't happen in valid strings)
|
||||
}
|
||||
|
||||
// Handle control characters (< 0x20)
|
||||
if (charCode < 0x20) {
|
||||
result += '%' + charCode.toString(16).padStart(2, '0');
|
||||
}
|
||||
// Handle ASCII printable characters (0x20-0x7F), space is NOT encoded
|
||||
else if (charCode <= 0x7F) {
|
||||
result += filename.charAt(i);
|
||||
}
|
||||
// Handle 2-byte UTF-8 (0x80-0x7FF)
|
||||
else if (charCode <= 0x7FF) {
|
||||
let byte1 = 0xC0 | ((charCode >> 6) & 0x1F);
|
||||
let byte2 = 0x80 | (charCode & 0x3F);
|
||||
result += '%' + byte1.toString(16).padStart(2, '0');
|
||||
result += '%' + byte2.toString(16).padStart(2, '0');
|
||||
}
|
||||
// Handle 3-byte UTF-8 (0x800-0xFFFF)
|
||||
else {
|
||||
let byte1 = 0xE0 | ((charCode >> 12) & 0x0F);
|
||||
let byte2 = 0x80 | ((charCode >> 6) & 0x3F);
|
||||
let byte3 = 0x80 | (charCode & 0x3F);
|
||||
result += '%' + byte1.toString(16).padStart(2, '0');
|
||||
result += '%' + byte2.toString(16).padStart(2, '0');
|
||||
result += '%' + byte3.toString(16).padStart(2, '0');
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -27,7 +27,7 @@ import { PiPedalModel, PiPedalModelFactory, State } from './PiPedalModel';
|
||||
import AppBar from '@mui/material/AppBar';
|
||||
import Toolbar from '@mui/material/Toolbar';
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import ReactMarkdown, { defaultUrlTransform, UrlTransform } from 'react-markdown';
|
||||
import rehypeRaw from 'rehype-raw';
|
||||
import rehypeSanitize, {defaultSchema} from 'rehype-sanitize';
|
||||
import rehypeExternalLinks from 'rehype-external-links'
|
||||
@@ -37,10 +37,28 @@ 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']
|
||||
tagNames: Array.from(new Set([...(defaultSchema.tagNames ?? []), 'img'])),
|
||||
protocols: {
|
||||
...defaultSchema.protocols,
|
||||
src: [...(defaultSchema.protocols?.src ?? []), ''],
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
const urlTransform: UrlTransform = (url, key, node) => {
|
||||
if (key === 'src' && node.tagName === 'img' && url.startsWith('data:')) {
|
||||
const allowedPrefixes = ['data:image/jpeg', 'data:image/png'];
|
||||
for (const prefix of allowedPrefixes) {
|
||||
if (url.startsWith(prefix)) {
|
||||
const nextChar = url.charAt(prefix.length);
|
||||
if (nextChar === ';' || nextChar === ',') {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
return defaultUrlTransform(url);
|
||||
};
|
||||
import DialogEx from './DialogEx';
|
||||
import ResizeResponsiveComponent from './ResizeResponsiveComponent';
|
||||
@@ -181,10 +199,10 @@ const TextInfoDialog = class extends ResizeResponsiveComponent<TextInfoDialogPro
|
||||
}}>
|
||||
<div style={{ flex: "1 1 0px" }} />
|
||||
<div className="text-info-dialog-content" style={{ flex: "1 1 auto",maxWidth: 650 }} >
|
||||
<ReactMarkdown rehypePlugins={
|
||||
<ReactMarkdown urlTransform={urlTransform} rehypePlugins={
|
||||
[
|
||||
[remarkGfm],
|
||||
rehypeRaw,
|
||||
[rehypeRaw],
|
||||
[rehypeSanitize, extendedSchema],
|
||||
[rehypeExternalLinks, {target: '_blank'}]
|
||||
]}>
|
||||
|
||||
+118
-196
@@ -1,34 +1,27 @@
|
||||
import { JSX } from "@emotion/react/jsx-dev-runtime";
|
||||
import { PiPedalModel, State, getErrorMessage } from "./PiPedalModel";
|
||||
import DialogEx from "./DialogEx";
|
||||
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";
|
||||
import Tone3000DownloadType from "./Tone3000DownloadType";
|
||||
|
||||
|
||||
|
||||
|
||||
export type OnTone3000DownloadHandler = (tone3000DownloadUrl: string) => void;
|
||||
|
||||
|
||||
// used to check for online status.
|
||||
let TONE3000_PING_URL = "https://www.tone3000.com/robots.txt";
|
||||
|
||||
export class Tone3000DownloadHandler {
|
||||
|
||||
private async handleTone3000Download(tone3000DownloadUrl: string): Promise<void> {
|
||||
private async handleTone3000Download(tone3000DownloadUrl: string, downloadType: Tone3000DownloadType): Promise<void> {
|
||||
try {
|
||||
// Dialog can close. We'll use the following await to manage
|
||||
// lifecycle from here onward.
|
||||
this.stopTone3000DialogMonitor();
|
||||
|
||||
this.handleTone3000DownloadComplete();
|
||||
|
||||
await this.model.downloadModelsFromTone3000(tone3000DownloadUrl, this.downloadPath);
|
||||
await this.model.downloadModelsFromTone3000(tone3000DownloadUrl, this.downloadPath, downloadType);
|
||||
return;
|
||||
} catch (e) {
|
||||
this.handleTone3000DownloadError(getErrorMessage(e));
|
||||
@@ -39,6 +32,7 @@ export class Tone3000DownloadHandler {
|
||||
private messageEventListener: ((event: MessageEvent) => void) | null = null;
|
||||
|
||||
private model: PiPedalModel;
|
||||
private downloadType: Tone3000DownloadType = Tone3000DownloadType.Nam;
|
||||
|
||||
public constructor(model: PiPedalModel) {
|
||||
this.model = model;
|
||||
@@ -46,7 +40,7 @@ export class Tone3000DownloadHandler {
|
||||
if (event.data.type !== "tone3000Download") {
|
||||
return;
|
||||
}
|
||||
this.handleTone3000Download(event.data.toneUrl as string);
|
||||
this.handleTone3000Download(event.data.toneUrl as string, this.downloadType);
|
||||
};
|
||||
|
||||
window.addEventListener("message", this.messageEventListener);
|
||||
@@ -61,7 +55,7 @@ export class Tone3000DownloadHandler {
|
||||
|
||||
private popupWindow: Window | null = null;
|
||||
|
||||
private appId: string = "pipedal_app3";
|
||||
private appId: string = "pipedal_app4";
|
||||
private redirectUrl(): string {
|
||||
let hostname = window.location.hostname;
|
||||
let port = window.location.port;
|
||||
@@ -77,217 +71,136 @@ export class Tone3000DownloadHandler {
|
||||
return `${serverUrl}/public/handleTone3000download.html`;
|
||||
}
|
||||
|
||||
private tone3000SelectUrl(): string {
|
||||
return `https://www.tone3000.com/api/v1/select?app_id=${this.appId}&redirect_url=${this.redirectUrl()}`;
|
||||
private tone3000SelectUrl(downloadType: Tone3000DownloadType): string {
|
||||
let redirectUrl_ = this.redirectUrl();
|
||||
let result = `https://www.tone3000.com/api/v1/select?app_id=${this.appId}&redirect_url=${redirectUrl_}`;
|
||||
if (downloadType === Tone3000DownloadType.CabIr) {
|
||||
result += "&gear=ir";
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
private handleTone3000DialogClosed() {
|
||||
let t = this.onClosedCallback;
|
||||
this.onClosedCallback = undefined;
|
||||
this.onErrorCallback = undefined;
|
||||
this.onDownloadCompleteCallback = undefined;
|
||||
if (t) {
|
||||
t();
|
||||
}
|
||||
this.stopTone3000DialogMonitor();
|
||||
}
|
||||
private handleTone3000Downloadstarted() {
|
||||
if (this.onDownloadStartedCallback) {
|
||||
this.onDownloadStartedCallback();
|
||||
}
|
||||
}
|
||||
private handleTone3000DownloadComplete() {
|
||||
let t = this.onDownloadCompleteCallback;
|
||||
this.onClosedCallback = undefined;
|
||||
this.onErrorCallback = undefined;
|
||||
this.onDownloadCompleteCallback = undefined;
|
||||
if (t) {
|
||||
t();
|
||||
if (this.popupWindow) {
|
||||
if (!this.popupWindow.closed) {
|
||||
this.popupWindow.close();
|
||||
}
|
||||
this.popupWindow = null;
|
||||
}
|
||||
this.stopTone3000DialogMonitor();
|
||||
this.model.hideTone3000DownloadStatus();
|
||||
|
||||
}
|
||||
closeTone3000Popup() {
|
||||
this.handleTone3000DownloadComplete();
|
||||
}
|
||||
private handleTone3000DownloadError(errorMessage: string) {
|
||||
let t = this.onErrorCallback;
|
||||
this.onClosedCallback = undefined;
|
||||
this.onErrorCallback = undefined;
|
||||
this.onDownloadCompleteCallback = undefined;
|
||||
if (t) {
|
||||
t(errorMessage);
|
||||
}
|
||||
this.stopTone3000DialogMonitor();
|
||||
}
|
||||
|
||||
private tone3000DialogTimeout: number | undefined = undefined;
|
||||
|
||||
private stopTone3000DialogMonitor() {
|
||||
if (this.tone3000DialogTimeout !== undefined) {
|
||||
clearTimeout(this.tone3000DialogTimeout);
|
||||
this.tone3000DialogTimeout = undefined;
|
||||
if (this.open)
|
||||
{
|
||||
this.handleTone3000DownloadComplete();
|
||||
this.model.showAlert(errorMessage);
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}, delay);
|
||||
}
|
||||
|
||||
private downloadPath: string = "";
|
||||
private onClosedCallback: (() => void) | undefined = undefined;
|
||||
private onErrorCallback: ((errorMessage: string) => void) | undefined = undefined;
|
||||
private onDownloadStartedCallback: (() => void) | undefined = undefined;
|
||||
private onDownloadCompleteCallback: (() => void) | undefined = undefined;
|
||||
private open = false;
|
||||
|
||||
public launchTone3000Dialog(
|
||||
private launchInstance = 0;
|
||||
public async launchTone3000Popup(
|
||||
downloadType: Tone3000DownloadType,
|
||||
downloadPath: string,
|
||||
onClosed: () => void,
|
||||
onError: (errorMessage: string) => void,
|
||||
onDownloadStarted: () => void,
|
||||
onDownloadComplete: () => void
|
||||
) {
|
||||
|
||||
): Promise<void> {
|
||||
this.closeTone3000Dialog();
|
||||
// online
|
||||
|
||||
this.downloadPath = downloadPath;
|
||||
this.onClosedCallback = onClosed;
|
||||
this.onDownloadCompleteCallback = onDownloadComplete;
|
||||
this.onDownloadStartedCallback = onDownloadStarted;
|
||||
this.onErrorCallback = onError;
|
||||
this.downloadType = downloadType;
|
||||
|
||||
let popupWidth = Math.floor(window.innerWidth * 0.8);
|
||||
let popupHeight = Math.floor(window.innerHeight * 0.8);
|
||||
if (window.innerWidth < 410) {
|
||||
popupWidth = 400;
|
||||
}
|
||||
|
||||
this.open = true;
|
||||
|
||||
this.model.showTone3000DownloadStatus();
|
||||
|
||||
let launchInstance = ++this.launchInstance;
|
||||
let cancelled = () => {
|
||||
return (launchInstance !== this.launchInstance) || this.popupWindow == null || this.popupWindow.closed;
|
||||
}
|
||||
|
||||
try {
|
||||
// online
|
||||
let popupWidth = Math.floor(window.innerWidth * 0.8);
|
||||
let popupHeight = Math.floor(window.innerHeight * 0.8);
|
||||
if (window.innerWidth < 410) {
|
||||
popupWidth = 400;
|
||||
}
|
||||
|
||||
this.popupWindow =
|
||||
window.open(
|
||||
this.tone3000SelectUrl(),
|
||||
"popup",
|
||||
`innerWidth=${popupWidth},innerHeight=${popupHeight},scrollbars=yes`
|
||||
);
|
||||
this.popupWindow =
|
||||
window.open(
|
||||
this.tone3000SelectUrl(downloadType),
|
||||
"popup",
|
||||
`innerWidth=${popupWidth},innerHeight=${popupHeight},scrollbars=yes`
|
||||
);
|
||||
|
||||
if (!this.popupWindow) {
|
||||
console.error("Failed to open Tone3000 dialog popup window.");
|
||||
this.handleTone3000DownloadError("Cannot open popup window.");
|
||||
return;
|
||||
}
|
||||
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.");
|
||||
});
|
||||
if (!this.popupWindow) {
|
||||
console.error("Failed to open TONE3000 dialog popup window.");
|
||||
throw new Error("Cannot open popup window.");
|
||||
}
|
||||
catch (error) {
|
||||
// offline
|
||||
this.handleTone3000DownloadError("Not online. Unable to connect to Tone3000 servers.");
|
||||
return;
|
||||
// No procedure for detecting 404, so let's ping the server to see if it's reachable, and cancel if it's not.
|
||||
try {
|
||||
let response = await fetch(TONE3000_PING_URL, { method: "HEAD", cache: "no-cache" });
|
||||
if (cancelled()) return;
|
||||
if (!response.ok) {
|
||||
// offline
|
||||
throw new Error("Unable to connect to the TONE3000 server.");
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error("Not online. Unable to connect to the TONE3000 server. Your client device must have access to the internet to use this feature..");
|
||||
};
|
||||
|
||||
}
|
||||
public closeTone3000Dialog(): void {
|
||||
this.handleTone3000DialogClosed();
|
||||
this.stopTone3000DialogMonitor();
|
||||
if (this.popupWindow) {
|
||||
let pipedalServerCanReachTone3000 = false;
|
||||
|
||||
// check to see that the PiPedal server can reach TONE3000.
|
||||
try {
|
||||
|
||||
let t = this.popupWindow;
|
||||
this.popupWindow = null;
|
||||
t.close();
|
||||
} catch (e) {
|
||||
console.error("Error closing Tone3000 dialog popup:", e);
|
||||
pipedalServerCanReachTone3000 = await this.model.pingTone3000Server();
|
||||
if (cancelled()) return;
|
||||
} catch (error) {
|
||||
}
|
||||
if (!pipedalServerCanReachTone3000) {
|
||||
throw new Error("The PiPedal server cannot reach a TONE3000 server. The PiPedal server must have access to the internet to use this feature.");
|
||||
}
|
||||
while (true) {
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
if (launchInstance != this.launchInstance) {
|
||||
// A new dialog launch has been initiated. Stop monitoring this one.
|
||||
return;
|
||||
}
|
||||
if (this.popupWindow == null || this.popupWindow.closed) {
|
||||
this.closeTone3000Dialog();
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.handleTone3000DownloadError(getErrorMessage(error));
|
||||
}
|
||||
}
|
||||
|
||||
public closeTone3000Dialog(): void {
|
||||
this.handleTone3000DownloadComplete();
|
||||
}
|
||||
}
|
||||
|
||||
export interface Tone3000DownloadDialogProps {
|
||||
|
||||
onClose: () => void,
|
||||
onDownloadComplete: () => void,
|
||||
downloadPath: string
|
||||
}
|
||||
export function Tone3000DownloadDialog(props: Tone3000DownloadDialogProps): JSX.Element {
|
||||
const model = PiPedalModel.getInstance();
|
||||
let { onClose, onDownloadComplete, downloadPath } = props;
|
||||
let [downloading, setDownloading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
model.showTone3000DownloadDialog(
|
||||
downloadPath,
|
||||
() => {
|
||||
onClose();
|
||||
},
|
||||
() => {
|
||||
setDownloading(true);
|
||||
},
|
||||
() => {
|
||||
onDownloadComplete();
|
||||
}
|
||||
);
|
||||
let onStateChanged = (state: State) => {
|
||||
if (state !== State.Ready) {
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
model.state.addOnChangedHandler(onStateChanged);
|
||||
return () => {
|
||||
model.closeTone3000DownloadDialog();
|
||||
model.state.removeOnChangedHandler(onStateChanged);
|
||||
};
|
||||
});
|
||||
return (
|
||||
<DialogEx tag="tone3000-download"
|
||||
open={true}
|
||||
onClose={() => {
|
||||
onClose();
|
||||
}}
|
||||
onEnterKey={() => { }}
|
||||
>
|
||||
<DialogContent>
|
||||
<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>
|
||||
<DialogActions >
|
||||
{!downloading && (
|
||||
<Button variant="dialogSecondary"
|
||||
onClick={() => {
|
||||
onClose();
|
||||
}}>
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
</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);
|
||||
const [downloading, setDownloading] = useState<boolean>(model.tone3000Downloading.get());
|
||||
const [progress, setProgress] = useState<Tone3000DownloadProgress | null>(model.tone3000DownloadProgress.get());
|
||||
|
||||
useEffect(() => {
|
||||
let onDownloadingChanged = (value: boolean) => {
|
||||
|
||||
setDownloading(value);
|
||||
}
|
||||
model.tone3000Downloading.addOnChangedHandler(onDownloadingChanged);
|
||||
@@ -302,30 +215,39 @@ export function Tone3000DownloadStaus(
|
||||
model.tone3000DownloadProgress.removeOnChangedHandler(onProgressChanged);
|
||||
}
|
||||
})
|
||||
let open = downloading && progress !== null && progress.title.length > 0;
|
||||
let open = downloading;
|
||||
if (model.state.get() !== State.Ready) {
|
||||
open = false;
|
||||
}
|
||||
let filename = progress?.title ?? "\u00A0";
|
||||
if (filename === "") {
|
||||
filename = "\u00A0";
|
||||
}
|
||||
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" }}>
|
||||
<DialogContent style={{ marginBottom: 0, paddingBottom: 0 }}>
|
||||
<div style={{ display: "flex", flexFlow: "column nowrap", alignItems: "stretch" }}>
|
||||
<Typography noWrap variant="body2">Downloading from TONE3000...</Typography>
|
||||
<LinearProgress
|
||||
style={{ marginTop: 16, flexGrow: 1 }}
|
||||
variant={(progress?.total ?? 0) === 0 ? "indeterminate" : "determinate"}
|
||||
style={{ visibility: progress !== null ? "visible" : "hidden", marginTop: 16 }}
|
||||
variant="determinate"
|
||||
value={(progress && progress.total !== 0) ? progress.progress / progress.total * 100 : 0}
|
||||
/>
|
||||
<Typography noWrap variant="caption"
|
||||
style={{
|
||||
width: 300, maxWidth: 300, paddingTop: 8, paddingLeft: 16, paddingRight: 16,
|
||||
paddingBottom: 0, marginBottom: 0
|
||||
}}
|
||||
>{filename}</Typography>
|
||||
</div>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button variant="dialogSecondary"
|
||||
onClick={() => {
|
||||
model.cancelTone3000Download();
|
||||
model.closeTone3000DownloadPopup();
|
||||
}}
|
||||
>Cancel</Button>
|
||||
</DialogActions>
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
|
||||
// Must agree with Tone3000DownloadType.hpp
|
||||
enum Tone3000DownloadType {
|
||||
Nam = "nam",
|
||||
CabIr = "cabir",
|
||||
}
|
||||
|
||||
|
||||
export default Tone3000DownloadType;
|
||||
@@ -6,19 +6,20 @@ import IconButtonEx from "./IconButtonEx";
|
||||
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Link from "@mui/material/Link";
|
||||
|
||||
import Tone3000DownloadType from "./Tone3000DownloadType";
|
||||
|
||||
|
||||
function Tone3000HelpDialog(props: {
|
||||
open: boolean;
|
||||
downloadType: Tone3000DownloadType
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const { open, onClose } = props;
|
||||
const { open, downloadType, onClose } = props;
|
||||
return (
|
||||
<DialogEx
|
||||
tag="tone3000-help"
|
||||
fullWidth={true}
|
||||
maxWidth="sm"
|
||||
fullWidth={true}
|
||||
maxWidth="sm"
|
||||
onEnterKey={() => { onClose(); }}
|
||||
onClose={() => { onClose(); }}
|
||||
open={open}>
|
||||
@@ -36,7 +37,7 @@ function Tone3000HelpDialog(props: {
|
||||
<ArrowBackIcon style={{ width: 24, height: 24 }} />
|
||||
</IconButtonEx>
|
||||
|
||||
<Typography id="tone3000-auth-dialog-title" noWrap component="div" sx={{ flexGrow: 1 }}>
|
||||
<Typography id="tone3000-help-dialog-title" noWrap component="div" sx={{ flexGrow: 1 }}>
|
||||
TONE3000 Help
|
||||
</Typography>
|
||||
|
||||
@@ -49,35 +50,82 @@ function Tone3000HelpDialog(props: {
|
||||
maxWidth: 500, marginLeft: "auto", marginRight: "auto",
|
||||
display: "flex", flexFlow: "column nowrap", gap: 16, alignItems: "start",
|
||||
}}>
|
||||
{downloadType === Tone3000DownloadType.CabIr && (
|
||||
<>
|
||||
<Typography variant="body1" component="div" display="block" >
|
||||
The <Link href="https://www.tone3000.com" target="_blank">TONE3000 website</Link> provides a
|
||||
massive collection of high-quality Neural Amp Modeler models that can be used by TooB Neural Amp Modeler. It also
|
||||
provides collections of cabinet Impulse Response (I/R) files that can be used with <i>TooB Cab IR</i>.
|
||||
</Typography>
|
||||
<Typography variant="body1" component="div" display="block">
|
||||
When you click on the button to download I/R files from TONE3000,
|
||||
selected I/R bundles will be directly downloaded from the TONE3000 website and
|
||||
uploaded to your PiPedal server. For this to work, the server must have
|
||||
internet access, and your client (Android app or browser) must have internet
|
||||
access as well.</Typography>
|
||||
<Typography variant="body1" component="div" display="block">
|
||||
If either of your devices does not have internet access, you can still use TONE3000.
|
||||
Use a browser on a device with internet access to download model files by connecting to
|
||||
<Link href="https://www.tone3000.com" target="_blank">https://www.tone3000.com</Link> and
|
||||
download model packs directly to your local device. You can then upload the model .zip
|
||||
files to the PiPedal Server using the <strong><em>Upload</em></strong> button in the
|
||||
<strong><em>File Properties</em></strong> dialog in the PiPedal app or web interface.
|
||||
PiPedal will extract I/R files from the .zip file bundles automatically, so there is no
|
||||
need to extract them first.
|
||||
</Typography>
|
||||
</>
|
||||
)}
|
||||
{downloadType === Tone3000DownloadType.Nam && (
|
||||
<>
|
||||
<Typography variant="body1" component="div" display="block" >
|
||||
The <Link href="https://www.tone3000.com" target="_blank">TONE3000 website</Link> provides a
|
||||
massive collection of high-quality Neural Amp Modeler files that can be used by TooB Neural Amp Modeler to
|
||||
perform high-quality simulations of amps and effect pedals.
|
||||
</Typography>
|
||||
<Typography variant="body1" component="div" display="block">
|
||||
When you click on the button to download Neural Amp Modeler files from TONE3000,
|
||||
model file bundles will be directly downloaded from the TONE3000 website and
|
||||
uploaded to your PiPedal server. For this to work, the server must have
|
||||
internet access, and your client (Android app or browser) must have internet
|
||||
access as well.</Typography>
|
||||
<Typography variant="body1" component="div" display="block">
|
||||
Neural Amp Modeler models that simulate amp heads will typically need a speaker/cabinet simulation to
|
||||
sound their best, or of course, need to be played through an actual physical amp speaker cabinet. The TONE3000
|
||||
website provides a broad selection of cabinet I/R files that can be used
|
||||
with <i>TooB Cab IR</i> to provide high-quality cabinet simulation for your NAM models. <i>TooB Cab IR</i> allows
|
||||
you to browse and download I/R files from TONE3000 directly within the PiPedal app.
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body1" component="div" display="block">
|
||||
If either of your devices does not have internet access, you can still use TONE3000.
|
||||
Use a browser on a device with internet access to download Neural Amp Modeler files by connecting to
|
||||
<Link href="https://www.tone3000.com" target="_blank">https://www.tone3000.com</Link> and
|
||||
download Neural Amp Modeler file packs directly to your local device as .zip file bundles. You can then
|
||||
upload the .zip file bundle to the PiPedal Server using the <strong><em>Upload</em></strong> button in the
|
||||
<strong><em>File Properties</em></strong> dialog. PiPedal will
|
||||
extract model files from the .zip archives automatically, so there is no need to extract them first.
|
||||
</Typography>
|
||||
</>
|
||||
)}
|
||||
<Typography variant="body1" component="div" display="block">
|
||||
TONE3000 is a community-driven platform where community members share Neural Amp Modeler profiles.
|
||||
The site showcases the collaborative spirit of the guitar modeling community, made
|
||||
possible by Steven Atkins' <Link href="https://www.neuralampmodeler.com/">Neural Amp Modeler library</Link>, which forms the foundation and
|
||||
core of TooB Neural Amp Modeler and other NAM-based plugins.
|
||||
</Typography>
|
||||
<Typography variant="body1" component="div" display="block">
|
||||
If you would like to profile your own amplifiers and effects, the TONE3000 website allows you to
|
||||
generate your own NAM profiles for free. Refer to
|
||||
the <Link href="https://www.tone3000.com/capture" target="_blank">TONE3000 Capture page</Link> for more details.
|
||||
</Typography>
|
||||
<Typography variant="body1" component="div" display="block" >
|
||||
The <Link href="https://www.tone3000.com" target="_blank">TONE3000 website</Link> provides a
|
||||
massive collection of neural amp models that can be used by TooB Neural Amp Modeler.
|
||||
</Typography>
|
||||
<Typography variant="body1" component="div" display="block">
|
||||
Using TONE3000 model files in PiPedal is a two step process. First, download model files from
|
||||
the website using your browser; and then upload
|
||||
the files in your browser's Downloads directory to the PiPedal server using
|
||||
the <strong><em>Upload</em></strong> button. PiPedal
|
||||
automatically extracts bundles of model files from zip archives, so manual extraction is unnecessary.
|
||||
</Typography>
|
||||
<Typography variant="body1" component="div" display="block">
|
||||
TONE3000 is a community-driven platform where community members share Neural Amp Modeler profiles.
|
||||
The site showcases the collaborative spirit of the guitar modeling community. And all of this is made
|
||||
possible by Steven Atkins' <Link href="https://www.neuralampmodeler.com/">Neural Amp Modeler library</Link>, which forms the foundation and core of TooB Neural Amp Modeler and other NAM-based plugins.
|
||||
</Typography>
|
||||
<Typography variant="body1" component="div" display="block">
|
||||
If you would like to profile your own amplifiers, and effects, the TONE3000 website allows you to
|
||||
generate your own NAM profiles for free. Refer to
|
||||
the <Link href="https://www.tone3000.com/capture">TONE3000 website</Link> for more details.
|
||||
</Typography>
|
||||
<Typography variant="body1" component="div" display="block" >
|
||||
When you click on the TONE3000 link, you will be taken to an external website. The TONE3000
|
||||
website is not part of PiPedal, and is not affiliated in any way with PiPedal.
|
||||
When you click on the TONE3000 link, you will be taken to an external website. The TONE3000
|
||||
website is not part of PiPedal and is not affiliated in any way with PiPedal.
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body2" component="div" display="block" style={{ marginTop: 32 }}>
|
||||
Privacy statement: PiPedal has no access to personal data used by the TONE3000 website. Please refer to
|
||||
the <Link href="https://www.tone3000.com/privacy" target="_blank">TONE3000 privacy policy</Link> for
|
||||
the <Link href="https://www.tone3000.com/privacy" target="_blank">TONE3000 Privacy Policy</Link> for
|
||||
information on how your data is used by TONE3000.
|
||||
</Typography>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user