diff --git a/.vscode/settings.json b/.vscode/settings.json index fc69f5c..a42c50e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -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": { diff --git a/src/Tone3000Download.cpp.bak b/src/Tone3000Download.cpp.bak index 0d8b722..6d3003c 100644 --- a/src/Tone3000Download.cpp.bak +++ b/src/Tone3000Download.cpp.bak @@ -314,7 +314,7 @@ static std::map gearEnumValues = {"full-rig", "Full rig"}, {"pedal", "Pedal"}, {"outboard", "Outboard"}, - {"ir", "I/R"}}; + {"ir", "IR"}}; static std::string mdGear(const std::string &gear) { diff --git a/src/Tone3000Downloader.cpp b/src/Tone3000Downloader.cpp index 30cf42f..8ceb91c 100644 --- a/src/Tone3000Downloader.cpp +++ b/src/Tone3000Downloader.cpp @@ -713,7 +713,7 @@ static std::map 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 platformEnumValues = { {"nam", "NAM"}, - {"ir", "I/R"}, + {"ir", "IR"}, {"aida-x", "Aida X"}, {"aa-snapshot", "aa-snapshot"}, {"proteus", "Proteus"}}; diff --git a/src/Tone3000Tone.cpp b/src/Tone3000Tone.cpp index a5ef391..e1f55cf 100644 --- a/src/Tone3000Tone.cpp +++ b/src/Tone3000Tone.cpp @@ -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 platformEnumValues = { {"nam", "NAM"}, - {"ir", "I/R"}, + {"ir", "IR"}, {"aida-x", "Aida X"}, {"aa-snapshot", "aa-snapshot"}, {"proteus", "Proteus"}}; diff --git a/vite/CMakeLists.txt b/vite/CMakeLists.txt index 0b746e9..4e525bf 100644 --- a/vite/CMakeLists.txt +++ b/vite/CMakeLists.txt @@ -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 diff --git a/vite/debug.sh b/vite/debug.sh index 418142b..6475b3b 100755 --- a/vite/debug.sh +++ b/vite/debug.sh @@ -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 diff --git a/vite/public/var/config.json.disabled b/vite/public/var/config.json similarity index 100% rename from vite/public/var/config.json.disabled rename to vite/public/var/config.json diff --git a/vite/src/pipedal/DialogEx.tsx b/vite/src/pipedal/DialogEx.tsx index c22a885..4179e78 100644 --- a/vite/src/pipedal/DialogEx.tsx +++ b/vite/src/pipedal/DialogEx.tsx @@ -29,7 +29,7 @@ interface DialogExState { interface DialogExProps extends DialogProps { tag: string; fullwidth?: boolean; - onEnterKey: () => void; + onEnterKey?: () => void; dlgRef?: (instance: HTMLDivElement | null) => void; } diff --git a/vite/src/pipedal/FilePropertyDialog.tsx b/vite/src/pipedal/FilePropertyDialog.tsx index 8ecb77e..d07ff22 100644 --- a/vite/src/pipedal/FilePropertyDialog.tsx +++ b/vite/src/pipedal/FilePropertyDialog.tsx @@ -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,14 +1115,13 @@ 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"); } this.model.showTone3000DownloadPopup( - downloadType, + downloadType, this.state.currentDirectory); } @@ -1147,14 +1161,14 @@ export default withStyles( // } // ); // } - + return true; } render() { const isTracksDirectory = this.isTracksDirectory(); const isToobNamModelFile = this.props.fileProperty.patchProperty === ToobNamModelFileUrl; - const isToobCabIrFile = ToobCabIrFileUrls.includes(this.props.fileProperty.patchProperty); + const isToobCabIrFile = ToobCabIrFileUrls.includes(this.props.fileProperty.patchProperty); const isToobMLModelFile = this.props.fileProperty.patchProperty === ToobMlModelFileUrl; const classes = withStyles.getClasses(this.props); @@ -1726,6 +1740,16 @@ export default withStyles( /> + {this.state.modelSelectionDialogParams && ( + { this.setState({ modelSelectionDialogParams: undefined }); } + } + /> + )} this.setState({ textFileName: undefined })} onT3kLinkClick={(href) => { - return this.handleT3kLinkClick(href); } + return this.handleT3kLinkClick(href); + } } /> )} @@ -1853,7 +1878,7 @@ export default withStyles( // open pdf in new tab this.model.displayMediaFile(fileName); } - + handleShowTextFile(fileName: string) { this.setState({ textFileName: fileName }); } diff --git a/vite/src/pipedal/ModelSelectionDialog.tsx b/vite/src/pipedal/ModelSelectionDialog.tsx new file mode 100644 index 0000000..c2d1019 --- /dev/null +++ b/vite/src/pipedal/ModelSelectionDialog.tsx @@ -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([]); + + 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 ( + { /* Do nothing */ }} + fullWidth={true} maxWidth="md" + fullScreen={windowSize[0].width < 450} + + + > + + + + { handleCancel(); }} + > + + + + {title} + + + + + + + + + + + + + {tableTitle} + + + + + {modelSelection.map((selection) => ( + toggleModel(selection.model.id)} + sx={{ cursor: 'pointer', backgroundColor: 'transparent' }} + > + + { event.stopPropagation(); }} + onChange={() => toggleModel(selection.model.id)} + inputProps={{ 'aria-labelledby': `model-selection-${selection.model.id}` }} + /> + + + + {selection.model.name} + + + + ))} + +
+
+
+ + + + + +
+ ); +} \ No newline at end of file diff --git a/vite/src/pipedal/PiPedalModel.tsx b/vite/src/pipedal/PiPedalModel.tsx index d75a631..6077a36 100644 --- a/vite/src/pipedal/PiPedalModel.tsx +++ b/vite/src/pipedal/PiPedalModel.tsx @@ -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,19 +714,16 @@ export class PiPedalModel //implements PiPedalModel } - async makeTone3000Pkce(redirectUrl: string) : Promise - { - if (this.webSocket === undefined) - { + async makeTone3000Pkce(redirectUrl: string): Promise { + if (this.webSocket === undefined) { throw new Error("Server disconnected."); } return await this.webSocket.request( "makeTone3000Pkce", redirectUrl); } - async sha256Base64url(intput: string) : Promise { - if (this.webSocket === undefined) - { + async sha256Base64url(intput: string): Promise { + if (this.webSocket === undefined) { throw new Error("Server disconnected."); } return this.webSocket.request( @@ -774,7 +777,7 @@ export class PiPedalModel //implements PiPedalModel showTone3000DownloadPopup( downloadType: Tone3000DownloadType, downloadPath: string, - options?: { + options?: { userName?: string; } ): void { @@ -1336,7 +1339,7 @@ export class PiPedalModel //implements PiPedalModel } async writeTone3000Readme( - filePath: string, + filePath: string, tone: Tone, thumbnailUrl: string @@ -3938,14 +3941,50 @@ export class PiPedalModel //implements PiPedalModel downloadType: Tone3000DownloadType ): void { if (this.webSocket) { - this.webSocket.send("DownloadModelsFromTone3000", { - responseUri: responseUri, + this.webSocket.send("DownloadModelsFromTone3000", { + responseUri: responseUri, tone3000PckceParams: tone3000PckceParams, downloadPath: downloadPath, - downloadType: downloadType === Tone3000DownloadType.Nam ? 0: 1 - }); + downloadType: downloadType === Tone3000DownloadType.Nam ? 0 : 1 + }); } } + 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(); + + } + } + ); + } }; diff --git a/vite/src/pipedal/Tone3000Downloader.tsx b/vite/src/pipedal/Tone3000Downloader.tsx index b14bf5a..30e1ecf 100644 --- a/vite/src/pipedal/Tone3000Downloader.tsx +++ b/vite/src/pipedal/Tone3000Downloader.tsx @@ -211,6 +211,28 @@ export class Tone3000DownloadHandler { this.progress.transferring = false; this.model.onTone3000DownloadComplete(resultPath); } + + + private async maybeSelectModels(toneName: string, models: Model[]): Promise { + let result = new Promise((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; diff --git a/vite/vite.config.ts b/vite/vite.config.ts index 1b4b930..d8da56c 100644 --- a/vite/vite.config.ts +++ b/vite/vite.config.ts @@ -20,7 +20,7 @@ export default defineConfig({ target: 'http://localhost:8080', changeOrigin: false, }, - '^/var/.*': { + '^/var/(?!config\\.json$).*': { target: 'http://localhost:8080', changeOrigin: false, },