ToobPlayer UI. .mdata file handling.

This commit is contained in:
Robin E. R. Davies
2025-06-19 16:04:32 -04:00
parent bf9846e37f
commit f35ee9330d
29 changed files with 671 additions and 425 deletions
@@ -309,6 +309,10 @@ namespace pipedal
const_iterator begin() const { return values.begin(); } const_iterator begin() const { return values.begin(); }
const_iterator end() const { return values.end(); } const_iterator end() const { return values.end(); }
iterator erase(iterator it)
{
return values.erase(it);
}
iterator find(const std::string &key); iterator find(const std::string &key);
const_iterator find(const std::string &key) const; const_iterator find(const std::string &key) const;
+2
View File
@@ -119,6 +119,8 @@ namespace pipedal
std::filesystem::path MakeRelativePath(const std::filesystem::path &path, const std::filesystem::path&parentPath); 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 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); std::string ToLower(const std::string&value);
+16
View File
@@ -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) bool pipedal::IsSubdirectory(const std::filesystem::path &path, const std::filesystem::path &basePath)
{ {
auto iPath = path.begin(); auto iPath = path.begin();
+30 -54
View File
@@ -145,8 +145,6 @@ namespace
virtual std::vector<AudioFileMetadata> GetFiles() override; virtual std::vector<AudioFileMetadata> GetFiles() override;
virtual ThumbnailTemporaryFile GetThumbnail(const std::string &fileNameOnly, int32_t width, int32_t height) 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 size_t TestGetNumberOfThumbnails() override; // test use only.
virtual void TestSetIndexPath(const std::filesystem::path &path) override virtual void TestSetIndexPath(const std::filesystem::path &path) override
{ {
@@ -160,15 +158,6 @@ namespace
int32_t fromPosition, int32_t fromPosition,
int32_t toPosition) override; 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: private:
std::vector<DbFileInfo> QueryTracks(); std::vector<DbFileInfo> QueryTracks();
@@ -470,7 +459,7 @@ void AudioDirectoryInfoImpl::DbDeleteFile(DbFileInfo *dbFile)
audioFilesDb->DeleteFile(dbFile); audioFilesDb->DeleteFile(dbFile);
} }
} }
ThumbnailTemporaryFile AudioDirectoryInfoImpl::DefaultThumbnailTemporaryFile() ThumbnailTemporaryFile AudioDirectoryInfo::DefaultThumbnailTemporaryFile()
{ {
ThumbnailTemporaryFile tempFile; ThumbnailTemporaryFile tempFile;
fs::path defaultThumbnail = GetResourceDirectory() / "img/missing_thumbnail.jpg"; 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 (files[i].fileName() == fileNameOnly)
{ {
if (i + 1 < files.size()) size_t next = i;
while (true)
{ {
return files[i + 1].fileName(); next = (next + 1) % files.size();
} if (!isFolderArtwork(files[next].fileName()))
else {
{ return files[next].fileName();
return files[0].fileName(); // wrap around to the first file. }
if (next == i)
{
break;
}
} }
} }
} }
@@ -806,13 +800,25 @@ std::string AudioDirectoryInfoImpl::GetPreviousAudioFile(const std::string &file
{ {
if (files[i].fileName() == fileNameOnly) if (files[i].fileName() == fileNameOnly)
{ {
if (i > 0) size_t next = i;
while (true)
{ {
return files[i - 1].fileName(); if (next == 0)
} {
else next = files.size() - 1; // wrap around to the last file.
{ }
return files[files.size() - 1].fileName(); // 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);
}
+1 -10
View File
@@ -95,20 +95,11 @@ namespace pipedal
virtual std::string GetNextAudioFile(const std::string &fileNameOnly) = 0; virtual std::string GetNextAudioFile(const std::string &fileNameOnly) = 0;
virtual std::string GetPreviousAudioFile(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( virtual void MoveAudioFile(
const std::string &directory, const std::string &directory,
int32_t fromPosition, int32_t fromPosition,
int32_t toPosition) = 0; int32_t toPosition) = 0;
virtual ThumbnailTemporaryFile DefaultThumbnailTemporaryFile() = 0; static ThumbnailTemporaryFile DefaultThumbnailTemporaryFile();
static void SetTemporaryDirectory(const std::filesystem::path &path); static void SetTemporaryDirectory(const std::filesystem::path &path);
static void SetResourceDirectory(const std::filesystem::path &path); static void SetResourceDirectory(const std::filesystem::path &path);
-49
View File
@@ -192,13 +192,6 @@ void AudioFilesDb::CreateDb(const std::filesystem::path &dbPathName)
"width INTEGER, " "width INTEGER, "
"height 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) catch (const SQLite::Exception &e)
{ {
@@ -510,45 +503,3 @@ void AudioFilesDb::UpdateFilePosition(
updateFileQuery->exec(); updateFileQuery->exec();
} }
void AudioFilesDb::DeleteFileExtraMetadata(
const std::string &fileNameOnly,
const std::string &key)
{
auto deleteExtraMetadataQuery = std::make_unique<SQLite::Statement>(
*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<SQLite::Statement>(
*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<SQLite::Statement>(
*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 "";
}
-11
View File
@@ -110,17 +110,6 @@ namespace pipedal::impl {
int64_t idFile, int64_t idFile,
int32_t position); 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: private:
void CreateDb(const std::filesystem::path &dbPathName); void CreateDb(const std::filesystem::path &dbPathName);
+167 -44
View File
@@ -22,9 +22,16 @@
#include "AudioFiles.hpp" #include "AudioFiles.hpp"
#include "Lv2Log.hpp" #include "Lv2Log.hpp"
#include "ss.hpp" #include "ss.hpp"
#include "util.hpp"
#include "json.hpp"
#include "json_variant.hpp"
#include <iostream>
#include <filesystem>
#include "lv2/lv2plug.in/ns/ext/buf-size/buf-size.h" #include "lv2/lv2plug.in/ns/ext/buf-size/buf-size.h"
using namespace pipedal; using namespace pipedal;
namespace fs = std::filesystem;
FileMetadataFeature::FileMetadataFeature() FileMetadataFeature::FileMetadataFeature()
{ {
@@ -33,12 +40,37 @@ FileMetadataFeature::FileMetadataFeature()
interface.handle = (void *)this; interface.handle = (void *)this;
interface.setFileMetadata = &FileMetadataFeature::S_setFileMetadata; interface.setFileMetadata = &FileMetadataFeature::S_setFileMetadata;
interface.getFileMetadata = &FileMetadataFeature::S_getFileMetadata; interface.getFileMetadata = &FileMetadataFeature::S_getFileMetadata;
interface.deleteFileMetadata = &FileMetadataFeature::S_deleteFileMetadata;
} }
FileMetadataFeature::~FileMetadataFeature() 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) void FileMetadataFeature::Prepare(MapFeature &map)
{ {
this->mapFeature = &map; this->mapFeature = &map;
@@ -46,88 +78,179 @@ void FileMetadataFeature::Prepare(MapFeature &map)
PIPEDAL_FileMetadata_Status FileMetadataFeature::setFileMetadata( PIPEDAL_FileMetadata_Status FileMetadataFeature::setFileMetadata(
const char *absolute_path, const char *absolute_path,
LV2_URID key, const char *key,
const char *fileMetadata) const char *value)
{ {
const char *strKey = mapFeature->UridToString(key); if (!IsValidPath(absolute_path))
if (strKey == nullptr)
{ {
return PIPEDAL_FILE_METADATA_INVALID_KEY; return PIPEDAL_FILE_METADATA_INVALID_PATH; // Cannot set metadata for files in the tracks directory
}
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
} }
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( uint32_t FileMetadataFeature::getFileMetadata(
const char *absolute_path, const char *absolute_path,
LV2_URID key, const char *key,
char *fileMetadata, char *fileMetadata,
uint32_t fileMetadataSize) uint32_t fileMetadataSize)
{ {
const char *strKey = mapFeature->UridToString(key); if (fileMetadata)
if (strKey == nullptr) 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 (fs::exists(path))
if (metadata.empty())
{ {
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; else
if (required >= std::numeric_limits<uint32_t>::max())
{ {
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<uint32_t>(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<uint32_t>(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); Lv2Log::debug(SS("Delete file metadata for " << absolute_path << " key: " << key));
return required;
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_FileMetadata_Status FileMetadataFeature::S_setFileMetadata(
PIPEDAL_FILE_METADATA_Handle handle, PIPEDAL_FILE_METADATA_Handle handle,
const char *absolute_path, const char *absolute_path,
LV2_URID key, const char *key,
const char *fileMetadata) const char *fileMetadata)
{ {
return ((FileMetadataFeature *)handle)->setFileMetadata(absolute_path, key, fileMetadata); return ((FileMetadataFeature *)handle)->setFileMetadata(absolute_path, key, fileMetadata);
} }
uint32_t FileMetadataFeature::S_getFileMetadata( uint32_t FileMetadataFeature::S_getFileMetadata(
PIPEDAL_FILE_METADATA_Handle handle, PIPEDAL_FILE_METADATA_Handle handle,
LV2_URID key,
const char *absolute_path, const char *absolute_path,
const char *key,
char *buffer, char *buffer,
uint32_t bufferSize) uint32_t bufferSize)
{ {
return ((FileMetadataFeature *)handle)->getFileMetadata(absolute_path, key, buffer, 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);
}
+27 -5
View File
@@ -21,7 +21,7 @@
#include "MapFeature.hpp" #include "MapFeature.hpp"
#include "PiPedalUI.hpp" #include "PiPedalUI.hpp"
#include "ext/PiPedal/FileMetadataFeature.h" #include "lv2ext/pipedal.lv2/ext/FileMetadataFeature.h"
namespace pipedal namespace pipedal
{ {
@@ -32,27 +32,49 @@ namespace pipedal
private: private:
LV2_Feature feature; LV2_Feature feature;
PIPEDAL_FileMetadata_Interface interface; 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); static PIPEDAL_FileMetadata_Status S_setFileMetadata(
PIPEDAL_FileMetadata_Status setFileMetadata(const char *absolute_path, LV2_URID key, const char *fileMetadata); 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( static uint32_t S_getFileMetadata(
PIPEDAL_FILE_METADATA_Handle handle, PIPEDAL_FILE_METADATA_Handle handle,
LV2_URID key,
const char *filePath, const char *filePath,
const char*key,
char *buffer, char *buffer,
uint32_t bufferSize); uint32_t bufferSize);
uint32_t getFileMetadata( uint32_t getFileMetadata(
const char *absolute_path, const char *absolute_path,
LV2_URID key, const char*key,
char *fileMetadata, char *fileMetadata,
uint32_t fileMetadataSize); 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: public:
FileMetadataFeature(); FileMetadataFeature();
void Prepare(MapFeature &map); void Prepare(MapFeature &map);
void SetPluginStoragePath(const std::filesystem::path &storagePath);
~FileMetadataFeature(); ~FileMetadataFeature();
public: public:
std::filesystem::path tracksPath;
MapFeature *mapFeature = nullptr; MapFeature *mapFeature = nullptr;
const LV2_Feature *GetFeature() const LV2_Feature *GetFeature()
{ {
+1
View File
@@ -95,6 +95,7 @@ namespace pipedal
virtual void OnNetworkChanging(bool hotspotConnected) = 0; virtual void OnNetworkChanging(bool hotspotConnected) = 0;
virtual void OnHasWifiChanged(bool hasWifi) = 0; virtual void OnHasWifiChanged(bool hasWifi) = 0;
virtual void Close() = 0; virtual void Close() = 0;
}; };
class HotspotManager; class HotspotManager;
+5
View File
@@ -249,6 +249,7 @@ static float nodeAsFloat(const LilvNode *node, float default_ = 0)
void PluginHost::SetPluginStoragePath(const std::filesystem::path &path) void PluginHost::SetPluginStoragePath(const std::filesystem::path &path)
{ {
pluginStoragePath = path; pluginStoragePath = path;
fileMetadataFeature.SetPluginStoragePath(path);
} }
std::string PluginHost::GetPluginStoragePath() const std::string PluginHost::GetPluginStoragePath() const
@@ -266,6 +267,9 @@ PluginHost::PluginHost()
optionsFeature.Prepare(mapFeature, 44100, this->GetMaxAudioBufferSize(), this->GetAtomBufferSize()); optionsFeature.Prepare(mapFeature, 44100, this->GetMaxAudioBufferSize(), this->GetAtomBufferSize());
lv2Features.push_back(optionsFeature.GetFeature()); lv2Features.push_back(optionsFeature.GetFeature());
fileMetadataFeature.Prepare(mapFeature);
lv2Features.push_back(fileMetadataFeature.GetFeature());
lv2Features.push_back(nullptr); lv2Features.push_back(nullptr);
@@ -902,6 +906,7 @@ std::vector<std::string> supportedFeatures = {
LV2_STATE__freePath, LV2_STATE__freePath,
LV2_CORE__inPlaceBroken, LV2_CORE__inPlaceBroken,
PIPEDAL_HOST_FEATURE, PIPEDAL_HOST_FEATURE,
PIPEDAL__FILE_METADATA_FEATURE,
// UI features that we can ignore, since we won't load their ui. // UI features that we can ignore, since we won't load their ui.
"http://lv2plug.in/ns/extensions/ui#makeResident", "http://lv2plug.in/ns/extensions/ui#makeResident",
+5 -1
View File
@@ -25,6 +25,7 @@
#include "PluginType.hpp" #include "PluginType.hpp"
#include <lilv/lilv.h> #include <lilv/lilv.h>
#include "MapFeature.hpp" #include "MapFeature.hpp"
#include "FileMetadataFeature.hpp"
#include "OptionsFeature.hpp" #include "OptionsFeature.hpp"
#include <filesystem> #include <filesystem>
#include <cmath> #include <cmath>
@@ -794,6 +795,7 @@ namespace pipedal
std::vector<const LV2_Feature *> lv2Features; std::vector<const LV2_Feature *> lv2Features;
MapFeature mapFeature; MapFeature mapFeature;
FileMetadataFeature fileMetadataFeature;
OptionsFeature optionsFeature; OptionsFeature optionsFeature;
std::string pluginStoragePath; std::string pluginStoragePath;
@@ -858,9 +860,11 @@ namespace pipedal
virtual std::shared_ptr<HostWorkerThread> GetHostWorkerThread(); virtual std::shared_ptr<HostWorkerThread> GetHostWorkerThread();
public: 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); void CheckForResourceInitialization(const std::string& pluginUri,const std::filesystem::path& pluginUploadDirectory);
std::string MapResourcePath(const std::string&uri, const std::string&relativePath); std::string MapResourcePath(const std::string&uri, const std::string&relativePath);
void ReloadPlugins(); void ReloadPlugins();
+66 -1
View File
@@ -140,6 +140,12 @@ void PresetBundleWriterImpl::WriteToFile(const std::filesystem::path &filePath)
{ {
Lv2Log::warning(SS("Media file not found: " << sourcePath)); 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(); zipFile->Close();
} }
@@ -236,7 +242,28 @@ private:
void ExtractMediaFile(const std::string &zipFileName); void ExtractMediaFile(const std::string &zipFileName);
bool IsSameFile(const std::string &zipFileName, const std::filesystem::path &filePath) 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); 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_) for (auto preset : pluginPresets.presets_)
@@ -363,6 +420,10 @@ void PresetBundleReaderImpl::ExtractMediaFile(const std::string &zipFileName)
namespace fs = std::filesystem; namespace fs = std::filesystem;
if (zipFileName.starts_with("media/")) if (zipFileName.starts_with("media/"))
{ {
if (zipFileName.ends_with(".mdata"))
{
return;
}
std::string baseName = zipFileName.substr(6); std::string baseName = zipFileName.substr(6);
fs::path targetFileName = this->pluginUploadDirectory / std::filesystem::path(baseName); fs::path targetFileName = this->pluginUploadDirectory / std::filesystem::path(baseName);
bool renamed = false; bool renamed = false;
@@ -383,6 +444,10 @@ void PresetBundleReaderImpl::ExtractMediaFile(const std::string &zipFileName)
else else
{ {
zipFile->ExtractTo(zipFileName, targetFileName); zipFile->ExtractTo(zipFileName, targetFileName);
if (zipFile->FileExists(SS(zipFileName << ".mdata")))
{
zipFile->ExtractTo(SS(zipFileName << ".mdata"), SS(targetFileName.string() << ".mdata"));
}
break; break;
} }
} }
+30 -2
View File
@@ -2023,6 +2023,11 @@ void Storage::DeleteSampleFile(const std::filesystem::path &fileName)
else else
{ {
std::filesystem::remove(fileName); std::filesystem::remove(fileName);
if (IsInAudioTracksDirectory(fileName))
{
// remove the metadata file as well.
std::filesystem::remove(fileName.string() + ".mdata");
}
} }
} }
catch (const std::exception &) catch (const std::exception &)
@@ -2180,6 +2185,10 @@ std::string Storage::RenameFilePropertyFile(
} }
std::filesystem::rename(oldPath, newPath); 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; return newPath;
} }
@@ -2209,11 +2218,17 @@ fs::path MakeVersionedPath(const fs::path &path)
} }
} }
int version = 1; int version = 0;
while (fs::exists(newPath)) while (fs::exists(newPath))
{ {
// append (n) to the file name. // 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; newPath = newPath.parent_path() / newFileName;
++version; ++version;
} }
@@ -2260,6 +2275,19 @@ std::string Storage::CopyFilePropertyFile(
} }
std::filesystem::create_hard_link(oldPath, newPath); 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; return newPath;
} }
+22 -26
View File
@@ -59,21 +59,6 @@ using namespace pipedal;
using namespace boost::system; using namespace boost::system;
namespace fs = std::filesystem; 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 class UserUploadResponse
{ {
@@ -535,30 +520,41 @@ public:
try try
{ {
fs::path path = request_uri.query("path"); 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 for folder thumbnails.
path = request_uri.query("ffile"); path = request_uri.query("ffile");
if (!fs::exists(path) || !this->model->IsInUploadsDirectory(path) || HasDotDot(path))
std::shared_ptr<TemporaryFile> 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<TemporaryFile>();
thumbnail->SetNonDeletedPath(path);
}
else
{
auto t = AudioDirectoryInfo::DefaultThumbnailTemporaryFile();
thumbnail = std::make_shared<TemporaryFile>();
thumbnail->SetNonDeletedPath(t.Path());
} }
std::shared_ptr<TemporaryFile> thumbnail = std::make_shared<TemporaryFile>();
thumbnail->SetNonDeletedPath(path);
res.set(HttpField::content_type, MimeTypes::instance().MimeTypeFromExtension(path.extension())); 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. res.set(HttpField::cache_control, CACHE_CONTROL_INDEFINITELY); // URL is cache-busted, and will change if the file ismodified.
setLastModifiedFromFile(res,path); setLastModifiedFromFile(res,thumbnail->Path());
res.set(HttpField::content_length, std::to_string(fs::file_size(path))); res.set(HttpField::content_length, std::to_string(fs::file_size(thumbnail->Path())));
res.setBodyFile(thumbnail); res.setBodyFile(thumbnail);
return; 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 width = ConvertThumbnailSize(request_uri.query("w"));
int32_t height = ConvertThumbnailSize(request_uri.query("h")); int32_t height = ConvertThumbnailSize(request_uri.query("h"));
+20 -14
View File
@@ -52,15 +52,27 @@ public:
{ {
throw std::runtime_error("Can't open zip file."); 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 ~ZipFileImpl();
virtual std::vector<std::string> GetFiles() override; virtual const std::vector<std::string>& GetFiles() override;
virtual bool CompareFiles(const std::string &zipName, const std::filesystem::path& path) 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 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 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 size_t GetFileSize(const std::string&filename) override;
virtual bool FileExists(const std::string &zipName) const override;
private: private:
std::vector<std::string> files;
std::map<std::string, zip_int64_t> nameMap; // avoid o(2) extraction operations. std::map<std::string, zip_int64_t> nameMap; // avoid o(2) extraction operations.
const std::filesystem::path path; const std::filesystem::path path;
zip_t *zipFile = nullptr; zip_t *zipFile = nullptr;
@@ -79,20 +91,9 @@ ZipFileImpl::~ZipFileImpl()
} }
} }
std::vector<std::string> ZipFileImpl::GetFiles() const std::vector<std::string>& ZipFileImpl::GetFiles()
{ {
std::vector<std::string> result; return files;
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;
} }
@@ -293,6 +294,11 @@ zip_file_input_stream ZipFileImpl::GetFileInputStream(const std::string& filenam
} }
return zip_file_input_stream(f,bufferSize); 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) size_t ZipFileImpl::GetFileSize(const std::string&filename)
{ {
zip_stat_t stat; zip_stat_t stat;
+2 -1
View File
@@ -68,11 +68,12 @@ namespace pipedal {
ZipFileReader&operator=(const ZipFileReader&) = delete; ZipFileReader&operator=(const ZipFileReader&) = delete;
virtual ~ZipFileReader(); virtual ~ZipFileReader();
virtual std::vector<std::string> GetFiles() = 0; virtual const std::vector<std::string>& GetFiles() = 0;
virtual void ExtractTo(const std::string &zipName, const std::filesystem::path& path) = 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 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 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 size_t GetFileSize(const std::string&filename) = 0;
virtual bool FileExists(const std::string&fileName) const = 0;
}; };
-105
View File
@@ -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
@@ -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
+1 -1
View File
@@ -1,3 +1,4 @@
Numeric up-down buttons blown in light theme. Numeric up-down buttons blown in light theme.
Tooltips on touch ui? Tooltips on touch ui?
@@ -28,4 +29,3 @@ pcm.pipedal_aux_in {
} }
} }
Move to /usr/local because...?q
+35 -1
View File
@@ -21,7 +21,7 @@
* SOFTWARE. * SOFTWARE.
*/ */
import { pathConcat, pathParentDirectory } from "./FileUtils"; import { pathConcat, pathParentDirectory, pathFileName } from "./FileUtils";
import { PiPedalModel } from "./PiPedalModel"; import { PiPedalModel } from "./PiPedalModel";
export class ThumbnailType { 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 { export default class AudioFileMetadata {
//AudioFileMetadata&operator=(const AudioFileMetadata&) = default; //AudioFileMetadata&operator=(const AudioFileMetadata&) = default;
deserialize(o: any) { deserialize(o: any) {
+13 -13
View File
@@ -18,7 +18,7 @@
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import React, { Component } from 'react'; import React, { Component } from 'react';
import IconButton from '@mui/material/IconButton'; import IconButtonEx from './IconButtonEx';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel'; import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
import { BankIndexEntry, BankIndex } from './Banks'; import { BankIndexEntry, BankIndex } from './Banks';
@@ -422,17 +422,17 @@ const BankDialog = withStyles(
<div style={{ flex: "0 0 auto" }}> <div style={{ flex: "0 0 auto" }}>
<AppBar className={classes.dialogAppBar} style={{ display: this.isEditMode() ? "none" : "block" }} > <AppBar className={classes.dialogAppBar} style={{ display: this.isEditMode() ? "none" : "block" }} >
<Toolbar> <Toolbar>
<IconButton edge="start" color="inherit" onClick={this.handleDialogClose} aria-label="back" <IconButtonEx tooltip="Back" edge="start" color="inherit" onClick={this.handleDialogClose} aria-label="back"
disabled={this.isEditMode()} disabled={this.isEditMode()}
> >
<ArrowBackIcon /> <ArrowBackIcon />
</IconButton> </IconButtonEx>
<Typography noWrap variant="h6" className={classes.dialogTitle}> <Typography noWrap variant="h6" className={classes.dialogTitle}>
Banks Banks
</Typography> </Typography>
<IconButton color="inherit" onClick={(e) => this.showActionBar(true)} > <IconButtonEx tooltip="Edit" color="inherit" onClick={(e) => this.showActionBar(true)} >
<EditIcon /> <EditIcon />
</IconButton> </IconButtonEx>
</Toolbar> </Toolbar>
</AppBar> </AppBar>
<AppBar className={actionBarClass} style={{ display: this.isEditMode() ? "block" : "none" }} <AppBar className={actionBarClass} style={{ display: this.isEditMode() ? "block" : "none" }}
@@ -440,14 +440,14 @@ const BankDialog = withStyles(
> >
<Toolbar> <Toolbar>
{(!this.props.isEditDialog) ? ( {(!this.props.isEditDialog) ? (
<IconButton edge="start" color="inherit" onClick={(e) => this.showActionBar(false)} aria-label="close"> <IconButtonEx tooltip="Close" edge="start" color="inherit" onClick={(e) => this.showActionBar(false)} aria-label="close">
<CloseIcon /> <CloseIcon />
</IconButton> </IconButtonEx>
) : ( ) : (
<IconButton edge="start" color="inherit" onClick={this.handleDialogClose} aria-label="back" <IconButtonEx tooltip="Back" edge="start" color="inherit" onClick={this.handleDialogClose} aria-label="back"
> >
<ArrowBackIcon /> <ArrowBackIcon />
</IconButton> </IconButtonEx>
)} )}
<Typography noWrap variant="h6" className={classes.dialogTitle}> <Typography noWrap variant="h6" className={classes.dialogTitle}>
@@ -477,12 +477,12 @@ const BankDialog = withStyles(
} }
} }
/> />
<IconButton color="inherit" onClick={(e) => this.handleDeleteClick()} > <IconButtonEx tooltip="Delete" color="inherit" onClick={(e) => this.handleDeleteClick()} >
<img src="/img/old_delete_outline_white_24dp.svg" alt="Delete" style={{ width: 24, height: 24, opacity: 0.6 }} /> <img src="/img/old_delete_outline_white_24dp.svg" alt="Delete" style={{ width: 24, height: 24, opacity: 0.6 }} />
</IconButton> </IconButtonEx>
<IconButton color="inherit" onClick={(e) => { this.onMoreClick(e) }} > <IconButtonEx tooltip="More..." color="inherit" onClick={(e) => { this.onMoreClick(e) }} >
<MoreVertIcon /> <MoreVertIcon />
</IconButton> </IconButtonEx>
<Menu <Menu
id="more-menu" id="more-menu"
anchorEl={this.state.moreMenuAnchorEl} anchorEl={this.state.moreMenuAnchorEl}
+4 -1
View File
@@ -42,7 +42,10 @@ function ButtonEx(props: ButtonExProps) {
placement="top-start" arrow placement="top-start" arrow
enterDelay={1500} enterNextDelay={1500} enterDelay={1500} enterNextDelay={1500}
> >
<Button {...extra} style={style} /> <span>
{/* Using span to prevent tooltip from disappearing when button is disabled */}
<Button {...extra} style={style} />
</span>
</Tooltip> </Tooltip>
); );
} }
+8 -26
View File
@@ -62,7 +62,7 @@ import UploadFileDialog from './UploadFileDialog';
import OkCancelDialog from './OkCancelDialog'; import OkCancelDialog from './OkCancelDialog';
import HomeIcon from '@mui/icons-material/Home'; import HomeIcon from '@mui/icons-material/Home';
import FilePropertyDirectorySelectDialog from './FilePropertyDirectorySelectDialog'; import FilePropertyDirectorySelectDialog from './FilePropertyDirectorySelectDialog';
import { getAlbumArtUri } from './AudioFileMetadata'; import { getAlbumArtUri, getTrackTitle } from './AudioFileMetadata';
interface Point { interface Point {
x: number; x: number;
@@ -95,6 +95,7 @@ const audioFileExtensions: { [name: string]: boolean } = {
}; };
function screenToClient(element: HTMLDivElement, point: Point): Point { function screenToClient(element: HTMLDivElement, point: Point): Point {
const dpr = (window.devicePixelRatio || 1) as number;; const dpr = (window.devicePixelRatio || 1) as number;;
let rect = element.getBoundingClientRect(); let rect = element.getBoundingClientRect();
@@ -650,15 +651,18 @@ export default withStyles(
} }
getCompactTrackTitle(fileEntry: FileEntry): string { getCompactTrackTitle(fileEntry: FileEntry): string {
let metadata = fileEntry.metadata; let metadata = fileEntry.metadata;
let title = getTrackTitle(fileEntry.pathname, metadata);
if (!metadata) { if (!metadata) {
return "#error"; return title;
} }
let title = this.getTrackTitle(fileEntry);
if (metadata.album !== "") { if (metadata.album !== "") {
title += " (" + metadata.album + ")"; title += " (" + metadata.album + ")";
} }
return title; return title;
} }
getAlbumTitle(fileEntry: FileEntry): string { getAlbumTitle(fileEntry: FileEntry): string {
let artist = fileEntry.metadata?.artist || ""; let artist = fileEntry.metadata?.artist || "";
if (artist == "") { if (artist == "") {
@@ -668,28 +672,6 @@ export default withStyles(
let joiner = (artist !== "" && album !== "") ? " - " : ""; let joiner = (artist !== "" && album !== "") ? " - " : "";
return album + joiner + artist; return album + joiner + artist;
} }
getTrackTitle(fileEntry: FileEntry): string {
if (!fileEntry.metadata) {
return pathFileName(fileEntry.pathname);
}
if (fileEntry.metadata.title === "") {
return pathFileName(fileEntry.pathname);
}
let metadata = fileEntry.metadata;
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(fileEntry.pathname);
}
return result;
}
getTrackThumbnail(fileEntry: FileEntry): string { getTrackThumbnail(fileEntry: FileEntry): string {
return getAlbumArtUri(this.model, fileEntry.metadata, fileEntry.pathname); return getAlbumArtUri(this.model, fileEntry.metadata, fileEntry.pathname);
} }
@@ -1100,7 +1082,7 @@ export default withStyles(
className={classes.secondaryText} className={classes.secondaryText}
variant="body2" variant="body2"
style={{ flex: "0 0 auto", textOverflow: "ellipsis", textAlign: "left", marginBottom: 4 }}> style={{ flex: "0 0 auto", textOverflow: "ellipsis", textAlign: "left", marginBottom: 4 }}>
{this.getTrackTitle(value)} {getTrackTitle(value.pathname, value.metadata)}
</Typography> </Typography>
<Typography noWrap className={classes.secondaryText} <Typography noWrap className={classes.secondaryText}
variant="body2" variant="body2"
+11 -4
View File
@@ -42,6 +42,7 @@ import { AndroidHostInterface, FakeAndroidHost } from './AndroidHost';
import { ColorTheme, getColorScheme, setColorScheme } from './DarkMode'; import { ColorTheme, getColorScheme, setColorScheme } from './DarkMode';
import FilePropertyDirectoryTree from './FilePropertyDirectoryTree'; import FilePropertyDirectoryTree from './FilePropertyDirectoryTree';
import AudioFileMetadata from './AudioFileMetadata'; import AudioFileMetadata from './AudioFileMetadata';
import { pathFileName } from './FileUtils';
export enum State { export enum State {
@@ -2445,10 +2446,13 @@ export class PiPedalModel //implements PiPedalModel
let downloadUrl = this.varServerUrl + "downloadMediaFile?path=" + encodeURIComponent(filePath); let downloadUrl = this.varServerUrl + "downloadMediaFile?path=" + encodeURIComponent(filePath);
// download with no flashing temporary tab. // download with no flashing temporary tab.
let link = window.document.createElement("A") as HTMLLinkElement; let link = window.document.createElement("A") as HTMLAnchorElement;
link.href = downloadUrl; link.href = downloadUrl;
link.setAttribute("download", ""); link.target = "_blank";
link.setAttribute("download", pathFileName(filePath));
document.body.appendChild(link);
link.click(); link.click();
document.body.removeChild(link);
} }
download(targetType: string, instanceId: number | string): void { download(targetType: string, instanceId: number | string): void {
@@ -2457,10 +2461,13 @@ export class PiPedalModel //implements PiPedalModel
// window.open(url, "_blank"); // window.open(url, "_blank");
// download with no flashing temporary tab. // download with no flashing temporary tab.
let link = window.document.createElement("A") as HTMLLinkElement; let link = window.document.createElement("A") as HTMLAnchorElement;
link.href = url; link.href = url;
link.setAttribute("download", ""); link.target = "_blank";
link.download = "download.piPreset";
document.body.appendChild(link);
link.click(); link.click();
document.body.removeChild(link);
} }
uploadUserFile(uploadPage: string, file: File, contentType: string = "application/octet-stream", abortController?: AbortController): Promise<string> { uploadUserFile(uploadPage: string, file: File, contentType: string = "application/octet-stream", abortController?: AbortController): Promise<string> {
+20 -21
View File
@@ -456,7 +456,7 @@ const PluginControlView =
let symbol = uiControl.symbol; let symbol = uiControl.symbol;
if (!uiControl.is_input) { if (!uiControl.is_input) {
return ( return (
<PluginOutputControl key={uiControl.symbol } instanceId={this.props.instanceId} uiControl={uiControl} /> <PluginOutputControl key={uiControl.symbol} instanceId={this.props.instanceId} uiControl={uiControl} />
); );
} }
@@ -472,7 +472,7 @@ const PluginControlView =
throw new PiPedalStateError("Missing control value."); throw new PiPedalStateError("Missing control value.");
} }
return (( return ((
<PluginControl key={uiControl.symbol} instanceId={this.props.instanceId} uiControl={uiControl} value={controlValue.value} <PluginControl key={"ppc" + uiControl.symbol} instanceId={this.props.instanceId} uiControl={uiControl} value={controlValue.value}
onChange={(value: number) => { this.onControlValueChanged(controlValue!.key, value) }} onChange={(value: number) => { this.onControlValueChanged(controlValue!.key, value) }}
onPreviewChange={(value: number) => { this.onPreviewChange(controlValue!.key, value) }} onPreviewChange={(value: number) => { this.onPreviewChange(controlValue!.key, value) }}
requestIMEEdit={(uiControl: any, value: any) => this.requestImeEdit(uiControl, value)} requestIMEEdit={(uiControl: any, value: any) => this.requestImeEdit(uiControl, value)}
@@ -498,7 +498,7 @@ const PluginControlView =
let previousControl = controls[controls.length - 1]; let previousControl = controls[controls.length - 1];
if (!(previousControl instanceof ControlGroup)) { if (!(previousControl instanceof ControlGroup)) {
let pair = ( let pair = (
<div key={"k"+this.ixKey++} className={classes.controlPair}> <div key={"k" + this.ixKey++} className={classes.controlPair}>
{previousControl as ReactNode} {previousControl as ReactNode}
{newControl} {newControl}
</div> </div>
@@ -508,7 +508,7 @@ const PluginControlView =
// (e.g.. inserting at position 4 still places the extended control after four previous controls // (e.g.. inserting at position 4 still places the extended control after four previous controls
controls.push(( controls.push((
<div key={"k"+this.ixKey++} className={classes.controlSpacer} /> <div key={"k" + this.ixKey++} className={classes.controlSpacer} />
)); ));
} else { } else {
controls.push(newControl); controls.push(newControl);
@@ -686,7 +686,7 @@ const PluginControlView =
let item = controlGroup.controls[j]; let item = controlGroup.controls[j];
controls.push( controls.push(
( (
<div key={"ctl" + (this.controlKeyIndex++)} className={classes.controlPadding}> <div key={"ctlx" + (this.controlKeyIndex++)} className={classes.controlPadding}>
{item} {item}
</div> </div>
@@ -695,7 +695,7 @@ const PluginControlView =
} }
result.push(( result.push((
<div key={"ctl" + (this.controlKeyIndex++)} className={!isLandscapeGrid ? classes.portGroup : classes.portGroupLandscape}> <div key={"ctlx" + (this.controlKeyIndex++)} className={!isLandscapeGrid ? classes.portGroup : classes.portGroupLandscape}>
<div className={classes.portGroupTitle}> <div className={classes.portGroupTitle}>
<Tooltip title={controlGroup.name} <Tooltip title={controlGroup.name}
placement="top-start" arrow enterDelay={1500} enterNextDelay={1500} placement="top-start" arrow enterDelay={1500} enterNextDelay={1500}
@@ -713,21 +713,20 @@ const PluginControlView =
)); ));
} else { } else {
if (this.fullScreen()) if (this.fullScreen()) {
{ result.push(
result.push( <div key={"fullScreen" + this.props.instanceId} style={{ position: "relative", width: "100%", height: "100%" }}
<div style={{position: "relative", width: "100%", height:"100%"}} >
> {node as ReactNode}
{node as ReactNode} </div>
</div> );
); } else {
} else { result.push((
result.push(( <div key={"ctl" + (this.controlKeyIndex++)} className={hasGroups ? classes.portgroupControlPadding : classes.controlPadding} >
<div key={"ctl" + (this.controlKeyIndex++)} className={hasGroups ? classes.portgroupControlPadding : classes.controlPadding} > {node as ReactNode}
{node as ReactNode} </div>
</div> ));
)); }
}
} }
} }
+15 -14
View File
@@ -20,10 +20,11 @@
import React, { SyntheticEvent,Component } from 'react'; import React, { SyntheticEvent,Component } from 'react';
import { css } from '@emotion/react'; import { css } from '@emotion/react';
import {isDarkMode} from './DarkMode'; import {isDarkMode} from './DarkMode';
import IconButton from '@mui/material/IconButton'; import IconButtonEx from './IconButtonEx';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import { PiPedalModel, PiPedalModelFactory, PresetIndexEntry, PresetIndex } from './PiPedalModel'; import { PiPedalModel, PiPedalModelFactory, PresetIndexEntry, PresetIndex } from './PiPedalModel';
import Button from "@mui/material/Button"; import Button from '@mui/material/Button';
import ButtonEx from './ButtonEx';
import ButtonBase from "@mui/material/ButtonBase"; import ButtonBase from "@mui/material/ButtonBase";
import DialogEx from './DialogEx'; import DialogEx from './DialogEx';
import AppBar from '@mui/material/AppBar'; import AppBar from '@mui/material/AppBar';
@@ -351,17 +352,17 @@ const PresetDialog = withStyles(
<div style={{ flex: "0 0 auto" }}> <div style={{ flex: "0 0 auto" }}>
<AppBar className={classes.dialogAppBar} style={{ display: this.isEditMode() ? "none" : "block" }} > <AppBar className={classes.dialogAppBar} style={{ display: this.isEditMode() ? "none" : "block" }} >
<Toolbar> <Toolbar>
<IconButton edge="start" color="inherit" onClick={this.handleDialogClose} aria-label="back" <IconButtonEx tooltip="Back" edge="start" color="inherit" onClick={this.handleDialogClose} aria-label="back"
disabled={this.isEditMode()} disabled={this.isEditMode()}
> >
<ArrowBackIcon /> <ArrowBackIcon />
</IconButton> </IconButtonEx>
<Typography variant="h6" className={classes.dialogTitle}> <Typography variant="h6" className={classes.dialogTitle}>
Presets Presets
</Typography> </Typography>
<IconButton color="inherit" onClick={(e) => this.showActionBar(true)} > <IconButtonEx tooltip="Edit"color="inherit" onClick={(e) => this.showActionBar(true)} >
<EditIcon /> <EditIcon />
</IconButton> </IconButtonEx>
</Toolbar> </Toolbar>
</AppBar> </AppBar>
<AppBar className={actionBarClass} style={{ display: this.isEditMode() ? "block" : "none" }} <AppBar className={actionBarClass} style={{ display: this.isEditMode() ? "block" : "none" }}
@@ -369,14 +370,14 @@ const PresetDialog = withStyles(
> >
<Toolbar> <Toolbar>
{(!this.props.isEditDialog) ? ( {(!this.props.isEditDialog) ? (
<IconButton edge="start" color="inherit" onClick={(e) => this.showActionBar(false)} aria-label="close"> <IconButtonEx tooltip="Close" edge="start" color="inherit" onClick={(e) => this.showActionBar(false)} aria-label="close">
<CloseIcon /> <CloseIcon />
</IconButton> </IconButtonEx>
) : ( ) : (
<IconButton edge="start" color="inherit" onClick={this.handleDialogClose} aria-label="back" <IconButtonEx edge="start" tooltip="Back" color="inherit" onClick={this.handleDialogClose} aria-label="back"
> >
<ArrowBackIcon /> <ArrowBackIcon />
</IconButton> </IconButtonEx>
)} )}
<Typography variant="h6" className={classes.dialogTitle}> <Typography variant="h6" className={classes.dialogTitle}>
@@ -402,12 +403,12 @@ const PresetDialog = withStyles(
} }
} }
/> />
<IconButton color="inherit" onClick={(e) => this.handleDeleteClick()} > <IconButtonEx tooltip="Delete" color="inherit" onClick={(e) => this.handleDeleteClick()} >
<img src="/img/old_delete_outline_white_24dp.svg" alt="Delete" style={{ width: 24, height: 24, opacity: 0.6 }} /> <img src="/img/old_delete_outline_white_24dp.svg" alt="Delete" style={{ width: 24, height: 24, opacity: 0.6 }} />
</IconButton> </IconButtonEx>
<IconButton color="inherit" onClick={(e) => { this.onMoreClick(e) }} > <IconButtonEx tooltip="More..." color="inherit" onClick={(e) => { this.onMoreClick(e) }} >
<MoreVertIcon /> <MoreVertIcon />
</IconButton> </IconButtonEx>
<Menu <Menu
id="more-menu" id="more-menu"
anchorEl={this.state.moreMenuAnchorEl} anchorEl={this.state.moreMenuAnchorEl}
+29 -14
View File
@@ -45,6 +45,8 @@ import { getAlbumArtUri } from './AudioFileMetadata';
import RepeatIcon from '@mui/icons-material/Repeat'; import RepeatIcon from '@mui/icons-material/Repeat';
import Timebase, { LoopParameters, TimebaseUnits } from './Timebase'; import Timebase, { LoopParameters, TimebaseUnits } from './Timebase';
import ControlSlider from './ControlSlider'; import ControlSlider from './ControlSlider';
import { getTrackTitle } from './AudioFileMetadata';
let Player__seek = "http://two-play.com/plugins/toob-player#seek" let Player__seek = "http://two-play.com/plugins/toob-player#seek"
const AUDIO_FILE_PROPERTY_URI = "http://two-play.com/plugins/toob-player#audioFile"; const AUDIO_FILE_PROPERTY_URI = "http://two-play.com/plugins/toob-player#audioFile";
@@ -320,19 +322,9 @@ export default function ToobPlayerControl(
} }
model.getAudioFileMetadata(path) model.getAudioFileMetadata(path)
.then((metadata) => { .then((metadata) => {
let strTrack: string = "";
if (metadata.track > 0) {
let disc = (metadata.track / 1000);
if (disc >= 1) {
strTrack = (metadata.track % 1000).toString() + '/' + Math.floor(disc).toString();
} else {
strTrack = metadata.track.toString();
}
strTrack += ". ";
}
let coverArtUri = getAlbumArtUri(model, metadata, path); let coverArtUri = getAlbumArtUri(model, metadata, path);
setCoverArt(coverArtUri); setCoverArt(coverArtUri);
setTitle(strTrack + metadata.title); setTitle(getTrackTitle(path, metadata));
setAlbum(metadata.album); setAlbum(metadata.album);
setArtist(metadata.artist); setArtist(metadata.artist);
setAlbumArtist(metadata.albumArtist); setAlbumArtist(metadata.albumArtist);
@@ -419,6 +411,8 @@ export default function ToobPlayerControl(
); );
} }
function FilePanel() { function FilePanel() {
let textColor = pluginState == PluginState.Error ? "error" : "textPrimary";
return ( return (
<ButtonBase style={{ display: "block", width: "100%", borderRadius: 10, textAlign: "left" }} <ButtonBase style={{ display: "block", width: "100%", borderRadius: 10, textAlign: "left" }}
onClick={() => { SelectFile() }} onClick={() => { SelectFile() }}
@@ -442,10 +436,10 @@ export default function ToobPlayerControl(
</Typography> </Typography>
) : ( ) : (
<div> <div>
<Typography variant="body1" noWrap> <Typography variant="body1" color={textColor} noWrap>
{titleLine} {titleLine}
</Typography> </Typography>
<Typography variant="body2" noWrap sx={{}}> <Typography variant="body2" color={textColor} noWrap sx={{}}>
{albumLine} {albumLine}
</Typography> </Typography>
</div> </div>
@@ -483,6 +477,27 @@ export default function ToobPlayerControl(
function onLoopPropertyChanged(loopSettingsJson: string) { function onLoopPropertyChanged(loopSettingsJson: string) {
try { try {
if (loopSettingsJson === "") {
setTimebase(
{
units: TimebaseUnits.Seconds,
tempo: 120.0,
timeSignature: { numerator: 4, denominator: 4 }
})
let loopParameters = {
start: 0.0,
loopEnable: false,
loopStart: 0.0,
loopEnd: 0.0
};
setLoopParameters(loopParameters);
setStart(0.0);
setLoopEnable(false);
setLoopStart(0.0);
setLoopEnd(0.0);
return;
}
let atomObject = JSON.parse(loopSettingsJson); let atomObject = JSON.parse(loopSettingsJson);
let loopParameters: LoopParameters = atomObject.loopParameters as LoopParameters; let loopParameters: LoopParameters = atomObject.loopParameters as LoopParameters;
let newTimebase: Timebase | undefined = atomObject.timebase as (Timebase | undefined);; let newTimebase: Timebase | undefined = atomObject.timebase as (Timebase | undefined);;
@@ -827,7 +842,7 @@ export default function ToobPlayerControl(
playing={!paused} playing={!paused}
onPreview={() => { handlePreview(); }} onPreview={() => { handlePreview(); }}
value={loopParameters} value={loopParameters}
onCancelPlaying= {()=> { handleCancelPlaying();}} onCancelPlaying={() => { handleCancelPlaying(); }}
timebase={timebase} timebase={timebase}
onTimebaseChange={(newTimebase) => { onTimebaseChange={(newTimebase) => {
setTimebase(newTimebase); setTimebase(newTimebase);
+4 -3
View File
@@ -110,14 +110,14 @@ const ToobPlayerView =
for (let mixControl of mixPanel.controls) { for (let mixControl of mixPanel.controls) {
extraControls.push( extraControls.push(
( (
<div key={"k"+ iKey++} style={{flex: "0 0 auto", position: "relative",height: "100%" }}> <div key={"kExtra"+ iKey++} style={{flex: "0 0 auto", position: "relative",height: "100%" }}>
{mixControl} {mixControl}
</div> </div>
)); ));
} }
let panel = ( let panel = (
<ToobPlayerControl key="MusicPlayer" instanceId={this.props.instanceId} extraControls={extraControls} /> <ToobPlayerControl key={"MusicPlayer" + this.props.instanceId} instanceId={this.props.instanceId} extraControls={extraControls} />
); );
let result: (React.ReactNode | ControlGroup)[] = []; let result: (React.ReactNode | ControlGroup)[] = [];
@@ -128,6 +128,7 @@ const ToobPlayerView =
render() { render() {
return ( return (
<PluginControlView <PluginControlView
key={"ToobPlayerView" + this.props.instanceId}
instanceId={this.props.instanceId} instanceId={this.props.instanceId}
item={this.props.item} item={this.props.item}
customization={this} customization={this}
@@ -145,7 +146,7 @@ class ToobPlayerViewFactory implements IControlViewFactory {
uri: string = "http://two-play.com/plugins/toob-player"; uri: string = "http://two-play.com/plugins/toob-player";
Create(model: PiPedalModel, pedalboardItem: PedalboardItem): React.ReactNode { Create(model: PiPedalModel, pedalboardItem: PedalboardItem): React.ReactNode {
return (<ToobPlayerView instanceId={pedalboardItem.instanceId} item={pedalboardItem} />); return (<ToobPlayerView key={"ppmv"+pedalboardItem.instanceId} instanceId={pedalboardItem.instanceId} item={pedalboardItem} />);
} }