From f35ee9330d2e433f9688f3f13fa516a22d88fb27 Mon Sep 17 00:00:00 2001 From: "Robin E. R. Davies" Date: Thu, 19 Jun 2025 16:04:32 -0400 Subject: [PATCH] ToobPlayer UI. .mdata file handling. --- PiPedalCommon/src/include/json_variant.hpp | 4 + PiPedalCommon/src/include/util.hpp | 2 + PiPedalCommon/src/util.cpp | 16 ++ src/AudioFiles.cpp | 84 +++---- src/AudioFiles.hpp | 11 +- src/AudioFilesDb.cpp | 49 ---- src/AudioFilesDb.hpp | 11 - src/FileMetadataFeature.cpp | 217 ++++++++++++++---- src/FileMetadataFeature.hpp | 32 ++- src/PiPedalModel.hpp | 1 + src/PluginHost.cpp | 5 + src/PluginHost.hpp | 6 +- src/PresetBundle.cpp | 67 +++++- src/Storage.cpp | 32 ++- src/WebServerConfig.cpp | 48 ++-- src/ZipFile.cpp | 34 +-- src/ZipFile.hpp | 3 +- src/ext/PiPedal/FileMetadataFeature.h | 105 --------- .../pipedal.lv2/ext/FileMetadataFeature.h | 129 +++++++++++ todo.txt | 2 +- vite/src/pipedal/AudioFileMetadata.tsx | 36 ++- vite/src/pipedal/BankDialog.tsx | 26 +-- vite/src/pipedal/ButtonEx.tsx | 5 +- vite/src/pipedal/FilePropertyDialog.tsx | 34 +-- vite/src/pipedal/PiPedalModel.tsx | 15 +- vite/src/pipedal/PluginControlView.tsx | 43 ++-- vite/src/pipedal/PresetDialog.tsx | 29 +-- vite/src/pipedal/ToobPlayerControl.tsx | 43 ++-- vite/src/pipedal/ToobPlayerView.tsx | 7 +- 29 files changed, 671 insertions(+), 425 deletions(-) delete mode 100644 src/ext/PiPedal/FileMetadataFeature.h create mode 100644 src/lv2ext/pipedal.lv2/ext/FileMetadataFeature.h diff --git a/PiPedalCommon/src/include/json_variant.hpp b/PiPedalCommon/src/include/json_variant.hpp index c972ace..8ed6145 100644 --- a/PiPedalCommon/src/include/json_variant.hpp +++ b/PiPedalCommon/src/include/json_variant.hpp @@ -309,6 +309,10 @@ namespace pipedal const_iterator begin() const { return values.begin(); } const_iterator end() const { return values.end(); } + iterator erase(iterator it) + { + return values.erase(it); + } iterator find(const std::string &key); const_iterator find(const std::string &key) const; diff --git a/PiPedalCommon/src/include/util.hpp b/PiPedalCommon/src/include/util.hpp index c4c4216..39b95dc 100644 --- a/PiPedalCommon/src/include/util.hpp +++ b/PiPedalCommon/src/include/util.hpp @@ -119,6 +119,8 @@ namespace pipedal std::filesystem::path MakeRelativePath(const std::filesystem::path &path, const std::filesystem::path&parentPath); bool IsSubdirectory(const std::filesystem::path &path, const std::filesystem::path &basePath); + bool HasDotDot(const std::filesystem::path &path); + std::string ToLower(const std::string&value); diff --git a/PiPedalCommon/src/util.cpp b/PiPedalCommon/src/util.cpp index fdf6df2..828ccf5 100644 --- a/PiPedalCommon/src/util.cpp +++ b/PiPedalCommon/src/util.cpp @@ -190,6 +190,22 @@ std::filesystem::path pipedal::MakeRelativePath(const std::filesystem::path &pat } +bool pipedal::HasDotDot(const std::filesystem::path &path) +{ + for (auto &part : path) + { + if (part == "..") + { + return true; + } + if (part == ".") + { + return true; + } + } + return false; +} + bool pipedal::IsSubdirectory(const std::filesystem::path &path, const std::filesystem::path &basePath) { auto iPath = path.begin(); diff --git a/src/AudioFiles.cpp b/src/AudioFiles.cpp index a144241..dd427ca 100644 --- a/src/AudioFiles.cpp +++ b/src/AudioFiles.cpp @@ -145,8 +145,6 @@ namespace virtual std::vector GetFiles() override; virtual ThumbnailTemporaryFile GetThumbnail(const std::string &fileNameOnly, int32_t width, int32_t height) override; - virtual ThumbnailTemporaryFile DefaultThumbnailTemporaryFile() override; - virtual size_t TestGetNumberOfThumbnails() override; // test use only. virtual void TestSetIndexPath(const std::filesystem::path &path) override { @@ -160,15 +158,6 @@ namespace int32_t fromPosition, int32_t toPosition) override; - virtual void SetFileMetadata( - const std::string &fileNameOnly, - const std::string &key, - const std::string &value) override; - - virtual std::string GetFileMetadata( - const std::string &fileNameOnly, - const std::string &key) override; - private: std::vector QueryTracks(); @@ -470,7 +459,7 @@ void AudioDirectoryInfoImpl::DbDeleteFile(DbFileInfo *dbFile) audioFilesDb->DeleteFile(dbFile); } } -ThumbnailTemporaryFile AudioDirectoryInfoImpl::DefaultThumbnailTemporaryFile() +ThumbnailTemporaryFile AudioDirectoryInfo::DefaultThumbnailTemporaryFile() { ThumbnailTemporaryFile tempFile; fs::path defaultThumbnail = GetResourceDirectory() / "img/missing_thumbnail.jpg"; @@ -783,13 +772,18 @@ std::string AudioDirectoryInfoImpl::GetNextAudioFile(const std::string &fileName { if (files[i].fileName() == fileNameOnly) { - if (i + 1 < files.size()) + size_t next = i; + while (true) { - return files[i + 1].fileName(); - } - else - { - return files[0].fileName(); // wrap around to the first file. + next = (next + 1) % files.size(); + if (!isFolderArtwork(files[next].fileName())) + { + return files[next].fileName(); + } + if (next == i) + { + break; + } } } } @@ -806,13 +800,25 @@ std::string AudioDirectoryInfoImpl::GetPreviousAudioFile(const std::string &file { if (files[i].fileName() == fileNameOnly) { - if (i > 0) + size_t next = i; + while (true) { - return files[i - 1].fileName(); - } - else - { - return files[files.size() - 1].fileName(); // wrap around to the last file. + if (next == 0) + { + next = files.size() - 1; // wrap around to the last file. + } + else + { + --next; // go to the previous file. + } + if (!isFolderArtwork(files[next].fileName())) + { + return files[next].fileName(); + } + if (next == i) + { + break; + } } } } @@ -912,33 +918,3 @@ namespace pipedal } } -void AudioDirectoryInfoImpl::SetFileMetadata( - const std::string &fileNameOnly, - const std::string &key, - const std::string &value) -{ - OpenAudioDb(); - if (!audioFilesDb) - { - throw std::runtime_error("Directory is not writable."); - } - this->UpdateDbFiles(); - - audioFilesDb->SetFileExtraMetadata(fileNameOnly, key, value); -} - -std::string AudioDirectoryInfoImpl::GetFileMetadata( - const std::string &fileNameOnly, - const std::string &key) -{ - OpenAudioDb(); - if (!audioFilesDb) - { - throw std::runtime_error("Directory is not writable."); - } - this->UpdateDbFiles(); // check for deletions. - - return audioFilesDb->GetFileExtraMetadata(fileNameOnly, key); -} - - diff --git a/src/AudioFiles.hpp b/src/AudioFiles.hpp index 168e1fc..132eb56 100644 --- a/src/AudioFiles.hpp +++ b/src/AudioFiles.hpp @@ -95,20 +95,11 @@ namespace pipedal virtual std::string GetNextAudioFile(const std::string &fileNameOnly) = 0; virtual std::string GetPreviousAudioFile(const std::string &fileNameOnly) = 0; - virtual void SetFileMetadata( - const std::string &fileNameOnly, - const std::string &key, - const std::string &value) = 0; - - virtual std::string GetFileMetadata( - const std::string &fileNameOnly, - const std::string &key) = 0; - virtual void MoveAudioFile( const std::string &directory, int32_t fromPosition, int32_t toPosition) = 0; - virtual ThumbnailTemporaryFile DefaultThumbnailTemporaryFile() = 0; + static ThumbnailTemporaryFile DefaultThumbnailTemporaryFile(); static void SetTemporaryDirectory(const std::filesystem::path &path); static void SetResourceDirectory(const std::filesystem::path &path); diff --git a/src/AudioFilesDb.cpp b/src/AudioFilesDb.cpp index b162f39..c4c5b7c 100644 --- a/src/AudioFilesDb.cpp +++ b/src/AudioFilesDb.cpp @@ -192,13 +192,6 @@ void AudioFilesDb::CreateDb(const std::filesystem::path &dbPathName) "width INTEGER, " "height INTEGER)"); - db->exec("CREATE TABLE IF NOT EXISTS extraMetadata (" - "idExtra INTEGER PRIMARY KEY AUTOINCREMENT, " - "idFile INT64 NOT NULL, " - "key TEXT NOT NULL, " - "value TEXT NOT NULL, " - "FOREIGN KEY (idFile) REFERENCES files(idFile) ON DELETE CASCADE, " - "UNIQUE(idFile, key))"); } catch (const SQLite::Exception &e) { @@ -510,45 +503,3 @@ void AudioFilesDb::UpdateFilePosition( updateFileQuery->exec(); } -void AudioFilesDb::DeleteFileExtraMetadata( - const std::string &fileNameOnly, - const std::string &key) -{ - auto deleteExtraMetadataQuery = std::make_unique( - *db, - "DELETE FROM extraMetadata WHERE idFile = (SELECT idFile FROM files WHERE fileName = ?) AND key = ?"); - deleteExtraMetadataQuery->bind(1, fileNameOnly); - deleteExtraMetadataQuery->bind(2, key); - deleteExtraMetadataQuery->exec(); -} -void AudioFilesDb::SetFileExtraMetadata( - const std::string &fileNameOnly, - const std::string &key, - const std::string &value) -{ - - auto setExtraMetadataQuery = std::make_unique( - *db, - "INSERT OR REPLACE INTO extraMetadata (idFile, key, value) " - "VALUES ((SELECT idFile FROM files WHERE fileName = ?), ?, ?)"); - setExtraMetadataQuery->bind(1, fileNameOnly); - setExtraMetadataQuery->bind(2, key); - setExtraMetadataQuery->bind(3, value); - setExtraMetadataQuery->exec(); -} -std::string AudioFilesDb::GetFileExtraMetadata( - const std::string &fileNameOnly, - const std::string &key) -{ - auto getExtraMetadataQuery = std::make_unique( - *db, - "SELECT value FROM extraMetadata WHERE idFile = (SELECT idFile FROM files WHERE fileName = ?) AND key = ?"); - getExtraMetadataQuery->bind(1, fileNameOnly); - getExtraMetadataQuery->bind(2, key); - - if (getExtraMetadataQuery->executeStep()) - { - return getExtraMetadataQuery->getColumn(0).getText(); - } - return ""; -} diff --git a/src/AudioFilesDb.hpp b/src/AudioFilesDb.hpp index 92d1286..1f44ecb 100644 --- a/src/AudioFilesDb.hpp +++ b/src/AudioFilesDb.hpp @@ -110,17 +110,6 @@ namespace pipedal::impl { int64_t idFile, int32_t position); - void DeleteFileExtraMetadata( - const std::string &fileNameOnly, - const std::string &key); - void SetFileExtraMetadata( - const std::string &fileNameOnly, - const std::string &key, - const std::string &value); - std::string GetFileExtraMetadata( - const std::string &fileNameOnly, - const std::string &key); - private: void CreateDb(const std::filesystem::path &dbPathName); diff --git a/src/FileMetadataFeature.cpp b/src/FileMetadataFeature.cpp index 50157aa..d118120 100644 --- a/src/FileMetadataFeature.cpp +++ b/src/FileMetadataFeature.cpp @@ -22,9 +22,16 @@ #include "AudioFiles.hpp" #include "Lv2Log.hpp" #include "ss.hpp" +#include "util.hpp" +#include "json.hpp" +#include "json_variant.hpp" + +#include +#include #include "lv2/lv2plug.in/ns/ext/buf-size/buf-size.h" using namespace pipedal; +namespace fs = std::filesystem; FileMetadataFeature::FileMetadataFeature() { @@ -33,12 +40,37 @@ FileMetadataFeature::FileMetadataFeature() interface.handle = (void *)this; interface.setFileMetadata = &FileMetadataFeature::S_setFileMetadata; interface.getFileMetadata = &FileMetadataFeature::S_getFileMetadata; - + interface.deleteFileMetadata = &FileMetadataFeature::S_deleteFileMetadata; } FileMetadataFeature::~FileMetadataFeature() { } + +void FileMetadataFeature::SetPluginStoragePath(const std::filesystem::path &storagePath) +{ + this->tracksPath = storagePath / "shared" / "audio" / "Tracks"; +} + +// Check if the path is within the tracks directory +bool FileMetadataFeature::IsValidPath(const std::filesystem::path &path) const +{ + if (tracksPath.empty()) + { + return false; + } + if (HasDotDot(path)) + { + return false; + } + // is a subdirectory of track path + // check for directory seperator + if (!IsSubdirectory(path, tracksPath)) + { + return false; // Valid subdirectory + } + return true; +} void FileMetadataFeature::Prepare(MapFeature &map) { this->mapFeature = ↦ @@ -46,88 +78,179 @@ void FileMetadataFeature::Prepare(MapFeature &map) PIPEDAL_FileMetadata_Status FileMetadataFeature::setFileMetadata( const char *absolute_path, - LV2_URID key, - const char *fileMetadata) + const char *key, + const char *value) { - const char *strKey = mapFeature->UridToString(key); - if (strKey == nullptr) + if (!IsValidPath(absolute_path)) { - return PIPEDAL_FILE_METADATA_INVALID_KEY; - } - std::filesystem::path path{absolute_path}; - - if (!std::filesystem::exists(path)) - { - return PIPEDAL_FILE_METADATA_INVALID_PATH; // File does not exist - } - if (!std::filesystem::is_regular_file(path)) - { - return PIPEDAL_FILE_METADATA_INVALID_PATH; // Not a regular file - } - try { - AudioDirectoryInfo::Ptr audioDirectoryInfo = AudioDirectoryInfo::Create(path.parent_path()); - audioDirectoryInfo->SetFileMetadata(path.filename(), strKey, fileMetadata); - } catch (const std::exception&e) { - Lv2Log::error(SS("Failed to create AudioDirectoryInfo for " << path << ": " << e.what())); - return PIPEDAL_FILE_METADATA_INVALID_PATH; // Failed to create AudioDirectoryInfo + return PIPEDAL_FILE_METADATA_INVALID_PATH; // Cannot set metadata for files in the tracks directory } + std::filesystem::path path{SS(absolute_path << ".mdata")}; - return PIPEDAL_FILE_METADATA_SUCCESS; // TODO: Implement this + json_variant jsonData; + if (fs::exists(path)) + { + try + { + std::ifstream f(path); + json_reader reader(f); + reader.read(&jsonData); + } + catch (const std::exception &e) + { + jsonData = json_variant::make_object(); + } + } + else + { + jsonData = json_variant::make_object(); + } + + (*jsonData.as_object())[key] = json_variant(value); + + { + std::ofstream f(path); + if (!f.is_open()) + { + Lv2Log::error(SS("Failed to write metadata file " << path)); + return PIPEDAL_FILE_METADATA_PERMISSION_DENIED; // Failed to open existing metadata + } + json_writer writer(f); + writer.write(jsonData); + } + Lv2Log::debug(SS("Set file metadata for " << absolute_path << " key: " << key << " value: " << value)); + return PIPEDAL_FILE_METADATA_SUCCESS; } uint32_t FileMetadataFeature::getFileMetadata( const char *absolute_path, - LV2_URID key, + const char *key, char *fileMetadata, uint32_t fileMetadataSize) { - const char *strKey = mapFeature->UridToString(key); - if (strKey == nullptr) + if (fileMetadata) + fileMetadata[0] = '\0'; // Ensure the buffer is null-terminated + + if (!IsValidPath(absolute_path)) { - return PIPEDAL_FILE_METADATA_INVALID_KEY; + return 0; // Cannot set metadata for files in the tracks directory } - std::filesystem::path path{absolute_path}; + std::filesystem::path path{SS(absolute_path << ".mdata")}; - AudioDirectoryInfo::Ptr audioDirectoryInfo = AudioDirectoryInfo::Create(path.parent_path()); + json_variant jsonData; + std::string result; - std::string metadata = audioDirectoryInfo->GetFileMetadata(path.filename(), strKey); - if (metadata.empty()) + if (fs::exists(path)) { - return 0; // No metadata found + try + { + std::ifstream f(path); + json_reader reader(f); + reader.read(&jsonData); + result = jsonData.as_object()->at(std::string(key)).as_string(); + } + catch (const std::exception &e) + { + return 0; + } } - size_t required = metadata.size() + 1; - if (required >= std::numeric_limits::max()) + else { - return PIPEDAL_FILE_METADATA_ERR_UNKNOWNM; // Metadata too large + Lv2Log::debug(SS("No metadata file found for " << absolute_path << " key: " << key)); + return 0; } - if (fileMetadata == nullptr || required > fileMetadataSize) + size_t len = result.length() + 1; // +1 for null terminator + if (len > fileMetadataSize || fileMetadata == nullptr) { - return static_cast(required); // Return size needed including null terminator + // Not enough space in the buffer, return the size needed. + return len; } - if (fileMetadataSize < metadata.length() + 1) + memcpy(fileMetadata, result.c_str(), len); + Lv2Log::debug(SS("Get file metadata for " << absolute_path << " key: " << key << " value: " << result)); + return len; +} +PIPEDAL_FileMetadata_Status FileMetadataFeature::deleteFileMetadata( + const char *absolute_path, + const char *key) +{ + if (!IsValidPath(absolute_path)) { - return static_cast(metadata.size() + 1); // Return size needed including null terminator + return PIPEDAL_FILE_METADATA_INVALID_PATH; // Cannot set metadata for files in the tracks directory } - std::strncpy(fileMetadata, metadata.c_str(), fileMetadataSize); - return required; + Lv2Log::debug(SS("Delete file metadata for " << absolute_path << " key: " << key)); + + std::filesystem::path path{SS(absolute_path << ".mdata")}; + + if (!fs::exists(path)) + { + return PIPEDAL_FILE_METADATA_NOT_FOUND; // Metadata file does not exist + } + + json_variant jsonData; + try + { + std::ifstream f(path); + json_reader reader(f); + reader.read(&jsonData); + } + catch (const std::exception &e) + { + return PIPEDAL_FILE_METADATA_NOT_FOUND; // Failed to read existing metadata + } + + auto obj = jsonData.as_object(); + auto it = obj->find(std::string(key)); + if (it == obj->end()) + { + return PIPEDAL_FILE_METADATA_NOT_FOUND; // Key not found + } + + obj->erase(it); + + if (obj->begin() == obj->end()) + { + // If the object is empty, delete the metadata file + fs::remove(path); + return PIPEDAL_FILE_METADATA_SUCCESS; // Metadata deleted successfully + } + + std::ofstream f(path); + if (!f.is_open()) + { + Lv2Log::error(SS("Failed to write metadata file " << path)); + return PIPEDAL_FILE_METADATA_PERMISSION_DENIED; // Failed to open existing metadata + } + + json_writer writer(f); + writer.write(jsonData); + + return PIPEDAL_FILE_METADATA_SUCCESS; } PIPEDAL_FileMetadata_Status FileMetadataFeature::S_setFileMetadata( PIPEDAL_FILE_METADATA_Handle handle, const char *absolute_path, - LV2_URID key, + const char *key, const char *fileMetadata) { return ((FileMetadataFeature *)handle)->setFileMetadata(absolute_path, key, fileMetadata); } uint32_t FileMetadataFeature::S_getFileMetadata( - PIPEDAL_FILE_METADATA_Handle handle, - LV2_URID key, - const char *absolute_path, - char *buffer, + PIPEDAL_FILE_METADATA_Handle handle, + const char *absolute_path, + const char *key, + char *buffer, uint32_t bufferSize) { return ((FileMetadataFeature *)handle)->getFileMetadata(absolute_path, key, buffer, bufferSize); } + +PIPEDAL_FileMetadata_Status FileMetadataFeature::S_deleteFileMetadata( + PIPEDAL_FILE_METADATA_Handle handle, + const char *absolute_path, + const char *key) +{ + return ((FileMetadataFeature *)handle)->deleteFileMetadata(absolute_path, key); +} diff --git a/src/FileMetadataFeature.hpp b/src/FileMetadataFeature.hpp index c25b78f..17092f0 100644 --- a/src/FileMetadataFeature.hpp +++ b/src/FileMetadataFeature.hpp @@ -21,7 +21,7 @@ #include "MapFeature.hpp" #include "PiPedalUI.hpp" -#include "ext/PiPedal/FileMetadataFeature.h" +#include "lv2ext/pipedal.lv2/ext/FileMetadataFeature.h" namespace pipedal { @@ -32,27 +32,49 @@ namespace pipedal private: LV2_Feature feature; PIPEDAL_FileMetadata_Interface interface; - static PIPEDAL_FileMetadata_Status S_setFileMetadata(PIPEDAL_FILE_METADATA_Handle handle, const char *absolute_path, LV2_URID key, const char *fileMetadata); - PIPEDAL_FileMetadata_Status setFileMetadata(const char *absolute_path, LV2_URID key, const char *fileMetadata); + static PIPEDAL_FileMetadata_Status S_setFileMetadata( + PIPEDAL_FILE_METADATA_Handle handle, + const char *absolute_path, + const char*key, + const char *fileMetadata); + + PIPEDAL_FileMetadata_Status setFileMetadata( + const char *absolute_path, + const char*key, + const char *fileMetadata); static uint32_t S_getFileMetadata( PIPEDAL_FILE_METADATA_Handle handle, - LV2_URID key, const char *filePath, + const char*key, char *buffer, uint32_t bufferSize); + uint32_t getFileMetadata( const char *absolute_path, - LV2_URID key, + const char*key, char *fileMetadata, uint32_t fileMetadataSize); + static PIPEDAL_FileMetadata_Status S_deleteFileMetadata( + PIPEDAL_FILE_METADATA_Handle handle, + const char *absolute_path, + const char*key); + + PIPEDAL_FileMetadata_Status deleteFileMetadata( + const char *absolute_path, + const char*key); + + + bool IsValidPath(const std::filesystem::path &path) const; public: FileMetadataFeature(); void Prepare(MapFeature &map); + void SetPluginStoragePath(const std::filesystem::path &storagePath); ~FileMetadataFeature(); public: + std::filesystem::path tracksPath; MapFeature *mapFeature = nullptr; const LV2_Feature *GetFeature() { diff --git a/src/PiPedalModel.hpp b/src/PiPedalModel.hpp index 1fba8e2..0f35c13 100644 --- a/src/PiPedalModel.hpp +++ b/src/PiPedalModel.hpp @@ -95,6 +95,7 @@ namespace pipedal virtual void OnNetworkChanging(bool hotspotConnected) = 0; virtual void OnHasWifiChanged(bool hasWifi) = 0; virtual void Close() = 0; + }; class HotspotManager; diff --git a/src/PluginHost.cpp b/src/PluginHost.cpp index 5125427..e6355cf 100644 --- a/src/PluginHost.cpp +++ b/src/PluginHost.cpp @@ -249,6 +249,7 @@ static float nodeAsFloat(const LilvNode *node, float default_ = 0) void PluginHost::SetPluginStoragePath(const std::filesystem::path &path) { pluginStoragePath = path; + fileMetadataFeature.SetPluginStoragePath(path); } std::string PluginHost::GetPluginStoragePath() const @@ -266,6 +267,9 @@ PluginHost::PluginHost() optionsFeature.Prepare(mapFeature, 44100, this->GetMaxAudioBufferSize(), this->GetAtomBufferSize()); lv2Features.push_back(optionsFeature.GetFeature()); + fileMetadataFeature.Prepare(mapFeature); + lv2Features.push_back(fileMetadataFeature.GetFeature()); + lv2Features.push_back(nullptr); @@ -902,6 +906,7 @@ std::vector supportedFeatures = { LV2_STATE__freePath, LV2_CORE__inPlaceBroken, PIPEDAL_HOST_FEATURE, + PIPEDAL__FILE_METADATA_FEATURE, // UI features that we can ignore, since we won't load their ui. "http://lv2plug.in/ns/extensions/ui#makeResident", diff --git a/src/PluginHost.hpp b/src/PluginHost.hpp index ddc5d84..cafa183 100644 --- a/src/PluginHost.hpp +++ b/src/PluginHost.hpp @@ -25,6 +25,7 @@ #include "PluginType.hpp" #include #include "MapFeature.hpp" +#include "FileMetadataFeature.hpp" #include "OptionsFeature.hpp" #include #include @@ -794,6 +795,7 @@ namespace pipedal std::vector lv2Features; MapFeature mapFeature; + FileMetadataFeature fileMetadataFeature; OptionsFeature optionsFeature; std::string pluginStoragePath; @@ -858,9 +860,11 @@ namespace pipedal virtual std::shared_ptr GetHostWorkerThread(); public: - virtual MapFeature &GetMapFeature() { return this->mapFeature; } + virtual MapFeature &GetMapFeature() override { return this->mapFeature; } void CheckForResourceInitialization(const std::string& pluginUri,const std::filesystem::path& pluginUploadDirectory); + + std::string MapResourcePath(const std::string&uri, const std::string&relativePath); void ReloadPlugins(); diff --git a/src/PresetBundle.cpp b/src/PresetBundle.cpp index 27ae4da..b0c93be 100644 --- a/src/PresetBundle.cpp +++ b/src/PresetBundle.cpp @@ -140,6 +140,12 @@ void PresetBundleWriterImpl::WriteToFile(const std::filesystem::path &filePath) { Lv2Log::warning(SS("Media file not found: " << sourcePath)); } + std::filesystem::path metadataPath = SS(sourcePath.string() << ".mdata"); + if (std::filesystem::exists(metadataPath)) + { + zipFile->WriteFile(SS(zipName << ".mdata"), metadataPath); + } + } zipFile->Close(); } @@ -236,7 +242,28 @@ private: void ExtractMediaFile(const std::string &zipFileName); bool IsSameFile(const std::string &zipFileName, const std::filesystem::path &filePath) { - return zipFile->CompareFiles(zipFileName, filePath); + if (!zipFile->CompareFiles(zipFileName, filePath)) + { + return false; + } + std::string metadataZipFilename = SS(zipFileName <<".mdata"); + std::filesystem::path metadataFilename = SS(filePath.string() <<".mdata"); + + if (!std::filesystem::exists(metadataFilename)) + { + if (!zipFile->FileExists(metadataZipFilename)) + { + return true; + } + return false; + } else { + if (!zipFile->FileExists(metadataZipFilename)) + { + return false; + } + return zipFile->CompareFiles(metadataZipFilename, metadataFilename); + } + return true; } void RenameMediaFileProperty(const std::string oldName, const std::string &newName); @@ -268,6 +295,36 @@ void PresetBundleReaderImpl::RenameMediaFileProperty(const std::string oldName, } } } + std::string fullOldPath = (pluginUploadDirectory / oldName).string(); + for (auto &property : plugin->pathProperties_) + { + std::stringstream ss(property.second); + json_reader reader(ss); + json_variant vProperty; + reader.read(&vProperty); + if (vProperty.is_object()) + { + auto obj = vProperty.as_object(); + if (obj->contains("otype_") && + (*obj)["otype_"].as_string() == "Path") + { + if (obj->contains("value")) + { + std::string value = (*obj)["value"].as_string(); + if (value == fullOldPath) + { + (*obj)["value"] = (pluginUploadDirectory / newName).string(); + std::ostringstream ss; + json_writer writer(ss); + writer.write(vProperty); + plugin->pathProperties_[property.first] = ss.str(); + break; + } + } + } + + } + } } } for (auto preset : pluginPresets.presets_) @@ -363,6 +420,10 @@ void PresetBundleReaderImpl::ExtractMediaFile(const std::string &zipFileName) namespace fs = std::filesystem; if (zipFileName.starts_with("media/")) { + if (zipFileName.ends_with(".mdata")) + { + return; + } std::string baseName = zipFileName.substr(6); fs::path targetFileName = this->pluginUploadDirectory / std::filesystem::path(baseName); bool renamed = false; @@ -383,6 +444,10 @@ void PresetBundleReaderImpl::ExtractMediaFile(const std::string &zipFileName) else { zipFile->ExtractTo(zipFileName, targetFileName); + if (zipFile->FileExists(SS(zipFileName << ".mdata"))) + { + zipFile->ExtractTo(SS(zipFileName << ".mdata"), SS(targetFileName.string() << ".mdata")); + } break; } } diff --git a/src/Storage.cpp b/src/Storage.cpp index 4ae1b77..f46b4d8 100644 --- a/src/Storage.cpp +++ b/src/Storage.cpp @@ -2023,6 +2023,11 @@ void Storage::DeleteSampleFile(const std::filesystem::path &fileName) else { std::filesystem::remove(fileName); + if (IsInAudioTracksDirectory(fileName)) + { + // remove the metadata file as well. + std::filesystem::remove(fileName.string() + ".mdata"); + } } } catch (const std::exception &) @@ -2180,6 +2185,10 @@ std::string Storage::RenameFilePropertyFile( } std::filesystem::rename(oldPath, newPath); + if (fs::exists(oldPath.string()+".mdata")) { + // rename the metadata file as well. + std::filesystem::rename(oldPath.string()+".mdata", newPath.string()+".mdata"); + } return newPath; } @@ -2209,11 +2218,17 @@ fs::path MakeVersionedPath(const fs::path &path) } } - int version = 1; + int version = 0; while (fs::exists(newPath)) { // append (n) to the file name. - std::string newFileName = stem + " (" + std::to_string(version) + ")" + newPath.extension().string(); + std::string newFileName; + if (version == 0) { + newFileName = SS(stem << newPath.extension().string()); + } else { + // append (n) to the file name. + newFileName = SS(stem << " (" << std::to_string(version) << ")" << newPath.extension().string()); + } newPath = newPath.parent_path() / newFileName; ++version; } @@ -2260,6 +2275,19 @@ std::string Storage::CopyFilePropertyFile( } std::filesystem::create_hard_link(oldPath, newPath); + + if (IsInAudioTracksDirectory(oldPath) + && IsInAudioTracksDirectory(newPath)) + { + std::filesystem::path metadataPath = SS(oldPath.string() << ".mdata"); + if (fs::exists(metadataPath)) { + // copy the metadata file as well. + std::filesystem::copy_file( + metadataPath, + SS(newPath.string() << ".mdata"), + fs::copy_options::overwrite_existing); + } + } return newPath; } diff --git a/src/WebServerConfig.cpp b/src/WebServerConfig.cpp index 140664d..63624da 100644 --- a/src/WebServerConfig.cpp +++ b/src/WebServerConfig.cpp @@ -59,21 +59,6 @@ using namespace pipedal; using namespace boost::system; namespace fs = std::filesystem; -static bool HasDotDot(const std::filesystem::path &path) -{ - for (auto &part : path) - { - if (part == "..") - { - return true; - } - if (part == ".") - { - return true; - } - } - return false; -} class UserUploadResponse { @@ -535,30 +520,41 @@ public: try { fs::path path = request_uri.query("path"); - if (path.empty()) + if (path.empty() || + !fs::exists(path) || + !this->model->IsInUploadsDirectory(path) || + HasDotDot(path)) { // path for folder thumbnails. path = request_uri.query("ffile"); - if (!fs::exists(path) || !this->model->IsInUploadsDirectory(path) || HasDotDot(path)) + + std::shared_ptr thumbnail; + + if (!path.empty() && fs::exists(path) && + this->model->IsInUploadsDirectory(path) && + !HasDotDot(path)) { - throw PiPedalException("File not found."); + // path is a folder. + thumbnail = std::make_shared(); + thumbnail->SetNonDeletedPath(path); + } + else + { + auto t = AudioDirectoryInfo::DefaultThumbnailTemporaryFile(); + thumbnail = std::make_shared(); + thumbnail->SetNonDeletedPath(t.Path()); + } - std::shared_ptr thumbnail = std::make_shared(); - thumbnail->SetNonDeletedPath(path); res.set(HttpField::content_type, MimeTypes::instance().MimeTypeFromExtension(path.extension())); res.set(HttpField::cache_control, CACHE_CONTROL_INDEFINITELY); // URL is cache-busted, and will change if the file ismodified. - setLastModifiedFromFile(res,path); - res.set(HttpField::content_length, std::to_string(fs::file_size(path))); + setLastModifiedFromFile(res,thumbnail->Path()); + res.set(HttpField::content_length, std::to_string(fs::file_size(thumbnail->Path()))); res.setBodyFile(thumbnail); return; } - if (!fs::exists(path) || !this->model->IsInUploadsDirectory(path) || HasDotDot(path)) - { - throw PiPedalException("File not found."); - } int32_t width = ConvertThumbnailSize(request_uri.query("w")); int32_t height = ConvertThumbnailSize(request_uri.query("h")); diff --git a/src/ZipFile.cpp b/src/ZipFile.cpp index 4c76c31..188ed52 100644 --- a/src/ZipFile.cpp +++ b/src/ZipFile.cpp @@ -52,15 +52,27 @@ public: { throw std::runtime_error("Can't open zip file."); } + zip_int64_t nEntries = zip_get_num_entries(zipFile, 0); + for (zip_int64_t i = 0; i < nEntries; ++i) + { + const char *name = zip_get_name(zipFile, i, ZIP_FL_ENC_STRICT); + if (name) + { + files.push_back(name); + nameMap[name] = i; + } + } } virtual ~ZipFileImpl(); - virtual std::vector GetFiles() override; + virtual const std::vector& GetFiles() override; virtual bool CompareFiles(const std::string &zipName, const std::filesystem::path& path) override; virtual void ExtractTo(const std::string &zipName, const std::filesystem::path &path) override; virtual zip_file_input_stream GetFileInputStream(const std::string& filename,size_t bufferSize = 16*1024) override; virtual size_t GetFileSize(const std::string&filename) override; + virtual bool FileExists(const std::string &zipName) const override; private: + std::vector files; std::map nameMap; // avoid o(2) extraction operations. const std::filesystem::path path; zip_t *zipFile = nullptr; @@ -79,20 +91,9 @@ ZipFileImpl::~ZipFileImpl() } } -std::vector ZipFileImpl::GetFiles() +const std::vector& ZipFileImpl::GetFiles() { - std::vector result; - zip_int64_t nEntries = zip_get_num_entries(zipFile, 0); - for (zip_int64_t i = 0; i < nEntries; ++i) - { - const char *name = zip_get_name(zipFile, i, ZIP_FL_ENC_STRICT); - if (name) - { - result.push_back(name); - nameMap[name] = i; - } - } - return result; + return files; } @@ -293,6 +294,11 @@ zip_file_input_stream ZipFileImpl::GetFileInputStream(const std::string& filenam } return zip_file_input_stream(f,bufferSize); } + +bool ZipFileImpl::FileExists(const std::string&fileName) const +{ + return nameMap.find(fileName) != nameMap.end(); +} size_t ZipFileImpl::GetFileSize(const std::string&filename) { zip_stat_t stat; diff --git a/src/ZipFile.hpp b/src/ZipFile.hpp index 4122d98..8dbc3c5 100644 --- a/src/ZipFile.hpp +++ b/src/ZipFile.hpp @@ -68,11 +68,12 @@ namespace pipedal { ZipFileReader&operator=(const ZipFileReader&) = delete; virtual ~ZipFileReader(); - virtual std::vector GetFiles() = 0; + virtual const std::vector& GetFiles() = 0; virtual void ExtractTo(const std::string &zipName, const std::filesystem::path& path) = 0; virtual bool CompareFiles(const std::string &zipName, const std::filesystem::path& path) = 0; virtual zip_file_input_stream GetFileInputStream(const std::string& filename,size_t bufferSize = 16*1024) = 0; virtual size_t GetFileSize(const std::string&filename) = 0; + virtual bool FileExists(const std::string&fileName) const = 0; }; diff --git a/src/ext/PiPedal/FileMetadataFeature.h b/src/ext/PiPedal/FileMetadataFeature.h deleted file mode 100644 index c1facf3..0000000 --- a/src/ext/PiPedal/FileMetadataFeature.h +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright (c) 2025 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. - */ - -#ifndef PIPEDAL_FILE_METADAtA_FEATURE_H -#define PIPEDAL_FILE_METADAtA_FEATURE_H -#include "lv2/core/lv2.h" -#include "lv2/urid/urid.h" - -#define PIPEDAL__FILE_METADATA_FEATURE "http://github.com/rerdavies/pipedal/ext/#fileMetadata" - -#ifdef __cplusplus -extern "C" -{ -#endif - typedef void *PIPEDAL_FILE_METADATA_Handle; - - typedef enum - { - PIPEDAL_FILE_METADATA_SUCCESS = 0, /**< Completed successfully. */ - PIPEDAL_FILE_METADATA_INVALID_PATH = 1, /**< Path is not in Tracks directory, or does not exist. */ - PIPEDAL_FILE_METADATA_PERMISSION_DENIED = 2, /**< Permission denied. */ - PIPEDAL_FILE_METADATA_ERR_UNKNOWNM = 3, /**< Unknown error. */ - PIPEDAL_FILE_METADATA_INVALID_KEY = 4 /**< Key is not a valid URID. */ - - } PIPEDAL_FileMetadata_Status; - - typedef struct - { - /** - Opaque host data. - */ - PIPEDAL_FILE_METADATA_Handle handle; - - /** - Save a piece of plugin-defined metadata for a file. - @param handle MUST be the `handle` member of this struct. - @param absolute_path The absolute path of a file. - @param key A plugin-defined key (LV2_URID) used to identify the metdadata. - @param metdata A string containing the metadata to be saved for the file. - @return A status code indicating success or failure. - - The path must be within the host-defined "Tracks" directory. The file must exist. The plugin does not - need write access to the directory containing the file in order to save metadata. - - Plugins MUST NOT make any assumptions about abstract paths except that - they can be mapped back to the absolute path of the "same" file (though - not necessarily the same original path) using absolute_path(). - - Metatadata will be automatically deleted if the file is deleted, of the file is modified, or moved - to another directory. - - This function should not be called from the plugin's realtime thread, as it may block for a long time. - */ - - PIPEDAL_FileMetadata_Status (*setFileMetadata)(PIPEDAL_FILE_METADATA_Handle handle, const char *absolute_path, LV2_URID key, const char *fileMetadata); - /** - Restore previously saved metdata for the file. - @param handle MUST be the `handle` member of this struct. - @param absolute_path The absolute path of a file. - @param key A plugin-defined key (LV2_URID) used to identify the metdadata. - @param buffer A buffer in which to store the metadata. - @param buffersize The size of the buffer in bytes, including space for the null terminator. - @return A value indicating the number of bytes written to the buffer, or 0 if the metdata was not found. - - If buffersize is insufficient to holed the metatdata, the function will return the number of bytes that would have been written, - including the space for the null terminator, but will not write any data to the buffer. The best way to find out - how much space is needed is to call this function with a buffer size of 0, which will return the size needed to contain the - metadata result. - - This function should not be called from the plugin's realtime thread, as it may block for a long time. - - */ - uint32_t (*getFileMetadata)(PIPEDAL_FILE_METADATA_Handle handle, LV2_URID key, const char *filePath, char *buffer, uint32_t bufferSize); - } PIPEDAL_FileMetadata_Interface; - - typedef struct { - const char* URI; - PIPEDAL_FileMetadata_Interface*interface; - } PIPEDAL_FileMetadata_Feature; - -#ifdef __cplusplus -} -#endif - -#endif // PIPEDAL_FILE_METADAtA_FEATURE_H \ No newline at end of file diff --git a/src/lv2ext/pipedal.lv2/ext/FileMetadataFeature.h b/src/lv2ext/pipedal.lv2/ext/FileMetadataFeature.h new file mode 100644 index 0000000..38f0a09 --- /dev/null +++ b/src/lv2ext/pipedal.lv2/ext/FileMetadataFeature.h @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2025 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. + */ + +#ifndef PIPEDAL_FILE_METADAtA_FEATURE_H +#define PIPEDAL_FILE_METADAtA_FEATURE_H +#include "lv2/core/lv2.h" +#include "lv2/urid/urid.h" + +#define PIPEDAL__FILE_METADATA_FEATURE "http://github.com/rerdavies/pipedal/ext/#fileMetadata" + +#ifdef __cplusplus +extern "C" +{ +#endif + typedef void *PIPEDAL_FILE_METADATA_Handle; + + typedef enum + { + PIPEDAL_FILE_METADATA_SUCCESS = 0, /**< Completed successfully. */ + PIPEDAL_FILE_METADATA_INVALID_PATH = 1, /**< Path is not in Tracks directory, or does not exist. */ + PIPEDAL_FILE_METADATA_PERMISSION_DENIED = 2, /**< Permission denied. */ + PIPEDAL_FILE_METADATA_ERR_UNKNOWNM = 3, /**< Unknown error. */ + PIPEDAL_FILE_METADATA_INVALID_KEY = 4, /**< Key is not a valid URID. */ + PIPEDAL_FILE_METADATA_NOT_FOUND = 5, /**< Metadata not found. */ + } PIPEDAL_FileMetadata_Status; + + typedef struct + { + /** + Opaque host data. + */ + PIPEDAL_FILE_METADATA_Handle handle; + + /** + Save a piece of plugin-defined metadata for a file. + @param handle MUST be the `handle` member of this struct. + @param absolute_path The absolute path of a file. + @param key A plugin-defined key (LV2_URID) used to identify the metdadata. + @param metdata A string containing the metadata to be saved for the file. + @return A status code indicating success or failure. + + The path must be within the host-defined "Tracks" directory. The file must exist. The plugin does not + need write access to the directory containing the file in order to save metadata. + + Plugins MUST NOT make any assumptions about abstract paths except that + they can be mapped back to the absolute path of the "same" file (though + not necessarily the same original path) using absolute_path(). + + Metatadata will be automatically deleted if the file is deleted, of the file is modified, or moved + to another directory. + + This function should not be called from the plugin's realtime thread, as it may block for a long time. + */ + + PIPEDAL_FileMetadata_Status (*setFileMetadata)( + PIPEDAL_FILE_METADATA_Handle handle, + const char *absolute_path, + const char *key, + const char *fileMetadata); + /** + Restore previously saved metdata for the file. + @param handle MUST be the `handle` member of this struct. + @param absolute_path The absolute path of a file. + @param key A plugin-defined key used to identify the metdadata for a particular appliction. + @param buffer A buffer in which to store the metadata. + @param buffersize The size of the buffer in bytes, including space for the null terminator. + @return A value indicating the number of bytes written to the buffer, or 0 if the metdata was not found. + + If buffersize is insufficient to holed the metatdata, the function will return the number of bytes that would have been written, + including the space for the null terminator, but will not write any data to the buffer. The best way to find out + how much space is needed is to call this function with a buffer size of 0, which will return the size needed to contain the + metadata result. + + This function should not be called from the plugin's realtime thread, as it may block for a long time. + + */ + uint32_t (*getFileMetadata)( + PIPEDAL_FILE_METADATA_Handle handle, + const char *filePath, + const char *key, + char *buffer, + uint32_t bufferSize); + /** + Restore previously saved metdata for the file. + @param handle MUST be the `handle` member of this struct. + @param absolute_path The absolute path of a file. + @param key A plugin-defined key used to identify the metdadata for a particular appliction. + @return A status code indicating success or failure. + This function will delete the metadata for the file, if it exists. If the file does not exist, + or is not in the Tracks directory, it will return PIPEDAL_FILE_METADATA_INVALID_PATH. + */ + + PIPEDAL_FileMetadata_Status (*deleteFileMetadata)( + PIPEDAL_FILE_METADATA_Handle handle, + const char *filePath, + const char *key); + } PIPEDAL_FileMetadata_Interface; + + typedef struct + { + const char *URI; + PIPEDAL_FileMetadata_Interface *interface; + } PIPEDAL_FileMetadata_Feature; + +#ifdef __cplusplus +} +#endif + +#endif // PIPEDAL_FILE_METADAtA_FEATURE_H \ No newline at end of file diff --git a/todo.txt b/todo.txt index ac43064..e62e535 100644 --- a/todo.txt +++ b/todo.txt @@ -1,3 +1,4 @@ + Numeric up-down buttons blown in light theme. Tooltips on touch ui? @@ -28,4 +29,3 @@ pcm.pipedal_aux_in { } } -Move to /usr/local because...?q diff --git a/vite/src/pipedal/AudioFileMetadata.tsx b/vite/src/pipedal/AudioFileMetadata.tsx index 170db7e..d38d435 100644 --- a/vite/src/pipedal/AudioFileMetadata.tsx +++ b/vite/src/pipedal/AudioFileMetadata.tsx @@ -21,7 +21,7 @@ * SOFTWARE. */ -import { pathConcat, pathParentDirectory } from "./FileUtils"; +import { pathConcat, pathParentDirectory, pathFileName } from "./FileUtils"; import { PiPedalModel } from "./PiPedalModel"; export class ThumbnailType { @@ -32,6 +32,40 @@ export class ThumbnailType { }; +const fileVersionRegex = /\((\d+)\)\.[0-9a-zA-Z]*$/; +function getVersionSuffix(filePath: string): string | null { + const match = filePath.match(fileVersionRegex); + return match ? match[1] : null; +} + + +export function getTrackTitle(pathname: string, metadata: AudioFileMetadata | null | undefined): string { + if (!metadata) { + return pathFileName(pathname); + } + if (metadata.title === "") { + return pathFileName(pathname); + } + let trackDisplay = ""; + if (metadata.track > 0) { + if (metadata.track >= 1000) { + trackDisplay = (metadata.track % 1000).toString() + "/" + Math.floor(metadata.track / 1000) + ". "; + } else { + trackDisplay = metadata.track.toString() + ". "; + } + } + let result = trackDisplay + metadata.title; + if (result === "") { + result = pathFileName(pathname); + } + let versionSuffix = getVersionSuffix(pathname); + if (versionSuffix) { + result += " (" + versionSuffix + ")"; + } + return result; +} + + export default class AudioFileMetadata { //AudioFileMetadata&operator=(const AudioFileMetadata&) = default; deserialize(o: any) { diff --git a/vite/src/pipedal/BankDialog.tsx b/vite/src/pipedal/BankDialog.tsx index ea12606..7831518 100644 --- a/vite/src/pipedal/BankDialog.tsx +++ b/vite/src/pipedal/BankDialog.tsx @@ -18,7 +18,7 @@ // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. import React, { Component } from 'react'; -import IconButton from '@mui/material/IconButton'; +import IconButtonEx from './IconButtonEx'; import Typography from '@mui/material/Typography'; import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel'; import { BankIndexEntry, BankIndex } from './Banks'; @@ -422,17 +422,17 @@ const BankDialog = withStyles(
- - + Banks - this.showActionBar(true)} > + this.showActionBar(true)} > - + {(!this.props.isEditDialog) ? ( - this.showActionBar(false)} aria-label="close"> + this.showActionBar(false)} aria-label="close"> - + ) : ( - - + )} @@ -477,12 +477,12 @@ const BankDialog = withStyles( } } /> - this.handleDeleteClick()} > + this.handleDeleteClick()} > Delete - - { this.onMoreClick(e) }} > + + { this.onMoreClick(e) }} > - + -