Sync.
This commit is contained in:
@@ -245,7 +245,7 @@ std::string HtmlHelper::Rfc5987EncodeFileName(const std::string &name)
|
||||
|
||||
#define MAX_FILE_NAME_LENGHT 96
|
||||
|
||||
const std::string SF_SPECIALS = " <>@;:\"\'/[]?=";
|
||||
const std::string SF_SPECIALS = " <>@;:\"\'/[]?=+";
|
||||
|
||||
std::string HtmlHelper::SafeFileName(const std::string &name)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Robin Davies
|
||||
// Copyright (c) 2026 Robin Davies
|
||||
//
|
||||
// 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
|
||||
@@ -57,6 +57,25 @@
|
||||
} \
|
||||
;
|
||||
|
||||
|
||||
#define JSON_GETTER_SETTER_REF(name) \
|
||||
const decltype(name##_) &name() const { return name##_; } \
|
||||
decltype(name##_) &name() { return name##_; } \
|
||||
void name(const decltype(name##_) &value) { name##_ = value; }
|
||||
|
||||
#define JSON_GETTER_SETTER(name) \
|
||||
decltype(name##_) name() const { return name##_; } \
|
||||
void name(decltype(name##_) value) { name##_ = value; }
|
||||
|
||||
|
||||
#define JSON_GETTER_REF(name) \
|
||||
const decltype(name##_) &name() const { return name##_; } \
|
||||
decltype(name##_) &name() { return name##_; }
|
||||
|
||||
#define JSON_GETTER(name) \
|
||||
decltype(name##_) name() const { return name##_; }
|
||||
|
||||
|
||||
namespace pipedal
|
||||
{
|
||||
class JsonException: public std::runtime_error {
|
||||
|
||||
@@ -124,5 +124,7 @@ namespace pipedal
|
||||
|
||||
std::string ToLower(const std::string&value);
|
||||
|
||||
std::string safeFilenameToString(const std::string &filename);
|
||||
std::string stringToSafeFilename(const std::string &name);
|
||||
|
||||
}
|
||||
|
||||
@@ -575,7 +575,7 @@ void json_reader::skip_object()
|
||||
}
|
||||
skip_string(); // name.
|
||||
consume(':');
|
||||
skip_object();
|
||||
skip_property();
|
||||
if (peek() == ',')
|
||||
{
|
||||
consume(',');
|
||||
|
||||
+126
-34
@@ -32,32 +32,29 @@
|
||||
|
||||
using namespace pipedal;
|
||||
|
||||
|
||||
void pipedal::SetThreadName(const std::string &name)
|
||||
{
|
||||
std::string threadName = "ppdl_" + name;
|
||||
if (threadName.length () > 15)
|
||||
if (threadName.length() > 15)
|
||||
{
|
||||
threadName = threadName.substr(0,15);
|
||||
threadName = threadName.substr(0, 15);
|
||||
}
|
||||
pthread_t pid = pthread_self();
|
||||
pthread_setname_np(pid,threadName.c_str());
|
||||
pthread_setname_np(pid, threadName.c_str());
|
||||
}
|
||||
|
||||
|
||||
static const uint8_t utf8extraBytes[256] = {
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, 3,3,3,3,3,3,3,3,4,4,4,4,5,5,5,5
|
||||
};
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5};
|
||||
|
||||
constexpr char32_t ILLEGAL_CHAR32 = U'⊗';
|
||||
static const uint8_t utf8Offset[] = { 0,0b11000000, 0b11100000,0b11110000,0b11111000,0b11111100};
|
||||
static const uint8_t utf8Offset[] = {0, 0b11000000, 0b11100000, 0b11110000, 0b11111000, 0b11111100};
|
||||
|
||||
std::u32string pipedal::ToUtf32(const std::string &s)
|
||||
{
|
||||
@@ -72,16 +69,21 @@ std::u32string pipedal::ToUtf32(const std::string &s)
|
||||
if (c < 0x80)
|
||||
{
|
||||
result << (char32_t)c;
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
auto extraBytes = utf8extraBytes[c];
|
||||
if (extraBytes == 0)
|
||||
{
|
||||
result << ILLEGAL_CHAR32;
|
||||
} else if (p+extraBytes > end)
|
||||
}
|
||||
else if (p + extraBytes > end)
|
||||
{
|
||||
result << ILLEGAL_CHAR32;
|
||||
break;
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
char32_t cResult = c -= utf8Offset[extraBytes];
|
||||
while (extraBytes != 0)
|
||||
{
|
||||
@@ -102,11 +104,10 @@ std::u32string pipedal::ToUtf32(const std::string &s)
|
||||
return result.str();
|
||||
}
|
||||
|
||||
|
||||
std::string pipedal::GetHostName()
|
||||
{
|
||||
char buffer[1024];
|
||||
if (gethostname(buffer,1024) != 0)
|
||||
if (gethostname(buffer, 1024) != 0)
|
||||
{
|
||||
buffer[0] = '\0';
|
||||
}
|
||||
@@ -114,7 +115,6 @@ std::string pipedal::GetHostName()
|
||||
return buffer;
|
||||
}
|
||||
|
||||
|
||||
std::vector<std::string> pipedal::split(const std::string &value, char delimiter)
|
||||
{
|
||||
size_t start = 0;
|
||||
@@ -138,8 +138,7 @@ std::vector<std::string> pipedal::split(const std::string &value, char delimiter
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
bool pipedal::IsChildDirectory(const std::filesystem::path &path, const std::filesystem::path&rootPath)
|
||||
bool pipedal::IsChildDirectory(const std::filesystem::path &path, const std::filesystem::path &rootPath)
|
||||
{
|
||||
auto iter = path.begin();
|
||||
|
||||
@@ -154,8 +153,7 @@ bool pipedal::IsChildDirectory(const std::filesystem::path &path, const std::fil
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
std::string pipedal::ToLower(const std::string&value)
|
||||
std::string pipedal::ToLower(const std::string &value)
|
||||
{
|
||||
std::string result;
|
||||
result.resize(value.length());
|
||||
@@ -166,16 +164,17 @@ std::string pipedal::ToLower(const std::string&value)
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
std::filesystem::path pipedal::MakeRelativePath(const std::filesystem::path &path, const std::filesystem::path&parentPath)
|
||||
std::filesystem::path pipedal::MakeRelativePath(const std::filesystem::path &path, const std::filesystem::path &parentPath)
|
||||
{
|
||||
if (path.is_relative()) return path;
|
||||
if (path.is_relative())
|
||||
return path;
|
||||
|
||||
auto iter = path.begin();
|
||||
|
||||
|
||||
for (auto i = parentPath.begin(); i != parentPath.end(); ++i)
|
||||
{
|
||||
if (iter == path.end() || *iter != *i) {
|
||||
if (iter == path.end() || *iter != *i)
|
||||
{
|
||||
// not a child directory of parent directory.
|
||||
throw std::runtime_error("Not a child directory.");
|
||||
}
|
||||
@@ -187,7 +186,6 @@ std::filesystem::path pipedal::MakeRelativePath(const std::filesystem::path &pat
|
||||
remander /= *iter++;
|
||||
}
|
||||
return remander;
|
||||
|
||||
}
|
||||
|
||||
bool pipedal::HasDotDot(const std::filesystem::path &path)
|
||||
@@ -218,7 +216,7 @@ bool pipedal::IsSubdirectory(const std::filesystem::path &path, const std::files
|
||||
|
||||
if ((*i) != (*iPath))
|
||||
{
|
||||
if (*i == "" && ++i == basePath.end()) // match xyz/ (trailing /)
|
||||
if (*i == "" && ++i == basePath.end()) // match xyz/ (trailing /)
|
||||
{
|
||||
break;
|
||||
}
|
||||
@@ -237,12 +235,106 @@ bool pipedal::IsSubdirectory(const std::filesystem::path &path, const std::files
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
bool pipedal::HasWritePermissions(const std::filesystem::path &path)
|
||||
{
|
||||
// posix, but may not work on windows.
|
||||
// posix, but may not work on windows.
|
||||
// allegedly windows provies an _access function, which is probably a superset of
|
||||
// access.
|
||||
return access(path.c_str(), W_OK) == 0;
|
||||
}
|
||||
|
||||
const std::string SF_SPECIAL_CHARS = " <>@;:\"\'/[]?=+%.";
|
||||
|
||||
static std::array<bool, 256> makeSfBits()
|
||||
{
|
||||
std::array<bool, 256> result;
|
||||
|
||||
for (char c : SF_SPECIAL_CHARS)
|
||||
{
|
||||
result[(unsigned char)c] = true;
|
||||
}
|
||||
for (size_t i = 128; i < 256; ++i)
|
||||
{
|
||||
result[i] = true;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static std::array<bool, 256> IS_SPECIAL_FILENAME_CHAR = makeSfBits();
|
||||
|
||||
static const char *HEX_CHARS = "0123456789abcdef";
|
||||
|
||||
static bool isHexChar(char c)
|
||||
{
|
||||
return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
|
||||
}
|
||||
|
||||
static int fromHexChar(char c)
|
||||
{
|
||||
if (c >= '0' && c <= '9')
|
||||
return (int)(c - '0');
|
||||
if (c >= 'a' && c <= 'f')
|
||||
return (int)(c - 'a') + 10;
|
||||
if (c >= 'A' && c <= 'F')
|
||||
return (int)(c - 'A') + 10;
|
||||
return 0;
|
||||
}
|
||||
static char fromHexChars(char c1, char c2)
|
||||
{
|
||||
int uc = fromHexChar(c1) * 16 + fromHexChar(c2);
|
||||
return (char)uc;
|
||||
}
|
||||
|
||||
std::string pipedal::safeFilenameToString(const std::string &filename)
|
||||
{
|
||||
size_t ix = 0;
|
||||
std::ostringstream os;
|
||||
|
||||
while (ix < filename.size())
|
||||
{
|
||||
char c = filename[ix++];
|
||||
if (c == '%')
|
||||
{
|
||||
// when faced with an invalid encoding, assume that user has done it.
|
||||
// So return the original.
|
||||
if (ix + 2 >= filename.size())
|
||||
{
|
||||
return filename; // just return the original.
|
||||
}
|
||||
if (!isHexChar(filename[ix]) || !isHexChar(filename[ix + 1]))
|
||||
{
|
||||
return filename;
|
||||
}
|
||||
os << fromHexChars(filename[ix], filename[ix + 1]);
|
||||
ix += 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
os << c;
|
||||
}
|
||||
}
|
||||
return os.str();
|
||||
}
|
||||
std::string pipedal::stringToSafeFilename(const std::string &name)
|
||||
{
|
||||
std::ostringstream os;
|
||||
for (char c : name)
|
||||
{
|
||||
unsigned char uc = (unsigned char)c;
|
||||
if (uc < 0x20)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (IS_SPECIAL_FILENAME_CHAR[uc])
|
||||
{
|
||||
os << '%'
|
||||
<< HEX_CHARS[uc >> 4]
|
||||
<< HEX_CHARS[uc & 0x0F];
|
||||
}
|
||||
else
|
||||
{
|
||||
os << c;
|
||||
}
|
||||
}
|
||||
return os.str();
|
||||
}
|
||||
|
||||
@@ -204,6 +204,10 @@ else()
|
||||
endif()
|
||||
|
||||
set (PIPEDAL_SOURCES
|
||||
|
||||
Tone3000Download.cpp Tone3000Download.hpp
|
||||
Tone3000Downloader.cpp Tone3000Downloader.hpp
|
||||
|
||||
CrashGuard.cpp CrashGuard.hpp
|
||||
ModTemplateGenerator.cpp ModTemplateGenerator.hpp
|
||||
WebServerMod.cpp WebServerMod.hpp
|
||||
@@ -265,6 +269,7 @@ set (PIPEDAL_SOURCES
|
||||
GovernorSettings.cpp GovernorSettings.hpp
|
||||
WebServer.cpp WebServer.hpp pch.h Uri.cpp Uri.hpp
|
||||
|
||||
|
||||
RequestHandler.hpp
|
||||
Scratch.cpp PluginHost.hpp PluginHost.cpp
|
||||
PluginType.hpp PluginType.cpp
|
||||
|
||||
+38
-17
@@ -46,6 +46,7 @@
|
||||
#include "DummyAudioDriver.hpp"
|
||||
#include "AudioFiles.hpp"
|
||||
#include "CrashGuard.hpp"
|
||||
#include "Tone3000Downloader.hpp"
|
||||
|
||||
#ifndef NO_MLOCK
|
||||
#include <sys/mman.h>
|
||||
@@ -135,10 +136,15 @@ void PiPedalModel::Close()
|
||||
std::unique_ptr<AudioHost> oldAudioHost;
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(mutex);
|
||||
|
||||
if (closed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (tone3000Downloader)
|
||||
{
|
||||
tone3000Downloader->Close();
|
||||
}
|
||||
closed = true;
|
||||
|
||||
CancelAudioRetry();
|
||||
@@ -225,6 +231,38 @@ void PiPedalModel::Init(const PiPedalConfiguration &configuration)
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
int64_t PiPedalModel::DownloadModelsFromTone3000(
|
||||
int64_t clientId,
|
||||
const std::string &downloadPath,
|
||||
const std::string &tone3000Url)
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(mutex);
|
||||
|
||||
if (!tone3000Downloader)
|
||||
{
|
||||
tone3000Downloader = Tone3000Downloader::Create();
|
||||
}
|
||||
return tone3000Downloader->RequestDownload(
|
||||
downloadPath,
|
||||
tone3000Url
|
||||
);
|
||||
}
|
||||
|
||||
void PiPedalModel::CancelTone3000Download(
|
||||
int64_t clientId,
|
||||
int64_t downloadHandle
|
||||
)
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(mutex);
|
||||
|
||||
if (tone3000Downloader)
|
||||
{
|
||||
tone3000Downloader->CancelDownload(downloadHandle);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void PiPedalModel::LoadLv2PluginInfo()
|
||||
{
|
||||
// Not all Lv2 directories have the lv2 base declarations. Load a full set of plugin classes generated on a default /usr/local/lib/lv2 directory.
|
||||
@@ -3289,23 +3327,6 @@ void PiPedalModel::SetPedalboardItemTitle(int64_t instanceId, const std::string
|
||||
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() != "";
|
||||
}
|
||||
|
||||
std::vector<PresetIndexEntry> PiPedalModel::RequestBankPresets(int64_t bankInstanceId)
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(mutex);
|
||||
|
||||
+25
-20
@@ -51,12 +51,13 @@ namespace pipedal
|
||||
class Updater;
|
||||
class AvahiService;
|
||||
class Lv2PluginState;
|
||||
class Tone3000Downloader;
|
||||
|
||||
class IPiPedalModelSubscriber
|
||||
{
|
||||
public:
|
||||
using ptr = std::shared_ptr<IPiPedalModelSubscriber>;
|
||||
|
||||
|
||||
virtual int64_t GetClientId() = 0;
|
||||
virtual void OnItemEnabledChanged(int64_t clientId, int64_t pedalItemId, bool enabled) = 0;
|
||||
virtual void OnItemUseModUiChanged(int64_t clientId, int64_t pedalItemId, bool enabled) = 0;
|
||||
@@ -97,8 +98,6 @@ 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;
|
||||
|
||||
};
|
||||
|
||||
class HotspotManager;
|
||||
@@ -113,8 +112,9 @@ namespace pipedal
|
||||
using NetworkChangedListener = std::function<void(void)>;
|
||||
|
||||
private:
|
||||
PedalboardItem* GetPedalboardItemForFileProperty(const UiFileProperty& fileProperty);
|
||||
PedalboardItem *GetPedalboardItemForFileProperty(const UiFileProperty &fileProperty);
|
||||
|
||||
std::shared_ptr<Tone3000Downloader> tone3000Downloader;
|
||||
void CancelAudioRetry();
|
||||
clock::time_point lastRestartTime = clock::time_point::min();
|
||||
int audioRestartRetries = 0;
|
||||
@@ -207,7 +207,7 @@ namespace pipedal
|
||||
void FireBanksChanged(int64_t clientId);
|
||||
void FireJackConfigurationChanged(const JackConfiguration &jackConfiguration);
|
||||
void FireLv2StateChanged(int64_t instanceId, const Lv2PluginState &lv2State);
|
||||
void UpdateDefaults(SnapshotValue&snapshotValue, const PedalboardItem*pedalboardItem);
|
||||
void UpdateDefaults(SnapshotValue &snapshotValue, const PedalboardItem *pedalboardItem);
|
||||
void UpdateDefaults(Snapshot *snapshot, std::unordered_map<int64_t, PedalboardItem *> &itemMap);
|
||||
void UpdateDefaults(PedalboardItem *pedalboardItem, std::unordered_map<int64_t, PedalboardItem *> &itemMap);
|
||||
void UpdateDefaults(Pedalboard *pedalboard);
|
||||
@@ -232,6 +232,7 @@ namespace pipedal
|
||||
IPiPedalModelSubscriber *GetNotificationSubscriber(int64_t clientId);
|
||||
std::atomic<bool> closed = false;
|
||||
bool SyncLv2State();
|
||||
|
||||
private: // IAudioHostCallbacks
|
||||
virtual void OnNotifyLv2StateChanged(uint64_t instanceId) override;
|
||||
virtual void OnNotifyMaybeLv2StateChanged(uint64_t instanceId) override;
|
||||
@@ -241,7 +242,7 @@ namespace pipedal
|
||||
virtual void OnNotifyMidiListen(uint8_t cc0, uint8_t cc1, uint8_t cc2) override;
|
||||
virtual void OnPatchSetReply(uint64_t instanceId, LV2_URID patchSetProperty, const LV2_Atom *atomValue) override;
|
||||
virtual void OnNotifyMidiRealtimeEvent(RealtimeMidiEventType eventType) override;
|
||||
virtual void OnNotifyMidiRealtimeSnapshotRequest(int32_t snapshotIndex,int64_t snapshotRequestId) override;
|
||||
virtual void OnNotifyMidiRealtimeSnapshotRequest(int32_t snapshotIndex, int64_t snapshotRequestId) override;
|
||||
virtual void OnAlsaDriverTerminatedAbnormally() override;
|
||||
virtual void OnAlsaSequencerDeviceAdded(int client, const std::string &clientName) override;
|
||||
virtual void OnAlsaSequencerDeviceRemoved(int client) override;
|
||||
@@ -267,8 +268,7 @@ namespace pipedal
|
||||
PiPedalConfiguration configuration;
|
||||
|
||||
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:
|
||||
PiPedalModel();
|
||||
@@ -280,7 +280,7 @@ namespace pipedal
|
||||
Decrease
|
||||
};
|
||||
|
||||
Storage&GetStorage() { return storage; }
|
||||
Storage &GetStorage() { return storage; }
|
||||
bool GetHasWifi();
|
||||
|
||||
void NextBank(Direction direction = Direction::Increase);
|
||||
@@ -288,6 +288,15 @@ namespace pipedal
|
||||
void NextPreset(Direction direction = Direction::Increase);
|
||||
void PreviousPreset() { NextPreset(Direction::Decrease); }
|
||||
|
||||
int64_t DownloadModelsFromTone3000(
|
||||
int64_t clientId,
|
||||
const std::string &downloadPath,
|
||||
const std::string &tone3000Url);
|
||||
|
||||
void CancelTone3000Download(
|
||||
int64_t clientId,
|
||||
int64_t downloadHandle
|
||||
);
|
||||
void RequestShutdown(bool restart);
|
||||
|
||||
virtual PostHandle Post(PostCallback &&fn);
|
||||
@@ -323,7 +332,7 @@ namespace pipedal
|
||||
void SetRestartListener(std::function<void(void)> &&listener);
|
||||
void OnLv2PluginsChanged();
|
||||
void SetOnboarding(bool value);
|
||||
std::map<std::string,std::string> GetWifiRegulatoryDomains();
|
||||
std::map<std::string, std::string> GetWifiRegulatoryDomains();
|
||||
|
||||
void UpdateDnsSd();
|
||||
|
||||
@@ -336,7 +345,7 @@ namespace pipedal
|
||||
|
||||
const PluginHost &GetPluginHost() const { return pluginHost; }
|
||||
PluginHost &GetPluginHost() { return pluginHost; }
|
||||
|
||||
|
||||
Pedalboard GetCurrentPedalboardCopy()
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> guard(mutex);
|
||||
@@ -381,7 +390,6 @@ namespace pipedal
|
||||
int64_t SaveCurrentPresetAs(int64_t clientId, int64_t bankInstanceId, const std::string &name, int64_t saveAfterInstanceId = -1);
|
||||
int64_t SavePluginPresetAs(int64_t instanceId, const std::string &name);
|
||||
|
||||
|
||||
void LoadPreset(int64_t clientId, int64_t instanceId);
|
||||
bool UpdatePresets(int64_t clientId, const PresetIndex &presets);
|
||||
void UpdatePluginPresets(const PluginUiPresets &pluginPresets);
|
||||
@@ -472,8 +480,8 @@ namespace pipedal
|
||||
void DeleteSampleFile(const std::filesystem::path &fileName);
|
||||
std::string CreateNewSampleDirectory(const std::string &relativePath, const UiFileProperty &uiFileProperty);
|
||||
std::string RenameFilePropertyFile(const std::string &oldRelativePath, const std::string &newRelativePath, const UiFileProperty &uiFileProperty);
|
||||
std::string CopyFilePropertyFile(const std::string &oldRelativePath, const std::string &newRelativePath, const UiFileProperty &uiFileProperty,bool overwrite);
|
||||
FilePropertyDirectoryTree::ptr GetFilePropertydirectoryTree(const UiFileProperty &uiFileProperty,const std::string&selectedPath);
|
||||
std::string CopyFilePropertyFile(const std::string &oldRelativePath, const std::string &newRelativePath, const UiFileProperty &uiFileProperty, bool overwrite);
|
||||
FilePropertyDirectoryTree::ptr GetFilePropertydirectoryTree(const UiFileProperty &uiFileProperty, const std::string &selectedPath);
|
||||
|
||||
bool IsInUploadsDirectory(const std::string &path);
|
||||
|
||||
@@ -483,14 +491,11 @@ namespace pipedal
|
||||
bool LoadCurrentPedalboard();
|
||||
|
||||
void MoveAudioFile(
|
||||
const std::string & path,
|
||||
int32_t from,
|
||||
const std::string &path,
|
||||
int32_t from,
|
||||
int32_t to);
|
||||
|
||||
void SetPedalboardItemTitle(int64_t instanceId, const std::string &title, const std::string&iconColor);
|
||||
|
||||
void SetTone3000Auth(const std::string &apiKey);
|
||||
bool HasTone3000Auth() const;
|
||||
void SetPedalboardItemTitle(int64_t instanceId, const std::string &title, const std::string &iconColor);
|
||||
|
||||
void SetSelectedPedalboardPlugin(uint64_t clientId, uint64_t pedalboardId);
|
||||
|
||||
|
||||
+30
-8
@@ -78,6 +78,21 @@ JSON_MAP_BEGIN(RequestBankPresetsBody)
|
||||
JSON_MAP_REFERENCE(RequestBankPresetsBody, bankInstanceId)
|
||||
JSON_MAP_END()
|
||||
|
||||
|
||||
class DownloadTone3000Body {
|
||||
public:
|
||||
int64_t handle_;
|
||||
std::string tone3000DownloadUrl_;
|
||||
|
||||
DECLARE_JSON_MAP(DownloadTone3000Body);
|
||||
};
|
||||
|
||||
JSON_MAP_BEGIN(DownloadTone3000Body)
|
||||
JSON_MAP_REFERENCE(DownloadTone3000Body, handle)
|
||||
JSON_MAP_REFERENCE(DownloadTone3000Body, tone3000DownloadUrl)
|
||||
JSON_MAP_END()
|
||||
|
||||
|
||||
class PathPatchPropertyChangedBody
|
||||
{
|
||||
public:
|
||||
@@ -1494,10 +1509,6 @@ 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")
|
||||
{
|
||||
@@ -1848,6 +1859,21 @@ public:
|
||||
auto result = this->model.CopyPresetsToBank(args.bankInstanceId_, args.presets_);
|
||||
this->Reply(replyTo,"copyPresetsToBank",result);
|
||||
}
|
||||
else if (message == "downloadModelsFromTone3000")
|
||||
{
|
||||
struct DownloadModelsFromTone3000Body {
|
||||
std::string downloadPath_;
|
||||
std::string tone3000Url_;
|
||||
};
|
||||
DownloadModelsFromTone3000Body args;
|
||||
pReader->read(&args);
|
||||
auto result = this->model.DownloadModelsFromTone3000(
|
||||
this->clientId,
|
||||
args.downloadPath_,
|
||||
args.tone3000Url_
|
||||
);
|
||||
this->Reply(replyTo,"downloadModelsFromTone3000",result);
|
||||
}
|
||||
else
|
||||
{
|
||||
Lv2Log::error("Unknown message received: %s", message.c_str());
|
||||
@@ -1964,10 +1990,6 @@ private:
|
||||
{
|
||||
}
|
||||
}
|
||||
virtual void OnTone3000AuthChanged(bool value)
|
||||
{
|
||||
Send("onTone3000AuthChanged", value);
|
||||
}
|
||||
|
||||
virtual void OnErrorMessage(const std::string &message)
|
||||
{
|
||||
|
||||
+12
-50
@@ -44,6 +44,7 @@
|
||||
#include "AtomConverter.hpp"
|
||||
#include "FileBrowserFilesFeature.hpp"
|
||||
#include <string.h>
|
||||
#include "util.hpp"
|
||||
|
||||
using namespace pipedal;
|
||||
namespace fs = std::filesystem;
|
||||
@@ -375,7 +376,6 @@ void Storage::Initialize()
|
||||
LoadPluginPresetIndex();
|
||||
LoadBankIndex();
|
||||
LoadCurrentBank();
|
||||
LoadTone3000Auth();
|
||||
try
|
||||
{
|
||||
LoadChannelSelection();
|
||||
@@ -434,10 +434,6 @@ 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()
|
||||
{
|
||||
@@ -2139,13 +2135,13 @@ static void AddFilesToResult(
|
||||
if (match && !name.starts_with("."))
|
||||
{
|
||||
resultFiles.push_back(
|
||||
FileEntry(path, name, false, false));
|
||||
FileEntry(path, safeFilenameToString(name), false, false));
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (dir_entry.is_directory())
|
||||
{
|
||||
resultFiles.push_back(FileEntry{path, name, true, fs::is_symlink(path)});
|
||||
resultFiles.push_back(FileEntry{path, safeFilenameToString(name), true, fs::is_symlink(path)});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2325,7 +2321,10 @@ FileRequestResult Storage::GetModFileList2(const std::string &relativePath, cons
|
||||
{
|
||||
rootModDirectory = modDirectoryInfo;
|
||||
modDirectoryPath = uploadsDirectory / modDirectoryInfo->pipedalPath;
|
||||
result.breadcrumbs_.push_back({modDirectoryPath.string(), modDirectoryInfo->displayName});
|
||||
result.breadcrumbs_.push_back({
|
||||
modDirectoryPath.string(),
|
||||
(modDirectoryInfo->displayName)}
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -2335,7 +2334,9 @@ FileRequestResult Storage::GetModFileList2(const std::string &relativePath, cons
|
||||
if (IsSubdirectory(fsRelativePath, uploadsDirectory / fileProperty.directory()))
|
||||
{
|
||||
modDirectoryPath = uploadsDirectory / fileProperty.directory();
|
||||
result.breadcrumbs_.push_back({modDirectoryPath.string(), fs::path(fileProperty.directory()).filename().string()});
|
||||
result.breadcrumbs_.push_back({
|
||||
modDirectoryPath.string(),
|
||||
safeFilenameToString(fs::path(fileProperty.directory()).filename().string())});
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -2360,7 +2361,7 @@ FileRequestResult Storage::GetModFileList2(const std::string &relativePath, cons
|
||||
while (iRp != rp.end())
|
||||
{
|
||||
cumulativePath /= (*iRp);
|
||||
result.breadcrumbs_.push_back({cumulativePath, *iRp});
|
||||
result.breadcrumbs_.push_back({cumulativePath, safeFilenameToString(*iRp)});
|
||||
++iRp;
|
||||
}
|
||||
}
|
||||
@@ -2450,7 +2451,7 @@ FileRequestResult Storage::GetFileList2(const std::string &relativePath_, const
|
||||
while (iAbsolutePath != fsAbsolutePath.end())
|
||||
{
|
||||
cumulativePath /= (*iAbsolutePath);
|
||||
result.breadcrumbs_.push_back({cumulativePath.string(), iAbsolutePath->string()});
|
||||
result.breadcrumbs_.push_back({cumulativePath.string(), safeFilenameToString(iAbsolutePath->string())});
|
||||
++iAbsolutePath;
|
||||
}
|
||||
}
|
||||
@@ -2978,46 +2979,7 @@ 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;
|
||||
}
|
||||
|
||||
std::filesystem::path Storage::FromAbstractPathString(const std::string &stringPath)
|
||||
{
|
||||
|
||||
@@ -75,7 +75,6 @@ private:
|
||||
BankIndex bankIndex;
|
||||
BankFile currentBank;
|
||||
PluginPresetIndex pluginPresetIndex;
|
||||
std::string tone3000Auth;
|
||||
|
||||
private:
|
||||
void FillSampleDirectoryTree(FilePropertyDirectoryTree*node, const std::filesystem::path&directory) const;
|
||||
@@ -90,7 +89,6 @@ private:
|
||||
std::filesystem::path GetChannelSelectionFileName();
|
||||
std::filesystem::path GetAlsaSequencerConfigurationFileName();
|
||||
std::filesystem::path GetCurrentPresetPath() const;
|
||||
std::filesystem::path GetTone3000AuthPath() const;
|
||||
|
||||
void LoadBankIndex();
|
||||
void SaveBankIndex();
|
||||
@@ -267,9 +265,6 @@ public:
|
||||
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;
|
||||
|
||||
std::vector<PresetIndexEntry> RequestBankPresets(int64_t bankInstanceId);
|
||||
int64_t ImportPresetsFromBank(int64_t bankInstanceId, const std::vector<int64_t> &presets);
|
||||
|
||||
@@ -0,0 +1,612 @@
|
||||
/*
|
||||
* 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 "ss.hpp"
|
||||
#include "Tone3000Download.hpp"
|
||||
#include "TemporaryFile.hpp"
|
||||
#include <array>
|
||||
#include <ctime>
|
||||
#include "util.hpp"
|
||||
|
||||
using namespace pipedal;
|
||||
using namespace pipedal::tone3000;
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
JSON_MAP_BEGIN(Tone3000Model)
|
||||
JSON_MAP_REFERENCE(Tone3000Model, id)
|
||||
JSON_MAP_REFERENCE(Tone3000Model, name)
|
||||
JSON_MAP_REFERENCE(Tone3000Model, model_url)
|
||||
JSON_MAP_REFERENCE(Tone3000Model, created_at)
|
||||
JSON_MAP_REFERENCE(Tone3000Model, size)
|
||||
JSON_MAP_REFERENCE(Tone3000Model, user_id)
|
||||
JSON_MAP_END()
|
||||
|
||||
JSON_MAP_BEGIN(Tone3000User)
|
||||
JSON_MAP_REFERENCE(Tone3000User, id)
|
||||
JSON_MAP_REFERENCE(Tone3000User, username)
|
||||
JSON_MAP_REFERENCE(Tone3000User, avatar_url)
|
||||
JSON_MAP_REFERENCE(Tone3000User, url)
|
||||
JSON_MAP_END()
|
||||
|
||||
JSON_MAP_BEGIN(Tone3000Download)
|
||||
JSON_MAP_REFERENCE(Tone3000Download, id)
|
||||
JSON_MAP_REFERENCE(Tone3000Download, user_id)
|
||||
JSON_MAP_REFERENCE(Tone3000Download, title)
|
||||
JSON_MAP_REFERENCE(Tone3000Download, description)
|
||||
JSON_MAP_REFERENCE(Tone3000Download, created_at)
|
||||
JSON_MAP_REFERENCE(Tone3000Download, updated_at)
|
||||
JSON_MAP_REFERENCE(Tone3000Download, platform)
|
||||
JSON_MAP_REFERENCE(Tone3000Download, gear)
|
||||
JSON_MAP_REFERENCE(Tone3000Download, images)
|
||||
JSON_MAP_REFERENCE(Tone3000Download, is_public)
|
||||
JSON_MAP_REFERENCE(Tone3000Download, links)
|
||||
JSON_MAP_REFERENCE(Tone3000Download, model_count)
|
||||
JSON_MAP_REFERENCE(Tone3000Download, favorites_count)
|
||||
JSON_MAP_REFERENCE(Tone3000Download, license)
|
||||
JSON_MAP_REFERENCE(Tone3000Download, sizes)
|
||||
JSON_MAP_REFERENCE(Tone3000Download, user)
|
||||
JSON_MAP_REFERENCE(Tone3000Download, models)
|
||||
JSON_MAP_END()
|
||||
|
||||
JSON_MAP_BEGIN(Tone3000DownloadResult)
|
||||
JSON_MAP_REFERENCE(Tone3000DownloadResult, success)
|
||||
JSON_MAP_REFERENCE(Tone3000DownloadResult, errorMessage)
|
||||
JSON_MAP_END()
|
||||
|
||||
static const std::filesystem::path WEB_TEMP_DIR{"/var/pipedal/web_temp"};
|
||||
|
||||
static std::string shellEscape(const std::string &input)
|
||||
{
|
||||
// Escape string for safe use in shell commands by using single quotes
|
||||
// and escaping any single quotes in the input
|
||||
std::string result = "'";
|
||||
for (char c : input)
|
||||
{
|
||||
if (c == '\'')
|
||||
{
|
||||
// End quote, add escaped single quote, start quote again
|
||||
result += "'\\''";
|
||||
}
|
||||
else
|
||||
{
|
||||
result += c;
|
||||
}
|
||||
}
|
||||
result += "'";
|
||||
return result;
|
||||
}
|
||||
|
||||
static std::string getCurlErrorFromExitCode(int exitCode)
|
||||
{
|
||||
|
||||
switch (exitCode)
|
||||
{
|
||||
case 3 * 256: // malformed URL
|
||||
return "Invalid URL.";
|
||||
case 6 * 256: // could not resolve hostname
|
||||
return "Could not resolve hostname. Check that the PiPedal server is connected to the Internet.";
|
||||
case 7 * 256: // failed to connect to host
|
||||
return "Failed to connect to host.";
|
||||
case 22 * 256: // HTTP error
|
||||
return "HTTP error downloading file.";
|
||||
case 28 * 256: // timeout
|
||||
return "Download timeout.";
|
||||
case 63 * 256: // file too large
|
||||
return "File too large.";
|
||||
default:
|
||||
return SS("Failed to download file (exit code: " << (exitCode / 256) << ")");
|
||||
}
|
||||
}
|
||||
static void downloadFile(const std::string &url, const fs::path &path)
|
||||
{
|
||||
// Create temporary file for HTTP status code
|
||||
TemporaryFile httpCodeFile{WEB_TEMP_DIR};
|
||||
fs::path httpCodePath = httpCodeFile.Path();
|
||||
|
||||
// Build curl command with error detection and proper escaping
|
||||
std::ostringstream os;
|
||||
os << "/usr/bin/curl -f -s -S -L --max-time 300 --max-filesize 104857600";
|
||||
os << " -w '%{http_code}' -o ";
|
||||
os << shellEscape(path.string());
|
||||
os << " ";
|
||||
os << shellEscape(url);
|
||||
os << " > ";
|
||||
os << shellEscape(httpCodePath.string());
|
||||
|
||||
std::string command = os.str();
|
||||
|
||||
// Execute curl command
|
||||
int exitCode = std::system(command.c_str());
|
||||
|
||||
// Read HTTP status code
|
||||
int httpCode = 0;
|
||||
if (fs::exists(httpCodePath))
|
||||
{
|
||||
std::ifstream httpCodeStream(httpCodePath);
|
||||
std::string httpCodeStr;
|
||||
std::getline(httpCodeStream, httpCodeStr);
|
||||
try
|
||||
{
|
||||
httpCode = std::stoi(httpCodeStr);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
// If parsing fails, keep httpCode as 0
|
||||
}
|
||||
}
|
||||
|
||||
if (exitCode != 0)
|
||||
{
|
||||
if (exitCode == 22 * 256) // HTTP error
|
||||
{
|
||||
throw std::runtime_error(
|
||||
SS("Server was unabled to download the file. Http error: " << httpCode << " url: " << url));
|
||||
}
|
||||
else
|
||||
{
|
||||
throw std::runtime_error(
|
||||
SS("Server was unable to download the file. "
|
||||
<< getCurlErrorFromExitCode(exitCode)
|
||||
<< " " << "url: " << url));
|
||||
}
|
||||
}
|
||||
|
||||
// Verify file was downloaded
|
||||
if (!fs::exists(path) || fs::file_size(path) == 0)
|
||||
{
|
||||
throw std::runtime_error("Downloaded file is empty or missing");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static std::string mdSanitize(const std::string &str, const std::string &illegalCharacters = "")
|
||||
{
|
||||
std::ostringstream os;
|
||||
for (char c : str)
|
||||
{
|
||||
if ((unsigned char)c < 0x20)
|
||||
continue;
|
||||
if (c == '<')
|
||||
{
|
||||
os << "<";
|
||||
continue;
|
||||
}
|
||||
auto npos = illegalCharacters.find_first_of(c);
|
||||
if (npos != std::string::npos)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
os << c;
|
||||
}
|
||||
return os.str();
|
||||
}
|
||||
|
||||
|
||||
static std::string mdDate(const tone3000_time_point &date_)
|
||||
{
|
||||
std::ostringstream os;
|
||||
// C++ doesn't handle timezones. Just zap the timezone, and replace it with "Z"
|
||||
std::string date = date_;
|
||||
|
||||
// Remove timezone offset if present and replace with Z
|
||||
size_t plusPos = date.find('+');
|
||||
if (plusPos != std::string::npos)
|
||||
{
|
||||
date = date.substr(0, plusPos) + "Z";
|
||||
}
|
||||
// Parse ISO 8601 date string into tm
|
||||
std::tm tm = {};
|
||||
std::istringstream ss(date);
|
||||
ss >> std::get_time(&tm, "%Y-%m-%dT%H:%M:%S");
|
||||
if (ss.fail())
|
||||
{
|
||||
os << date;
|
||||
return os.str();
|
||||
}
|
||||
|
||||
os << std::put_time(&tm, "%x");
|
||||
return os.str();
|
||||
}
|
||||
|
||||
static std::string mdEnum(const std::string &value, const std::map<std::string, std::string> &enumValues)
|
||||
{
|
||||
auto ff = enumValues.find(value);
|
||||
if (ff == enumValues.end())
|
||||
{
|
||||
return value;
|
||||
}
|
||||
return ff->second;
|
||||
}
|
||||
|
||||
static std::string mdEnumList(
|
||||
const std::vector<std::string> &values,
|
||||
const std::map<std::string, std::string> &enumValues)
|
||||
{
|
||||
if (values.size() == 1)
|
||||
{
|
||||
return mdEnum(values[0], enumValues);
|
||||
}
|
||||
std::ostringstream os;
|
||||
os << '[';
|
||||
bool first = true;
|
||||
for (const auto &value : values)
|
||||
{
|
||||
if (first)
|
||||
{
|
||||
first = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
os << ", ";
|
||||
}
|
||||
os << mdEnum(value, enumValues);
|
||||
}
|
||||
os << ']';
|
||||
return os.str();
|
||||
}
|
||||
|
||||
static std::map<std::string, std::string> sizeEnumValues =
|
||||
{
|
||||
{"standard", "Standard"},
|
||||
{"lite", "Lite"},
|
||||
{"feather", "Feather"},
|
||||
{"nano", "Nano"},
|
||||
{"custom", "Custom"}};
|
||||
static std::string mdSizes(const std::vector<std::string> &sizes)
|
||||
{
|
||||
return mdEnumList(sizes, sizeEnumValues);
|
||||
}
|
||||
|
||||
static std::map<std::string, std::string> gearEnumValues =
|
||||
{
|
||||
{"amp", "Amp only"},
|
||||
{"full-rig", "Full rig"},
|
||||
{"pedal", "Pedal"},
|
||||
{"outboard", "Outboard"},
|
||||
{"ir", "I/R"}};
|
||||
|
||||
static std::string mdGear(const std::string &gear)
|
||||
{
|
||||
return mdEnum(gear, gearEnumValues);
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
enum class LicenseFlags
|
||||
{
|
||||
None = 0,
|
||||
Cc = 1,
|
||||
By = 2,
|
||||
CcBy = 3, // = Cc | By
|
||||
Sa = 4,
|
||||
Nc = 8,
|
||||
Nd = 16,
|
||||
Cc0 = 32,
|
||||
};
|
||||
|
||||
static bool operator&(LicenseFlags v1, LicenseFlags v2)
|
||||
{
|
||||
return (((int)v1) & ((int)v2)) != 0;
|
||||
}
|
||||
static LicenseFlags operator|(LicenseFlags v1, LicenseFlags v2)
|
||||
{
|
||||
return (LicenseFlags)(((int)(v1)) | ((int)(v2)));
|
||||
}
|
||||
|
||||
struct LicenseInfo
|
||||
{
|
||||
std::string key;
|
||||
std::string displayName;
|
||||
std::string url;
|
||||
LicenseFlags licenseFlags;
|
||||
};
|
||||
}
|
||||
|
||||
static std::vector<LicenseInfo> licenses{
|
||||
{
|
||||
.key = "t3k",
|
||||
.displayName = "t3k",
|
||||
.url = "",
|
||||
.licenseFlags = LicenseFlags::None
|
||||
},
|
||||
{
|
||||
.key = "cc-by",
|
||||
.displayName = "CC BY 4.0",
|
||||
.url = "https://creativecommons.org/licenses/by/4.0/",
|
||||
.licenseFlags = LicenseFlags::CcBy
|
||||
},
|
||||
{
|
||||
.key = "cc-by-sa",
|
||||
.displayName = "CC BY-SA 4.0",
|
||||
.url = "https://creativecommons.org/licenses/by-sa/4.0/",
|
||||
.licenseFlags = LicenseFlags::CcBy | LicenseFlags::Sa
|
||||
},
|
||||
{
|
||||
.key = "cc-by-nc",
|
||||
.displayName = "CC BY-NC 4.0",
|
||||
.url = "https://creativecommons.org/licenses/by-nc/4.0/",
|
||||
.licenseFlags = LicenseFlags::CcBy | LicenseFlags::Nc
|
||||
},
|
||||
{
|
||||
.key = "cc-by-nc-sa",
|
||||
.displayName = "CC BY-NC-SA 4.0",
|
||||
.url = "https://creativecommons.org/licenses/by-nc-sa/4.0/",
|
||||
.licenseFlags = LicenseFlags::CcBy | LicenseFlags::Nc | LicenseFlags::Sa
|
||||
},
|
||||
{
|
||||
.key = "cc-by-nd",
|
||||
.displayName = "CC BY-ND",
|
||||
.url = "https://creativecommons.org/licenses/by-nd/4.0/",
|
||||
.licenseFlags = LicenseFlags::CcBy | LicenseFlags::Nd
|
||||
},
|
||||
{
|
||||
.key = "cc-by-nc-nd",
|
||||
.displayName = "CC BY-NC-ND",
|
||||
.url = "https://creativecommons.org/licenses/by-nc-nd/4.0/",
|
||||
.licenseFlags = LicenseFlags::CcBy | LicenseFlags::Nc | LicenseFlags::Nd
|
||||
},
|
||||
{
|
||||
.key = "cco",
|
||||
.displayName = "CC0",
|
||||
.url = "https://creativecommons.org/publicdomain/zero/1.0/",
|
||||
.licenseFlags = LicenseFlags::Cc | LicenseFlags::Cc0,
|
||||
|
||||
},
|
||||
};
|
||||
|
||||
static std::map<std::string, LicenseInfo> makeLicenseEnum()
|
||||
{
|
||||
std::map<std::string, LicenseInfo> result;
|
||||
for (const auto &license : licenses)
|
||||
{
|
||||
result[license.key] = license;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static std::map<std::string, LicenseInfo> licenseEnum = makeLicenseEnum();
|
||||
|
||||
static std::string mdLicenseIcon(LicenseFlags licenseFlag)
|
||||
{
|
||||
std::ostringstream os;
|
||||
std::string url;
|
||||
|
||||
switch (licenseFlag)
|
||||
{
|
||||
case LicenseFlags::Cc:
|
||||
url = "img/cc.svg";
|
||||
break;
|
||||
case LicenseFlags::By:
|
||||
url = "img/by.svg";
|
||||
break;
|
||||
case LicenseFlags::Sa:
|
||||
url = "img/sa.svg";
|
||||
break;
|
||||
case LicenseFlags::Nc:
|
||||
url = "img/nc.svg";
|
||||
break;
|
||||
case LicenseFlags::Nd:
|
||||
url = "img/nd.svg";
|
||||
break;
|
||||
case LicenseFlags::Cc0:
|
||||
url = "img/cc0.svg";
|
||||
break;
|
||||
|
||||
case LicenseFlags::None:
|
||||
throw std::runtime_error("Invalid argument.");
|
||||
}
|
||||
os << "<img id='cc_img' src='" << url << "'/>";
|
||||
return os.str();
|
||||
}
|
||||
static std::string mdLicense(const std::string &license)
|
||||
{
|
||||
auto ff = licenseEnum.find(license);
|
||||
LicenseInfo licenseInfo;
|
||||
if (ff != licenseEnum.end())
|
||||
{
|
||||
licenseInfo = ff->second;
|
||||
}
|
||||
else
|
||||
{
|
||||
licenseInfo.displayName = license;
|
||||
}
|
||||
|
||||
std::ostringstream os;
|
||||
|
||||
if (licenseInfo.licenseFlags != LicenseFlags::None) {
|
||||
for (LicenseFlags licenseFlag: std::vector<LicenseFlags>{LicenseFlags::Cc, LicenseFlags::By, LicenseFlags::Cc0, LicenseFlags::Nc, LicenseFlags::Sa, LicenseFlags::Nd})
|
||||
{
|
||||
if (licenseInfo.licenseFlags & licenseFlag)
|
||||
{
|
||||
os << mdLicenseIcon(licenseFlag);
|
||||
}
|
||||
}
|
||||
os << " ";
|
||||
}
|
||||
|
||||
if (licenseInfo.url != "")
|
||||
{
|
||||
os << "<a target='_blank' rel='noopener noreferrer' href='"
|
||||
<< licenseInfo.url
|
||||
<< "'>" << mdSanitize(licenseInfo.displayName) << "</a>";
|
||||
}
|
||||
else
|
||||
{
|
||||
os << mdSanitize(licenseInfo.displayName);
|
||||
}
|
||||
return os.str();
|
||||
}
|
||||
static std::map<std::string, std::string> platformEnumValues =
|
||||
{
|
||||
{"nam", "NAM"},
|
||||
{"ir", "I/R"},
|
||||
{"aida-x", "Aida X"},
|
||||
{"aa-snapshot", "aa-snapshot"},
|
||||
{"proteus", "Proteus"}};
|
||||
static std::string mdPlatform(const std::string &platform)
|
||||
{
|
||||
return mdEnum(platform, platformEnumValues);
|
||||
}
|
||||
static std::string mdUser(const tone3000::Tone3000User &user)
|
||||
{
|
||||
// clang-format off
|
||||
std::ostringstream os;
|
||||
os <<
|
||||
"<a target='_blank' rel='noopener noreferrer' href='"
|
||||
<< mdSanitize(user.url(),"'")
|
||||
<< "'>" << mdSanitize(user.username()) << "</a>";
|
||||
return os.str();
|
||||
// clang-format on
|
||||
}
|
||||
|
||||
static void mdEscapeString(std::ostream &f, const std::string &str)
|
||||
{
|
||||
size_t ix = 0;
|
||||
while (ix < str.size())
|
||||
{
|
||||
char c = str[ix++];
|
||||
if (c == '\n')
|
||||
{
|
||||
if (ix < str.size() && str[ix] == '\n')
|
||||
{
|
||||
f << "\n\n";
|
||||
}
|
||||
else
|
||||
{
|
||||
f << " \n"; // a <br> in md.
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
f << c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void mdLink(std::ostream &f, const std::string &label, const std::string &url)
|
||||
{
|
||||
f << "[" << mdSanitize(label, "]") << "](" << mdSanitize(url, ")") << ")";
|
||||
}
|
||||
|
||||
static void mdImage(std::ostream &f, const std::string url)
|
||||
{
|
||||
f << "![" << mdSanitize("Plugin Thumbname", "]") << "](" << mdSanitize(url, "]") << "#tone3000_thumbnail" << ")";
|
||||
f << "\n\n";
|
||||
}
|
||||
static void writeReadme(const fs::path &path, const Tone3000Download &download)
|
||||
{
|
||||
std::ofstream of{path};
|
||||
if (!of.is_open())
|
||||
{
|
||||
throw std::runtime_error(SS("Unable to create file " << path));
|
||||
}
|
||||
std::string imageLink;
|
||||
if (download.images().size() > 0)
|
||||
{
|
||||
imageLink = download.images()[0];
|
||||
}
|
||||
|
||||
// clang-format off
|
||||
of <<
|
||||
|
||||
"<div id='tone3000_float' >\n"
|
||||
<< " <img id='tone3000_thumbnail' src='"
|
||||
<< imageLink << "' alt='' />\n"
|
||||
<< " <div id='tone3000_float_content' >\n"
|
||||
<< " <h3 id='tone3000_title'>" << mdSanitize(download.title()) << "</h3>\n"
|
||||
<< " <div>\n"
|
||||
<< " <p id='tone3000_subtitle'>\n"
|
||||
<< mdDate(download.updated_at())
|
||||
<< " "
|
||||
<< mdUser(download.user())
|
||||
<< " <br/>\n"
|
||||
<< " " << mdSizes(download.sizes()) << ", "
|
||||
<< mdSizes(download.sizes()) << ", "
|
||||
<< mdGear(download.gear()) << ", "
|
||||
<< mdPlatform(download.platform())
|
||||
<< "<br/>\n"
|
||||
<< " License: xxx" << mdLicense(download.license()) << "\n"
|
||||
<< " </p>\n"
|
||||
<< " </div>\n"
|
||||
<< " </div>\n"
|
||||
<< "</div>\n";
|
||||
// clang-format on
|
||||
of << "\n\n";
|
||||
mdEscapeString(of, download.description());
|
||||
|
||||
if (download.links().size() > 0)
|
||||
{
|
||||
of << std::endl;
|
||||
of << "## Links" << std::endl
|
||||
<< std::endl;
|
||||
for (auto &link : download.links())
|
||||
{
|
||||
of << "- " << "[" << link << "](" << link << ")" << std::endl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void pipedal::tone3000::DownloadTone3000Bundle(
|
||||
const std::filesystem::path &destinationFolder,
|
||||
const std::string &downloadUrl)
|
||||
{
|
||||
TemporaryFile downloadTemporaryFile{WEB_TEMP_DIR};
|
||||
fs::path downloadPath = downloadTemporaryFile.Path();
|
||||
|
||||
downloadFile(downloadUrl, downloadPath);
|
||||
|
||||
std::ifstream f{downloadPath};
|
||||
if (!f.is_open())
|
||||
{
|
||||
throw std::runtime_error(SS("Can't open file " << downloadPath));
|
||||
}
|
||||
json_reader reader(f);
|
||||
|
||||
Tone3000Download tone3000Download;
|
||||
reader.read(&tone3000Download);
|
||||
|
||||
if (tone3000Download.platform() != "nam")
|
||||
{
|
||||
throw std::runtime_error(SS("Unexpected file format. (platform=\"" << tone3000Download.platform() << "\")"));
|
||||
}
|
||||
fs::path bundlePath = destinationFolder / stringToSafeFilename(tone3000Download.title());
|
||||
|
||||
fs::create_directories(bundlePath);
|
||||
|
||||
for (auto &model : tone3000Download.models())
|
||||
{
|
||||
fs::path modelPath = bundlePath / SS(stringToSafeFilename(model.name()) << ".nam");
|
||||
downloadFile(model.model_url(), modelPath);
|
||||
}
|
||||
fs::path readmePath = bundlePath / "README.md";
|
||||
writeReadme(readmePath, tone3000Download);
|
||||
}
|
||||
|
||||
Tone3000DownloadResult::Tone3000DownloadResult()
|
||||
: success_(true)
|
||||
{
|
||||
}
|
||||
Tone3000DownloadResult::Tone3000DownloadResult(const std::exception &e)
|
||||
: success_(false), errorMessage_(e.what())
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
* 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 <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include "json.hpp"
|
||||
#include <filesystem>
|
||||
#include <chrono>
|
||||
|
||||
namespace pipedal
|
||||
{
|
||||
namespace tone3000
|
||||
{
|
||||
using tone3000_time_point = std::string; // Date in ISO 8601 format. (C++ doesn't handle timezones)
|
||||
class Tone3000Model
|
||||
{
|
||||
private:
|
||||
int64_t id_ = -1;
|
||||
std::string name_;
|
||||
std::string model_url_;
|
||||
tone3000_time_point created_at_;
|
||||
std::string size_;
|
||||
std::string user_id_;
|
||||
|
||||
public:
|
||||
JSON_GETTER_SETTER(id)
|
||||
JSON_GETTER_SETTER_REF(name)
|
||||
JSON_GETTER_SETTER_REF(model_url)
|
||||
JSON_GETTER_SETTER_REF(created_at)
|
||||
JSON_GETTER_SETTER_REF(size)
|
||||
JSON_GETTER_SETTER_REF(user_id)
|
||||
|
||||
DECLARE_JSON_MAP(Tone3000Model);
|
||||
};
|
||||
|
||||
class Tone3000User
|
||||
{
|
||||
private:
|
||||
std::string id_;
|
||||
std::string avatar_url_;
|
||||
std::string username_;
|
||||
std::string url_;
|
||||
|
||||
public:
|
||||
JSON_GETTER_SETTER_REF(id)
|
||||
JSON_GETTER_SETTER(username)
|
||||
JSON_GETTER_SETTER(url)
|
||||
|
||||
DECLARE_JSON_MAP(Tone3000User);
|
||||
};
|
||||
|
||||
class Tone3000Download
|
||||
{
|
||||
private:
|
||||
int64_t id_ = -1;
|
||||
std::string user_id_;
|
||||
std::string title_;
|
||||
std::string platform_;
|
||||
std::string description_;
|
||||
tone3000_time_point created_at_;
|
||||
tone3000_time_point updated_at_;
|
||||
std::string gear_;
|
||||
std::vector<std::string> images_;
|
||||
bool is_public_ = true;
|
||||
std::vector<std::string> links_;
|
||||
int64_t model_count_ = 0;
|
||||
int64_t favorites_count_ = 0;
|
||||
std::string license_;
|
||||
std::vector<std::string> sizes_;
|
||||
Tone3000User user_;
|
||||
std::vector<Tone3000Model> models_;
|
||||
|
||||
public:
|
||||
JSON_GETTER_SETTER(id)
|
||||
JSON_GETTER_SETTER_REF(user_id)
|
||||
JSON_GETTER_SETTER_REF(title)
|
||||
JSON_GETTER_SETTER_REF(platform)
|
||||
JSON_GETTER_SETTER_REF(description)
|
||||
JSON_GETTER_SETTER_REF(created_at)
|
||||
JSON_GETTER_SETTER_REF(updated_at)
|
||||
JSON_GETTER_SETTER_REF(gear)
|
||||
JSON_GETTER_SETTER_REF(images)
|
||||
JSON_GETTER_SETTER(is_public)
|
||||
JSON_GETTER_SETTER_REF(links)
|
||||
JSON_GETTER_SETTER(model_count)
|
||||
JSON_GETTER_SETTER(favorites_count)
|
||||
JSON_GETTER_SETTER_REF(license)
|
||||
JSON_GETTER_SETTER_REF(sizes)
|
||||
JSON_GETTER_SETTER_REF(user)
|
||||
JSON_GETTER_SETTER_REF(models)
|
||||
|
||||
DECLARE_JSON_MAP(Tone3000Download);
|
||||
};
|
||||
|
||||
class Tone3000DownloadResult {
|
||||
private:
|
||||
bool success_;
|
||||
std::string errorMessage_;
|
||||
public:
|
||||
JSON_GETTER_SETTER(success);
|
||||
JSON_GETTER_SETTER_REF(errorMessage);
|
||||
|
||||
Tone3000DownloadResult();
|
||||
Tone3000DownloadResult(const std::exception &e);
|
||||
|
||||
DECLARE_JSON_MAP(Tone3000DownloadResult);
|
||||
};
|
||||
|
||||
extern void DownloadTone3000Bundle
|
||||
(
|
||||
const std::filesystem::path&destinationFolder,
|
||||
const std::string &downloadUrl
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
/*
|
||||
* 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 "Tone3000DownloaderImpl.hpp"
|
||||
|
||||
using namespace pipedal;
|
||||
|
||||
std::shared_ptr<Tone3000Downloader> Tone3000Downloader::Create()
|
||||
{
|
||||
return std::make_shared<Tone3000DownloaderImpl>();
|
||||
}
|
||||
|
||||
Tone3000Downloader::Tone3000Downloader()
|
||||
{
|
||||
}
|
||||
|
||||
Tone3000Downloader::~Tone3000Downloader()
|
||||
{
|
||||
}
|
||||
|
||||
Tone3000DownloaderImpl::~Tone3000DownloaderImpl()
|
||||
{
|
||||
Close();
|
||||
this->thread = nullptr;
|
||||
}
|
||||
|
||||
void Tone3000DownloaderImpl::SetListener(Listener *listener)
|
||||
{
|
||||
std::lock_guard<std::mutex> lockGuard{this->mutex};
|
||||
this->listener = listener;
|
||||
}
|
||||
|
||||
Tone3000DownloaderImpl::handle_t Tone3000DownloaderImpl::NextHandle()
|
||||
{
|
||||
handle_t result = this->nextHandle++;
|
||||
return result;
|
||||
}
|
||||
Tone3000DownloaderImpl::handle_t Tone3000DownloaderImpl::RequestDownload(
|
||||
const std::string &path,
|
||||
const std::string &url)
|
||||
{
|
||||
handle_t handle;
|
||||
{
|
||||
std::lock_guard<std::mutex> lockGuard{this->mutex};
|
||||
handle = NextHandle();
|
||||
std::shared_ptr<DownloadRequest> request = std::make_shared<DownloadRequest>(false, handle, path, url);
|
||||
|
||||
this->requestQueue.push_back(request);
|
||||
request = nullptr;
|
||||
if (!this->thread)
|
||||
{
|
||||
this->thread = std::make_unique<std::jthread>([this]{ this->ThreadProc(); });
|
||||
}
|
||||
}
|
||||
this->thread_cv.notify_one();
|
||||
return handle;
|
||||
}
|
||||
|
||||
void Tone3000DownloaderImpl::CancelDownload(
|
||||
handle_t handle)
|
||||
{
|
||||
std::lock_guard<std::mutex> lockGuard{this->mutex};
|
||||
for (auto it = requestQueue.begin(); it != requestQueue.end(); ++it)
|
||||
{
|
||||
if ((*it)->handle == handle)
|
||||
{
|
||||
requestQueue.erase(it);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (activeRequest)
|
||||
{
|
||||
if (activeRequest->handle == handle)
|
||||
{
|
||||
activeRequest->cancelled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Tone3000DownloaderImpl::Close() {
|
||||
bool notify = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lockGuard{this->mutex};
|
||||
if (!closed)
|
||||
{
|
||||
closed = true;
|
||||
notify = true;
|
||||
}
|
||||
|
||||
this->requestQueue.resize(0);
|
||||
|
||||
DownloadProgress progress;
|
||||
fgDownloadProgress = progress;
|
||||
|
||||
if (listener)
|
||||
{
|
||||
listener->OnTone3000Progress(progress);
|
||||
}
|
||||
if (this->activeRequest != nullptr)
|
||||
{
|
||||
activeRequest->cancelled = true;
|
||||
}
|
||||
}
|
||||
if (notify) {
|
||||
thread_cv.notify_one();
|
||||
}
|
||||
}
|
||||
Tone3000DownloaderImpl::DownloadProgress Tone3000DownloaderImpl::GetDownloadStatus() {
|
||||
DownloadProgress result;
|
||||
{
|
||||
std::lock_guard<std::mutex> lockGuard{this->mutex};
|
||||
result = fgDownloadProgress;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void Tone3000DownloaderImpl::bgUpdateDownloadProgress(const DownloadProgress &progress)
|
||||
{
|
||||
std::lock_guard<std::mutex> lockGuard{this->mutex};
|
||||
if (closed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
this->fgDownloadProgress = progress;
|
||||
if (listener)
|
||||
{
|
||||
listener->OnTone3000Progress(this->fgDownloadProgress);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void Tone3000DownloaderImpl::ThreadProc()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
std::shared_ptr<DownloadRequest> downloadRequest;
|
||||
{
|
||||
std::unique_lock lock { this->mutex};
|
||||
this->activeRequest = nullptr;
|
||||
|
||||
this->thread_cv.wait(lock,[this] { return this->closed || this->requestQueue.size() != 0;});
|
||||
|
||||
if (this->closed) {
|
||||
return;
|
||||
}
|
||||
if (this->requestQueue.size() != 0)
|
||||
{
|
||||
this->activeRequest = this->requestQueue[0];
|
||||
this->requestQueue.erase(requestQueue.begin());
|
||||
}
|
||||
downloadRequest = this->activeRequest;
|
||||
}
|
||||
if (!downloadRequest)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* 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 <memory>
|
||||
#include <cstdint>
|
||||
|
||||
namespace pipedal {
|
||||
|
||||
class Tone3000Downloader {
|
||||
protected:
|
||||
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
|
||||
(
|
||||
handle_t handle,
|
||||
const std::string &title
|
||||
) = 0;
|
||||
virtual void OnTone3000Progress(
|
||||
const DownloadProgress& downloadProgress
|
||||
) = 0;
|
||||
virtual void OnTone3000DownloadComplete(
|
||||
handle_t handle
|
||||
) = 0;
|
||||
|
||||
};
|
||||
|
||||
|
||||
using self = Tone3000Downloader;
|
||||
static std::shared_ptr<self> Create();
|
||||
|
||||
virtual void SetListener(Listener*listener) = 0;
|
||||
|
||||
virtual handle_t RequestDownload(
|
||||
const std::string &path,
|
||||
const std::string &url
|
||||
) = 0;
|
||||
|
||||
virtual void CancelDownload(
|
||||
handle_t handle
|
||||
) = 0;
|
||||
|
||||
virtual void Close() = 0;
|
||||
virtual DownloadProgress GetDownloadStatus() = 0;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* 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 <memory>
|
||||
#include <cstdint>
|
||||
#include "Tone3000Downloader.hpp"
|
||||
#include <thread>
|
||||
#include <mutex>
|
||||
#include <condition_variable>
|
||||
#include <vector>
|
||||
|
||||
namespace pipedal {
|
||||
|
||||
class Tone3000DownloaderImpl: public Tone3000Downloader {
|
||||
public:
|
||||
virtual ~Tone3000DownloaderImpl();
|
||||
|
||||
virtual void SetListener(Listener*listener) override;
|
||||
|
||||
virtual handle_t RequestDownload(
|
||||
const std::string &path,
|
||||
const std::string &url
|
||||
) override;
|
||||
|
||||
virtual void CancelDownload(
|
||||
handle_t handle
|
||||
) override;
|
||||
|
||||
virtual void Close() override;
|
||||
virtual DownloadProgress GetDownloadStatus() override;
|
||||
|
||||
private:
|
||||
Listener *listener = nullptr;
|
||||
|
||||
void ThreadProc();
|
||||
struct DownloadRequest {
|
||||
std::atomic<bool> cancelled = false;
|
||||
handle_t handle;
|
||||
const std::string downloadPath;
|
||||
const std::string downloadUrl;
|
||||
};
|
||||
|
||||
std::atomic<bool> closed = false;
|
||||
handle_t nextHandle = 1;
|
||||
handle_t NextHandle();
|
||||
|
||||
std::vector<std::shared_ptr<DownloadRequest>> requestQueue;
|
||||
|
||||
DownloadProgress fgDownloadProgress;
|
||||
void bgUpdateDownloadProgress(const DownloadProgress &progress);
|
||||
|
||||
std::shared_ptr<DownloadRequest> activeRequest;
|
||||
|
||||
std::mutex mutex;
|
||||
std::condition_variable thread_cv;
|
||||
|
||||
|
||||
std::unique_ptr<std::jthread> thread;
|
||||
};
|
||||
}
|
||||
+47
-19
@@ -40,6 +40,7 @@
|
||||
#include "util.hpp"
|
||||
#include "HtmlHelper.hpp"
|
||||
#include "WebServerMod.hpp"
|
||||
#include "Tone3000Download.hpp"
|
||||
|
||||
#define OLD_PRESET_EXTENSION ".piPreset"
|
||||
#define PRESET_EXTENSION ".piPreset"
|
||||
@@ -145,6 +146,11 @@ public:
|
||||
return false;
|
||||
}
|
||||
std::string segment = request_uri.segment(1);
|
||||
|
||||
if (segment == "tone3000Upload")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (segment == "downloadMediaFile")
|
||||
{
|
||||
return true;
|
||||
@@ -189,10 +195,6 @@ public:
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else if (segment == "Tone3000Auth")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else if (segment == "PluginPresets")
|
||||
{
|
||||
return true;
|
||||
@@ -418,11 +420,11 @@ public:
|
||||
return true;
|
||||
}
|
||||
auto filename = path.stem();
|
||||
if (filename == "LICENSE" || filename == "README") {
|
||||
if (filename == "LICENSE" || filename == "README")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
virtual void get_response(
|
||||
@@ -435,6 +437,44 @@ 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");
|
||||
@@ -765,19 +805,7 @@ public:
|
||||
{
|
||||
std::string segment = request_uri.segment(1);
|
||||
|
||||
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")
|
||||
if (segment == "uploadPluginPresets")
|
||||
{
|
||||
PluginPresets presets;
|
||||
fs::path filePath = req.get_body_temporary_file();
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
|
||||
Tooltip on File Property Select: add to release notes.
|
||||
sAFEfILEnAMES: RENAME, &C. Safe bank names?
|
||||
NEW BANK
|
||||
WINDOW SIZE CALCUALTIONS ARE WRONG ON LINUX HIDEF DISPLAYS.
|
||||
|
||||
ls
|
||||
Carla Project icons.
|
||||
|
||||
Generated
+1730
-3
File diff suppressed because it is too large
Load Diff
@@ -25,6 +25,9 @@
|
||||
"react-virtualized-auto-sizer": "^1.0.25",
|
||||
"react-window": "^1.8.11",
|
||||
"rehype-external-links": "^3.0.0",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"rehype-sanitize": "^6.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"tss-react": "^4.9.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,26 +1,42 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Tone3000 Download Received</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<script>
|
||||
// Parse URL parameters
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const toneUrl = urlParams.get('tone_url');
|
||||
|
||||
// Send the data back to the opener window
|
||||
if (window.opener) {
|
||||
window.opener.postMessage({
|
||||
type: 'tone3000Download',
|
||||
toneUrl: toneUrl
|
||||
}, '*');
|
||||
}
|
||||
|
||||
// Close the window after sending the data
|
||||
var popupWindow = null;;
|
||||
try {
|
||||
// Send the data back to the opener window
|
||||
if (window.opener) {
|
||||
popupWindow =
|
||||
window.opener.postMessage({
|
||||
type: 'tone3000Download',
|
||||
toneUrl: toneUrl,
|
||||
popupX: window.screenX,
|
||||
popupY: window.screenY,
|
||||
popupWidth: window.innerWidth,
|
||||
popupHeight: window.innerHeight
|
||||
}, '*');
|
||||
} else {
|
||||
alert("No opener window found.");
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
// Close the window after sending the data
|
||||
alert(e.message);
|
||||
}
|
||||
window.close();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 13.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 14948) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="64px" height="64px" viewBox="5.5 -3.5 64 64" enable-background="new 5.5 -3.5 64 64" xml:space="preserve">
|
||||
<g>
|
||||
<circle fill="#FFFFFF" cx="37.637" cy="28.806" r="28.276"/>
|
||||
<g>
|
||||
<path d="M37.443-3.5c8.988,0,16.57,3.085,22.742,9.257C66.393,11.967,69.5,19.548,69.5,28.5c0,8.991-3.049,16.476-9.145,22.456
|
||||
C53.879,57.319,46.242,60.5,37.443,60.5c-8.649,0-16.153-3.144-22.514-9.43C8.644,44.784,5.5,37.262,5.5,28.5
|
||||
c0-8.761,3.144-16.342,9.429-22.742C21.101-0.415,28.604-3.5,37.443-3.5z M37.557,2.272c-7.276,0-13.428,2.553-18.457,7.657
|
||||
c-5.22,5.334-7.829,11.525-7.829,18.572c0,7.086,2.59,13.22,7.77,18.398c5.181,5.182,11.352,7.771,18.514,7.771
|
||||
c7.123,0,13.334-2.607,18.629-7.828c5.029-4.838,7.543-10.952,7.543-18.343c0-7.276-2.553-13.465-7.656-18.571
|
||||
C50.967,4.824,44.795,2.272,37.557,2.272z M46.129,20.557v13.085h-3.656v15.542h-9.944V33.643h-3.656V20.557
|
||||
c0-0.572,0.2-1.057,0.599-1.457c0.401-0.399,0.887-0.6,1.457-0.6h13.144c0.533,0,1.01,0.2,1.428,0.6
|
||||
C45.918,19.5,46.129,19.986,46.129,20.557z M33.042,12.329c0-3.008,1.485-4.514,4.458-4.514s4.457,1.504,4.457,4.514
|
||||
c0,2.971-1.486,4.457-4.457,4.457S33.042,15.3,33.042,12.329z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 13.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 14948) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="64px" height="64px" viewBox="5.5 -3.5 64 64" enable-background="new 5.5 -3.5 64 64" xml:space="preserve">
|
||||
<g>
|
||||
<circle fill="#FFFFFF" cx="37.785" cy="28.501" r="28.836"/>
|
||||
<path d="M37.441-3.5c8.951,0,16.572,3.125,22.857,9.372c3.008,3.009,5.295,6.448,6.857,10.314
|
||||
c1.561,3.867,2.344,7.971,2.344,12.314c0,4.381-0.773,8.486-2.314,12.313c-1.543,3.828-3.82,7.21-6.828,10.143
|
||||
c-3.123,3.085-6.666,5.448-10.629,7.086c-3.961,1.638-8.057,2.457-12.285,2.457s-8.276-0.808-12.143-2.429
|
||||
c-3.866-1.618-7.333-3.961-10.4-7.027c-3.067-3.066-5.4-6.524-7-10.372S5.5,32.767,5.5,28.5c0-4.229,0.809-8.295,2.428-12.2
|
||||
c1.619-3.905,3.972-7.4,7.057-10.486C21.08-0.394,28.565-3.5,37.441-3.5z M37.557,2.272c-7.314,0-13.467,2.553-18.458,7.657
|
||||
c-2.515,2.553-4.448,5.419-5.8,8.6c-1.354,3.181-2.029,6.505-2.029,9.972c0,3.429,0.675,6.734,2.029,9.913
|
||||
c1.353,3.183,3.285,6.021,5.8,8.516c2.514,2.496,5.351,4.399,8.515,5.715c3.161,1.314,6.476,1.971,9.943,1.971
|
||||
c3.428,0,6.75-0.665,9.973-1.999c3.219-1.335,6.121-3.257,8.713-5.771c4.99-4.876,7.484-10.99,7.484-18.344
|
||||
c0-3.543-0.648-6.895-1.943-10.057c-1.293-3.162-3.18-5.98-5.654-8.458C50.984,4.844,44.795,2.272,37.557,2.272z M37.156,23.187
|
||||
l-4.287,2.229c-0.458-0.951-1.019-1.619-1.685-2c-0.667-0.38-1.286-0.571-1.858-0.571c-2.856,0-4.286,1.885-4.286,5.657
|
||||
c0,1.714,0.362,3.084,1.085,4.113c0.724,1.029,1.791,1.544,3.201,1.544c1.867,0,3.181-0.915,3.944-2.743l3.942,2
|
||||
c-0.838,1.563-2,2.791-3.486,3.686c-1.484,0.896-3.123,1.343-4.914,1.343c-2.857,0-5.163-0.875-6.915-2.629
|
||||
c-1.752-1.752-2.628-4.19-2.628-7.313c0-3.048,0.886-5.466,2.657-7.257c1.771-1.79,4.009-2.686,6.715-2.686
|
||||
C32.604,18.558,35.441,20.101,37.156,23.187z M55.613,23.187l-4.229,2.229c-0.457-0.951-1.02-1.619-1.686-2
|
||||
c-0.668-0.38-1.307-0.571-1.914-0.571c-2.857,0-4.287,1.885-4.287,5.657c0,1.714,0.363,3.084,1.086,4.113
|
||||
c0.723,1.029,1.789,1.544,3.201,1.544c1.865,0,3.18-0.915,3.941-2.743l4,2c-0.875,1.563-2.057,2.791-3.541,3.686
|
||||
c-1.486,0.896-3.105,1.343-4.857,1.343c-2.896,0-5.209-0.875-6.941-2.629c-1.736-1.752-2.602-4.19-2.602-7.313
|
||||
c0-3.048,0.885-5.466,2.658-7.257c1.77-1.79,4.008-2.686,6.713-2.686C51.117,18.558,53.938,20.101,55.613,23.187z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 13.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 14948) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="64px" height="64px" viewBox="-0.5 0.5 64 64" enable-background="new -0.5 0.5 64 64" xml:space="preserve">
|
||||
<g>
|
||||
<circle fill="#FFFFFF" cx="31.325" cy="32.873" r="30.096"/>
|
||||
<path id="text2809_1_" d="M31.5,14.08c-10.565,0-13.222,9.969-13.222,18.42c0,8.452,2.656,18.42,13.222,18.42
|
||||
c10.564,0,13.221-9.968,13.221-18.42C44.721,24.049,42.064,14.08,31.5,14.08z M31.5,21.026c0.429,0,0.82,0.066,1.188,0.157
|
||||
c0.761,0.656,1.133,1.561,0.403,2.823l-7.036,12.93c-0.216-1.636-0.247-3.24-0.247-4.437C25.808,28.777,26.066,21.026,31.5,21.026z
|
||||
M36.766,26.987c0.373,1.984,0.426,4.056,0.426,5.513c0,3.723-0.258,11.475-5.69,11.475c-0.428,0-0.822-0.045-1.188-0.136
|
||||
c-0.07-0.021-0.134-0.043-0.202-0.067c-0.112-0.032-0.23-0.068-0.336-0.11c-1.21-0.515-1.972-1.446-0.874-3.093L36.766,26.987z"/>
|
||||
<path id="path2815_1_" d="M31.433,0.5c-8.877,0-16.359,3.09-22.454,9.3c-3.087,3.087-5.443,6.607-7.082,10.532
|
||||
C0.297,24.219-0.5,28.271-0.5,32.5c0,4.268,0.797,8.32,2.397,12.168c1.6,3.85,3.921,7.312,6.969,10.396
|
||||
c3.085,3.049,6.549,5.399,10.398,7.037c3.886,1.602,7.939,2.398,12.169,2.398c4.229,0,8.34-0.826,12.303-2.465
|
||||
c3.962-1.639,7.496-3.994,10.621-7.081c3.011-2.933,5.289-6.297,6.812-10.106C62.73,41,63.5,36.883,63.5,32.5
|
||||
c0-4.343-0.77-8.454-2.33-12.303c-1.562-3.885-3.848-7.32-6.857-10.33C48.025,3.619,40.385,0.5,31.433,0.5z M31.567,6.259
|
||||
c7.238,0,13.412,2.566,18.554,7.709c2.477,2.477,4.375,5.31,5.67,8.471c1.296,3.162,1.949,6.518,1.949,10.061
|
||||
c0,7.354-2.516,13.454-7.506,18.33c-2.592,2.516-5.502,4.447-8.74,5.781c-3.2,1.334-6.498,1.994-9.927,1.994
|
||||
c-3.468,0-6.788-0.653-9.949-1.948c-3.163-1.334-6.001-3.238-8.516-5.716c-2.515-2.514-4.455-5.353-5.826-8.516
|
||||
c-1.333-3.199-2.017-6.498-2.017-9.927c0-3.467,0.684-6.787,2.017-9.949c1.371-3.2,3.312-6.074,5.826-8.628
|
||||
C18.092,8.818,24.252,6.259,31.567,6.259z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 13.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 14948) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="64px" height="64px" viewBox="5.5 -3.5 64 64" enable-background="new 5.5 -3.5 64 64" xml:space="preserve">
|
||||
<g>
|
||||
<circle fill="#FFFFFF" cx="37.47" cy="28.736" r="29.471"/>
|
||||
<g>
|
||||
<path d="M37.442-3.5c8.99,0,16.571,3.085,22.743,9.256C66.393,11.928,69.5,19.509,69.5,28.5c0,8.992-3.048,16.476-9.145,22.458
|
||||
C53.88,57.32,46.241,60.5,37.442,60.5c-8.686,0-16.19-3.162-22.513-9.485C8.644,44.728,5.5,37.225,5.5,28.5
|
||||
c0-8.762,3.144-16.343,9.429-22.743C21.1-0.414,28.604-3.5,37.442-3.5z M12.7,19.872c-0.952,2.628-1.429,5.505-1.429,8.629
|
||||
c0,7.086,2.59,13.22,7.77,18.4c5.219,5.144,11.391,7.715,18.514,7.715c7.201,0,13.409-2.608,18.63-7.829
|
||||
c1.867-1.79,3.332-3.657,4.398-5.602l-12.056-5.371c-0.421,2.02-1.439,3.667-3.057,4.942c-1.622,1.276-3.535,2.011-5.744,2.2
|
||||
v4.915h-3.714v-4.915c-3.543-0.036-6.782-1.312-9.714-3.827l4.4-4.457c2.094,1.942,4.476,2.913,7.143,2.913
|
||||
c1.104,0,2.048-0.246,2.83-0.743c0.78-0.494,1.172-1.312,1.172-2.457c0-0.801-0.287-1.448-0.858-1.943l-3.085-1.315l-3.771-1.715
|
||||
l-5.086-2.229L12.7,19.872z M37.557,2.214c-7.276,0-13.428,2.571-18.457,7.714c-1.258,1.258-2.439,2.686-3.543,4.287L27.786,19.7
|
||||
c0.533-1.676,1.542-3.019,3.029-4.028c1.484-1.009,3.218-1.571,5.2-1.686V9.071h3.715v4.915c2.934,0.153,5.6,1.143,8,2.971
|
||||
l-4.172,4.286c-1.793-1.257-3.619-1.885-5.486-1.885c-0.991,0-1.876,0.191-2.656,0.571c-0.781,0.381-1.172,1.029-1.172,1.943
|
||||
c0,0.267,0.095,0.533,0.285,0.8l4.057,1.83l2.8,1.257l5.144,2.285l16.397,7.314c0.535-2.248,0.801-4.533,0.801-6.857
|
||||
c0-7.353-2.552-13.543-7.656-18.573C51.005,4.785,44.831,2.214,37.557,2.214z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 13.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 14948) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="64.000977px" height="64px" viewBox="0 0 64.000977 64" enable-background="new 0 0 64.000977 64" xml:space="preserve">
|
||||
<g>
|
||||
<circle fill="#FFFFFF" cx="32.064453" cy="31.788086" r="29.012695"/>
|
||||
<g>
|
||||
<path d="M31.943848,0C40.896484,0,48.476562,3.105469,54.6875,9.314453C60.894531,15.486328,64.000977,23.045898,64.000977,32
|
||||
s-3.048828,16.457031-9.145508,22.513672C48.417969,60.837891,40.779297,64,31.942871,64
|
||||
c-8.648926,0-16.152832-3.142578-22.513672-9.429688C3.144043,48.286133,0,40.761719,0,32.000977
|
||||
c0-8.723633,3.144043-16.285156,9.429199-22.68457C15.640137,3.105469,23.14502,0,31.943848,0z M32.060547,5.771484
|
||||
c-7.275391,0-13.429688,2.570312-18.458496,7.714844C8.381836,18.783203,5.772949,24.954102,5.772949,32
|
||||
c0,7.125,2.589844,13.256836,7.77002,18.400391c5.181152,5.181641,11.352051,7.770508,18.515625,7.770508
|
||||
c7.123047,0,13.332031-2.608398,18.626953-7.828125C55.713867,45.466797,58.228516,39.353516,58.228516,32
|
||||
c0-7.3125-2.553711-13.484375-7.65625-18.513672C45.504883,8.341797,39.333984,5.771484,32.060547,5.771484z M44.117188,24.456055
|
||||
v5.485352H20.859863v-5.485352H44.117188z M44.117188,34.743164v5.481445H20.859863v-5.481445H44.117188z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 13.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 14948) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="64px" height="64px" viewBox="5.5 -3.5 64 64" enable-background="new 5.5 -3.5 64 64" xml:space="preserve">
|
||||
<g>
|
||||
<circle fill="#FFFFFF" cx="36.944" cy="28.631" r="29.105"/>
|
||||
<g>
|
||||
<path d="M37.443-3.5c8.951,0,16.531,3.105,22.742,9.315C66.393,11.987,69.5,19.548,69.5,28.5c0,8.954-3.049,16.457-9.145,22.514
|
||||
C53.918,57.338,46.279,60.5,37.443,60.5c-8.649,0-16.153-3.143-22.514-9.429C8.644,44.786,5.5,37.264,5.5,28.501
|
||||
c0-8.723,3.144-16.285,9.429-22.685C21.138-0.395,28.643-3.5,37.443-3.5z M37.557,2.272c-7.276,0-13.428,2.572-18.457,7.715
|
||||
c-5.22,5.296-7.829,11.467-7.829,18.513c0,7.125,2.59,13.257,7.77,18.4c5.181,5.182,11.352,7.771,18.514,7.771
|
||||
c7.123,0,13.334-2.609,18.629-7.828c5.029-4.876,7.543-10.99,7.543-18.343c0-7.313-2.553-13.485-7.656-18.513
|
||||
C51.004,4.842,44.832,2.272,37.557,2.272z M23.271,23.985c0.609-3.924,2.189-6.962,4.742-9.114
|
||||
c2.552-2.152,5.656-3.228,9.314-3.228c5.027,0,9.029,1.62,12,4.856c2.971,3.238,4.457,7.391,4.457,12.457
|
||||
c0,4.915-1.543,9-4.627,12.256c-3.088,3.256-7.086,4.886-12.002,4.886c-3.619,0-6.743-1.085-9.371-3.257
|
||||
c-2.629-2.172-4.209-5.257-4.743-9.257H31.1c0.19,3.886,2.533,5.829,7.029,5.829c2.246,0,4.057-0.972,5.428-2.914
|
||||
c1.373-1.942,2.059-4.534,2.059-7.771c0-3.391-0.629-5.971-1.885-7.743c-1.258-1.771-3.066-2.657-5.43-2.657
|
||||
c-4.268,0-6.667,1.885-7.2,5.656h2.343l-6.342,6.343l-6.343-6.343L23.271,23.985L23.271,23.985z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,44 @@
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Test Tone3000 Select</title>
|
||||
|
||||
<script>
|
||||
window.addEventListener("message", (event) => {
|
||||
localStorage.setItem("t3000popupX", event.data.popupX);
|
||||
localStorage.setItem("t3000popupY", event.data.popupY);
|
||||
localStorage.setItem("t3000popupWidth", event.data.popupWidth);
|
||||
localStorage.setItem("t3000popupHeight", event.data.popupHeight);
|
||||
alert("Message from popup:\n\n" +
|
||||
JSON.stringify(event.data));
|
||||
});
|
||||
|
||||
var popup = null;
|
||||
function onPopupClick() {
|
||||
let savedWidth = localStorage.getItem("t3000popupWidth") ?? window.innerWidth *0.8;
|
||||
let savedHeight = localStorage.getItem("t3000popupHeight") ?? window.innerHeight *0.8;
|
||||
let savedX = localStorage.getItem("t3000popupX") || 40;
|
||||
let savedY = localStorage.getItem("t3000popupY") || 40;
|
||||
|
||||
if (savedX < 0) savedX = 0;
|
||||
if (savedY < 0) savedY = 0;
|
||||
if (savedWidth < 640) savedWidth = 640;
|
||||
if (savedHeight < 480) savedHeight = 480;
|
||||
|
||||
let popupWin =
|
||||
window.open(
|
||||
"https://www.tone3000.com/api/v1/select?app_id=pipedal_app&redirect_url=http://localhost:5173/public/handleTone3000download.html",
|
||||
"tone3000SelectPopup",
|
||||
`screenX=${savedX},screenY=${savedY},innerWidth=${savedWidth},innerHeight=${savedHeight},scrollbars=yes`
|
||||
);
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>Test Tone3000 Select Page</h1>
|
||||
<p>This is a test page for Tone3000 select functionality.</p>
|
||||
<Button onclick="onPopupClick()">Open Tone 3000</Button>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -38,13 +38,78 @@ function getVersionSuffix(filePath: string): string | null {
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
export function safeFilenameDecode(filename: string): string {
|
||||
let originalFilename = filename;
|
||||
let pos = filename.indexOf('%');
|
||||
if (pos < 0) {
|
||||
return filename;
|
||||
}
|
||||
let result = "";
|
||||
while (true) {
|
||||
if (pos < 0) {
|
||||
result += filename;
|
||||
return result;
|
||||
}
|
||||
result += filename.substring(0, pos);
|
||||
if (pos+3 >= filename.length) {
|
||||
// invalid encoding. just use the original
|
||||
return originalFilename;
|
||||
}
|
||||
let hex = filename.charAt(pos+1) + filename.charAt(pos+2);
|
||||
let byte = parseInt(hex, 16);
|
||||
|
||||
// Handle UTF-8 to UTF-16 conversion
|
||||
if (byte < 0x80) {
|
||||
// Single-byte character (ASCII)
|
||||
result += String.fromCharCode(byte);
|
||||
filename = filename.substring(pos+3);
|
||||
} else if ((byte & 0xE0) === 0xC0) {
|
||||
// 2-byte sequence
|
||||
if (pos+6 > filename.length || filename.charAt(pos+3) !== '%') {
|
||||
return originalFilename; // invalid encoding
|
||||
}
|
||||
let byte2 = parseInt(filename.charAt(pos+4) + filename.charAt(pos+5), 16);
|
||||
let codePoint = ((byte & 0x1F) << 6) | (byte2 & 0x3F);
|
||||
result += String.fromCharCode(codePoint);
|
||||
filename = filename.substring(pos+6);
|
||||
} else if ((byte & 0xF0) === 0xE0) {
|
||||
// 3-byte sequence
|
||||
if (pos+9 > filename.length || filename.charAt(pos+3) !== '%' || filename.charAt(pos+6) !== '%') {
|
||||
return originalFilename; // invalid encoding
|
||||
}
|
||||
let byte2 = parseInt(filename.charAt(pos+4) + filename.charAt(pos+5), 16);
|
||||
let byte3 = parseInt(filename.charAt(pos+7) + filename.charAt(pos+8), 16);
|
||||
let codePoint = ((byte & 0x0F) << 12) | ((byte2 & 0x3F) << 6) | (byte3 & 0x3F);
|
||||
result += String.fromCharCode(codePoint);
|
||||
filename = filename.substring(pos+9);
|
||||
} else if ((byte & 0xF8) === 0xF0) {
|
||||
// 4-byte sequence (surrogate pair needed)
|
||||
if (pos+12 > filename.length || filename.charAt(pos+3) !== '%' || filename.charAt(pos+6) !== '%' || filename.charAt(pos+9) !== '%') {
|
||||
return originalFilename; // invalid encoding
|
||||
}
|
||||
let byte2 = parseInt(filename.charAt(pos+4) + filename.charAt(pos+5), 16);
|
||||
let byte3 = parseInt(filename.charAt(pos+7) + filename.charAt(pos+8), 16);
|
||||
let byte4 = parseInt(filename.charAt(pos+10) + filename.charAt(pos+11), 16);
|
||||
let codePoint = ((byte & 0x07) << 18) | ((byte2 & 0x3F) << 12) | ((byte3 & 0x3F) << 6) | (byte4 & 0x3F);
|
||||
// Convert to UTF-16 surrogate pair
|
||||
codePoint -= 0x10000;
|
||||
result += String.fromCharCode(0xD800 + (codePoint >> 10));
|
||||
result += String.fromCharCode(0xDC00 + (codePoint & 0x3FF));
|
||||
filename = filename.substring(pos+12);
|
||||
} else {
|
||||
// Invalid UTF-8 start byte
|
||||
return originalFilename;
|
||||
}
|
||||
pos = filename.indexOf('%');
|
||||
}
|
||||
}
|
||||
|
||||
export function getTrackTitle(pathname: string, metadata: AudioFileMetadata | null | undefined): string {
|
||||
if (!metadata) {
|
||||
return pathFileName(pathname);
|
||||
return safeFilenameDecode(pathFileName(pathname));
|
||||
}
|
||||
if (metadata.title === "") {
|
||||
return pathFileName(pathname);
|
||||
return safeFilenameDecode(pathFileName(pathname));
|
||||
}
|
||||
let trackDisplay = "";
|
||||
if (metadata.track > 0) {
|
||||
@@ -56,7 +121,7 @@ export function getTrackTitle(pathname: string, metadata: AudioFileMetadata | nu
|
||||
}
|
||||
let result = trackDisplay + metadata.title;
|
||||
if (result === "") {
|
||||
result = pathFileName(pathname);
|
||||
result = safeFilenameDecode(pathFileName(pathname));
|
||||
}
|
||||
let versionSuffix = getVersionSuffix(pathname);
|
||||
if (versionSuffix) {
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
import { Component, SyntheticEvent } from 'react';
|
||||
import { Theme } from '@mui/material/styles';
|
||||
import { css } from '@emotion/react';
|
||||
import ToolTipEx from './ToolTipEx';
|
||||
|
||||
import { UiFileProperty } from './Lv2Plugin';
|
||||
import Typography from '@mui/material/Typography';
|
||||
@@ -33,15 +34,16 @@ import ButtonBase from '@mui/material/ButtonBase'
|
||||
import MoreHorizIcon from '@mui/icons-material/MoreHoriz';
|
||||
import { PedalboardItem } from './Pedalboard';
|
||||
import { isDarkMode } from './DarkMode';
|
||||
|
||||
|
||||
export const StandardItemSize = { width: 80, height: 140 }
|
||||
import {safeFilenameDecode} from './AudioFileMetadata';
|
||||
|
||||
|
||||
import { withStyles } from "tss-react/mui";
|
||||
import WithStyles from './WithStyles';
|
||||
import { withTheme } from './WithStyles';
|
||||
|
||||
|
||||
export const StandardItemSize = { width: 80, height: 140 }
|
||||
|
||||
const styles = (theme: Theme) => {
|
||||
return {
|
||||
frame: css({
|
||||
@@ -209,11 +211,13 @@ const FilePropertyControl =
|
||||
let fileProperty = this.props.fileProperty;
|
||||
|
||||
let value = "\u00A0";
|
||||
let toolTipText: string | null = null;
|
||||
if (this.state.hasValue) {
|
||||
if (this.state.value.length === 0) {
|
||||
value = "<none>";
|
||||
} else {
|
||||
value = this.fileNameOnly(this.state.value);
|
||||
value = safeFilenameDecode(this.fileNameOnly(this.state.value));
|
||||
toolTipText = value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,7 +237,7 @@ const FilePropertyControl =
|
||||
{/* CONTROL SECTION */}
|
||||
|
||||
<div className={classes.midSection} style={{ width: "100%", paddingLeft: 8 }}>
|
||||
|
||||
<ToolTipEx title={toolTipText}>
|
||||
<ButtonBase style={{ width: "100%", borderRadius: "4px 4px 0px 0px", overflow: "hidden" }} onClick={() => { this.onFileClick() }} >
|
||||
<div style={{
|
||||
width: "100%", background:
|
||||
@@ -249,6 +253,7 @@ const FilePropertyControl =
|
||||
<div style={{ height: "1px", width: "100%", background: this.props.theme.palette.text.secondary }}> </div>
|
||||
</div>
|
||||
</ButtonBase>
|
||||
</ToolTipEx>
|
||||
</div>
|
||||
|
||||
{/* LABEL/EDIT SECTION*/}
|
||||
|
||||
@@ -21,8 +21,6 @@
|
||||
import React from 'react';
|
||||
import { createStyles } from './WithStyles';
|
||||
|
||||
import Tone3000Dialog from './Tone3000Dialog';
|
||||
|
||||
import TextInfoDialog from './TextInfoDialog';
|
||||
import Tone3000HelpDialog from './Tone3000HelpDialog';
|
||||
import GuitarMLHelpDialog from './GuitarMlHelpDialog';
|
||||
@@ -69,6 +67,7 @@ import OkCancelDialog from './OkCancelDialog';
|
||||
import HomeIcon from '@mui/icons-material/Home';
|
||||
import FilePropertyDirectorySelectDialog from './FilePropertyDirectorySelectDialog';
|
||||
import { getAlbumArtUri, getTrackTitle } from './AudioFileMetadata';
|
||||
import { Tone3000DownloadDialog } from './Tone3000Dialog';
|
||||
|
||||
|
||||
const ToobNamModelFileUrl = "http://two-play.com/plugins/toob-nam#modelFile";
|
||||
@@ -705,7 +704,7 @@ export default withStyles(
|
||||
if (this.state.previousSelection == selectedItem) {
|
||||
return;
|
||||
}
|
||||
if (!this.isLicenseFile(selectedItem) && !this.isFolderArtwork(selectedItem)) {
|
||||
if (!this.isTextFile(selectedItem) && !this.isFolderArtwork(selectedItem)) {
|
||||
this.props.onApply(fileProperty, selectedItem);
|
||||
}
|
||||
this.setState({ previousSelection: selectedItem });
|
||||
@@ -1071,9 +1070,7 @@ export default withStyles(
|
||||
handleTone3000Dialog(e: React.MouseEvent<HTMLButtonElement>) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
this.setState({ openTone3000Dialog: true });
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -1743,14 +1740,13 @@ export default withStyles(
|
||||
)
|
||||
}
|
||||
{this.state.openTone3000Dialog && (
|
||||
<Tone3000Dialog
|
||||
open={this.state.openTone3000Dialog}
|
||||
onCancel={() => this.setState({ openTone3000Dialog: false })}
|
||||
onDownload={(toneUrl: string) => {
|
||||
alert("Donload tone url: " + toneUrl);
|
||||
<Tone3000DownloadDialog
|
||||
onClose={() => this.setState({ openTone3000Dialog: false })}
|
||||
downloadPath={this.state.currentDirectory}
|
||||
onDownloadComplete={() => {
|
||||
this.setState({ openTone3000Dialog: false });
|
||||
}
|
||||
}
|
||||
this.requestFiles(this.state.navDirectory);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{this.state.openTone3000Help && (
|
||||
@@ -1774,15 +1770,11 @@ export default withStyles(
|
||||
</DialogEx>
|
||||
);
|
||||
}
|
||||
isLicenseFile(fileName: string) {
|
||||
isTextFile(fileName: string) {
|
||||
let extension = pathExtension(fileName);
|
||||
if (extension === ".txt" || extension === ".md") {
|
||||
return true;
|
||||
}
|
||||
let fileNameOnly = pathFileNameOnly(fileName);
|
||||
if (fileNameOnly.toUpperCase() === "LICENSE" || fileNameOnly.toUpperCase() === "README") {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
}
|
||||
@@ -1801,7 +1793,7 @@ export default withStyles(
|
||||
this.requestFiles(this.state.selectedFile);
|
||||
this.setState({ navDirectory: this.state.selectedFile });
|
||||
} else {
|
||||
if (this.isLicenseFile(this.state.selectedFile)) {
|
||||
if (this.isTextFile(this.state.selectedFile)) {
|
||||
this.handleShowTextFile(this.state.selectedFile);
|
||||
} else {
|
||||
this.props.onOk(this.props.fileProperty, this.state.selectedFile);
|
||||
|
||||
@@ -27,6 +27,7 @@ import { Pedalboard, PedalboardItem, ControlValue, Snapshot } from './Pedalboard
|
||||
import PluginClass from './PluginClass';
|
||||
import ScreenOrientation from './ScreenOrientation';
|
||||
import PiPedalSocket, { PiPedalMessageHeader } from './PiPedalSocket';
|
||||
import {Tone3000DownloadHandler} from './Tone3000Dialog';
|
||||
import { nullCast } from './Utility'
|
||||
import { JackConfiguration, JackChannelSelection } from './Jack';
|
||||
import { BankIndex } from './Banks';
|
||||
@@ -60,7 +61,7 @@ export enum State {
|
||||
HotspotChanging,
|
||||
};
|
||||
|
||||
function getErrorMessage(error: any) {
|
||||
export function getErrorMessage(error: any) {
|
||||
if (error instanceof Error) {
|
||||
return (error as Error).message;
|
||||
}
|
||||
@@ -494,7 +495,6 @@ export class PiPedalModel //implements PiPedalModel
|
||||
static getInstance(): PiPedalModel {
|
||||
return PiPedalModelFactory.getInstance();
|
||||
}
|
||||
hasTone3000Auth: ObservableProperty<boolean> = new ObservableProperty<boolean>(false);
|
||||
|
||||
canKeepScreenOn: boolean = false;
|
||||
keepScreenOn: ObservableProperty<boolean> = new ObservableProperty<boolean>(false);
|
||||
@@ -552,6 +552,8 @@ export class PiPedalModel //implements PiPedalModel
|
||||
|
||||
uiPluginsByUri: Map<string, UiPlugin> = new Map<string, UiPlugin>();
|
||||
|
||||
private tone3000DownloadHandler: Tone3000DownloadHandler | null = null;
|
||||
|
||||
svgImgUrl(svgImage: string): string {
|
||||
// return this.varServerUrl + "img/" + svgImage;
|
||||
return "img/" + svgImage;
|
||||
@@ -651,6 +653,53 @@ export class PiPedalModel //implements PiPedalModel
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
async downloadModelsFromTone3000(
|
||||
tone3000DownloadUrl: string,
|
||||
downloadPath: string
|
||||
): Promise<void> {
|
||||
try {
|
||||
if (!this.webSocket) {
|
||||
return;
|
||||
}
|
||||
this.webSocket.send("downloadModelsFromTone3000",
|
||||
{
|
||||
downloadPath: downloadPath,
|
||||
tone3000Url: tone3000DownloadUrl
|
||||
});
|
||||
} catch (error) {
|
||||
this.showAlert(getErrorMessage(error));
|
||||
}
|
||||
}
|
||||
|
||||
showTone3000DownloadDialog(
|
||||
downloadPath: string,
|
||||
onClosed: () => void,
|
||||
onDownloadStarted: () => void,
|
||||
onDownloadComplete: () => void
|
||||
|
||||
): void {
|
||||
if (this.tone3000DownloadHandler === null) {
|
||||
this.tone3000DownloadHandler = new Tone3000DownloadHandler(this);
|
||||
}
|
||||
this.tone3000DownloadHandler.launchTone3000Dialog(
|
||||
downloadPath,
|
||||
onClosed,
|
||||
(errorMessage: string) => {
|
||||
this.showAlert(errorMessage);
|
||||
onClosed();
|
||||
},
|
||||
onDownloadStarted,
|
||||
onDownloadComplete
|
||||
);
|
||||
|
||||
};
|
||||
closeTone3000DownloadDialog(): void {
|
||||
if (this.tone3000DownloadHandler) {
|
||||
this.tone3000DownloadHandler.closeTone3000Dialog();
|
||||
}
|
||||
}
|
||||
|
||||
private updateEnabledItems(pedalboard: Pedalboard) {
|
||||
for (let item of pedalboard.itemsGenerator()) {
|
||||
this.updatePedalboardItemEnabled(item.instanceId, item.isEnabled);
|
||||
@@ -862,10 +911,7 @@ 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") {
|
||||
} else if (message === "onLv2PluginsChanging") {
|
||||
this.onLv2PluginsChanging();
|
||||
} else if (message === "onUpdateStatusChanged") {
|
||||
let updateStatus = new UpdateStatus().deserialize(body);
|
||||
@@ -1256,9 +1302,6 @@ 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"));
|
||||
@@ -1539,8 +1582,7 @@ export class PiPedalModel //implements PiPedalModel
|
||||
this.webSocket?.send("setSnapshot", index);
|
||||
}
|
||||
|
||||
private pruneSnapshotValues(pedalboard: Pedalboard)
|
||||
{
|
||||
private pruneSnapshotValues(pedalboard: Pedalboard) {
|
||||
let validPluginIds: Set<number> = new Set<number>();
|
||||
|
||||
let it = pedalboard.itemsGenerator();
|
||||
@@ -1572,7 +1614,7 @@ export class PiPedalModel //implements PiPedalModel
|
||||
pedalboard.selectedSnapshot = selectedSnapshot;
|
||||
}
|
||||
this.pruneSnapshotValues(pedalboard);
|
||||
|
||||
|
||||
this.setModelPedalboard(pedalboard);
|
||||
|
||||
this.webSocket?.send("setSnapshots", { snapshots: pedalboard.snapshots, selectedSnapshot: selectedSnapshot });
|
||||
@@ -2752,6 +2794,29 @@ export class PiPedalModel //implements PiPedalModel
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
|
||||
async uploadTone3000File(url: string) : Promise<boolean> {
|
||||
try {
|
||||
let response = await fetch(
|
||||
url,
|
||||
{
|
||||
method: "GET",
|
||||
}
|
||||
);
|
||||
|
||||
let json = await response.json();
|
||||
|
||||
if (json.success === true) {
|
||||
return true;
|
||||
}
|
||||
if (json.errorMessage !== undefined)
|
||||
{
|
||||
throw new Error(json.errorMessage);
|
||||
}
|
||||
throw new Error("Invalid server response.");
|
||||
} catch (error) {
|
||||
throw new Error("Upload to PiPedal server failed. " + getErrorMessage(error));
|
||||
}
|
||||
}
|
||||
uploadUserFile(uploadPage: string, file: File, contentType: string = "application/octet-stream", abortController?: AbortController): Promise<string> {
|
||||
let result = new Promise<string>((resolve, reject) => {
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
|
||||
.text-info-dialog-content #user-content-tone3000_float {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
align-items: justify;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.text-info-dialog-content #user-content-tone3000_float_content {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: space-between;
|
||||
|
||||
}
|
||||
|
||||
.text-info-dialog-content #user-content-tone3000_title {
|
||||
margin-top: 0;
|
||||
margin-bottom: 12px;
|
||||
padding: 0;
|
||||
}
|
||||
.text-info-dialog-content #user-content-tone3000_subtitle {
|
||||
font-size: 0.8em;
|
||||
margin-top: 0px;
|
||||
margin-bottom: 0px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.text-info-dialog-content #user-content-cc_img {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
.text-info-dialog-content #user-content-tone3000_thumbnail {
|
||||
width: 130px;
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
margin-right: 24px;
|
||||
border: 1px solid var(--pd-border-color);
|
||||
border-radius: 6px;
|
||||
align-self: flex-start;
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.text-info-dialog-content #user-content-tone3000_thumbnail {
|
||||
width: 80px;
|
||||
margin-right: 20px;
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@
|
||||
|
||||
|
||||
import React, { SyntheticEvent } from 'react';
|
||||
import './TextInfoDialog.css';
|
||||
import IconButtonEx from './IconButtonEx';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { PiPedalModel, PiPedalModelFactory, State } from './PiPedalModel';
|
||||
@@ -27,7 +28,10 @@ import AppBar from '@mui/material/AppBar';
|
||||
import Toolbar from '@mui/material/Toolbar';
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import rehypeRaw from 'rehype-raw';
|
||||
import rehypeSanitize, {defaultSchema} from 'rehype-sanitize';
|
||||
import rehypeExternalLinks from 'rehype-external-links'
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import { PiPedalError } from './PiPedalError';
|
||||
import DialogEx from './DialogEx';
|
||||
import ResizeResponsiveComponent from './ResizeResponsiveComponent';
|
||||
@@ -139,7 +143,6 @@ const TextInfoDialog = class extends ResizeResponsiveComponent<TextInfoDialogPro
|
||||
onEnterKey={() => { this.props.onClose() }}
|
||||
style={{ userSelect: "none" }}
|
||||
>
|
||||
|
||||
<div style={{ display: "flex", flexDirection: "column", flexWrap: "nowrap", width: "100%", height: "100%", overflow: "hidden" }}>
|
||||
<div style={{ flex: "0 0 auto" }}>
|
||||
<AppBar style={{
|
||||
@@ -168,8 +171,14 @@ const TextInfoDialog = class extends ResizeResponsiveComponent<TextInfoDialogPro
|
||||
display: "flex", flexDirection: "row", flexWrap: "nowrap", alignItems: "start",
|
||||
}}>
|
||||
<div style={{ flex: "1 1 0px" }} />
|
||||
<div style={{ flex: "1 1 auto",maxWidth: 650 }} >
|
||||
<ReactMarkdown rehypePlugins={[[rehypeExternalLinks, {target: '_blank'}]]}>
|
||||
<div className="text-info-dialog-content" style={{ flex: "1 1 auto",maxWidth: 650 }} >
|
||||
<ReactMarkdown rehypePlugins={
|
||||
[
|
||||
[remarkGfm],
|
||||
[rehypeExternalLinks, {target: '_blank'}],
|
||||
rehypeRaw,
|
||||
[rehypeSanitize, defaultSchema]
|
||||
]}>
|
||||
{this.state.textFileContent}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
|
||||
+244
-108
@@ -1,125 +1,261 @@
|
||||
import { JSX } from "@emotion/react/jsx-dev-runtime";
|
||||
import { PiPedalModel, 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 React from 'react';
|
||||
import AppBar from '@mui/material/AppBar';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
|
||||
import DialogEx from './DialogEx';
|
||||
import Toolbar from '@mui/material/Toolbar';
|
||||
import IconButtonEx from './IconButtonEx';
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
export type OnTone3000DownloadHandler = (tone3000DownloadUrl: string) => void;
|
||||
|
||||
|
||||
// used to check for online status.
|
||||
let TONE3000_PING_URL = "https://www.tone3000.com/robots.txt";
|
||||
|
||||
export class Tone3000DownloadHandler {
|
||||
|
||||
export interface Tone3000DialogProps {
|
||||
open: boolean;
|
||||
onCancel: () => void;
|
||||
onDownload: (toneUrl: string) => void;
|
||||
}
|
||||
private async handleTone3000Download(tone3000DownloadUrl: string): Promise<void> {
|
||||
try {
|
||||
// Dialog can close. We'll use the following await to manage
|
||||
// lifecycle from here onward.
|
||||
this.stopTone3000DialogMonitor();
|
||||
|
||||
interface Tone3000AuthState {
|
||||
authToken: string;
|
||||
refreshToken: string;
|
||||
expiry: number;
|
||||
};
|
||||
|
||||
// function getAuthToken(): Tone3000AuthState | null {
|
||||
|
||||
// let authToken = window.localStorage.getItem("tone300_auth_token");
|
||||
// if (authToken === null) {
|
||||
// return null;
|
||||
// }
|
||||
// let refreshToken = window.localStorage.getItem("tone3000_refresh_token");
|
||||
// if (refreshToken === null) {
|
||||
// return null;
|
||||
// }
|
||||
// let expiry = window.localStorage.getTime("tone3000_auth_expiry");
|
||||
// return { authToken: authToken, refreshToken: refreshToken, expiry: expiry };
|
||||
// }
|
||||
|
||||
// function setAuthToken(authToken: string, refreshToken: string, expirySeconds: number) {
|
||||
// window.localStorage.setItem("tone3000_auth_token", authToken);
|
||||
// window.localStorage.setItem("tone3000_refresh_token", refreshToken);
|
||||
// window.localStorage.setItem("tone3000_auth_expiry", String(Date.now() + (expirySeconds * 1000)));
|
||||
// }
|
||||
|
||||
// enum AuthState {
|
||||
// OnBoarding,
|
||||
// TokenExpired,
|
||||
// Authenticated
|
||||
// }
|
||||
|
||||
function Tone3000Dialog(props: Tone3000DialogProps) {
|
||||
const { onCancel,onDownload } = props;
|
||||
|
||||
function RedirectUrl() {
|
||||
let baseUrl = new URL(window.location.href);
|
||||
let result = "http://" + baseUrl.hostname + ":" + baseUrl.port + "/public/handleTone3000download.html";
|
||||
return result;
|
||||
}
|
||||
const appId = 'pipedal_app';
|
||||
const redirectUrl = RedirectUrl();
|
||||
|
||||
let selectUrl = `https://www.tone3000.com/api/v1/select?app_id=${appId}&redirect_url=${redirectUrl}`;
|
||||
|
||||
const model: PiPedalModel = PiPedalModelFactory.getInstance();
|
||||
if (model) {
|
||||
this.handleTone3000DownloadComplete();
|
||||
|
||||
await this.model.downloadModelsFromTone3000(tone3000DownloadUrl,this.downloadPath);
|
||||
return;
|
||||
} catch (e) {
|
||||
this.handleTone3000DownloadError(getErrorMessage(e));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
React.useEffect(()=> {
|
||||
private messageEventListener: ((event: MessageEvent) => void) | null = null;
|
||||
|
||||
let messagehandler = (event: MessageEvent) => {
|
||||
// Handle messages received from the iframe
|
||||
if (event.origin !== "https://www.tone3000.com") {
|
||||
private model: PiPedalModel;
|
||||
|
||||
public constructor(model: PiPedalModel) {
|
||||
this.model = model;
|
||||
this.messageEventListener = (event: MessageEvent) => {
|
||||
if (event.data.type !== "tone3000Download") {
|
||||
return;
|
||||
}
|
||||
const data = event.data;
|
||||
if (data.type === "tone3000_selection") {
|
||||
onDownload(data.toneUrl);
|
||||
this.handleTone3000Download(event.data.toneUrl as string);
|
||||
};
|
||||
|
||||
window.addEventListener("message", this.messageEventListener);
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
if (this.messageEventListener) {
|
||||
window.removeEventListener("message", this.messageEventListener);
|
||||
this.messageEventListener = null;
|
||||
}
|
||||
}
|
||||
|
||||
private popupWindow: Window | null = null;
|
||||
|
||||
private appId: string = "pipedal_app";
|
||||
private redirectUrl(): string {
|
||||
let hostname = window.location.hostname;
|
||||
let port = window.location.port;
|
||||
let protocol = window.location.protocol;
|
||||
let serverUrl: string;
|
||||
if (protocol === "http:" && (port === "80" || port === "")) {
|
||||
serverUrl = `${protocol}//${hostname}`;
|
||||
} else if (protocol === "https:" && (port === "443" || port === "")) {
|
||||
serverUrl = `${protocol}//${hostname}`;
|
||||
} else {
|
||||
serverUrl = `${protocol}//${hostname}:${port}`;
|
||||
}
|
||||
return `${serverUrl}/public/handleTone3000download.html`;
|
||||
}
|
||||
|
||||
private tone3000SelectUrl(): string {
|
||||
return `https://www.tone3000.com/api/v1/select?app_id=${this.appId}&redirect_url=${this.redirectUrl()}`;
|
||||
}
|
||||
|
||||
|
||||
private handleTone3000DialogClosed() {
|
||||
let t = this.onClosedCallback;
|
||||
this.onClosedCallback = undefined;
|
||||
this.onErrorCallback = undefined;
|
||||
this.onDownloadCompleteCallback = undefined;
|
||||
if (t) {
|
||||
t();
|
||||
}
|
||||
this.stopTone3000DialogMonitor();
|
||||
}
|
||||
private handleTone3000Downloadstarted() {
|
||||
if (this.onDownloadStartedCallback) {
|
||||
this.onDownloadStartedCallback();
|
||||
}
|
||||
}
|
||||
private handleTone3000DownloadComplete() {
|
||||
let t = this.onDownloadCompleteCallback;
|
||||
this.onClosedCallback = undefined;
|
||||
this.onErrorCallback = undefined;
|
||||
this.onDownloadCompleteCallback = undefined;
|
||||
if (t) {
|
||||
t();
|
||||
}
|
||||
this.stopTone3000DialogMonitor();
|
||||
}
|
||||
private handleTone3000DownloadError(errorMessage: string) {
|
||||
let t = this.onErrorCallback;
|
||||
this.onClosedCallback = undefined;
|
||||
this.onErrorCallback = undefined;
|
||||
this.onDownloadCompleteCallback = undefined;
|
||||
if (t) {
|
||||
t(errorMessage);
|
||||
}
|
||||
this.stopTone3000DialogMonitor();
|
||||
}
|
||||
|
||||
private tone3000DialogInterval: number | undefined = undefined;
|
||||
|
||||
private stopTone3000DialogMonitor() {
|
||||
if (this.tone3000DialogInterval !== undefined) {
|
||||
clearInterval(this.tone3000DialogInterval);
|
||||
this.tone3000DialogInterval = undefined;
|
||||
}
|
||||
}
|
||||
private startTone3000DialogMonitor() {
|
||||
this.tone3000DialogInterval = setInterval(() => {
|
||||
if (this.popupWindow == null || this.popupWindow.closed) {
|
||||
this.stopTone3000DialogMonitor();
|
||||
this.popupWindow = null;
|
||||
this.handleTone3000DialogClosed();
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
private downloadPath: string = "";
|
||||
private onClosedCallback: (() => void) | undefined = undefined;
|
||||
private onErrorCallback: ((errorMessage: string) => void) | undefined = undefined;
|
||||
private onDownloadStartedCallback: (() => void) | undefined = undefined;
|
||||
private onDownloadCompleteCallback: (() => void) | undefined = undefined;
|
||||
|
||||
public launchTone3000Dialog(
|
||||
downloadPath: string,
|
||||
onClosed: () => void,
|
||||
onError: (errorMessage: string) => void,
|
||||
onDownloadStarted: () => void,
|
||||
onDownloadComplete: () => void
|
||||
) {
|
||||
this.closeTone3000Dialog();
|
||||
this.downloadPath = downloadPath;
|
||||
this.onClosedCallback = onClosed;
|
||||
this.onDownloadCompleteCallback = onDownloadComplete;
|
||||
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;
|
||||
}
|
||||
|
||||
// online
|
||||
let popupWidth = Math.floor(window.innerWidth * 0.8);
|
||||
let popupHeight = Math.floor(window.innerHeight * 0.8);
|
||||
if (window.innerWidth < 410) {
|
||||
popupWidth = 400;
|
||||
}
|
||||
|
||||
this.popupWindow =
|
||||
window.open(
|
||||
this.tone3000SelectUrl(),
|
||||
"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) => {
|
||||
// offline
|
||||
this.handleTone3000DownloadError("Not online. Unable to connect to Tone3000 servers.");
|
||||
return;
|
||||
});
|
||||
|
||||
}
|
||||
public closeTone3000Dialog(): void {
|
||||
this.handleTone3000DialogClosed();
|
||||
this.stopTone3000DialogMonitor();
|
||||
if (this.popupWindow) {
|
||||
try {
|
||||
|
||||
let t = this.popupWindow;
|
||||
this.popupWindow = null;
|
||||
t.close();
|
||||
} catch (e) {
|
||||
console.error("Error closing Tone3000 dialog popup:", e);
|
||||
}
|
||||
}
|
||||
window.addEventListener("message", messagehandler);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("message", messagehandler);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
return (
|
||||
<DialogEx open={props.open} onClose={()=>{ onCancel()}} fullScreen={true} tag="tone3000Dlg" onEnterKey={()=>{}} >
|
||||
|
||||
<div style={{display: "flex", flexDirection: "column", width: "100%", height: "100%"}}>
|
||||
<div style={{ flex: "0 0 auto" }}>
|
||||
<AppBar style={{
|
||||
position: 'relative',
|
||||
top: 0, left: 0
|
||||
}} >
|
||||
<Toolbar>
|
||||
<IconButtonEx tooltip="Back" edge="start" color="inherit" onClick={()=> onCancel()}
|
||||
aria-label="back"
|
||||
>
|
||||
<ArrowBackIcon />
|
||||
</IconButtonEx>
|
||||
<Typography noWrap variant="h6"
|
||||
sx={{ marginLeft: 2, flex: 1 }}
|
||||
>
|
||||
Tone3000 Direct Download
|
||||
</Typography>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
</div>
|
||||
|
||||
<iframe style={{flex: "1 1 1px", border: "none"}}
|
||||
src={ selectUrl}
|
||||
>
|
||||
</iframe>
|
||||
</div>
|
||||
</DialogEx>
|
||||
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Tone3000Dialog;
|
||||
export interface Tone3000DownloadDialogProps {
|
||||
|
||||
onClose: () => void,
|
||||
onDownloadComplete: () => void,
|
||||
downloadPath: string
|
||||
}
|
||||
export function Tone3000DownloadDialog(props: Tone3000DownloadDialogProps): JSX.Element {
|
||||
const model = PiPedalModel.getInstance();
|
||||
let { onClose, onDownloadComplete, downloadPath } = props;
|
||||
let [downloading, setDownloading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
model.showTone3000DownloadDialog(
|
||||
downloadPath,
|
||||
() => {
|
||||
onClose();
|
||||
},
|
||||
() => {
|
||||
setDownloading(true);
|
||||
},
|
||||
() => {
|
||||
onDownloadComplete();
|
||||
}
|
||||
);
|
||||
return () => {
|
||||
model.closeTone3000DownloadDialog();
|
||||
};
|
||||
});
|
||||
return (
|
||||
<DialogEx tag="tone3000-download"
|
||||
open={true}
|
||||
onClose={() => {
|
||||
onClose();
|
||||
}}
|
||||
onEnterKey={() => { }}
|
||||
>
|
||||
<DialogContent>
|
||||
<Typography variant="body2">Downloading from Tone3000...</Typography>
|
||||
{downloading &&
|
||||
<div style={{ display: "flex", justifyContent: "center", alignItems: "center" }}>
|
||||
<LinearProgress style={{ marginTop: 16, flexGrow: 1 }} />
|
||||
</div>
|
||||
}
|
||||
</DialogContent>
|
||||
<DialogAction>
|
||||
{!downloading && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
onClose();
|
||||
}}>
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
</DialogAction>
|
||||
</DialogEx>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user