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:
@@ -3038,3 +3038,22 @@ void PiPedalModel::SetPedalboardItemTitle(int64_t instanceId, const std::string
|
||||
this->SetPresetChanged(-1, true);
|
||||
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() != "";
|
||||
}
|
||||
|
||||
|
||||
@@ -96,6 +96,7 @@ namespace pipedal
|
||||
virtual void OnHasWifiChanged(bool hasWifi) = 0;
|
||||
virtual void OnAlsaSequencerConfigurationChanged(const AlsaSequencerConfiguration &alsaSequencerConfiguration) = 0;
|
||||
virtual void Close() = 0;
|
||||
virtual void OnTone3000AuthChanged(bool value) = 0;
|
||||
|
||||
};
|
||||
|
||||
@@ -266,6 +267,7 @@ namespace pipedal
|
||||
void CheckForResourceInitialization(Pedalboard &pedalboard);
|
||||
UiFileProperty::ptr FindLoadedPatchProperty(int64_t instanceId,const std::string&patchPropertyUri);
|
||||
|
||||
|
||||
public:
|
||||
PiPedalModel();
|
||||
virtual ~PiPedalModel();
|
||||
@@ -479,6 +481,10 @@ namespace pipedal
|
||||
int32_t to);
|
||||
|
||||
void SetPedalboardItemTitle(int64_t instanceId, const std::string &title);
|
||||
|
||||
void SetTone3000Auth(const std::string &apiKey);
|
||||
bool HasTone3000Auth() const;
|
||||
|
||||
};
|
||||
|
||||
} // namespace pipedal.
|
||||
@@ -1416,6 +1416,10 @@ public:
|
||||
pReader->read(&instanceId);
|
||||
uint64_t result = model.DeleteBank(this->clientId, instanceId);
|
||||
this->Reply(replyTo, "deleteBankItem", result);
|
||||
} else if (message == "getHasTone3000Auth")
|
||||
{
|
||||
bool result = model.HasTone3000Auth();
|
||||
this->Reply(replyTo, "getHasTone3000Auth", result);
|
||||
}
|
||||
else if (message == "renameBank")
|
||||
{
|
||||
@@ -1863,6 +1867,10 @@ private:
|
||||
{
|
||||
}
|
||||
}
|
||||
virtual void OnTone3000AuthChanged(bool value)
|
||||
{
|
||||
Send("onTone3000AuthChanged", value);
|
||||
}
|
||||
|
||||
virtual void OnErrorMessage(const std::string &message)
|
||||
{
|
||||
|
||||
+72
-23
@@ -254,6 +254,7 @@ void Storage::Initialize()
|
||||
LoadPluginPresetIndex();
|
||||
LoadBankIndex();
|
||||
LoadCurrentBank();
|
||||
LoadTone3000Auth();
|
||||
try
|
||||
{
|
||||
LoadChannelSelection();
|
||||
@@ -311,6 +312,11 @@ std::filesystem::path Storage::GetCurrentPresetPath() const
|
||||
return this->dataRoot / "currentPreset.json";
|
||||
}
|
||||
|
||||
std::filesystem::path Storage::GetTone3000AuthPath() const
|
||||
{
|
||||
return this->dataRoot / "tone3000.json";
|
||||
}
|
||||
|
||||
std::filesystem::path Storage::GetChannelSelectionFileName()
|
||||
{
|
||||
return this->dataRoot / "JackChannelSelection.json";
|
||||
@@ -741,34 +747,35 @@ JackChannelSelection Storage::GetJackChannelSelection(const JackConfiguration &j
|
||||
return jackChannelSelection.RemoveInvalidChannels(jackConfiguration);
|
||||
}
|
||||
|
||||
|
||||
static AlsaSequencerConfiguration MigrateRawMidiToAlsaSequencer(std::vector<AlsaMidiDeviceInfo> &selectedDevices)
|
||||
{
|
||||
AlsaSequencerConfiguration result;
|
||||
try {
|
||||
auto sequencerPorts = AlsaSequencer::EnumeratePorts();
|
||||
try
|
||||
{
|
||||
auto sequencerPorts = AlsaSequencer::EnumeratePorts();
|
||||
|
||||
// Prepare Migrate raw MIDI devices to ALSA sequencer ports.
|
||||
|
||||
for (auto i = selectedDevices.begin(); i != selectedDevices.end(); ++i)
|
||||
{
|
||||
if (i->name_.starts_with("hw:")) {
|
||||
for (const auto &port: sequencerPorts)
|
||||
if (i->name_.starts_with("hw:"))
|
||||
{
|
||||
for (const auto &port : sequencerPorts)
|
||||
{
|
||||
if (i->name_ == port.rawMidiDevice)
|
||||
{
|
||||
result.connections().push_back(
|
||||
AlsaSequencerPortSelection(port.id, port.name,port.displaySortOrder)
|
||||
);
|
||||
AlsaSequencerPortSelection(port.id, port.name, port.displaySortOrder));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (const std::exception&e) {
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
Lv2Log::error(SS("Failed to migrate MIDI settings. " << e.what()));
|
||||
// ick.
|
||||
|
||||
// ick.
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -790,23 +797,25 @@ void Storage::LoadAlsaSequencerConfiguration()
|
||||
{
|
||||
Lv2Log::error("I/O error reading %s: %s", fileName.c_str(), e.what());
|
||||
}
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
// migrate legacy settings from JackConfiguration?
|
||||
if (this->isJackChannelSelectionValid)
|
||||
if (this->isJackChannelSelectionValid)
|
||||
{
|
||||
this->alsaSequencerConfiguration =
|
||||
this->alsaSequencerConfiguration =
|
||||
MigrateRawMidiToAlsaSequencer(
|
||||
this->jackChannelSelection.LegacyGetInputMidiDevices());
|
||||
this->jackChannelSelection.LegacyGetInputMidiDevices().clear(); // let them be clear, the next it gets updated.
|
||||
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
// no legacy settings, so just create a default configuration.
|
||||
this->alsaSequencerConfiguration = AlsaSequencerConfiguration();
|
||||
}
|
||||
SaveAlsaSequencerConfiguration();
|
||||
}
|
||||
|
||||
}
|
||||
void Storage::SaveAlsaSequencerConfiguration()
|
||||
{
|
||||
@@ -814,7 +823,7 @@ void Storage::SaveAlsaSequencerConfiguration()
|
||||
|
||||
try
|
||||
{
|
||||
pipedal::ofstream_synced s(fileName);
|
||||
pipedal::ofstream_synced s(fileName);
|
||||
json_writer writer(s);
|
||||
writer.write(this->alsaSequencerConfiguration);
|
||||
}
|
||||
@@ -822,7 +831,6 @@ void Storage::SaveAlsaSequencerConfiguration()
|
||||
{
|
||||
Lv2Log::error("I/O error writing %s: %s", fileName.c_str(), e.what());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void Storage::SetAlsaSequencerConfiguration(const AlsaSequencerConfiguration &alsaSequencerConfiguration)
|
||||
@@ -836,7 +844,6 @@ AlsaSequencerConfiguration Storage::GetAlsaSequencerConfiguration() const
|
||||
return this->alsaSequencerConfiguration;
|
||||
}
|
||||
|
||||
|
||||
void Storage::LoadChannelSelection()
|
||||
{
|
||||
auto fileName = this->GetChannelSelectionFileName();
|
||||
@@ -861,7 +868,7 @@ void Storage::SaveChannelSelection()
|
||||
try
|
||||
{
|
||||
// replaced with AlsaSequencerConfiguration. Delete legacy data.
|
||||
this->jackChannelSelection.LegacyGetInputMidiDevices().clear();
|
||||
this->jackChannelSelection.LegacyGetInputMidiDevices().clear();
|
||||
pipedal::ofstream_synced s(fileName);
|
||||
json_writer writer(s, false);
|
||||
writer.write(this->jackChannelSelection);
|
||||
@@ -1849,12 +1856,14 @@ static void AddTracksToResult(
|
||||
}
|
||||
const auto &path = dir_entry.path();
|
||||
auto name = path.filename().string();
|
||||
try {
|
||||
try
|
||||
{
|
||||
if (dir_entry.is_directory())
|
||||
{
|
||||
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()));
|
||||
}
|
||||
@@ -1903,7 +1912,6 @@ static void AddTracksToResult(
|
||||
throw std::logic_error(
|
||||
SS("AddTracksToResult failed to enumerate audio files. " << rootPath.string() << " - " << error.what()));
|
||||
}
|
||||
|
||||
}
|
||||
catch (const std::exception &error)
|
||||
{
|
||||
@@ -2602,6 +2610,47 @@ const PluginPresetIndex &Storage::GetPluginPresetIndex()
|
||||
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_REFERENCE(UserSettings, governor)
|
||||
JSON_MAP_REFERENCE(UserSettings, showStatusMonitor)
|
||||
|
||||
@@ -73,6 +73,7 @@ private:
|
||||
BankIndex bankIndex;
|
||||
BankFile currentBank;
|
||||
PluginPresetIndex pluginPresetIndex;
|
||||
std::string tone3000Auth;
|
||||
|
||||
private:
|
||||
void FillSampleDirectoryTree(FilePropertyDirectoryTree*node, const std::filesystem::path&directory) const;
|
||||
@@ -87,6 +88,7 @@ private:
|
||||
std::filesystem::path GetChannelSelectionFileName();
|
||||
std::filesystem::path GetAlsaSequencerConfigurationFileName();
|
||||
std::filesystem::path GetCurrentPresetPath() const;
|
||||
std::filesystem::path GetTone3000AuthPath() const;
|
||||
|
||||
void LoadBankIndex();
|
||||
void SaveBankIndex();
|
||||
@@ -254,6 +256,10 @@ public:
|
||||
const UiFileProperty&uiFileProperty,
|
||||
bool overwrite = false);
|
||||
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
@@ -188,6 +188,10 @@ public:
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else if (segment == "Tone3000Auth")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else if (segment == "PluginPresets")
|
||||
{
|
||||
return true;
|
||||
@@ -734,7 +738,17 @@ public:
|
||||
{
|
||||
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;
|
||||
fs::path filePath = req.get_body_temporary_file();
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
Exctly one underrun per seek in Toob Player
|
||||
|
||||
Test autohotspot detection.
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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" }}> </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" }}> </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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user