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
+19
View File
@@ -3038,3 +3038,22 @@ void PiPedalModel::SetPedalboardItemTitle(int64_t instanceId, const std::string
this->SetPresetChanged(-1, true); this->SetPresetChanged(-1, true);
this->FirePedalboardChanged(-1, false); this->FirePedalboardChanged(-1, false);
} }
void PiPedalModel::SetTone3000Auth(const std::string &apiKey)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
storage.SetTone3000Auth(apiKey);
std::vector<IPiPedalModelSubscriber::ptr> t{subscribers.begin(), subscribers.end()};
bool hasAuth = apiKey != "";
for (auto &subscriber : t)
{
subscriber->OnTone3000AuthChanged(hasAuth);
}
}
bool PiPedalModel::HasTone3000Auth() const
{
return storage.GetTone3000Auth() != "";
}
+6
View File
@@ -96,6 +96,7 @@ namespace pipedal
virtual void OnHasWifiChanged(bool hasWifi) = 0; virtual void OnHasWifiChanged(bool hasWifi) = 0;
virtual void OnAlsaSequencerConfigurationChanged(const AlsaSequencerConfiguration &alsaSequencerConfiguration) = 0; virtual void OnAlsaSequencerConfigurationChanged(const AlsaSequencerConfiguration &alsaSequencerConfiguration) = 0;
virtual void Close() = 0; virtual void Close() = 0;
virtual void OnTone3000AuthChanged(bool value) = 0;
}; };
@@ -266,6 +267,7 @@ namespace pipedal
void CheckForResourceInitialization(Pedalboard &pedalboard); void CheckForResourceInitialization(Pedalboard &pedalboard);
UiFileProperty::ptr FindLoadedPatchProperty(int64_t instanceId,const std::string&patchPropertyUri); UiFileProperty::ptr FindLoadedPatchProperty(int64_t instanceId,const std::string&patchPropertyUri);
public: public:
PiPedalModel(); PiPedalModel();
virtual ~PiPedalModel(); virtual ~PiPedalModel();
@@ -479,6 +481,10 @@ namespace pipedal
int32_t to); int32_t to);
void SetPedalboardItemTitle(int64_t instanceId, const std::string &title); void SetPedalboardItemTitle(int64_t instanceId, const std::string &title);
void SetTone3000Auth(const std::string &apiKey);
bool HasTone3000Auth() const;
}; };
} // namespace pipedal. } // namespace pipedal.
+8
View File
@@ -1416,6 +1416,10 @@ public:
pReader->read(&instanceId); pReader->read(&instanceId);
uint64_t result = model.DeleteBank(this->clientId, instanceId); uint64_t result = model.DeleteBank(this->clientId, instanceId);
this->Reply(replyTo, "deleteBankItem", result); this->Reply(replyTo, "deleteBankItem", result);
} else if (message == "getHasTone3000Auth")
{
bool result = model.HasTone3000Auth();
this->Reply(replyTo, "getHasTone3000Auth", result);
} }
else if (message == "renameBank") else if (message == "renameBank")
{ {
@@ -1863,6 +1867,10 @@ private:
{ {
} }
} }
virtual void OnTone3000AuthChanged(bool value)
{
Send("onTone3000AuthChanged", value);
}
virtual void OnErrorMessage(const std::string &message) virtual void OnErrorMessage(const std::string &message)
{ {
+72 -23
View File
@@ -254,6 +254,7 @@ void Storage::Initialize()
LoadPluginPresetIndex(); LoadPluginPresetIndex();
LoadBankIndex(); LoadBankIndex();
LoadCurrentBank(); LoadCurrentBank();
LoadTone3000Auth();
try try
{ {
LoadChannelSelection(); LoadChannelSelection();
@@ -311,6 +312,11 @@ std::filesystem::path Storage::GetCurrentPresetPath() const
return this->dataRoot / "currentPreset.json"; return this->dataRoot / "currentPreset.json";
} }
std::filesystem::path Storage::GetTone3000AuthPath() const
{
return this->dataRoot / "tone3000.json";
}
std::filesystem::path Storage::GetChannelSelectionFileName() std::filesystem::path Storage::GetChannelSelectionFileName()
{ {
return this->dataRoot / "JackChannelSelection.json"; return this->dataRoot / "JackChannelSelection.json";
@@ -741,34 +747,35 @@ JackChannelSelection Storage::GetJackChannelSelection(const JackConfiguration &j
return jackChannelSelection.RemoveInvalidChannels(jackConfiguration); return jackChannelSelection.RemoveInvalidChannels(jackConfiguration);
} }
static AlsaSequencerConfiguration MigrateRawMidiToAlsaSequencer(std::vector<AlsaMidiDeviceInfo> &selectedDevices) static AlsaSequencerConfiguration MigrateRawMidiToAlsaSequencer(std::vector<AlsaMidiDeviceInfo> &selectedDevices)
{ {
AlsaSequencerConfiguration result; AlsaSequencerConfiguration result;
try { try
auto sequencerPorts = AlsaSequencer::EnumeratePorts(); {
auto sequencerPorts = AlsaSequencer::EnumeratePorts();
// Prepare Migrate raw MIDI devices to ALSA sequencer ports. // Prepare Migrate raw MIDI devices to ALSA sequencer ports.
for (auto i = selectedDevices.begin(); i != selectedDevices.end(); ++i) for (auto i = selectedDevices.begin(); i != selectedDevices.end(); ++i)
{ {
if (i->name_.starts_with("hw:")) { if (i->name_.starts_with("hw:"))
for (const auto &port: sequencerPorts) {
for (const auto &port : sequencerPorts)
{ {
if (i->name_ == port.rawMidiDevice) if (i->name_ == port.rawMidiDevice)
{ {
result.connections().push_back( result.connections().push_back(
AlsaSequencerPortSelection(port.id, port.name,port.displaySortOrder) AlsaSequencerPortSelection(port.id, port.name, port.displaySortOrder));
);
break; break;
} }
} }
} }
} }
} catch (const std::exception&e) { }
catch (const std::exception &e)
{
Lv2Log::error(SS("Failed to migrate MIDI settings. " << e.what())); Lv2Log::error(SS("Failed to migrate MIDI settings. " << e.what()));
// ick. // ick.
} }
return result; return result;
} }
@@ -790,23 +797,25 @@ void Storage::LoadAlsaSequencerConfiguration()
{ {
Lv2Log::error("I/O error reading %s: %s", fileName.c_str(), e.what()); Lv2Log::error("I/O error reading %s: %s", fileName.c_str(), e.what());
} }
} else { }
else
{
// migrate legacy settings from JackConfiguration? // migrate legacy settings from JackConfiguration?
if (this->isJackChannelSelectionValid) if (this->isJackChannelSelectionValid)
{ {
this->alsaSequencerConfiguration = this->alsaSequencerConfiguration =
MigrateRawMidiToAlsaSequencer( MigrateRawMidiToAlsaSequencer(
this->jackChannelSelection.LegacyGetInputMidiDevices()); this->jackChannelSelection.LegacyGetInputMidiDevices());
this->jackChannelSelection.LegacyGetInputMidiDevices().clear(); // let them be clear, the next it gets updated. this->jackChannelSelection.LegacyGetInputMidiDevices().clear(); // let them be clear, the next it gets updated.
}
} else { else
{
// no legacy settings, so just create a default configuration. // no legacy settings, so just create a default configuration.
this->alsaSequencerConfiguration = AlsaSequencerConfiguration(); this->alsaSequencerConfiguration = AlsaSequencerConfiguration();
} }
SaveAlsaSequencerConfiguration(); SaveAlsaSequencerConfiguration();
} }
} }
void Storage::SaveAlsaSequencerConfiguration() void Storage::SaveAlsaSequencerConfiguration()
{ {
@@ -814,7 +823,7 @@ void Storage::SaveAlsaSequencerConfiguration()
try try
{ {
pipedal::ofstream_synced s(fileName); pipedal::ofstream_synced s(fileName);
json_writer writer(s); json_writer writer(s);
writer.write(this->alsaSequencerConfiguration); writer.write(this->alsaSequencerConfiguration);
} }
@@ -822,7 +831,6 @@ void Storage::SaveAlsaSequencerConfiguration()
{ {
Lv2Log::error("I/O error writing %s: %s", fileName.c_str(), e.what()); Lv2Log::error("I/O error writing %s: %s", fileName.c_str(), e.what());
} }
} }
void Storage::SetAlsaSequencerConfiguration(const AlsaSequencerConfiguration &alsaSequencerConfiguration) void Storage::SetAlsaSequencerConfiguration(const AlsaSequencerConfiguration &alsaSequencerConfiguration)
@@ -836,7 +844,6 @@ AlsaSequencerConfiguration Storage::GetAlsaSequencerConfiguration() const
return this->alsaSequencerConfiguration; return this->alsaSequencerConfiguration;
} }
void Storage::LoadChannelSelection() void Storage::LoadChannelSelection()
{ {
auto fileName = this->GetChannelSelectionFileName(); auto fileName = this->GetChannelSelectionFileName();
@@ -861,7 +868,7 @@ void Storage::SaveChannelSelection()
try try
{ {
// replaced with AlsaSequencerConfiguration. Delete legacy data. // replaced with AlsaSequencerConfiguration. Delete legacy data.
this->jackChannelSelection.LegacyGetInputMidiDevices().clear(); this->jackChannelSelection.LegacyGetInputMidiDevices().clear();
pipedal::ofstream_synced s(fileName); pipedal::ofstream_synced s(fileName);
json_writer writer(s, false); json_writer writer(s, false);
writer.write(this->jackChannelSelection); writer.write(this->jackChannelSelection);
@@ -1849,12 +1856,14 @@ static void AddTracksToResult(
} }
const auto &path = dir_entry.path(); const auto &path = dir_entry.path();
auto name = path.filename().string(); auto name = path.filename().string();
try { try
{
if (dir_entry.is_directory()) if (dir_entry.is_directory())
{ {
resultFiles.push_back(FileEntry{path, name, true, dir_entry.is_symlink()}); resultFiles.push_back(FileEntry{path, name, true, dir_entry.is_symlink()});
} }
} catch (const std::exception &e) }
catch (const std::exception &e)
{ {
Lv2Log::warning(SS("Failed to add directory entry: " << path.string() << " - " << e.what())); Lv2Log::warning(SS("Failed to add directory entry: " << path.string() << " - " << e.what()));
} }
@@ -1903,7 +1912,6 @@ static void AddTracksToResult(
throw std::logic_error( throw std::logic_error(
SS("AddTracksToResult failed to enumerate audio files. " << rootPath.string() << " - " << error.what())); SS("AddTracksToResult failed to enumerate audio files. " << rootPath.string() << " - " << error.what()));
} }
} }
catch (const std::exception &error) catch (const std::exception &error)
{ {
@@ -2602,6 +2610,47 @@ const PluginPresetIndex &Storage::GetPluginPresetIndex()
return pluginPresetIndex; return pluginPresetIndex;
} }
void Storage::LoadTone3000Auth()
{
fs::path path = GetTone3000AuthPath();
try
{
if (!fs::exists(path))
{
this->tone3000Auth = "";
return;
}
std::ifstream s(path);
json_reader reader(s);
reader.read(&(this->tone3000Auth));
}
catch (const std::exception &e)
{
Lv2Log::error("Failed to load tone3000Auth: %s", e.what());
}
}
void Storage::SetTone3000Auth(const std::string &apiKey)
{
if (tone3000Auth != apiKey)
{
tone3000Auth = apiKey;
pipedal::ofstream_synced os(this->GetTone3000AuthPath());
if (!os.is_open())
{
Lv2Log::error("Failed to open Tone3000 auth file for writing.");
return;
}
json_writer writer(os);
writer.write(apiKey);
}
}
std::string Storage::GetTone3000Auth() const
{
return tone3000Auth;
}
JSON_MAP_BEGIN(UserSettings) JSON_MAP_BEGIN(UserSettings)
JSON_MAP_REFERENCE(UserSettings, governor) JSON_MAP_REFERENCE(UserSettings, governor)
JSON_MAP_REFERENCE(UserSettings, showStatusMonitor) JSON_MAP_REFERENCE(UserSettings, showStatusMonitor)
+6
View File
@@ -73,6 +73,7 @@ private:
BankIndex bankIndex; BankIndex bankIndex;
BankFile currentBank; BankFile currentBank;
PluginPresetIndex pluginPresetIndex; PluginPresetIndex pluginPresetIndex;
std::string tone3000Auth;
private: private:
void FillSampleDirectoryTree(FilePropertyDirectoryTree*node, const std::filesystem::path&directory) const; void FillSampleDirectoryTree(FilePropertyDirectoryTree*node, const std::filesystem::path&directory) const;
@@ -87,6 +88,7 @@ private:
std::filesystem::path GetChannelSelectionFileName(); std::filesystem::path GetChannelSelectionFileName();
std::filesystem::path GetAlsaSequencerConfigurationFileName(); std::filesystem::path GetAlsaSequencerConfigurationFileName();
std::filesystem::path GetCurrentPresetPath() const; std::filesystem::path GetCurrentPresetPath() const;
std::filesystem::path GetTone3000AuthPath() const;
void LoadBankIndex(); void LoadBankIndex();
void SaveBankIndex(); void SaveBankIndex();
@@ -254,6 +256,10 @@ public:
const UiFileProperty&uiFileProperty, const UiFileProperty&uiFileProperty,
bool overwrite = false); bool overwrite = false);
FilePropertyDirectoryTree::ptr GetFilePropertydirectoryTree(const UiFileProperty&uiFileProperty,const std::filesystem::path&selectedPath); FilePropertyDirectoryTree::ptr GetFilePropertydirectoryTree(const UiFileProperty&uiFileProperty,const std::filesystem::path&selectedPath);
void LoadTone3000Auth();
void SetTone3000Auth(const std::string&apiKey);
std::string GetTone3000Auth() const;
}; };
+15 -1
View File
@@ -188,6 +188,10 @@ public:
{ {
return true; return true;
} }
else if (segment == "Tone3000Auth")
{
return true;
}
else if (segment == "PluginPresets") else if (segment == "PluginPresets")
{ {
return true; return true;
@@ -734,7 +738,17 @@ public:
{ {
std::string segment = request_uri.segment(1); std::string segment = request_uri.segment(1);
if (segment == "uploadPluginPresets") if (segment == "Tone3000Auth") {
// https://www.tone3000.com/api/v1/auth?redirect_url=http://10.0.0.151:8080/var/Tone3000Auth&otp_only=true
std::string apiKey = request_uri.query("api_key");
model->SetTone3000Auth(apiKey);
res.set(HttpField::content_type, "application/json");
res.set(HttpField::cache_control, "no-cache");
res.setBody("\"OK\"");
} else if (segment == "uploadPluginPresets")
{ {
PluginPresets presets; PluginPresets presets;
fs::path filePath = req.get_body_temporary_file(); fs::path filePath = req.get_body_temporary_file();
+2
View File
@@ -1,3 +1,5 @@
Exctly one underrun per seek in Toob Player
Test autohotspot detection. Test autohotspot detection.
+13 -2
View File
@@ -24,6 +24,8 @@ import CssBaseline from '@mui/material/CssBaseline';
import VirtualKeyboardHandler from './VirtualKeyboardHandler'; import VirtualKeyboardHandler from './VirtualKeyboardHandler';
import AppThemed from "./AppThemed"; import AppThemed from "./AppThemed";
import { isDarkMode } from './DarkMode'; import { isDarkMode } from './DarkMode';
import Tone3000AuthComplete from './Tone3000AuthComplete';
declare module '@mui/material/styles' { declare module '@mui/material/styles' {
interface Theme { 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 { const App = (class extends React.Component {
// Before the component mounts, we initialise our state // Before the component mounts, we initialise our state
@@ -240,8 +248,11 @@ const App = (class extends React.Component {
<StyledEngineProvider injectFirst> <StyledEngineProvider injectFirst>
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<CssBaseline /> <CssBaseline />
{isTone3000Auth() ? (
<AppThemed /> <Tone3000AuthComplete />
) : (
<AppThemed />
)}
</ThemeProvider> </ThemeProvider>
</StyledEngineProvider> </StyledEngineProvider>
); );
+98 -41
View File
@@ -21,6 +21,10 @@
import React from 'react'; import React from 'react';
import { createStyles } from './WithStyles'; 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 DraggableButtonBase from './DraggableButtonBase';
import CloseIcon from '@mui/icons-material/Close'; import CloseIcon from '@mui/icons-material/Close';
import { Theme } from '@mui/material/styles'; import { Theme } from '@mui/material/styles';
@@ -64,6 +68,8 @@ import FilePropertyDirectorySelectDialog from './FilePropertyDirectorySelectDial
import { getAlbumArtUri, getTrackTitle } from './AudioFileMetadata'; import { getAlbumArtUri, getTrackTitle } from './AudioFileMetadata';
const ToobNamModelFileUrl = "http://two-play.com/plugins/toob-nam#modelFile";
const AUTOSCROLL_TICK_DELAY = 30; const AUTOSCROLL_TICK_DELAY = 30;
const AUTOSCROLL_THRESHOLD = 48; const AUTOSCROLL_THRESHOLD = 48;
const AUTOSCROLL_SCROLL_PER_SECOND = 500; const AUTOSCROLL_SCROLL_PER_SECOND = 500;
@@ -161,6 +167,8 @@ export interface FilePropertyDialogState {
initialSelection: string; initialSelection: string;
multiSelect: boolean, multiSelect: boolean,
selectedFiles: string[], selectedFiles: string[],
//openTone3000Dialog: boolean,
openTone3000Help: boolean
}; };
@@ -231,7 +239,9 @@ export default withStyles(
copyDialogOpen: false, copyDialogOpen: false,
initialSelection: this.props.selectedFile, initialSelection: this.props.selectedFile,
multiSelect: false, multiSelect: false,
selectedFiles: [] selectedFiles: [],
//openTone3000Dialog: false,
openTone3000Help: false
}; };
this.requestScroll = true; this.requestScroll = true;
} }
@@ -362,7 +372,7 @@ export default withStyles(
dragElementDy: 0, dragElementDy: 0,
from: index, from: index,
to: index, to: index,
lastMousePoint: {...this.longPressStartPoint} lastMousePoint: { ...this.longPressStartPoint }
}; };
this.setState({ dragState: dragState }); 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" }}> style={{ marginLeft: 16, marginBottom: 8, marginRight: 8, textOverflow: "", display: "flex", flexFlow: "row wrap", alignItems: "center", justifyContent: "start" }}>
{breadcrumbs} {breadcrumbs}
</div> </div>
<Divider />
</div> </div>
); );
} }
@@ -998,8 +1007,7 @@ export default withStyles(
let dragElementDy = dragState.lastMousePoint.y - this.longPressStartPoint.y; let dragElementDy = dragState.lastMousePoint.y - this.longPressStartPoint.y;
let originalY = dragState.from * dragState.height; let originalY = dragState.from * dragState.height;
let newY = originalY + dragElementDy; let newY = originalY + dragElementDy;
if (newY < 0) if (newY < 0) {
{
newY = 0; newY = 0;
dragElementDy = -originalY; dragElementDy = -originalY;
} }
@@ -1040,9 +1048,26 @@ export default withStyles(
() => { this.handleAutoScrollTick() }, AUTOSCROLL_TICK_DELAY); () => { 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() { render() {
const isTracksDirectory = this.isTracksDirectory(); const isTracksDirectory = this.isTracksDirectory();
const isToobNamModelFile = this.props.fileProperty.patchProperty === ToobNamModelFileUrl;
const classes = withStyles.getClasses(this.props); const classes = withStyles.getClasses(this.props);
let columnWidth = this.state.columnWidth; let columnWidth = this.state.columnWidth;
@@ -1102,7 +1127,7 @@ export default withStyles(
background: this.state.reordering || this.state.multiSelect ? background: this.state.reordering || this.state.multiSelect ?
(isDarkMode() ? "#523" : "#EDF") (isDarkMode() ? "#523" : "#EDF")
: undefined, : undefined,
marginBottom: 16, paddingTop: 0 marginBottom: 8, paddingTop: 0
}} > }} >
{this.state.reordering && ( {this.state.reordering && (
<Toolbar style={{ padding: 0 }}> <Toolbar style={{ padding: 0 }}>
@@ -1257,7 +1282,7 @@ export default withStyles(
</div> </div>
</DialogTitle> </DialogTitle>
<DialogContent style={{ <DialogContent dividers style={{
paddingLeft: 0, paddingRight: 0, paddingTop: 0, colorScheme: (isDarkMode() ? "dark" : "light"), paddingLeft: 0, paddingRight: 0, paddingTop: 0, colorScheme: (isDarkMode() ? "dark" : "light"),
position: "relative" position: "relative"
}} }}
@@ -1284,7 +1309,8 @@ export default withStyles(
ref={(element) => { this.listContainerElementRef = element; this.onMeasureRef(element); }} ref={(element) => { this.listContainerElementRef = element; this.onMeasureRef(element); }}
style={{ style={{
flex: "1 1 100%", display: "flex", flexFlow: "row wrap", 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. (this.state.columns !== 0) && // don't render until we have number of columns derived from layout.
@@ -1461,45 +1487,64 @@ export default withStyles(
</DialogContent> </DialogContent>
{(!this.state.reordering && !this.state.multiSelect) && ( {(!this.state.reordering && !this.state.multiSelect) && (
<> <>
<Divider />
<DialogActions style={{ justifyContent: "stretch" }}> <DialogActions style={{ justifyContent: "stretch" }}>
{this.state.windowWidth > 500 ? ( {this.state.windowWidth > 500 ? (
<div style={{ display: "flex", width: "100%", alignItems: "center", flexFlow: "row nowrap" }}> <div style={{ display: "flex", width: "100%", alignItems: "center", flexFlow: "column nowrap" }}>
<IconButtonEx style={{ visibility: (this.state.hasSelection ? "visible" : "hidden") }} aria-label="delete" component="label" color="primary" {isToobNamModelFile && (
tooltip="Delete selected file or folder" <div style={{
disabled={!this.state.hasSelection || this.state.selectedFile === "" || protectedItem} display: "flex", flexFlow: "row nowrap", justifyContent: "center",
onClick={() => this.handleDelete()} > alignItems: "center", width: "100%"
<OldDeleteIcon fontSize='small' /> }}>
</IconButtonEx> <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 />} <div style={{ flex: "1 1 100%", display: "flex", width: "100%", alignItems: "center", flexFlow: "row nowrap" }}>
onClick={() => { this.handleDownloadFile(); }} disabled={protectedDirectory || !this.state.hasFileSelection} <IconButtonEx style={{ visibility: (this.state.hasSelection ? "visible" : "hidden") }} aria-label="delete" component="label" color="primary"
> tooltip="Delete selected file or folder"
<div>Download</div> disabled={!this.state.hasSelection || this.state.selectedFile === "" || protectedItem}
</ButtonEx> 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={() => { <ButtonEx tooltip="Download file" style={{ flex: "0 0 auto" }} aria-label="upload" variant="text" startIcon={<FileDownloadIcon />}
this.props.onApply(this.props.fileProperty, this.state.initialSelection); onClick={() => { this.handleDownloadFile(); }} disabled={protectedDirectory || !this.state.hasFileSelection}
this.props.onCancel(); >
}} aria-label="cancel"> <div>Download</div>
Cancel </ButtonEx>
</Button>
<Button variant="dialogPrimary" style={{ flex: "0 0 auto" }}
onClick={() => { this.openSelectedFile(); }}
disabled={(!canSelectFile) <div style={{ flex: "1 1 auto" }}>&nbsp;</div>
} aria-label="select"
> <Button variant="dialogSecondary" onClick={() => {
{okButtonText} this.props.onApply(this.props.fileProperty, this.state.initialSelection);
</Button> 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>
) : ( ) : (
<div style={{ width: "100%" }}> <div style={{ width: "100%" }}>
@@ -1607,7 +1652,7 @@ export default withStyles(
{ {
this.state.renameDialogOpen && ( this.state.renameDialogOpen && (
<RenameDialog open={this.state.renameDialogOpen} defaultName={this.renameDefaultName()} <RenameDialog open={this.state.renameDialogOpen} defaultName={this.renameDefaultName()}
title="Rename" title="Rename"
onOk={(newName) => { this.setState({ renameDialogOpen: false }); this.onExecuteRename(newName) }} onOk={(newName) => { this.setState({ renameDialogOpen: false }); this.onExecuteRename(newName) }}
onClose={() => { this.setState({ renameDialogOpen: false }); }} onClose={() => { this.setState({ renameDialogOpen: false }); }}
acceptActionName="OK" 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> </DialogEx>
); );
} }
+7
View File
@@ -420,6 +420,8 @@ export class PiPedalModel //implements PiPedalModel
webSocket?: PiPedalSocket; webSocket?: PiPedalSocket;
hasTone3000Auth: ObservableProperty<boolean> = new ObservableProperty<boolean>(false);
hasWifiDevice: ObservableProperty<boolean> = new ObservableProperty<boolean>(false); hasWifiDevice: ObservableProperty<boolean> = new ObservableProperty<boolean>(false);
onSnapshotModified: ObservableEvent<SnapshotModifiedEvent> = new ObservableEvent<SnapshotModifiedEvent>(); onSnapshotModified: ObservableEvent<SnapshotModifiedEvent> = new ObservableEvent<SnapshotModifiedEvent>();
@@ -760,6 +762,8 @@ export class PiPedalModel //implements PiPedalModel
} else if (message === "onErrorMessage") { } else if (message === "onErrorMessage") {
this.showAlert(body as string); this.showAlert(body as string);
} else if (message == "onTone3000AuthChanged") {
this.hasTone3000Auth.set(body as boolean);
} }
else if (message === "onLv2PluginsChanging") { else if (message === "onLv2PluginsChanging") {
this.onLv2PluginsChanging(); this.onLv2PluginsChanging();
@@ -1142,6 +1146,9 @@ export class PiPedalModel //implements PiPedalModel
this.alsaSequencerConfiguration.set(new AlsaSequencerConfiguration().deserialize( this.alsaSequencerConfiguration.set(new AlsaSequencerConfiguration().deserialize(
await this.getWebSocket().request<any>("getAlsaSequencerConfiguration") 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.banks.set(new BankIndex().deserialize(await this.getWebSocket().request<any>("getBankIndex")));
this.favorites.set(await this.getWebSocket().request<FavoritesList>("getFavorites")); 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;