Model/IR Select dialog for large downloads

This commit is contained in:
Robin E.R. Davies
2026-05-30 00:46:45 -04:00
parent 356c0e42a7
commit 448e373841
13 changed files with 338 additions and 33 deletions
+1 -1
View File
@@ -175,7 +175,7 @@
"cSpell.ignoreWords": [
"nammodel"
],
"cSpell.enabled": true,
"cSpell.enabled": false,
// Disable all automatic completion suggestions - only show when Ctrl+Space is pressed
"editor.quickSuggestions": {
+1 -1
View File
@@ -314,7 +314,7 @@ static std::map<std::string, std::string> gearEnumValues =
{"full-rig", "Full rig"},
{"pedal", "Pedal"},
{"outboard", "Outboard"},
{"ir", "I/R"}};
{"ir", "IR"}};
static std::string mdGear(const std::string &gear)
{
+2 -2
View File
@@ -713,7 +713,7 @@ static std::map<std::string, std::string> gearEnumValues =
{"full-rig", "Full rig"},
{"pedal", "Pedal"},
{"outboard", "Outboard"},
{"ir", "I/R"}};
{"ir", "IR"}};
static std::string mdGear(const std::string &gear)
{
@@ -926,7 +926,7 @@ static std::string mdLicense(const std::string &license)
static std::map<std::string, std::string> platformEnumValues =
{
{"nam", "NAM"},
{"ir", "I/R"},
{"ir", "IR"},
{"aida-x", "Aida X"},
{"aa-snapshot", "aa-snapshot"},
{"proteus", "Proteus"}};
+2 -2
View File
@@ -143,7 +143,7 @@ namespace
{"full-rig", "Full rig"},
{"pedal", "Pedal"},
{"outboard", "Outboard"},
{"ir", "I/R"}};
{"ir", "IR"}};
static std::string mdGear(const std::string &gear)
{
@@ -356,7 +356,7 @@ namespace
static std::map<std::string, std::string> platformEnumValues =
{
{"nam", "NAM"},
{"ir", "I/R"},
{"ir", "IR"},
{"aida-x", "Aida X"},
{"aa-snapshot", "aa-snapshot"},
{"proteus", "Proteus"}};
+1 -1
View File
@@ -26,7 +26,7 @@ add_custom_command(
public/css/roboto.css
public/html/t3k_response.html
public/var/current_pedalboard.json
public/var/config.json.disabled
public/var/config.json
public/var/uiplugins.json
public/var/plugin_classes.json
public/testTone300Popup.html
+1 -1
View File
@@ -1,2 +1,2 @@
#!/bin/bash
npm run dev -- --host 0.0.0.0 --host lothlorien.local
npm run dev -- --host 0.0.0.0
+1 -1
View File
@@ -29,7 +29,7 @@ interface DialogExState {
interface DialogExProps extends DialogProps {
tag: string;
fullwidth?: boolean;
onEnterKey: () => void;
onEnterKey?: () => void;
dlgRef?: (instance: HTMLDivElement | null) => void;
}
+30 -5
View File
@@ -68,6 +68,7 @@ import HomeIcon from '@mui/icons-material/Home';
import FilePropertyDirectorySelectDialog from './FilePropertyDirectorySelectDialog';
import { getAlbumArtUri, getTrackTitle } from './AudioFileMetadata';
import Tone3000DownloadType from './Tone3000DownloadType';
import { ModelSelectionDialog, ModelSelectionDialogParams } from './ModelSelectionDialog';
const ToobNamModelFileUrl = "http://two-play.com/plugins/toob-nam#modelFile";
@@ -182,6 +183,7 @@ export interface FilePropertyDialogState {
textFileName?: string;
modelSelectionDialogParams?: ModelSelectionDialogParams;
};
interface DragState {
@@ -255,7 +257,8 @@ export default withStyles(
multiSelect: false,
selectedFiles: [],
openTone3000Help: false,
openGuitarMlHelp: false
openGuitarMlHelp: false,
modelSelectionDialogParams: undefined,
};
this.requestScroll = true;
}
@@ -588,8 +591,19 @@ export default withStyles(
this.requestFiles(this.state.navDirectory)
this.model.onTone3000DownloadCompleteEvent.addEventHandler(this.handleTone3000DownloadComplete);
this.requestScroll = true;
this.model.setT3kModelSelectionDialogListener({
onShowDialog: (modelSelectionDialogParams: ModelSelectionDialogParams) => {
this.setState({
modelSelectionDialogParams: modelSelectionDialogParams
});
}
});
}
componentWillUnmount() {
this.model.setT3kModelSelectionDialogListener(undefined);
this.stopAutoScroll();
this.cancelProgressTimeout();
this.model.onTone3000DownloadCompleteEvent.removeEventHandler(this.handleTone3000DownloadComplete);
@@ -939,7 +953,8 @@ export default withStyles(
);
}
getDefaultPath(): string {
// yyy: depends on dialog type!!!
private getDefaultPath(): string {
try {
let storage = window.localStorage;
let result = storage.getItem("fpDefaultPath");
@@ -1100,8 +1115,7 @@ export default withStyles(
let downloadType: Tone3000DownloadType = Tone3000DownloadType.Nam;
if (this.props.fileProperty.patchProperty === ToobNamModelFileUrl) {
downloadType = Tone3000DownloadType.Nam;
} else if (ToobCabIrFileUrls.includes(this.props.fileProperty.patchProperty))
{
} else if (ToobCabIrFileUrls.includes(this.props.fileProperty.patchProperty)) {
downloadType = Tone3000DownloadType.CabIr;
} else {
throw new Error("Unsupported file property for Tone3000 dialog");
@@ -1726,6 +1740,16 @@ export default withStyles(
/>
{this.state.modelSelectionDialogParams && (
<ModelSelectionDialog open={true}
params={
this.state.modelSelectionDialogParams
}
onClose={
() => { this.setState({ modelSelectionDialogParams: undefined }); }
}
/>
)}
<OkCancelDialog open={this.state.openConfirmDeleteDialog}
text={
@@ -1826,7 +1850,8 @@ export default withStyles(
title={pathFileNameOnly(this.state.textFileName)}
fileName={this.state.textFileName} onClose={() => this.setState({ textFileName: undefined })}
onT3kLinkClick={(href) => {
return this.handleT3kLinkClick(href); }
return this.handleT3kLinkClick(href);
}
}
/>
)}
+211
View File
@@ -0,0 +1,211 @@
/*
* 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.
*/
import Button from "@mui/material/Button";
import Checkbox from "@mui/material/Checkbox";
import DialogActions from "@mui/material/DialogActions";
import DialogContent from "@mui/material/DialogContent";
import Paper from "@mui/material/Paper";
import Table from "@mui/material/Table";
import TableBody from "@mui/material/TableBody";
import TableCell from "@mui/material/TableCell";
import TableContainer from "@mui/material/TableContainer";
import TableHead from "@mui/material/TableHead";
import TableRow from "@mui/material/TableRow";
import React, { useEffect } from "react";
import DialogEx from "./DialogEx";
import { Model } from "./t3k/types.ts";
import Tone3000DownloadType from "./Tone3000DownloadType.tsx";
import Divider from "@mui/material/Divider";
import Toolbar from "@mui/material/Toolbar";
import Typography from "@mui/material/Typography";
import IconButtonEx from "./IconButtonEx.tsx";
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import DialogTitle from "@mui/material/DialogTitle";
import useWindowSize from "./UseWindowSize.tsx";
interface ModelSelection {
selected: boolean;
model: Model;
}
export interface ModelSelectionDialogParams {
downloadType: Tone3000DownloadType;
toneName: string;
models: Model[];
onCancel: () => void;
onModelsSelected: (selectedModels: Model[]) => void;
};
export function ModelSelectionDialog(
props: {
open: boolean;
params: ModelSelectionDialogParams;
onClose: () => void;
}) {
const { models, toneName, onCancel, onModelsSelected } = props.params;
const windowSize = useWindowSize();
const [modelSelection, setModelSelection] = React.useState<ModelSelection[]>([]);
useEffect(() => {
setModelSelection(models.map((model) => ({
selected: false,
model,
})));
}, [models]);
const selectedCount = modelSelection.filter((selection) => selection.selected).length;
const allSelected = modelSelection.length > 0 && selectedCount === modelSelection.length;
const partiallySelected = selectedCount > 0 && selectedCount < modelSelection.length;
const toggleAll = () => {
setModelSelection((currentSelection) => {
const nextSelected = !allSelected;
return currentSelection.map((selection) => ({
...selection,
selected: nextSelected,
}));
});
};
const toggleModel = (modelId: number) => {
setModelSelection((currentSelection) => currentSelection.map((selection) => {
if (selection.model.id !== modelId) {
return selection;
}
return {
...selection,
selected: !selection.selected,
};
}));
};
const handleCancel = () => {
onCancel();
props.onClose();
};
const handleOk = () => {
onModelsSelected(
modelSelection
.filter((selection) => selection.selected)
.map((selection) => selection.model)
);
props.onClose();
};
let title = props.params.downloadType === Tone3000DownloadType.Nam
? "Select Models" :
"Select IRs ";
let tableTitle =
"(" + selectedCount.toString() + " of " + models.length.toString() + " selected) - " + toneName
return (
<DialogEx tag="selectModels" open={props.open} onClose={() => { /* Do nothing */ }}
fullWidth={true} maxWidth="md"
fullScreen={windowSize[0].width < 450}
>
<DialogTitle style={{
paddingBottom: 0,
marginBottom: 0, paddingTop: 0,
}} >
<Toolbar style={{ padding: 0 }}>
<IconButtonEx
tooltip="Back"
edge="start"
color="inherit"
aria-label="back"
style={{ opacity: 0.6 }}
onClick={() => { handleCancel(); }}
>
<ArrowBackIcon style={{ width: 24, height: 24 }} />
</IconButtonEx>
<Typography noWrap component="div" sx={{ flexGrow: 1 }}>
{title}
</Typography>
</Toolbar>
</DialogTitle>
<DialogContent sx={{ marginLeft: 0, marginRight: 0, paddingLeft: 0, paddingRight: 0 }}>
<TableContainer component={Paper} variant="outlined" sx={{ backgroundColor: "transparent", border: "0px" }} >
<Table sx={{ backgroundColor: "transparent" }}>
<TableHead style={{ backgroundColor: "transparent" }}>
<TableRow sx={{ backgroundColor: "transparent" }} onClick={toggleAll}>
<TableCell padding="checkbox" sx={{ backgroundColor: "transparent" }}>
<Checkbox
checked={allSelected}
indeterminate={partiallySelected}
onChange={toggleAll}
sx={{marginLeft: 1}}
inputProps={{ 'aria-label': 'select all models' }}
/>
</TableCell>
<TableCell sx={{ backgroundColor: "transparent" }}>
<Typography noWrap variant="body1">{tableTitle}</Typography>
</TableCell>
</TableRow>
</TableHead>
<TableBody style={{ backgroundColor: "transparent" }}>
{modelSelection.map((selection) => (
<TableRow
key={selection.model.id}
onClick={() => toggleModel(selection.model.id)}
sx={{ cursor: 'pointer', backgroundColor: 'transparent' }}
>
<TableCell padding="checkbox" sx={{ backgroundColor: "transparent", paddingLeft: 3, borderBottom: "0px" }}>
<Checkbox
checked={selection.selected}
onClick={(event) => { event.stopPropagation(); }}
onChange={() => toggleModel(selection.model.id)}
inputProps={{ 'aria-labelledby': `model-selection-${selection.model.id}` }}
/>
</TableCell>
<TableCell id={`model-selection-${selection.model.id}`} sx={{ backgroundColor: "transparent", borderBottom: "0px" }}>
<Typography noWrap variant="body2">
{selection.model.name}
</Typography>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</DialogContent>
<Divider />
<DialogActions>
<Button onClick={handleCancel} variant="dialogSecondary">
Cancel
</Button>
<Button onClick={handleOk} variant="dialogPrimary" disabled={selectedCount === 0}>
OK
</Button>
</DialogActions>
</DialogEx>
);
}
+46 -7
View File
@@ -50,7 +50,8 @@ import { AlsaSequencerConfiguration, AlsaSequencerPortSelection } from './AlsaSe
import { getDefaultModGuiPreference } from './ModGuiHost';
import ChannelRouterSettings from './ChannelRouterSettings';
import Tone3000DownloadProgress from './Tone3000DownloadProgress';
import { Tone } from './t3k/types';
import { Model, Tone } from './t3k/types';
import { ModelSelectionDialogParams } from './ModelSelectionDialog';
export enum State {
@@ -66,6 +67,11 @@ export enum State {
HotspotChanging,
};
export interface T3kModelSelectionDialogListener {
onShowDialog: (
modelSelectionDialogParams: ModelSelectionDialogParams
) => void;
};
export interface Tone3000PkceParams {
publishableKey: string;
redirectUrl: string;
@@ -708,10 +714,8 @@ export class PiPedalModel //implements PiPedalModel
}
async makeTone3000Pkce(redirectUrl: string) : Promise<Tone3000PkceParams>
{
if (this.webSocket === undefined)
{
async makeTone3000Pkce(redirectUrl: string): Promise<Tone3000PkceParams> {
if (this.webSocket === undefined) {
throw new Error("Server disconnected.");
}
return await this.webSocket.request<Tone3000PkceParams>(
@@ -719,8 +723,7 @@ export class PiPedalModel //implements PiPedalModel
}
async sha256Base64url(intput: string): Promise<string> {
if (this.webSocket === undefined)
{
if (this.webSocket === undefined) {
throw new Error("Server disconnected.");
}
return this.webSocket.request<string>(
@@ -3946,6 +3949,42 @@ export class PiPedalModel //implements PiPedalModel
});
}
}
private t3kModelSelectionDialogListener?: T3kModelSelectionDialogListener = undefined;
setT3kModelSelectionDialogListener(
listener: T3kModelSelectionDialogListener | undefined
) {
this.t3kModelSelectionDialogListener = listener;
}
showT3kModelSelectionDialog(
downloadType: Tone3000DownloadType,
toneName: string,
models: Model[],
onModelsSelected: (selectedModels: Model[]) => void,
onCancel: () => void
) {
if (!this.t3kModelSelectionDialogListener) {
onCancel();
return;
}
this.tone3000Downloading.set(false);
this.t3kModelSelectionDialogListener.onShowDialog(
{
downloadType: downloadType,
toneName: toneName,
models: models,
onModelsSelected: (selectedModels: Model[]) => {
this.tone3000Downloading.set(true);
onModelsSelected(selectedModels);
},
onCancel: () => {
this.tone3000Downloading.set(false);
onCancel();
}
}
);
}
};
+30
View File
@@ -211,6 +211,28 @@ export class Tone3000DownloadHandler {
this.progress.transferring = false;
this.model.onTone3000DownloadComplete(resultPath);
}
private async maybeSelectModels(toneName: string, models: Model[]): Promise<Model[]> {
let result = new Promise<Model[]>((resolve, reject) => {
if (models.length < 12) {
resolve(models);
return;
}
this.model.showT3kModelSelectionDialog(
this.downloadType,
toneName,
models,
(models: Model[])=> {
resolve(models);
},
() => {
reject(new Error("canceled"));
});
}
);
return result;
}
private async DownloadModelsFromTone3000(
responseUri: string,
tone3000PckceParams: Tone3000PkceParams,
@@ -293,6 +315,14 @@ export class Tone3000DownloadHandler {
}
++page;
}
try {
models = await this.maybeSelectModels(tone.title, models);
} catch (e) {
// canceled.
this.onTone3000DownloadComplete("");
return;
}
let uploadPath = this.downloadPath;
let extension: string;
+1 -1
View File
@@ -20,7 +20,7 @@ export default defineConfig({
target: 'http://localhost:8080',
changeOrigin: false,
},
'^/var/.*': {
'^/var/(?!config\\.json$).*': {
target: 'http://localhost:8080',
changeOrigin: false,
},