Roll back TONE3000 direct download (not ready for prime time); add direct link to TONE3000 downloads from File Property dialog.

This commit is contained in:
Robin E. R. Davies
2025-06-30 23:54:25 -04:00
parent 3bf1a667c0
commit e092aed47b
13 changed files with 586 additions and 67 deletions
+13 -2
View File
@@ -24,6 +24,8 @@ import CssBaseline from '@mui/material/CssBaseline';
import VirtualKeyboardHandler from './VirtualKeyboardHandler';
import AppThemed from "./AppThemed";
import { isDarkMode } from './DarkMode';
import Tone3000AuthComplete from './Tone3000AuthComplete';
declare module '@mui/material/styles' {
interface Theme {
@@ -221,6 +223,12 @@ type AppThemeProps = {
};
function isTone3000Auth() {
let url = new URL(window.location.href);
let param = url.searchParams.get("api_key");
return (param !== null && param !== "")
}
const App = (class extends React.Component {
// Before the component mounts, we initialise our state
@@ -240,8 +248,11 @@ const App = (class extends React.Component {
<StyledEngineProvider injectFirst>
<ThemeProvider theme={theme}>
<CssBaseline />
<AppThemed />
{isTone3000Auth() ? (
<Tone3000AuthComplete />
) : (
<AppThemed />
)}
</ThemeProvider>
</StyledEngineProvider>
);
+98 -41
View File
@@ -21,6 +21,10 @@
import React from 'react';
import { createStyles } from './WithStyles';
// import Tone3000Dialog from './Tone3000Dialog';
import Tone3000HelpDialog from './Tone3000HelpDialog';
import HelpOutlineIcon from '@mui/icons-material/HelpOutline';
import Link from '@mui/material/Link';
import DraggableButtonBase from './DraggableButtonBase';
import CloseIcon from '@mui/icons-material/Close';
import { Theme } from '@mui/material/styles';
@@ -64,6 +68,8 @@ import FilePropertyDirectorySelectDialog from './FilePropertyDirectorySelectDial
import { getAlbumArtUri, getTrackTitle } from './AudioFileMetadata';
const ToobNamModelFileUrl = "http://two-play.com/plugins/toob-nam#modelFile";
const AUTOSCROLL_TICK_DELAY = 30;
const AUTOSCROLL_THRESHOLD = 48;
const AUTOSCROLL_SCROLL_PER_SECOND = 500;
@@ -161,6 +167,8 @@ export interface FilePropertyDialogState {
initialSelection: string;
multiSelect: boolean,
selectedFiles: string[],
//openTone3000Dialog: boolean,
openTone3000Help: boolean
};
@@ -231,7 +239,9 @@ export default withStyles(
copyDialogOpen: false,
initialSelection: this.props.selectedFile,
multiSelect: false,
selectedFiles: []
selectedFiles: [],
//openTone3000Dialog: false,
openTone3000Help: false
};
this.requestScroll = true;
}
@@ -362,7 +372,7 @@ export default withStyles(
dragElementDy: 0,
from: index,
to: index,
lastMousePoint: {...this.longPressStartPoint}
lastMousePoint: { ...this.longPressStartPoint }
};
this.setState({ dragState: dragState });
}
@@ -882,7 +892,6 @@ export default withStyles(
style={{ marginLeft: 16, marginBottom: 8, marginRight: 8, textOverflow: "", display: "flex", flexFlow: "row wrap", alignItems: "center", justifyContent: "start" }}>
{breadcrumbs}
</div>
<Divider />
</div>
);
}
@@ -998,8 +1007,7 @@ export default withStyles(
let dragElementDy = dragState.lastMousePoint.y - this.longPressStartPoint.y;
let originalY = dragState.from * dragState.height;
let newY = originalY + dragElementDy;
if (newY < 0)
{
if (newY < 0) {
newY = 0;
dragElementDy = -originalY;
}
@@ -1040,9 +1048,26 @@ export default withStyles(
() => { this.handleAutoScrollTick() }, AUTOSCROLL_TICK_DELAY);
}
// handleTone3000Dialog(e: React.MouseEvent<HTMLButtonElement>) {
// e.stopPropagation();
// e.preventDefault();
// this.setState({ openTone3000Dialog: true });
// }
handleTone3000Help(e: React.MouseEvent<HTMLButtonElement>) {
e.stopPropagation();
e.preventDefault();
this.setState({ openTone3000Help: true });
}
render() {
const isTracksDirectory = this.isTracksDirectory();
const isToobNamModelFile = this.props.fileProperty.patchProperty === ToobNamModelFileUrl;
const classes = withStyles.getClasses(this.props);
let columnWidth = this.state.columnWidth;
@@ -1102,7 +1127,7 @@ export default withStyles(
background: this.state.reordering || this.state.multiSelect ?
(isDarkMode() ? "#523" : "#EDF")
: undefined,
marginBottom: 16, paddingTop: 0
marginBottom: 8, paddingTop: 0
}} >
{this.state.reordering && (
<Toolbar style={{ padding: 0 }}>
@@ -1257,7 +1282,7 @@ export default withStyles(
</div>
</DialogTitle>
<DialogContent style={{
<DialogContent dividers style={{
paddingLeft: 0, paddingRight: 0, paddingTop: 0, colorScheme: (isDarkMode() ? "dark" : "light"),
position: "relative"
}}
@@ -1284,7 +1309,8 @@ export default withStyles(
ref={(element) => { this.listContainerElementRef = element; this.onMeasureRef(element); }}
style={{
flex: "1 1 100%", display: "flex", flexFlow: "row wrap",
position: "relative", justifyContent: "flex-start", alignContent: "flex-start", paddingLeft: 16, paddingBottom: 16
position: "relative", justifyContent: "flex-start", alignContent: "flex-start",
paddingLeft: 16, paddingBottom: 16, paddingTop: 16,
}}>
{
(this.state.columns !== 0) && // don't render until we have number of columns derived from layout.
@@ -1461,45 +1487,64 @@ export default withStyles(
</DialogContent>
{(!this.state.reordering && !this.state.multiSelect) && (
<>
<Divider />
<DialogActions style={{ justifyContent: "stretch" }}>
{this.state.windowWidth > 500 ? (
<div style={{ display: "flex", width: "100%", alignItems: "center", flexFlow: "row nowrap" }}>
<IconButtonEx style={{ visibility: (this.state.hasSelection ? "visible" : "hidden") }} aria-label="delete" component="label" color="primary"
tooltip="Delete selected file or folder"
disabled={!this.state.hasSelection || this.state.selectedFile === "" || protectedItem}
onClick={() => this.handleDelete()} >
<OldDeleteIcon fontSize='small' />
</IconButtonEx>
<div style={{ display: "flex", width: "100%", alignItems: "center", flexFlow: "column nowrap" }}>
{isToobNamModelFile && (
<div style={{
display: "flex", flexFlow: "row nowrap", justifyContent: "center",
alignItems: "center", width: "100%"
}}>
<Typography variant="body2" >
Download model files from <Link
href="https://www.tone3000.com/search" target="_blank">TONE3000</Link>
</Typography>
<IconButtonEx tooltip="Help"
onClick={(e) => { this.handleTone3000Help(e); }} aria-label="help" edge="end" color="inherit" style={{ opacity: 0.6, marginLeft: 8 }}
>
<HelpOutlineIcon />
</IconButtonEx>
</div>
<ButtonEx tooltip="Upload file" style={{ flex: "0 0 auto" }} aria-label="upload" variant="text" startIcon={<FileUploadIcon />}
onClick={() => { this.setState({ openUploadFileDialog: true }) }} disabled={protectedDirectory}
>
<div>Upload</div>
</ButtonEx>
)}
<ButtonEx tooltip="Download file" style={{ flex: "0 0 auto" }} aria-label="upload" variant="text" startIcon={<FileDownloadIcon />}
onClick={() => { this.handleDownloadFile(); }} disabled={protectedDirectory || !this.state.hasFileSelection}
>
<div>Download</div>
</ButtonEx>
<div style={{ flex: "1 1 100%", display: "flex", width: "100%", alignItems: "center", flexFlow: "row nowrap" }}>
<IconButtonEx style={{ visibility: (this.state.hasSelection ? "visible" : "hidden") }} aria-label="delete" component="label" color="primary"
tooltip="Delete selected file or folder"
disabled={!this.state.hasSelection || this.state.selectedFile === "" || protectedItem}
onClick={() => this.handleDelete()} >
<OldDeleteIcon fontSize='small' />
</IconButtonEx>
<div style={{ flex: "1 1 auto" }}>&nbsp;</div>
<ButtonEx tooltip="Upload file" style={{ flex: "0 0 auto" }} aria-label="upload" variant="text" startIcon={<FileUploadIcon />}
onClick={() => { this.setState({ openUploadFileDialog: true }) }} disabled={protectedDirectory}
>
<div>Upload</div>
</ButtonEx>
<Button variant="dialogSecondary" onClick={() => {
this.props.onApply(this.props.fileProperty, this.state.initialSelection);
this.props.onCancel();
}} aria-label="cancel">
Cancel
</Button>
<Button variant="dialogPrimary" style={{ flex: "0 0 auto" }}
onClick={() => { this.openSelectedFile(); }}
<ButtonEx tooltip="Download file" style={{ flex: "0 0 auto" }} aria-label="upload" variant="text" startIcon={<FileDownloadIcon />}
onClick={() => { this.handleDownloadFile(); }} disabled={protectedDirectory || !this.state.hasFileSelection}
>
<div>Download</div>
</ButtonEx>
disabled={(!canSelectFile)
} aria-label="select"
>
{okButtonText}
</Button>
<div style={{ flex: "1 1 auto" }}>&nbsp;</div>
<Button variant="dialogSecondary" onClick={() => {
this.props.onApply(this.props.fileProperty, this.state.initialSelection);
this.props.onCancel();
}} aria-label="cancel">
Cancel
</Button>
<Button variant="dialogPrimary" style={{ flex: "0 0 auto" }}
onClick={() => { this.openSelectedFile(); }}
disabled={(!canSelectFile)
} aria-label="select"
>
{okButtonText}
</Button>
</div>
</div>
) : (
<div style={{ width: "100%" }}>
@@ -1607,7 +1652,7 @@ export default withStyles(
{
this.state.renameDialogOpen && (
<RenameDialog open={this.state.renameDialogOpen} defaultName={this.renameDefaultName()}
title="Rename"
title="Rename"
onOk={(newName) => { this.setState({ renameDialogOpen: false }); this.onExecuteRename(newName) }}
onClose={() => { this.setState({ renameDialogOpen: false }); }}
acceptActionName="OK"
@@ -1645,6 +1690,18 @@ export default withStyles(
)
)
}
{/* {this.state.openTone3000Dialog && (
<Tone3000Dialog
open={this.state.openTone3000Dialog}
onClose={() => this.setState({ openTone3000Dialog: false })}
/>
)} */}
{this.state.openTone3000Help && (
<Tone3000HelpDialog
open={this.state.openTone3000Help}
onClose={() => this.setState({ openTone3000Help: false })}
/>
)}
</DialogEx>
);
}
+7
View File
@@ -420,6 +420,8 @@ export class PiPedalModel //implements PiPedalModel
webSocket?: PiPedalSocket;
hasTone3000Auth: ObservableProperty<boolean> = new ObservableProperty<boolean>(false);
hasWifiDevice: ObservableProperty<boolean> = new ObservableProperty<boolean>(false);
onSnapshotModified: ObservableEvent<SnapshotModifiedEvent> = new ObservableEvent<SnapshotModifiedEvent>();
@@ -760,6 +762,8 @@ export class PiPedalModel //implements PiPedalModel
} else if (message === "onErrorMessage") {
this.showAlert(body as string);
} else if (message == "onTone3000AuthChanged") {
this.hasTone3000Auth.set(body as boolean);
}
else if (message === "onLv2PluginsChanging") {
this.onLv2PluginsChanging();
@@ -1142,6 +1146,9 @@ export class PiPedalModel //implements PiPedalModel
this.alsaSequencerConfiguration.set(new AlsaSequencerConfiguration().deserialize(
await this.getWebSocket().request<any>("getAlsaSequencerConfiguration")
));
this.hasTone3000Auth.set(
await this.getWebSocket().request<boolean>("getHasTone3000Auth")
);
this.banks.set(new BankIndex().deserialize(await this.getWebSocket().request<any>("getBankIndex")));
this.favorites.set(await this.getWebSocket().request<FavoritesList>("getFavorites"));
+85
View File
@@ -0,0 +1,85 @@
import React from 'react';
import Typography from '@mui/material/Typography';
import { styled } from '@mui/material/styles';
const Frame = styled('div', {
name: 'MuiStat', // The component name
slot: 'root', // The slot name
})(({ theme }) => ({
flex: "1 1 auto",
gap: theme.spacing(0.5),
padding: theme.spacing(3, 4),
backgroundColor: theme.palette.background.paper,
width: "100%",
height: "100%"
}));
interface Tone3000AuthCompleteProps {
}
function Tone3000AuthComplete(props: Tone3000AuthCompleteProps) {
function postApiKey() {
let url = new URL(window.location.href);
let apiKey = url.searchParams.get("api_key");
if (apiKey) {
let myUrl = new URL(window.location.href);
let port = myUrl.port;
if (port === "5173") { // when running debug server.
port = "8080";
}
let postUrl = "http://" + myUrl.hostname + ":" + port + "/var/Tone3000Auth?api_key=" + encodeURIComponent(apiKey);
fetch(postUrl, {
method: "POST",
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ api_key: apiKey })
}).then((response) => {
if (!response.ok) {
alert("Failed to post API key to Pipedal: " + response.statusText);
}
return response.json();
})
.catch((error) => {
alert("Error posting API key to Pipedal: " + error.message);
})
} else {
alert("No API key found in URL.");
}
}
React.useEffect(() => {
postApiKey();
return () => { };
})
return (
<Frame style={{
position: "absolute", top: 0, left: 0, right: 0, bottom: 0,
}}>
<div style={{ display: "flex", flexFlow: "row nowrap", alignItems: "start" }}>
<div style={{ flex: "1 1 1px" }} />
<div style={{
display: "flex", flexFlow: "column nowrap", alignItems: "start",
maxWidth: 600, marginLeft: "auto", marginRight: "auto", gap: 16, padding: 32
}}>
<Typography variant="h5">TONE3000 Authorization Complete</Typography>
<Typography variant="body1">
You have successfully obtained an access token from TONE3000, and can now download Toob Neural Amp models from TONE3000
directly.</Typography>
<Typography variant="body1">
Close this page and return to Pipedal in order to continue.</Typography>
</div>
<div style={{ flex: "2 2 1px" }} />
</div>
</Frame>
);
}
export default Tone3000AuthComplete;
+163
View File
@@ -0,0 +1,163 @@
import React from 'react';
import Dialog from '@mui/material/Dialog';
import DialogTitle from '@mui/material/DialogTitle';
import DialogContent from '@mui/material/DialogContent';
import DialogActions from '@mui/material/DialogActions';
import Button from '@mui/material/Button';
import Typography from '@mui/material/Typography';
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
import DialogEx from './DialogEx';
import Link from '@mui/material/Link';
import Toolbar from '@mui/material/Toolbar';
import IconButtonEx from './IconButtonEx';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
export interface Tone3000DialogProps {
open: boolean;
onClose: () => void;
}
function Tone3000Dialog(props: Tone3000DialogProps) {
const { open, onClose } = props;
const model: PiPedalModel = PiPedalModelFactory.getInstance();
let [openAuthDialog, setOpenAuthDialog] = React.useState(!model.hasTone3000Auth.get());
React.useEffect(()=> {
let authListener = (value: boolean) => {
setOpenAuthDialog(!value);
}
model.hasTone3000Auth.addOnChangedHandler(authListener);
return () => {
model.hasTone3000Auth.removeOnChangedHandler(authListener);
}
});
function RedirectUrl() {
let baseUrl = new URL(window.location.href);
let result = "http://" + baseUrl.hostname + ":" + baseUrl.port + "/";
return result;
}
function AuthUrl() {
//return "https://www.tone3000.com/api/v1/auth?redirect_url=" + encodeURIComponent(RedirectUrl()) + "&opt_only=true";
return "https://www.tone3000.com/api/v1/auth?redirect_url=" + encodeURIComponent(RedirectUrl()) + "&otp_only=true";
}
return (
<DialogEx
tag="tone3000"
fullScreen={true}
open={open}
onEnterKey={() => { onClose(); }}
onClose={() => { onClose(); }}
aria-labelledby="tone3000-dialog-title"
aria-describedby="tone3000-dialog-description"
>
<DialogTitle id="tone3000-dialog-title">
<Toolbar style={{ padding: 0 }}>
<IconButtonEx
tooltip="Close"
edge="start"
color="inherit"
aria-label="cancel"
style={{ opacity: 0.6 }}
onClick={() => { onClose(); }}
>
<ArrowBackIcon style={{ width: 24, height: 24 }} />
</IconButtonEx>
<Typography noWrap component="div" sx={{ flexGrow: 1 }}>
TONE3000 Models
</Typography>
</Toolbar>
</DialogTitle>
<DialogContent>
<Typography variant="body1">
This is a placeholder for the Tone 3000 dialog.
</Typography>
</DialogContent>
<DialogActions>
<Button onClick={onClose} color="primary">
Close
</Button>
</DialogActions>
{
openAuthDialog && (
<Dialog
fullScreen={true}
open={openAuthDialog}>
<DialogTitle id="tone3000-auth-dialog-title">
<Toolbar style={{ padding: 0 }}>
<IconButtonEx
tooltip="Close"
edge="start"
color="inherit"
aria-label="cancel"
style={{ opacity: 0.6 }}
onClick={() => { onClose(); }}
>
<ArrowBackIcon style={{ width: 24, height: 24 }} />
</IconButtonEx>
<Typography noWrap component="div" sx={{ flexGrow: 1 }}>
TONE3000 Authorization
</Typography>
</Toolbar>
</DialogTitle>
<DialogContent>
<div style={{ display: "flex", flexFlow: "row nowrap", alignItems: "start" }}>
<div style={{ flex: "1 1 1px" }} />
<div style={{
maxWidth: 500, marginLeft: "auto", marginRight: "auto",
display: "flex", flexFlow: "column nowrap", gap: 16, alignItems: "start",
}}>
<Typography variant="body1" component="div" display="block" >
The <Link href="https://www.tone3000.com" target="_blank">TONE3000 website</Link> provides an
online library of models for use with TooB Neural Amp Modeller. You can download
models from the TONE3000 website onto your local machine and then upload them to
PiPedal; or, more conveniently, you can browse the TONE3000 model database directly, and download models directly
to the PiPedal server from the TONE3000 model database in a single step.
</Typography>
<Typography variant="body1" component="div" display="block" >
In order to access the TONE3000 database, you must first obtain an access token from the
TONE3000 website. Clicking on the button will take you to an external website in
order to complete the authorization process.
</Typography>
<Button variant="contained" color="primary" style={{ alignSelf: "end", marginRight: 32 }}
onClick={() => {
window.open(AuthUrl(), "_blank");
}}
>
Continue to TONE3000
</Button>
<Typography variant="body2" component="div" display="block" style={{ marginTop: 32 }}>
Privacy statement: PiPedal will only have access to your authorization token
which does not contain personally identifying information, and which is stored locally on
your PiPedal server. Please refer to
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>
<div style={{ flex: "2 2 1px" }} />
</div>
</DialogContent>
</Dialog>
)}
</DialogEx>
);
}
export default Tone3000Dialog;
+92
View File
@@ -0,0 +1,92 @@
import DialogEx from "./DialogEx";
import DialogTitle from "@mui/material/DialogTitle";
import DialogContent from "@mui/material/DialogContent";
import Toolbar from "@mui/material/Toolbar";
import IconButtonEx from "./IconButtonEx";
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import Typography from "@mui/material/Typography";
import Link from "@mui/material/Link";
function Tone3000HelpDialog(props: {
open: boolean;
onClose: () => void;
}) {
const { open, onClose } = props;
return (
<DialogEx
tag="tone3000-help"
fullWidth={true}
maxWidth="sm"
onEnterKey={() => { onClose(); }}
onClose={() => { onClose(); }}
open={open}>
<DialogTitle >
<Toolbar style={{ padding: 0 }}>
<IconButtonEx
tooltip="Close"
edge="start"
color="inherit"
aria-label="cancel"
style={{ opacity: 0.6 }}
onClick={() => { onClose(); }}
>
<ArrowBackIcon style={{ width: 24, height: 24 }} />
</IconButtonEx>
<Typography id="tone3000-auth-dialog-title" noWrap component="div" sx={{ flexGrow: 1 }}>
TONE3000 Help
</Typography>
</Toolbar>
</DialogTitle>
<DialogContent>
<div style={{ display: "flex", flexFlow: "row nowrap", alignItems: "start" }}>
<div style={{ flex: "1 1 1px" }} />
<div style={{
maxWidth: 500, marginLeft: "auto", marginRight: "auto",
display: "flex", flexFlow: "column nowrap", gap: 16, alignItems: "start",
}}>
<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.
</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
information on how your data is used by TONE3000.
</Typography>
</div>
<div style={{ flex: "2 2 1px" }} />
</div>
</DialogContent>
</DialogEx>
);
}
export default Tone3000HelpDialog;