\n"
<< " \n"
<< " \n"
@@ -566,14 +636,28 @@ static void writeReadme(const fs::path &path, const Tone3000Download &download)
}
}
-void pipedal::tone3000::DownloadTone3000Bundle(
+std::string pipedal::tone3000::DownloadTone3000Bundle(
const std::filesystem::path &destinationFolder,
- const std::string &downloadUrl)
+ const std::string &downloadUrl,
+ int64_t handle,
+ std::function progressCallback,
+ std::function isCancelled)
{
TemporaryFile downloadTemporaryFile{WEB_TEMP_DIR};
fs::path downloadPath = downloadTemporaryFile.Path();
- downloadFile(downloadUrl, downloadPath);
+ std::string resultDirectory;
+ Tone3000DownloadProgress progress;
+ progress.handle(handle);
+ progressCallback(progress);
+
+ downloadFile(downloadUrl, downloadPath, isCancelled);
+
+ if (isCancelled())
+ {
+
+ return resultDirectory;
+ }
std::ifstream f{downloadPath};
if (!f.is_open())
@@ -585,21 +669,48 @@ void pipedal::tone3000::DownloadTone3000Bundle(
Tone3000Download tone3000Download;
reader.read(&tone3000Download);
+ if (isCancelled())
+ {
+ return resultDirectory;
+ }
+ progress.title(tone3000Download.title());
+ progress.total(tone3000Download.models().size());
+ progressCallback(progress);
+
if (tone3000Download.platform() != "nam")
{
throw std::runtime_error(SS("Unexpected file format. (platform=\"" << tone3000Download.platform() << "\")"));
}
fs::path bundlePath = destinationFolder / stringToSafeFilename(tone3000Download.title());
+ resultDirectory = bundlePath;
fs::create_directories(bundlePath);
-
+ uint64_t nModel = 0;
for (auto &model : tone3000Download.models())
{
+ if (isCancelled())
+ {
+ return resultDirectory;
+ }
fs::path modelPath = bundlePath / SS(stringToSafeFilename(model.name()) << ".nam");
- downloadFile(model.model_url(), modelPath);
+ downloadFile(model.model_url(), modelPath, isCancelled);
+ if (isCancelled())
+ {
+ return resultDirectory;
+ }
+
+ progress.progress(++nModel);
+ progressCallback(progress);
+ }
+ if (isCancelled())
+ {
+ return resultDirectory;
}
fs::path readmePath = bundlePath / "README.md";
writeReadme(readmePath, tone3000Download);
+
+ return resultDirectory;
+
}
Tone3000DownloadResult::Tone3000DownloadResult()
diff --git a/src/Tone3000Download.hpp b/src/Tone3000Download.hpp
index 822f529..6cf2a25 100644
--- a/src/Tone3000Download.hpp
+++ b/src/Tone3000Download.hpp
@@ -25,9 +25,12 @@
#include
#include
#include
+#include
#include "json.hpp"
#include
#include
+#include
+#include "Tone3000DownloadProgress.hpp"
namespace pipedal
{
@@ -59,7 +62,7 @@ namespace pipedal
{
private:
std::string id_;
- std::string avatar_url_;
+ std::optional avatar_url_;
std::string username_;
std::string url_;
@@ -128,10 +131,13 @@ namespace pipedal
DECLARE_JSON_MAP(Tone3000DownloadResult);
};
- extern void DownloadTone3000Bundle
+ extern std::string DownloadTone3000Bundle
(
const std::filesystem::path&destinationFolder,
- const std::string &downloadUrl
+ const std::string &downloadUrl,
+ int64_t downloadHandle,
+ std::function progressCallback,
+ std::function isCancelled
);
}
diff --git a/src/Tone3000DownloadProgress.cpp b/src/Tone3000DownloadProgress.cpp
new file mode 100644
index 0000000..3227f4c
--- /dev/null
+++ b/src/Tone3000DownloadProgress.cpp
@@ -0,0 +1,33 @@
+/*
+ * 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.
+ */
+
+#include "Tone3000DownloadProgress.hpp"
+
+using namespace pipedal;
+
+JSON_MAP_BEGIN(Tone3000DownloadProgress)
+JSON_MAP_REFERENCE(Tone3000DownloadProgress, handle)
+JSON_MAP_REFERENCE(Tone3000DownloadProgress, title)
+JSON_MAP_REFERENCE(Tone3000DownloadProgress, progress)
+JSON_MAP_REFERENCE(Tone3000DownloadProgress, total)
+JSON_MAP_END()
diff --git a/src/Tone3000DownloadProgress.hpp b/src/Tone3000DownloadProgress.hpp
new file mode 100644
index 0000000..7aa30b4
--- /dev/null
+++ b/src/Tone3000DownloadProgress.hpp
@@ -0,0 +1,50 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include
+#include
+#include "json.hpp"
+
+namespace pipedal {
+
+
+ class Tone3000DownloadProgress {
+ private:
+ int64_t handle_ = -1;
+ std::string title_;
+ uint64_t progress_ = 0;
+ uint64_t total_ = 0;
+ public:
+ JSON_GETTER_SETTER(handle);
+ JSON_GETTER_SETTER_REF(title);
+ JSON_GETTER_SETTER(progress);
+ JSON_GETTER_SETTER(total);
+
+
+ DECLARE_JSON_MAP(Tone3000DownloadProgress);
+
+ };
+
+}
diff --git a/src/Tone3000Downloader.cpp b/src/Tone3000Downloader.cpp
index 24f19eb..eba3bab 100644
--- a/src/Tone3000Downloader.cpp
+++ b/src/Tone3000Downloader.cpp
@@ -22,6 +22,10 @@
*/
#include "Tone3000DownloaderImpl.hpp"
+#include "Tone3000DownloadProgress.hpp"
+#include "Tone3000Download.hpp"
+#include "Uri.hpp"
+#include
using namespace pipedal;
@@ -67,9 +71,10 @@ Tone3000DownloaderImpl::handle_t Tone3000DownloaderImpl::RequestDownload(
this->requestQueue.push_back(request);
request = nullptr;
- if (!this->thread)
+ if (!this->thread)
{
- this->thread = std::make_unique([this]{ this->ThreadProc(); });
+ this->thread = std::make_unique([this]
+ { this->ThreadProc(); });
}
}
this->thread_cv.notify_one();
@@ -97,7 +102,8 @@ void Tone3000DownloaderImpl::CancelDownload(
}
}
-void Tone3000DownloaderImpl::Close() {
+void Tone3000DownloaderImpl::Close()
+{
bool notify = false;
{
std::lock_guard lockGuard{this->mutex};
@@ -109,7 +115,7 @@ void Tone3000DownloaderImpl::Close() {
this->requestQueue.resize(0);
- DownloadProgress progress;
+ Tone3000DownloadProgress progress;
fgDownloadProgress = progress;
if (listener)
@@ -121,12 +127,14 @@ void Tone3000DownloaderImpl::Close() {
activeRequest->cancelled = true;
}
}
- if (notify) {
+ if (notify)
+ {
thread_cv.notify_one();
}
}
-Tone3000DownloaderImpl::DownloadProgress Tone3000DownloaderImpl::GetDownloadStatus() {
- DownloadProgress result;
+Tone3000DownloadProgress Tone3000DownloaderImpl::GetDownloadStatus()
+{
+ Tone3000DownloadProgress result;
{
std::lock_guard lockGuard{this->mutex};
result = fgDownloadProgress;
@@ -134,47 +142,139 @@ Tone3000DownloaderImpl::DownloadProgress Tone3000DownloaderImpl::GetDownloadStat
return result;
}
-void Tone3000DownloaderImpl::bgUpdateDownloadProgress(const DownloadProgress &progress)
+void Tone3000DownloaderImpl::bgUpdateDownloadProgress(const Tone3000DownloadProgress &progress)
{
std::lock_guard lockGuard{this->mutex};
- if (closed)
+ if (closed)
{
return;
}
- this->fgDownloadProgress = progress;
- if (listener)
+ if (activeRequest != nullptr && !activeRequest->cancelled)
{
- listener->OnTone3000Progress(this->fgDownloadProgress);
+ this->fgDownloadProgress = progress;
+ if (listener)
+ {
+ listener->OnTone3000Progress(this->fgDownloadProgress);
+ }
+ }
+}
+
+std::string Tone3000DownloaderImpl::PerformDownload(
+ const std::string &downloadPath,
+ const std::string &downloadUrl,
+ int64_t downloadHandle,
+ std::function isCancelled)
+{
+ std::string downloadedDirectory;
+ // Validate Tone3000 URL
+ uri downloadUri{downloadUrl};
+ if (!downloadUri.authority().ends_with(".tone3000.com"))
+ {
+ throw std::runtime_error("Invalid Tone3000 URL address.");
}
+ // Check for cancellation before starting
+ if (isCancelled())
+ {
+ return downloadedDirectory;
+ }
+
+ Tone3000DownloadProgress progress;
+ this->bgUpdateDownloadProgress(progress);
+ // Perform the download using existing code
+ downloadedDirectory = tone3000::DownloadTone3000Bundle(
+ downloadPath,
+ downloadUrl,
+ downloadHandle,
+ [this](const Tone3000DownloadProgress &progress)
+ {
+ this->bgUpdateDownloadProgress(progress);
+ },
+ [isCancelled]()
+ {
+ return isCancelled();
+ });
+
+ // Check for cancellation after download
+ if (isCancelled())
+ {
+ // maybe there's a partial result.
+ return downloadedDirectory;
+ }
+ return downloadedDirectory;
}
void Tone3000DownloaderImpl::ThreadProc()
{
while (true)
{
- std::shared_ptr downloadRequest;
+ std::shared_ptr request;
+ Listener *currentListener = nullptr;
+
+ Tone3000DownloadProgress progress;
+
{
- std::unique_lock lock { this->mutex};
- this->activeRequest = nullptr;
+ std::unique_lock lock{this->mutex};
+ thread_cv.wait(lock, [this]()
+ { return this->closed || this->requestQueue.size() != 0; });
- this->thread_cv.wait(lock,[this] { return this->closed || this->requestQueue.size() != 0;});
-
- if (this->closed) {
+ if (closed)
+ {
return;
}
- if (this->requestQueue.size() != 0)
+
+ if (requestQueue.empty())
{
- this->activeRequest = this->requestQueue[0];
- this->requestQueue.erase(requestQueue.begin());
+ continue;
}
- downloadRequest = this->activeRequest;
- }
- if (!downloadRequest)
- {
- continue;
+
+ // Dequeue the next request
+ request = requestQueue.front();
+ requestQueue.erase(requestQueue.begin());
+ activeRequest = request;
+ currentListener = listener;
}
-
+ // Notify download started
+ if (currentListener)
+ {
+ currentListener->OnStartTone3000Download(request->handle, request->downloadUrl);
+ }
+
+ // Create cancellation predicate that checks the atomic flag
+ auto isCancelled = [request]() -> bool
+ {
+ return request->cancelled.load();
+ };
+
+ try
+ {
+ std::string outputPath = PerformDownload(request->downloadPath, request->downloadUrl, request->handle, isCancelled);
+
+ // Notify completion if not cancelled
+ {
+ std::lock_guard lockGuard{mutex};
+ if (!request->cancelled.load() && currentListener)
+ {
+ currentListener->OnTone3000DownloadComplete(request->handle,outputPath);
+ }
+ }
+ }
+ catch (const std::exception &e)
+ {
+ {
+ std::lock_guard lockGuard{mutex};
+ // Notify error
+ if (!request->cancelled.load() && currentListener)
+ {
+ currentListener->OnTone3000DownloadError(request->handle, e.what());
+ }
+ }
+ }
+
+ {
+ std::lock_guard lock{this->mutex};
+ activeRequest = nullptr;
+ }
}
}
\ No newline at end of file
diff --git a/src/Tone3000Downloader.hpp b/src/Tone3000Downloader.hpp
index f57eaf3..e341c63 100644
--- a/src/Tone3000Downloader.hpp
+++ b/src/Tone3000Downloader.hpp
@@ -25,6 +25,7 @@
#include
#include
+#include "Tone3000DownloadProgress.hpp"
namespace pipedal {
@@ -33,20 +34,9 @@ namespace pipedal {
Tone3000Downloader();
public:
using handle_t = int64_t;
- static constexpr handle_t INVALID_HANDLE = (handle_t)(int64_t)-1;
-
virtual ~Tone3000Downloader();
- class DownloadProgress {
- public:
- private:
- handle_t handle_ = INVALID_HANDLE;
- std::string title_;
- uint64_t progress_ = 0;
- uint64_t total_ = 0;
- };
-
class Listener {
public:
virtual void OnStartTone3000Download
@@ -55,10 +45,16 @@ namespace pipedal {
const std::string &title
) = 0;
virtual void OnTone3000Progress(
- const DownloadProgress& downloadProgress
+ const Tone3000DownloadProgress& downloadProgress
) = 0;
virtual void OnTone3000DownloadComplete(
- handle_t handle
+ handle_t handle,
+ const std::string&resultPath
+ ) = 0;
+
+ virtual void OnTone3000DownloadError(
+ handle_t handle,
+ const std::string &errorMessage
) = 0;
};
@@ -79,6 +75,6 @@ namespace pipedal {
) = 0;
virtual void Close() = 0;
- virtual DownloadProgress GetDownloadStatus() = 0;
+ virtual Tone3000DownloadProgress GetDownloadStatus() = 0;
};
}
\ No newline at end of file
diff --git a/src/Tone3000DownloaderImpl.hpp b/src/Tone3000DownloaderImpl.hpp
index be4b538..083ebae 100644
--- a/src/Tone3000DownloaderImpl.hpp
+++ b/src/Tone3000DownloaderImpl.hpp
@@ -30,6 +30,7 @@
#include
#include
#include
+#include
namespace pipedal {
@@ -49,12 +50,21 @@ namespace pipedal {
) override;
virtual void Close() override;
- virtual DownloadProgress GetDownloadStatus() override;
+ virtual Tone3000DownloadProgress GetDownloadStatus() override;
private:
Listener *listener = nullptr;
void ThreadProc();
+
+ // Performs the actual download with cancellation support
+ std::string PerformDownload(
+ const std::string &downloadPath,
+ const std::string &downloadUrl,
+ int64_t downloadHandle,
+ std::function isCancelled
+ );
+
struct DownloadRequest {
std::atomic cancelled = false;
handle_t handle;
@@ -68,8 +78,8 @@ namespace pipedal {
std::vector> requestQueue;
- DownloadProgress fgDownloadProgress;
- void bgUpdateDownloadProgress(const DownloadProgress &progress);
+ Tone3000DownloadProgress fgDownloadProgress;
+ void bgUpdateDownloadProgress(const Tone3000DownloadProgress &progress);
std::shared_ptr activeRequest;
diff --git a/src/WebServerConfig.cpp b/src/WebServerConfig.cpp
index 18f42fa..cc41471 100644
--- a/src/WebServerConfig.cpp
+++ b/src/WebServerConfig.cpp
@@ -40,7 +40,6 @@
#include "util.hpp"
#include "HtmlHelper.hpp"
#include "WebServerMod.hpp"
-#include "Tone3000Download.hpp"
#define OLD_PRESET_EXTENSION ".piPreset"
#define PRESET_EXTENSION ".piPreset"
@@ -147,10 +146,6 @@ public:
}
std::string segment = request_uri.segment(1);
- if (segment == "tone3000Upload")
- {
- return true;
- }
if (segment == "downloadMediaFile")
{
return true;
@@ -437,44 +432,6 @@ public:
{
std::string segment = request_uri.segment(1);
- if (segment == "tone3000Upload")
- {
-
- tone3000::Tone3000DownloadResult result;
- try
- {
- fs::path downloadPath = request_uri.query("path");
- if (downloadPath.empty())
- {
- throw std::runtime_error("Invalid query parameters.");
- }
- if (!this->model->IsInUploadsDirectory(downloadPath) || HasDotDot(downloadPath))
- {
- throw std::runtime_error("Invalid path.");
- }
- std::string downloadUrl = request_uri.query("url");
- uri downloadUri{downloadUrl};
- if (!downloadUri.authority().ends_with(".tone3000.com"))
- {
- throw std::runtime_error("Invalid Tone3000 URL address.");
- }
- tone3000::DownloadTone3000Bundle(downloadPath, downloadUrl);
- }
- catch (const std::exception &e)
- {
- result = tone3000::Tone3000DownloadResult(e);
- }
- std::ostringstream os;
- json_writer writer(os);
- writer.write(&result);
- std::string strResult = os.str();
-
- res.set(HttpField::content_type,"application/json");
- res.set(HttpField::cache_control, "no-cache");
- res.setBody(strResult);
-
- return;
- }
if (segment == "downloadMediaFile")
{
fs::path path = request_uri.query("path");
diff --git a/todo.txt b/todo.txt
index 8431bd7..f5b26db 100644
--- a/todo.txt
+++ b/todo.txt
@@ -1,3 +1,5 @@
+ Refresh on download complete
+ // yyy: only if the property changed!.
Tooltip on File Property Select: add to release notes.
sAFEfILEnAMES: RENAME, &C. Safe bank names?
NEW BANK
diff --git a/vite/public/licenses/t3k_license.html b/vite/public/licenses/t3k_license.html
new file mode 100644
index 0000000..1d727f0
--- /dev/null
+++ b/vite/public/licenses/t3k_license.html
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+ T3k License
+
+
+
+
T3K License
+
Users may download and use the data file in software and publish the resulting outputs without royalties or restrictions. However, they may not upload, republish, or distribute the data file without the author's permission.
+
+
\ No newline at end of file
diff --git a/vite/public/manifest.json b/vite/public/manifest.json
index a2d368b..105f57a 100644
--- a/vite/public/manifest.json
+++ b/vite/public/manifest.json
@@ -19,7 +19,6 @@
}
],
"start_url": ".",
- "display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}
diff --git a/vite/src/pipedal/AppThemed.tsx b/vite/src/pipedal/AppThemed.tsx
index c2334c4..fc1992b 100644
--- a/vite/src/pipedal/AppThemed.tsx
+++ b/vite/src/pipedal/AppThemed.tsx
@@ -52,6 +52,7 @@ import PresetSelector from './PresetSelector';
import SettingsDialog from './SettingsDialog';
import AboutDialog from './AboutDialog';
import BankDialog from './BankDialog';
+import {Tone3000DownloadStaus as Tone3000DownloadStatus} from './Tone3000Dialog';
import { PiPedalModelFactory, PiPedalModel, State, ZoomedControlInfo, wantsReloadingScreen } from './PiPedalModel';
import ZoomedUiControl from './ZoomedUiControl'
@@ -1099,7 +1100,9 @@ export
-
+ {/* Status of Tone3000 download */}
+
+ {/* Fatal error mask */}
= new ObservableProperty(false);
onSnapshotModified: ObservableEvent = new ObservableEvent();
+ onTone3000DownloadCompleteEvent: ObservableEvent = new ObservableEvent();
+
ui_plugins: ObservableProperty
= new ObservableProperty([]);
state: ObservableProperty = new ObservableProperty(State.Loading);
@@ -550,6 +553,9 @@ export class PiPedalModel //implements PiPedalModel
);
zoomedUiControl: ObservableProperty = new ObservableProperty(undefined);
+ tone3000Downloading : ObservableProperty = new ObservableProperty(false);
+ tone3000DownloadProgress : ObservableProperty = new ObservableProperty(null);
+
uiPluginsByUri: Map = new Map();
private tone3000DownloadHandler: Tone3000DownloadHandler | null = null;
@@ -671,6 +677,18 @@ export class PiPedalModel //implements PiPedalModel
this.showAlert(getErrorMessage(error));
}
}
+ cancelTone3000Download(): void {
+ let downloadProgress = this.tone3000DownloadProgress.get();
+ if (downloadProgress === null) {
+ return;
+ }
+ if (downloadProgress.handle !== -1) {
+ if (!this.webSocket) {
+ return;
+ }
+ this.webSocket.send("cancelTone3000Download",downloadProgress.handle);
+ }
+ }
showTone3000DownloadDialog(
downloadPath: string,
@@ -923,6 +941,22 @@ export class PiPedalModel //implements PiPedalModel
let hasWifi = body as boolean;
this.hasWifiDevice.set(hasWifi);
}
+ else if (message === "onTone3000DownloadStarted") {
+ let { handle, title } = body as { handle: number, title: string };
+ this.onTone3000DownloadStarted(handle, title);
+ }
+ else if (message === "onTone3000DownloadProgress") {
+ // body is DownloadProgress
+ this.onTone3000DownloadProgress(body);
+ }
+ else if (message === "onTone3000DownloadComplete") {
+ let resultPath = body as string;
+ this.onTone3000DownloadComplete(resultPath);
+ }
+ else if (message === "onTone3000DownloadError") {
+ let { handle, errorMessage } = body as { handle: number, errorMessage: string };
+ this.onTone3000DownloadError(handle, errorMessage);
+ }
}
@@ -1022,6 +1056,32 @@ export class PiPedalModel //implements PiPedalModel
// this.webSocket?.reconnect(); // let the server do it for us.
}
+
+ // Tone3000 download notification handlers (stub implementations)
+ private onTone3000DownloadStarted(handle: number, title: string): void {
+ this.tone3000Downloading.set(true);
+ }
+
+ private onTone3000DownloadProgress(progress: Tone3000DownloadProgress): void {
+ this.tone3000Downloading.set(true);
+ this.tone3000DownloadProgress.set(progress);
+ }
+
+ private onTone3000DownloadComplete(resultPath: string): void {
+ this.tone3000Downloading.set(false);
+ this.onTone3000DownloadCompleteEvent.fire(resultPath);
+ }
+
+ private onTone3000DownloadError(handle: number, errorMessage: string): void {
+ console.error(`Tone3000 download error: handle=${handle}, error=${errorMessage}`);
+ this.tone3000Downloading.set(false);
+ this.onTone3000DownloadCompleteEvent.fire("");
+
+ setTimeout(() => {
+ this.showAlert(errorMessage);
+ }, 100);
+ }
+
setError(message: string): void {
this.errorMessage.set(message);
this.setState(State.Error);
@@ -1084,6 +1144,9 @@ export class PiPedalModel //implements PiPedalModel
this.vuSubscriptions = [];
this.monitorPatchPropertyListeners = [];
+ this.tone3000Downloading.set(false);
+ this.tone3000DownloadProgress.set(null);
+
if (this.isAndroidHosted()) {
// if unexpected, go back to the device browser immediately.
if (this.reconnectReason === ReconnectReason.Disconnected) {
diff --git a/vite/src/pipedal/TextInfoDialog.css b/vite/src/pipedal/TextInfoDialog.css
index aa5a678..559bb57 100644
--- a/vite/src/pipedal/TextInfoDialog.css
+++ b/vite/src/pipedal/TextInfoDialog.css
@@ -30,8 +30,11 @@
}
.text-info-dialog-content #user-content-cc_img {
- width: 24px;
- height: 24px;
+ width: 16px;
+ height: 16px;
+ margin: 1px;
+ opacity: 0.6;
+ vertical-align: middle;
}
.text-info-dialog-content #user-content-tone3000_thumbnail {
width: 130px;
diff --git a/vite/src/pipedal/TextInfoDialog.tsx b/vite/src/pipedal/TextInfoDialog.tsx
index be9f32c..7daf0a7 100644
--- a/vite/src/pipedal/TextInfoDialog.tsx
+++ b/vite/src/pipedal/TextInfoDialog.tsx
@@ -33,6 +33,15 @@ import rehypeSanitize, {defaultSchema} from 'rehype-sanitize';
import rehypeExternalLinks from 'rehype-external-links'
import remarkGfm from 'remark-gfm';
import { PiPedalError } from './PiPedalError';
+
+// Extend the default schema to allow target and rel attributes on anchor tags
+const extendedSchema = {
+ ...defaultSchema,
+ attributes: {
+ ...defaultSchema.attributes,
+ a: [...(defaultSchema.attributes?.a || []), 'target', 'rel']
+ }
+};
import DialogEx from './DialogEx';
import ResizeResponsiveComponent from './ResizeResponsiveComponent';
@@ -175,9 +184,9 @@ const TextInfoDialog = class extends ResizeResponsiveComponent
{this.state.textFileContent}
diff --git a/vite/src/pipedal/Tone3000Dialog.tsx b/vite/src/pipedal/Tone3000Dialog.tsx
index 7d094d4..5a862b1 100644
--- a/vite/src/pipedal/Tone3000Dialog.tsx
+++ b/vite/src/pipedal/Tone3000Dialog.tsx
@@ -1,11 +1,15 @@
import { JSX } from "@emotion/react/jsx-dev-runtime";
-import { PiPedalModel, getErrorMessage } from "./PiPedalModel";
+import { PiPedalModel, State, getErrorMessage } from "./PiPedalModel";
import DialogEx from "./DialogEx";
-import DialogContent from "@mui/material/DialogContent";
-import DialogAction from "@mui/material/DialogActions";
import { Button, Typography } from "@mui/material";
import { useEffect, useState } from "react";
import LinearProgress from "@mui/material/LinearProgress";
+import Tone3000DownloadProgress from "./Tone3000DownloadProgress";
+import Dialog from "@mui/material/Dialog";
+import DialogContent from "@mui/material/DialogContent";
+import DialogTitle from "@mui/material/DialogTitle";
+import DialogActions from "@mui/material/DialogActions";
+
export type OnTone3000DownloadHandler = (tone3000DownloadUrl: string) => void;
@@ -24,7 +28,7 @@ export class Tone3000DownloadHandler {
this.handleTone3000DownloadComplete();
- await this.model.downloadModelsFromTone3000(tone3000DownloadUrl,this.downloadPath);
+ await this.model.downloadModelsFromTone3000(tone3000DownloadUrl, this.downloadPath);
return;
} catch (e) {
this.handleTone3000DownloadError(getErrorMessage(e));
@@ -57,7 +61,7 @@ export class Tone3000DownloadHandler {
private popupWindow: Window | null = null;
- private appId: string = "pipedal_app";
+ private appId: string = "pipedal_app3";
private redirectUrl(): string {
let hostname = window.location.hostname;
let port = window.location.port;
@@ -114,22 +118,26 @@ export class Tone3000DownloadHandler {
this.stopTone3000DialogMonitor();
}
- private tone3000DialogInterval: number | undefined = undefined;
+ private tone3000DialogTimeout: number | undefined = undefined;
private stopTone3000DialogMonitor() {
- if (this.tone3000DialogInterval !== undefined) {
- clearInterval(this.tone3000DialogInterval);
- this.tone3000DialogInterval = undefined;
+ if (this.tone3000DialogTimeout !== undefined) {
+ clearTimeout(this.tone3000DialogTimeout);
+ this.tone3000DialogTimeout = undefined;
}
}
- private startTone3000DialogMonitor() {
- this.tone3000DialogInterval = setInterval(() => {
+ private startTone3000DialogMonitor(delay: number = 2000) {
+ this.stopTone3000DialogMonitor();
+ this.tone3000DialogTimeout = window.setTimeout(() => {
if (this.popupWindow == null || this.popupWindow.closed) {
this.stopTone3000DialogMonitor();
this.popupWindow = null;
this.handleTone3000DialogClosed();
+ this.tone3000DialogTimeout = undefined;
+ } else {
+ this.startTone3000DialogMonitor(500);
}
- }, 500);
+ }, delay);
}
private downloadPath: string = "";
private onClosedCallback: (() => void) | undefined = undefined;
@@ -151,13 +159,7 @@ export class Tone3000DownloadHandler {
this.onDownloadStartedCallback = onDownloadStarted;
this.onErrorCallback = onError;
- fetch(TONE3000_PING_URL, { cache: "no-cache" })
- .then((response) => {
- if (!response.ok) {
- this.handleTone3000DownloadError(`Unable to connect to Tone3000: ${response.status}${response.statusText}`);
- return;
- }
-
+ try {
// online
let popupWidth = Math.floor(window.innerWidth * 0.8);
let popupHeight = Math.floor(window.innerHeight * 0.8);
@@ -171,19 +173,28 @@ export class Tone3000DownloadHandler {
"popup",
`innerWidth=${popupWidth},innerHeight=${popupHeight},scrollbars=yes`
);
- this.startTone3000DialogMonitor();
if (!this.popupWindow) {
console.error("Failed to open Tone3000 dialog popup window.");
this.handleTone3000DownloadError("Cannot open popup window.");
return;
}
- })
- .catch((error) => {
+ this.startTone3000DialogMonitor();
+ fetch(TONE3000_PING_URL, { method: "HEAD", cache: "no-cache" }).then(response => {
+ if (!response.ok) {
+ // offline
+ this.handleTone3000DownloadError("Unable to connect to Tone3000 servers. An internet connection is required.");
+ }
+ }).catch(error => {
+ // offline
+ this.handleTone3000DownloadError("Not online. Unable to connect to Tone3000 servers.");
+ });
+ }
+ catch (error) {
// offline
this.handleTone3000DownloadError("Not online. Unable to connect to Tone3000 servers.");
return;
- });
+ };
}
public closeTone3000Dialog(): void {
@@ -226,8 +237,15 @@ export function Tone3000DownloadDialog(props: Tone3000DownloadDialogProps): JSX.
onDownloadComplete();
}
);
+ let onStateChanged = (state: State) => {
+ if (state !== State.Ready) {
+ onClose();
+ }
+ }
+ model.state.addOnChangedHandler(onStateChanged);
return () => {
model.closeTone3000DownloadDialog();
+ model.state.removeOnChangedHandler(onStateChanged);
};
});
return (
@@ -239,23 +257,80 @@ export function Tone3000DownloadDialog(props: Tone3000DownloadDialogProps): JSX.
onEnterKey={() => { }}
>
- Downloading from Tone3000...
+ Downloading from Tone3000...
{downloading &&
}
-
+
{!downloading && (
-
)}
-
+
);
}
+
+export function Tone3000DownloadStaus(
+ props: {
+ zindex: number;
+ }): JSX.Element {
+ const model = PiPedalModel.getInstance();
+ const [downloading, setDownloading] = useState(false);
+ const [progress, setProgress] = useState(null);
+
+ useEffect(() => {
+ let onDownloadingChanged = (value: boolean) => {
+ setDownloading(value);
+ }
+ model.tone3000Downloading.addOnChangedHandler(onDownloadingChanged);
+
+ let onProgressChanged = (value: Tone3000DownloadProgress | null) => {
+ setProgress(value);
+ }
+ model.tone3000DownloadProgress.addOnChangedHandler(onProgressChanged);
+
+ return () => {
+ model.tone3000Downloading.removeOnChangedHandler(onDownloadingChanged);
+ model.tone3000DownloadProgress.removeOnChangedHandler(onProgressChanged);
+ }
+ })
+ let open = downloading && progress !== null && progress.title.length > 0;
+ if (model.state.get() !== State.Ready) {
+ open = false;
+ }
+ return (
+
+
+ );
+}
diff --git a/vite/src/pipedal/Tone3000DownloadProgress.tsx b/vite/src/pipedal/Tone3000DownloadProgress.tsx
new file mode 100644
index 0000000..3dee7d0
--- /dev/null
+++ b/vite/src/pipedal/Tone3000DownloadProgress.tsx
@@ -0,0 +1,30 @@
+/*
+ * 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.
+ */
+
+
+export default interface Tone3000DownloadProgress {
+ handle: number;
+ title: string;
+ progress: number;
+ total: number;
+};
\ No newline at end of file