Toob Player
This commit is contained in:
Vendored
+21
-1
@@ -135,5 +135,25 @@
|
|||||||
"cSpell.ignoreWords": [
|
"cSpell.ignoreWords": [
|
||||||
"nammodel"
|
"nammodel"
|
||||||
],
|
],
|
||||||
"cSpell.enabled": false
|
"cSpell.enabled": false,
|
||||||
|
|
||||||
|
// Disable all automatic completion suggestions - only show when Ctrl+Space is pressed
|
||||||
|
"editor.quickSuggestions": {
|
||||||
|
"other": false,
|
||||||
|
"comments": false,
|
||||||
|
"strings": false
|
||||||
|
},
|
||||||
|
|
||||||
|
// Disable suggestions on trigger characters (like . or :)
|
||||||
|
"editor.suggestOnTriggerCharacters": false,
|
||||||
|
|
||||||
|
// Disable word-based suggestions from open files
|
||||||
|
"editor.wordBasedSuggestions": "off",
|
||||||
|
|
||||||
|
// Don't accept suggestions on Enter (optional)
|
||||||
|
"editor.acceptSuggestionOnEnter": "off",
|
||||||
|
|
||||||
|
// Disable parameter hints popup (optional)
|
||||||
|
"editor.parameterHints.enabled": false,
|
||||||
|
|
||||||
}
|
}
|
||||||
Binary file not shown.
+113
-55
@@ -43,6 +43,41 @@ namespace fs = std::filesystem;
|
|||||||
|
|
||||||
static constexpr size_t INVALID_INDEX = std::numeric_limits<size_t>::max();
|
static constexpr size_t INVALID_INDEX = std::numeric_limits<size_t>::max();
|
||||||
|
|
||||||
|
|
||||||
|
// All the varous cover art files I found on my personal music collection.
|
||||||
|
// These are used to find the cover art for a directory. Based on
|
||||||
|
// scanning a large music collection that have been tagged by various tools
|
||||||
|
// including iTunes, Windows Media Playe, and a variety of taggers.
|
||||||
|
//
|
||||||
|
// Files are listed in order of preference.
|
||||||
|
|
||||||
|
static std::vector<std::string> COVER_ART_FILES = {
|
||||||
|
"Folder.jpg", // window media player/explorer.
|
||||||
|
"Cover.jpg", // itunes.
|
||||||
|
"folder.jpg", // Linux audio players plex, kodi, Rythmbox, Amorok, Clementine.
|
||||||
|
"cover.jpg", // Linux audio players plex, kodi, Rythmbox, Amorok, Clementine.
|
||||||
|
"Artwork.jpg", // itunes.
|
||||||
|
"Front.jpg",
|
||||||
|
"front.jpg", // linux.
|
||||||
|
"AlbumArt.jpg",
|
||||||
|
"albumArt.jpg",
|
||||||
|
"Frontcover.jpg",
|
||||||
|
"AlbumArtSmall.jpg" // windows media player.
|
||||||
|
};
|
||||||
|
|
||||||
|
bool pipedal::isArtworkFileName(const std::string &fileName)
|
||||||
|
{
|
||||||
|
for (const auto &coverArtFile : COVER_ART_FILES)
|
||||||
|
{
|
||||||
|
if (fileName == coverArtFile)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
void AudioDirectoryInfo::SetTemporaryDirectory(const std::filesystem::path &path)
|
void AudioDirectoryInfo::SetTemporaryDirectory(const std::filesystem::path &path)
|
||||||
{
|
{
|
||||||
temporaryDirectory = path;
|
temporaryDirectory = path;
|
||||||
@@ -96,10 +131,9 @@ namespace
|
|||||||
class AudioDirectoryInfoImpl : public AudioDirectoryInfo
|
class AudioDirectoryInfoImpl : public AudioDirectoryInfo
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
AudioDirectoryInfoImpl(const std::filesystem::path &path, const std::filesystem::path&indexPath)
|
AudioDirectoryInfoImpl(const std::filesystem::path &path, const std::filesystem::path &indexPath)
|
||||||
: path(path), indexPath(
|
: path(path), indexPath(
|
||||||
(indexPath.empty() ? path: indexPath) / ".index.pipedal"
|
(indexPath.empty() ? path : indexPath) / ".index.pipedal")
|
||||||
)
|
|
||||||
{
|
{
|
||||||
if (this->indexPath.parent_path() != path)
|
if (this->indexPath.parent_path() != path)
|
||||||
{
|
{
|
||||||
@@ -129,7 +163,6 @@ namespace
|
|||||||
private:
|
private:
|
||||||
std::vector<DbFileInfo> QueryTracks();
|
std::vector<DbFileInfo> QueryTracks();
|
||||||
|
|
||||||
|
|
||||||
bool opened = false;
|
bool opened = false;
|
||||||
std::filesystem::path indexPath;
|
std::filesystem::path indexPath;
|
||||||
void OpenAudioDb();
|
void OpenAudioDb();
|
||||||
@@ -139,7 +172,6 @@ namespace
|
|||||||
fs::path path;
|
fs::path path;
|
||||||
using id_t = int64_t;
|
using id_t = int64_t;
|
||||||
|
|
||||||
|
|
||||||
std::vector<DbFileInfo> UpdateDbFiles();
|
std::vector<DbFileInfo> UpdateDbFiles();
|
||||||
void DbSetThumbnailType(
|
void DbSetThumbnailType(
|
||||||
int64_t idFile,
|
int64_t idFile,
|
||||||
@@ -156,16 +188,14 @@ namespace
|
|||||||
void UpdateMetadata(DbFileInfo *dbFile);
|
void UpdateMetadata(DbFileInfo *dbFile);
|
||||||
static constexpr int DB_VERSION = 1;
|
static constexpr int DB_VERSION = 1;
|
||||||
std::filesystem::path GetFolderFile() const;
|
std::filesystem::path GetFolderFile() const;
|
||||||
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
AudioDirectoryInfo::Ptr AudioDirectoryInfo::Create(const std::filesystem::path &path, const std::filesystem::path&indexPath)
|
AudioDirectoryInfo::Ptr AudioDirectoryInfo::Create(const std::filesystem::path &path, const std::filesystem::path &indexPath)
|
||||||
{
|
{
|
||||||
return std::make_shared<AudioDirectoryInfoImpl>(path,indexPath);
|
return std::make_shared<AudioDirectoryInfoImpl>(path, indexPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
std::vector<DbFileInfo> AudioDirectoryInfoImpl::QueryTracks()
|
std::vector<DbFileInfo> AudioDirectoryInfoImpl::QueryTracks()
|
||||||
{
|
{
|
||||||
if (audioFilesDb)
|
if (audioFilesDb)
|
||||||
@@ -264,7 +294,18 @@ std::vector<DbFileInfo> AudioDirectoryInfoImpl::UpdateDbFiles()
|
|||||||
auto path = dirEntry.path();
|
auto path = dirEntry.path();
|
||||||
std::string name = path.filename();
|
std::string name = path.filename();
|
||||||
std::string extension = path.extension();
|
std::string extension = path.extension();
|
||||||
if (isAudioExtension(extension))
|
if (isAudioExtension(extension) || isArtworkFileName(name))
|
||||||
|
{
|
||||||
|
// If the file is an audio file or a cover art file, we need to check it.
|
||||||
|
if (name.empty() || name[0] == '.')
|
||||||
|
{
|
||||||
|
continue; // skip hidden files.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
continue; // not an audio file or cover art file.
|
||||||
|
}
|
||||||
{
|
{
|
||||||
int64_t lastModified = fileTimeToInt64(dirEntry.last_write_time());
|
int64_t lastModified = fileTimeToInt64(dirEntry.last_write_time());
|
||||||
|
|
||||||
@@ -322,7 +363,7 @@ std::vector<DbFileInfo> AudioDirectoryInfoImpl::UpdateDbFiles()
|
|||||||
}
|
}
|
||||||
for (auto &dbFile : dbFiles)
|
for (auto &dbFile : dbFiles)
|
||||||
{
|
{
|
||||||
if (dbFile.thumbnailType() == ThumbnailType::Unknown)
|
if (dbFile.thumbnailType() == ThumbnailType::None)
|
||||||
{
|
{
|
||||||
if (!folderFileName.empty())
|
if (!folderFileName.empty())
|
||||||
{
|
{
|
||||||
@@ -410,27 +451,67 @@ void AudioDirectoryInfoImpl::DbDeleteFile(DbFileInfo *dbFile)
|
|||||||
audioFilesDb->DeleteFile(dbFile);
|
audioFilesDb->DeleteFile(dbFile);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ThumbnailTemporaryFile AudioDirectoryInfoImpl::DefaultThumbnailTemporaryFile()
|
||||||
|
{
|
||||||
|
ThumbnailTemporaryFile tempFile;
|
||||||
|
fs::path defaultThumbnail = GetResourceDirectory() / "img/missing_thumbnail.jpg";
|
||||||
|
tempFile.SetNonDeletedPath(defaultThumbnail, "image/jpeg");
|
||||||
|
return tempFile;
|
||||||
|
}
|
||||||
|
|
||||||
// All the varous cover art files I found on my personal music collection.
|
ThumbnailTemporaryFile AudioDirectoryInfoImpl::GetUnindexedThunbnail(
|
||||||
// These are used to find the cover art for a directory. Based on
|
const std::string &fileNameOnly, int32_t width, int32_t height)
|
||||||
// scanning a large music collection that have been tagged by various tools
|
{
|
||||||
// including iTunes, Windows Media Playe, and a variety of taggers.
|
fs::path file = this->path / fileNameOnly;
|
||||||
//
|
if (!fs::exists(file))
|
||||||
// Files are listed in order of preference.
|
{
|
||||||
|
return DefaultThumbnailTemporaryFile();
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
auto tempFile = pipedal::GetAudioFileThumbnail(
|
||||||
|
file, width, height,
|
||||||
|
GetTemporaryDirectory());
|
||||||
|
auto thumbnailPath = tempFile.Path();
|
||||||
|
ThumbnailTemporaryFile result;
|
||||||
|
result.Attach(tempFile.Detach(), "image/jpeg");
|
||||||
|
|
||||||
static std::vector<std::filesystem::path> COVER_ART_FILES = {
|
// Read the thumbnail data from the temporary file.
|
||||||
"Folder.jpg", // window media player/explorer.
|
std::ifstream thumbnailStream(thumbnailPath, std::ios::binary);
|
||||||
"Cover.jpg", // itunes.
|
if (!thumbnailStream)
|
||||||
"folder.jpg", // Linux audio players plex, kodi, Rythmbox, Amorok, Clementine.
|
{
|
||||||
"cover.jpg", // Linux audio players plex, kodi, Rythmbox, Amorok, Clementine.
|
throw std::runtime_error("Failed to open thumbnail file: " + thumbnailPath.string());
|
||||||
"Artwork.jpg", // itunes.
|
}
|
||||||
"Front.jpg",
|
std::vector<uint8_t> thumbnailData(
|
||||||
"front.jpg", // linux.
|
(std::istreambuf_iterator<char>(thumbnailStream)),
|
||||||
"AlbumArt.jpg",
|
std::istreambuf_iterator<char>());
|
||||||
"albumArt.jpg",
|
|
||||||
"Frontcover.jpg",
|
if (!thumbnailData.empty())
|
||||||
"AlbumArtSmall.jpg" // windows media player.
|
{
|
||||||
};
|
audioFilesDb->AddThumbnail(
|
||||||
|
fileNameOnly,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
thumbnailData);
|
||||||
|
|
||||||
|
// Set the MIME type for the thumbnail.
|
||||||
|
result.SetMimeType("image/jpeg");
|
||||||
|
audioFilesDb->UpdateThumbnailInfo(
|
||||||
|
fileNameOnly,
|
||||||
|
ThumbnailType::Embedded);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw std::runtime_error("Thumbnail data is empty.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (const std::exception &e)
|
||||||
|
{
|
||||||
|
Lv2Log::error("GetUnindexedThunbnail failed: %s", e.what());
|
||||||
|
}
|
||||||
|
return DefaultThumbnailTemporaryFile();
|
||||||
|
}
|
||||||
|
|
||||||
fs::path AudioDirectoryInfoImpl::GetFolderFile() const
|
fs::path AudioDirectoryInfoImpl::GetFolderFile() const
|
||||||
{
|
{
|
||||||
@@ -604,22 +685,6 @@ ThumbnailTemporaryFile AudioDirectoryInfoImpl::GetThumbnail(const std::string &f
|
|||||||
return GetUnindexedThunbnail(fileNameOnly, width, height);
|
return GetUnindexedThunbnail(fileNameOnly, width, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
ThumbnailTemporaryFile AudioDirectoryInfoImpl::GetUnindexedThunbnail(const std::string &fileNameOnly, int32_t width, int32_t height)
|
|
||||||
{
|
|
||||||
fs::path file = this->path / fileNameOnly;
|
|
||||||
ThumbnailTemporaryFile tempFile = ThumbnailTemporaryFile::CreateTemporaryFile(GetTemporaryDirectory(), "image/jpeg");
|
|
||||||
try
|
|
||||||
{
|
|
||||||
auto thumbnailPath = pipedal::GetAudioFileThumbnail(file, width, height, GetTemporaryDirectory());
|
|
||||||
tempFile.Attach(thumbnailPath.Detach(), "image/jpeg");
|
|
||||||
return tempFile;
|
|
||||||
}
|
|
||||||
catch (const std::exception &e)
|
|
||||||
{
|
|
||||||
// If we cannot get the thumbnail, return a default one.
|
|
||||||
return DefaultThumbnailTemporaryFile();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void AudioDirectoryInfoImpl::UpdateMetadata(DbFileInfo *dbFile)
|
void AudioDirectoryInfoImpl::UpdateMetadata(DbFileInfo *dbFile)
|
||||||
{
|
{
|
||||||
@@ -687,13 +752,6 @@ void AudioDirectoryInfoImpl::OpenAudioDb()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ThumbnailTemporaryFile AudioDirectoryInfoImpl::DefaultThumbnailTemporaryFile()
|
|
||||||
{
|
|
||||||
ThumbnailTemporaryFile tempFile;
|
|
||||||
fs::path defaultThumbnail = GetResourceDirectory() / "img/missing_thumbnail.jpg";
|
|
||||||
tempFile.SetNonDeletedPath(defaultThumbnail, "image/jpeg");
|
|
||||||
return tempFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string AudioDirectoryInfoImpl::GetNextAudioFile(const std::string &fileNameOnly)
|
std::string AudioDirectoryInfoImpl::GetNextAudioFile(const std::string &fileNameOnly)
|
||||||
{
|
{
|
||||||
@@ -801,8 +859,8 @@ void AudioDirectoryInfoImpl::MoveAudioFile(
|
|||||||
transaction->commit();
|
transaction->commit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace pipedal
|
||||||
namespace pipedal {
|
{
|
||||||
std::filesystem::path IndexPathToShadowIndexPath(const std::filesystem::path &audioRootDirectory, const std::filesystem::path &path)
|
std::filesystem::path IndexPathToShadowIndexPath(const std::filesystem::path &audioRootDirectory, const std::filesystem::path &path)
|
||||||
{
|
{
|
||||||
std::filesystem::path relativePath = MakeRelativePath(path, audioRootDirectory);
|
std::filesystem::path relativePath = MakeRelativePath(path, audioRootDirectory);
|
||||||
|
|||||||
@@ -124,4 +124,5 @@ namespace pipedal
|
|||||||
std::filesystem::path ShadowIndexPathToIndexPath(const std::filesystem::path &audioRootDirectory, const std::filesystem::path &path);
|
std::filesystem::path ShadowIndexPathToIndexPath(const std::filesystem::path &audioRootDirectory, const std::filesystem::path &path);
|
||||||
std::filesystem::path GetShadowIndexDirectory(const std::filesystem::path &audioRootDirectory, const std::filesystem::path &path);
|
std::filesystem::path GetShadowIndexDirectory(const std::filesystem::path &audioRootDirectory, const std::filesystem::path &path);
|
||||||
|
|
||||||
|
bool isArtworkFileName(const std::string &fileName);
|
||||||
}
|
}
|
||||||
@@ -2407,6 +2407,17 @@ std::string PiPedalModel::RenameFilePropertyFile(
|
|||||||
return storage.RenameFilePropertyFile(oldRelativePath, newRelativePath, uiFileProperty);
|
return storage.RenameFilePropertyFile(oldRelativePath, newRelativePath, uiFileProperty);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string PiPedalModel::CopyFilePropertyFile(
|
||||||
|
const std::string &oldRelativePath,
|
||||||
|
const std::string &newRelativePath,
|
||||||
|
const UiFileProperty &uiFileProperty,
|
||||||
|
bool overwrite)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::recursive_mutex> lock(mutex);
|
||||||
|
return storage.CopyFilePropertyFile(oldRelativePath, newRelativePath, uiFileProperty,overwrite);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
void PiPedalModel::DeleteSampleFile(const std::filesystem::path &fileName)
|
void PiPedalModel::DeleteSampleFile(const std::filesystem::path &fileName)
|
||||||
{
|
{
|
||||||
std::lock_guard<std::recursive_mutex> lock(mutex);
|
std::lock_guard<std::recursive_mutex> lock(mutex);
|
||||||
|
|||||||
@@ -454,6 +454,7 @@ namespace pipedal
|
|||||||
void DeleteSampleFile(const std::filesystem::path &fileName);
|
void DeleteSampleFile(const std::filesystem::path &fileName);
|
||||||
std::string CreateNewSampleDirectory(const std::string &relativePath, const UiFileProperty &uiFileProperty);
|
std::string CreateNewSampleDirectory(const std::string &relativePath, const UiFileProperty &uiFileProperty);
|
||||||
std::string RenameFilePropertyFile(const std::string &oldRelativePath, const std::string &newRelativePath, const UiFileProperty &uiFileProperty);
|
std::string RenameFilePropertyFile(const std::string &oldRelativePath, const std::string &newRelativePath, const UiFileProperty &uiFileProperty);
|
||||||
|
std::string CopyFilePropertyFile(const std::string &oldRelativePath, const std::string &newRelativePath, const UiFileProperty &uiFileProperty,bool overwrite);
|
||||||
FilePropertyDirectoryTree::ptr GetFilePropertydirectoryTree(const UiFileProperty &uiFileProperty,const std::string&selectedPath);
|
FilePropertyDirectoryTree::ptr GetFilePropertydirectoryTree(const UiFileProperty &uiFileProperty,const std::string&selectedPath);
|
||||||
|
|
||||||
bool IsInUploadsDirectory(const std::string &path);
|
bool IsInUploadsDirectory(const std::string &path);
|
||||||
|
|||||||
@@ -115,6 +115,23 @@ JSON_MAP_REFERENCE(RenameSampleFileArgs, newRelativePath)
|
|||||||
JSON_MAP_REFERENCE(RenameSampleFileArgs, uiFileProperty)
|
JSON_MAP_REFERENCE(RenameSampleFileArgs, uiFileProperty)
|
||||||
JSON_MAP_END()
|
JSON_MAP_END()
|
||||||
|
|
||||||
|
class CopySampleFileArgs
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
std::string oldRelativePath_;
|
||||||
|
std::string newRelativePath_;
|
||||||
|
UiFileProperty uiFileProperty_;
|
||||||
|
bool overwrite_ = false;
|
||||||
|
DECLARE_JSON_MAP(CopySampleFileArgs);
|
||||||
|
};
|
||||||
|
|
||||||
|
JSON_MAP_BEGIN(CopySampleFileArgs)
|
||||||
|
JSON_MAP_REFERENCE(CopySampleFileArgs, oldRelativePath)
|
||||||
|
JSON_MAP_REFERENCE(CopySampleFileArgs, newRelativePath)
|
||||||
|
JSON_MAP_REFERENCE(CopySampleFileArgs, uiFileProperty)
|
||||||
|
JSON_MAP_REFERENCE(CopySampleFileArgs, overwrite)
|
||||||
|
JSON_MAP_END()
|
||||||
|
|
||||||
class GetFilePropertyDirectoryTreeArgs
|
class GetFilePropertyDirectoryTreeArgs
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@@ -1645,6 +1662,14 @@ public:
|
|||||||
std::string newFileName = this->model.RenameFilePropertyFile(args.oldRelativePath_, args.newRelativePath_, args.uiFileProperty_);
|
std::string newFileName = this->model.RenameFilePropertyFile(args.oldRelativePath_, args.newRelativePath_, args.uiFileProperty_);
|
||||||
this->Reply(replyTo, "renameFilePropertyFile", newFileName);
|
this->Reply(replyTo, "renameFilePropertyFile", newFileName);
|
||||||
}
|
}
|
||||||
|
else if (message == "copyFilePropertyFile")
|
||||||
|
{
|
||||||
|
CopySampleFileArgs args;
|
||||||
|
pReader->read(&args);
|
||||||
|
|
||||||
|
std::string newFileName = this->model.CopyFilePropertyFile(args.oldRelativePath_, args.newRelativePath_, args.uiFileProperty_, args.overwrite_);
|
||||||
|
this->Reply(replyTo, "copyFilePropertyFile", newFileName);
|
||||||
|
}
|
||||||
else if (message == "getFilePropertyDirectoryTree")
|
else if (message == "getFilePropertyDirectoryTree")
|
||||||
{
|
{
|
||||||
GetFilePropertyDirectoryTreeArgs args;
|
GetFilePropertyDirectoryTreeArgs args;
|
||||||
|
|||||||
+68
-2
@@ -1705,6 +1705,13 @@ static void AddTracksToResult(
|
|||||||
std::set<std::string> validExtensions = fileProperty.GetPermittedFileExtensions(
|
std::set<std::string> validExtensions = fileProperty.GetPermittedFileExtensions(
|
||||||
modDirectoryInfo ? modDirectoryInfo->modType : "");
|
modDirectoryInfo ? modDirectoryInfo->modType : "");
|
||||||
|
|
||||||
|
if (validExtensions.size() == 0)
|
||||||
|
{
|
||||||
|
const auto &audioExtensions = MimeTypes::instance().AudioExtensions();
|
||||||
|
validExtensions.insert(audioExtensions.begin(), audioExtensions.end());
|
||||||
|
}
|
||||||
|
validExtensions.insert(".jpg");
|
||||||
|
validExtensions.insert(".png");
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Add directories first.
|
// Add directories first.
|
||||||
@@ -1828,9 +1835,11 @@ FileRequestResult Storage::GetModFileList2(const std::string &relativePath, cons
|
|||||||
++iRp;
|
++iRp;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fs::path cumulativePath = modDirectoryPath;
|
||||||
while (iRp != rp.end())
|
while (iRp != rp.end())
|
||||||
{
|
{
|
||||||
result.breadcrumbs_.push_back({*iRp, *iRp});
|
cumulativePath /= (*iRp);
|
||||||
|
result.breadcrumbs_.push_back({cumulativePath, *iRp});
|
||||||
++iRp;
|
++iRp;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2036,6 +2045,17 @@ std::filesystem::path Storage::MakeUserFilePath(const std::string &directory, co
|
|||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool Storage::IsValidArtworkFile(const std::filesystem::path& fullPath)
|
||||||
|
{
|
||||||
|
if (IsInAudioTracksDirectory(fullPath) && isArtworkFileName(fullPath.filename().string()))
|
||||||
|
{
|
||||||
|
// allow artwork files.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
std::string Storage::UploadUserFile(const std::string &directory,
|
std::string Storage::UploadUserFile(const std::string &directory,
|
||||||
UiFileProperty::ptr uiFileProperty,
|
UiFileProperty::ptr uiFileProperty,
|
||||||
const std::string &filename,
|
const std::string &filename,
|
||||||
@@ -2060,7 +2080,7 @@ std::string Storage::UploadUserFile(const std::string &directory,
|
|||||||
throw std::logic_error("Permission denied. Path is outside the upload storage directory.");
|
throw std::logic_error("Permission denied. Path is outside the upload storage directory.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!uiFileProperty->IsValidExtension(relativePath))
|
if (!(uiFileProperty->IsValidExtension(relativePath) || IsValidArtworkFile(path)))
|
||||||
{
|
{
|
||||||
throw std::logic_error("Permission denied. Invalid file extension for this directory.");
|
throw std::logic_error("Permission denied. Invalid file extension for this directory.");
|
||||||
}
|
}
|
||||||
@@ -2162,6 +2182,52 @@ std::string Storage::RenameFilePropertyFile(
|
|||||||
std::filesystem::rename(oldPath, newPath);
|
std::filesystem::rename(oldPath, newPath);
|
||||||
return newPath;
|
return newPath;
|
||||||
}
|
}
|
||||||
|
std::string Storage::CopyFilePropertyFile(
|
||||||
|
const std::string &oldRelativePath,
|
||||||
|
const std::string &newRelativePath,
|
||||||
|
const UiFileProperty &uiFileProperty,
|
||||||
|
bool overwrite)
|
||||||
|
{
|
||||||
|
if (uiFileProperty.directory().empty())
|
||||||
|
{
|
||||||
|
throw std::runtime_error("Invalid UI File Property.");
|
||||||
|
}
|
||||||
|
std::filesystem::path oldPath = this->GetPluginUploadDirectory() / uiFileProperty.directory() / oldRelativePath;
|
||||||
|
if (!this->IsValidSampleFileName(oldPath))
|
||||||
|
{
|
||||||
|
throw std::runtime_error("Invalid file name.");
|
||||||
|
}
|
||||||
|
if (!std::filesystem::exists(oldPath))
|
||||||
|
{
|
||||||
|
throw std::runtime_error("Original path does not exist.");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::filesystem::path newPath = this->GetPluginUploadDirectory() / uiFileProperty.directory() / newRelativePath;
|
||||||
|
|
||||||
|
|
||||||
|
if (!this->IsValidSampleFileName(newPath))
|
||||||
|
{
|
||||||
|
throw std::runtime_error("Invalid file name.");
|
||||||
|
}
|
||||||
|
if (std::filesystem::exists(newPath))
|
||||||
|
{
|
||||||
|
if (std::filesystem::is_directory(newPath))
|
||||||
|
{
|
||||||
|
throw std::runtime_error("A directory with that name already exists.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!overwrite) {
|
||||||
|
return ""; // signal a portential overwrite.
|
||||||
|
} else {
|
||||||
|
fs::remove(newPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::filesystem::create_hard_link(oldPath, newPath);
|
||||||
|
return newPath;
|
||||||
|
}
|
||||||
|
|
||||||
void Storage::FillSampleDirectoryTree(FilePropertyDirectoryTree *node, const std::filesystem::path &directory) const
|
void Storage::FillSampleDirectoryTree(FilePropertyDirectoryTree *node, const std::filesystem::path &directory) const
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -156,6 +156,7 @@ public:
|
|||||||
|
|
||||||
bool IsInUploadsDirectory(const std::filesystem::path&path) const;
|
bool IsInUploadsDirectory(const std::filesystem::path&path) const;
|
||||||
bool IsInAudioTracksDirectory(const std::filesystem::path&path) const;
|
bool IsInAudioTracksDirectory(const std::filesystem::path&path) const;
|
||||||
|
bool IsValidArtworkFile(const std::filesystem::path& fullPath);
|
||||||
|
|
||||||
FileRequestResult GetFileList2(const std::string&relativePath,const UiFileProperty&fileProperty);
|
FileRequestResult GetFileList2(const std::string&relativePath,const UiFileProperty&fileProperty);
|
||||||
|
|
||||||
@@ -232,6 +233,11 @@ public:
|
|||||||
const std::string&oldRelativePath,
|
const std::string&oldRelativePath,
|
||||||
const std::string&newRelativePath,
|
const std::string&newRelativePath,
|
||||||
const UiFileProperty&uiFileProperty);
|
const UiFileProperty&uiFileProperty);
|
||||||
|
std::string CopyFilePropertyFile(
|
||||||
|
const std::string&oldRelativePath,
|
||||||
|
const std::string&newRelativePath,
|
||||||
|
const UiFileProperty&uiFileProperty,
|
||||||
|
bool overwrite = false);
|
||||||
FilePropertyDirectoryTree::ptr GetFilePropertydirectoryTree(const UiFileProperty&uiFileProperty,const std::filesystem::path&selectedPath);
|
FilePropertyDirectoryTree::ptr GetFilePropertydirectoryTree(const UiFileProperty&uiFileProperty,const std::filesystem::path&selectedPath);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
Serialize sort order.
|
Numeric up-down buttons blown in light theme.
|
||||||
|
|
||||||
|
Tooltips on touch ui?
|
||||||
|
|
||||||
|
pre-cue next decoder stream in big loops.
|
||||||
|
|
||||||
|
convolution reverb broken.
|
||||||
|
|
||||||
|
|
||||||
- pipewire aux in?
|
- pipewire aux in?
|
||||||
|
|
||||||
libsqlite3-dev install
|
|
||||||
libsqlitecpp-dev install
|
|
||||||
|
|
||||||
Check that OnNotifyPatchProperty still worsk for
|
Check that OnNotifyPatchProperty still worsk for
|
||||||
- FilePropertyControl
|
- FilePropertyControl
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 11 KiB |
+62
-13
@@ -58,8 +58,50 @@ const theme = createTheme(
|
|||||||
{
|
{
|
||||||
cssVariables: true,
|
cssVariables: true,
|
||||||
components: {
|
components: {
|
||||||
|
// MuiTouchRipple: {
|
||||||
|
// styleOverrides: {
|
||||||
|
// root: {
|
||||||
|
// borderRadius: 'inherit',
|
||||||
|
// overflow: 'hidden',
|
||||||
|
// },
|
||||||
|
// ripple: {
|
||||||
|
// color: '#F88 !important',
|
||||||
|
// borderRadius: 'inherit',
|
||||||
|
|
||||||
|
// '&.MuiTouchRipple-ripplePulsate': {
|
||||||
|
// //animation: 'none !important',
|
||||||
|
|
||||||
|
// // Make focus ripple fill the entire button
|
||||||
|
// '&.MuiTouchRipple-child': {
|
||||||
|
// width: '100%',
|
||||||
|
// height: '100%',
|
||||||
|
// borderRadius: 'inherit',
|
||||||
|
// transform: 'scale(1.4)', // Override the default scaling
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// '&.MuiTouchRipple-ripple': {
|
||||||
|
// '&:focus': {
|
||||||
|
// // Make focus ripple fill the entire button
|
||||||
|
// transform: 'scale(1.4)',
|
||||||
|
// width: '100%',
|
||||||
|
// height: '100%',
|
||||||
|
// color: '#F88',
|
||||||
|
// borderRadius: 'inherit',
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// child: {
|
||||||
|
// borderRadius: 'inherit',
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// },
|
||||||
MuiButton: {
|
MuiButton: {
|
||||||
styleOverrides: {
|
styleOverrides: {
|
||||||
|
root: {
|
||||||
|
'& .MuiTouchRipple-ripple': {
|
||||||
|
transform: 'scale(1.9)',
|
||||||
|
}
|
||||||
|
},
|
||||||
containedPrimary: {
|
containedPrimary: {
|
||||||
borderRadius: '9999px',
|
borderRadius: '9999px',
|
||||||
paddingLeft: "16px", paddingRight: "16px",
|
paddingLeft: "16px", paddingRight: "16px",
|
||||||
@@ -80,7 +122,7 @@ const theme = createTheme(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
props: { variant: 'dialogSecondary', },
|
props: { variant: 'dialogSecondary', },
|
||||||
style: {
|
style: {
|
||||||
color: "rgb(255,255,255,0.7)"
|
color: "rgb(255,255,255,0.7)"
|
||||||
},
|
},
|
||||||
@@ -108,18 +150,26 @@ const theme = createTheme(
|
|||||||
/* make the selection state for MuiListItemButtons a smidgen darker (light theme only) */
|
/* make the selection state for MuiListItemButtons a smidgen darker (light theme only) */
|
||||||
MuiListItemButton: {
|
MuiListItemButton: {
|
||||||
styleOverrides: {
|
styleOverrides: {
|
||||||
root: ({ theme }) => ({
|
root: ({ theme }) => ({
|
||||||
'&.Mui-selected': {
|
'&.Mui-selected': {
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0.2)', // Adjust for desired darkness
|
backgroundColor: 'rgba(0, 0, 0, 0.2)', // Adjust for desired darkness
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0.25)', // Slightly darker on hover
|
backgroundColor: 'rgba(0, 0, 0, 0.25)', // Slightly darker on hover
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
MuiButton: {
|
MuiButton: {
|
||||||
styleOverrides: {
|
styleOverrides: {
|
||||||
|
root: {
|
||||||
|
'& .MuiTouchRipple-root': {
|
||||||
|
borderRadius: 'inherit',
|
||||||
|
},
|
||||||
|
'& .MuiTouchRipple-ripple': {
|
||||||
|
transform: 'scale(1.9)!important',
|
||||||
|
}
|
||||||
|
},
|
||||||
containedPrimary: {
|
containedPrimary: {
|
||||||
borderRadius: '9999px',
|
borderRadius: '9999px',
|
||||||
paddingLeft: "16px", paddingRight: "16px",
|
paddingLeft: "16px", paddingRight: "16px",
|
||||||
@@ -140,7 +190,7 @@ const theme = createTheme(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
props: { variant: 'dialogSecondary', },
|
props: { variant: 'dialogSecondary', },
|
||||||
style: {
|
style: {
|
||||||
color: "rgb(0,0,0,0.6)"
|
color: "rgb(0,0,0,0.6)"
|
||||||
},
|
},
|
||||||
@@ -178,8 +228,7 @@ const App = (class extends React.Component {
|
|||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
};
|
};
|
||||||
if (!App.virtualKeyboardHandler)
|
if (!App.virtualKeyboardHandler) {
|
||||||
{
|
|
||||||
App.virtualKeyboardHandler = new VirtualKeyboardHandler();
|
App.virtualKeyboardHandler = new VirtualKeyboardHandler();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -192,7 +241,7 @@ const App = (class extends React.Component {
|
|||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
|
|
||||||
<AppThemed />
|
<AppThemed />
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</StyledEngineProvider>
|
</StyledEngineProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -43,3 +43,4 @@ input[type=number] {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Button, {ButtonProps} from '@mui/material/Button';
|
||||||
|
import Tooltip from "@mui/material/Tooltip";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
|
||||||
|
|
||||||
|
interface ButtonExProps extends ButtonProps {
|
||||||
|
tooltip: string;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
};
|
||||||
|
function ButtonEx(props: ButtonExProps) {
|
||||||
|
const { tooltip, style, ...extra } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip title={
|
||||||
|
(
|
||||||
|
<Typography variant="caption">{tooltip || extra['aria-label']}</Typography>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
placement="top-start" arrow
|
||||||
|
enterDelay={1500} enterNextDelay={1500}
|
||||||
|
>
|
||||||
|
<Button {...extra} style={style} />
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export default ButtonEx;
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, {useEffect} from "react";
|
||||||
|
import Slider,{ SliderProps } from "@mui/material/Slider";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import { PiPedalModelFactory,State } from "./PiPedalModel";
|
||||||
|
|
||||||
|
function formatDuration(value_: number) {
|
||||||
|
let value = Math.ceil(value_);
|
||||||
|
const minute = Math.floor(value / 60);
|
||||||
|
const secondLeft = value - minute * 60;
|
||||||
|
return `${minute}:${secondLeft < 10 ? `0${secondLeft}` : secondLeft}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ControlSliderProps extends SliderProps {
|
||||||
|
instanceId: number;
|
||||||
|
controlKey: string;
|
||||||
|
duration: number;
|
||||||
|
onPreviewValue: (value: number) => void;
|
||||||
|
onValueChanged: (value: number) => void; // Callback when the value changes
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function ControlSlider(props: ControlSliderProps) {
|
||||||
|
const { style,instanceId, controlKey, duration,
|
||||||
|
onPreviewValue, onValueChanged, ...extras} = props;
|
||||||
|
|
||||||
|
const model = PiPedalModelFactory.getInstance();
|
||||||
|
|
||||||
|
let [sliderValue, setSliderValue] = React.useState(0);
|
||||||
|
let [effectiveValue, setEffectiveValue] = React.useState(0);
|
||||||
|
let [dragging, setDragging] = React.useState(false);
|
||||||
|
let [serverConnected,setServerConnected] = React.useState(model.state.get() === State.Ready);
|
||||||
|
const handleStateChanged = (state: State) => {
|
||||||
|
setServerConnected(state === State.Ready);
|
||||||
|
};
|
||||||
|
useEffect(() => {
|
||||||
|
model.state.addOnChangedHandler(handleStateChanged);
|
||||||
|
if (model.state.get() !== State.Ready) {
|
||||||
|
return () => {
|
||||||
|
model.state.removeOnChangedHandler(handleStateChanged);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
let handle = model.monitorPort(instanceId, controlKey, 1.0/15.0,(value: number) => {
|
||||||
|
setSliderValue(value);
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
model.state.removeOnChangedHandler(handleStateChanged);
|
||||||
|
model.unmonitorPort(handle);
|
||||||
|
};
|
||||||
|
}, [instanceId,controlKey,serverConnected]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ display: "flex", flexFlow: "column nowrap" }}>
|
||||||
|
|
||||||
|
<Slider
|
||||||
|
{...extras}
|
||||||
|
value={dragging ? effectiveValue : sliderValue}
|
||||||
|
min={0}
|
||||||
|
step={1}
|
||||||
|
max={duration}
|
||||||
|
onChange={(_, val: any) => {
|
||||||
|
let v = parseFloat(val);
|
||||||
|
setDragging(true);
|
||||||
|
setEffectiveValue(v);
|
||||||
|
onPreviewValue(v);
|
||||||
|
}}
|
||||||
|
onChangeCommitted={(_, val: any) => {
|
||||||
|
let v = parseFloat(val);
|
||||||
|
setDragging(false);
|
||||||
|
setSliderValue(v);
|
||||||
|
onValueChanged(v);
|
||||||
|
}}
|
||||||
|
onPointerDown={(e) => {
|
||||||
|
setDragging(true);
|
||||||
|
}}
|
||||||
|
onPointerUp={(e) => {
|
||||||
|
// setDragging(false);
|
||||||
|
}}
|
||||||
|
disabled={duration == 0}
|
||||||
|
sx={(t) => ({
|
||||||
|
width: "100%",
|
||||||
|
color: 'rgba(0,0,0,0.87)',
|
||||||
|
height: 4,
|
||||||
|
'& .MuiSlider-thumb': {
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
transition: '0.0s',
|
||||||
|
transitionProperty: 'none',
|
||||||
|
'&::before': {
|
||||||
|
boxShadow: '0 2px 12px 0 rgba(0,0,0,0.4)',
|
||||||
|
},
|
||||||
|
'&:hover, &.Mui-focusVisible': {
|
||||||
|
boxShadow: `0px 0px 0px 8px ${'rgb(0 0 0 / 16%)'}`,
|
||||||
|
...t.applyStyles('dark', {
|
||||||
|
boxShadow: `0px 0px 0px 8px ${'rgb(255 255 255 / 16%)'}`,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
'&.Mui-active': {
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'& .MuiSlider-rail': {
|
||||||
|
opacity: 0.28,
|
||||||
|
},
|
||||||
|
'& .MuiSlider-track': {
|
||||||
|
transition: '0.0s',
|
||||||
|
transitionProperty: 'none',
|
||||||
|
},
|
||||||
|
...t.applyStyles('dark', {
|
||||||
|
color: '#fff',
|
||||||
|
}),
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
flex: "0 0 auto",
|
||||||
|
width: "100%",
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'top',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
marginTop: -4,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="caption">{formatDuration(dragging ? effectiveValue : sliderValue)}</Typography>
|
||||||
|
<Typography variant="caption">{formatDuration(duration)}</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ControlSlider;
|
||||||
@@ -33,6 +33,7 @@ import MoreIcon from '@mui/icons-material/MoreVert';
|
|||||||
import { PiPedalModel, PiPedalModelFactory, FileEntry, BreadcrumbEntry, FileRequestResult } from './PiPedalModel';
|
import { PiPedalModel, PiPedalModelFactory, FileEntry, BreadcrumbEntry, FileRequestResult } from './PiPedalModel';
|
||||||
import { isDarkMode } from './DarkMode';
|
import { isDarkMode } from './DarkMode';
|
||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
|
import ButtonEx from './ButtonEx';
|
||||||
import FileUploadIcon from '@mui/icons-material/FileUpload';
|
import FileUploadIcon from '@mui/icons-material/FileUpload';
|
||||||
import FileDownloadIcon from '@mui/icons-material/FileDownload';
|
import FileDownloadIcon from '@mui/icons-material/FileDownload';
|
||||||
import AudioFileIcon from '@mui/icons-material/AudioFile';
|
import AudioFileIcon from '@mui/icons-material/AudioFile';
|
||||||
@@ -44,12 +45,13 @@ import DialogActions from '@mui/material/DialogActions';
|
|||||||
import DialogTitle from '@mui/material/DialogTitle';
|
import DialogTitle from '@mui/material/DialogTitle';
|
||||||
import DialogContent from '@mui/material/DialogContent';
|
import DialogContent from '@mui/material/DialogContent';
|
||||||
import IconButton from '@mui/material/IconButton';
|
import IconButton from '@mui/material/IconButton';
|
||||||
|
import IconButtonEx from './IconButtonEx';
|
||||||
import OldDeleteIcon from './OldDeleteIcon';
|
import OldDeleteIcon from './OldDeleteIcon';
|
||||||
import Toolbar from '@mui/material/Toolbar';
|
import Toolbar from '@mui/material/Toolbar';
|
||||||
import WithStyles from './WithStyles';
|
import WithStyles from './WithStyles';
|
||||||
import { withStyles } from "tss-react/mui";
|
import { withStyles } from "tss-react/mui";
|
||||||
import CircularProgress from '@mui/material/CircularProgress';
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
import { pathConcat,pathParentDirectory,pathFileName,pathFileNameOnly, pathExtension } from './FileUtils'; './FileUtils';
|
import { pathConcat, pathParentDirectory, pathFileName, pathFileNameOnly, pathExtension } from './FileUtils'; './FileUtils';
|
||||||
|
|
||||||
|
|
||||||
import ResizeResponsiveComponent from './ResizeResponsiveComponent';
|
import ResizeResponsiveComponent from './ResizeResponsiveComponent';
|
||||||
@@ -77,13 +79,14 @@ const styles = (theme: Theme) => createStyles({
|
|||||||
const audioFileExtensions: { [name: string]: boolean } = {
|
const audioFileExtensions: { [name: string]: boolean } = {
|
||||||
".wav": true,
|
".wav": true,
|
||||||
".flac": true,
|
".flac": true,
|
||||||
|
".mp3": true,
|
||||||
|
".m4a": true,
|
||||||
".ogg": true,
|
".ogg": true,
|
||||||
".aac": true,
|
".aac": true,
|
||||||
".au": true,
|
".au": true,
|
||||||
".snd": true,
|
".snd": true,
|
||||||
".mid": true,
|
".mid": true,
|
||||||
".rmi": true,
|
".rmi": true,
|
||||||
".mp3": true,
|
|
||||||
".mp4": true,
|
".mp4": true,
|
||||||
".aif": true,
|
".aif": true,
|
||||||
".aifc": true,
|
".aifc": true,
|
||||||
@@ -146,10 +149,16 @@ export interface FilePropertyDialogState {
|
|||||||
columnWidth: number;
|
columnWidth: number;
|
||||||
openUploadFileDialog: boolean;
|
openUploadFileDialog: boolean;
|
||||||
openConfirmDeleteDialog: boolean;
|
openConfirmDeleteDialog: boolean;
|
||||||
|
confirmCopyDialogState: {
|
||||||
|
fileName: string;
|
||||||
|
oldFilePath: string;
|
||||||
|
newFilePath: string;
|
||||||
|
} | null;
|
||||||
menuAnchorEl: null | HTMLElement;
|
menuAnchorEl: null | HTMLElement;
|
||||||
newFolderDialogOpen: boolean;
|
newFolderDialogOpen: boolean;
|
||||||
renameDialogOpen: boolean;
|
renameDialogOpen: boolean;
|
||||||
moveDialogOpen: boolean;
|
moveDialogOpen: boolean;
|
||||||
|
copyDialogOpen: boolean;
|
||||||
initialSelection: string;
|
initialSelection: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -173,6 +182,12 @@ export default withStyles(
|
|||||||
isTracksDirectory(): boolean {
|
isTracksDirectory(): boolean {
|
||||||
return this.state.currentDirectory.startsWith("/var/pipedal/audio_uploads/shared/audio/Tracks");
|
return this.state.currentDirectory.startsWith("/var/pipedal/audio_uploads/shared/audio/Tracks");
|
||||||
}
|
}
|
||||||
|
isFolderArtwork(filePath: string): boolean {
|
||||||
|
if (!this.isTracksDirectory()) return false;
|
||||||
|
let extension = pathExtension(filePath);
|
||||||
|
return extension === ".png" || extension === ".jpg" || extension === ".jpeg";
|
||||||
|
}
|
||||||
|
|
||||||
constructor(props: FilePropertyDialogProps) {
|
constructor(props: FilePropertyDialogProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
@@ -202,10 +217,12 @@ export default withStyles(
|
|||||||
isProtectedDirectory: true,
|
isProtectedDirectory: true,
|
||||||
openUploadFileDialog: false,
|
openUploadFileDialog: false,
|
||||||
openConfirmDeleteDialog: false,
|
openConfirmDeleteDialog: false,
|
||||||
|
confirmCopyDialogState: null,
|
||||||
menuAnchorEl: null,
|
menuAnchorEl: null,
|
||||||
newFolderDialogOpen: false,
|
newFolderDialogOpen: false,
|
||||||
renameDialogOpen: false,
|
renameDialogOpen: false,
|
||||||
moveDialogOpen: false,
|
moveDialogOpen: false,
|
||||||
|
copyDialogOpen: false,
|
||||||
initialSelection: this.props.selectedFile
|
initialSelection: this.props.selectedFile
|
||||||
};
|
};
|
||||||
this.requestScroll = true;
|
this.requestScroll = true;
|
||||||
@@ -485,6 +502,7 @@ export default withStyles(
|
|||||||
newFolderDialogOpen: false,
|
newFolderDialogOpen: false,
|
||||||
renameDialogOpen: false,
|
renameDialogOpen: false,
|
||||||
moveDialogOpen: false,
|
moveDialogOpen: false,
|
||||||
|
copyDialogOpen: false,
|
||||||
navDirectory: navDirectory
|
navDirectory: navDirectory
|
||||||
});
|
});
|
||||||
this.requestFiles(navDirectory)
|
this.requestFiles(navDirectory)
|
||||||
@@ -531,7 +549,9 @@ export default withStyles(
|
|||||||
}
|
}
|
||||||
this.requestScroll = true;
|
this.requestScroll = true;
|
||||||
if (!fileEntry.isDirectory) {
|
if (!fileEntry.isDirectory) {
|
||||||
this.props.onApply(this.props.fileProperty, fileEntry.pathname);
|
if (!this.isFolderArtwork(fileEntry.pathname)) {
|
||||||
|
this.props.onApply(this.props.fileProperty, fileEntry.pathname);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.setState({
|
this.setState({
|
||||||
selectedFile: fileEntry.pathname,
|
selectedFile: fileEntry.pathname,
|
||||||
@@ -641,7 +661,7 @@ export default withStyles(
|
|||||||
}
|
}
|
||||||
getAlbumTitle(fileEntry: FileEntry): string {
|
getAlbumTitle(fileEntry: FileEntry): string {
|
||||||
let artist = fileEntry.metadata?.artist || "";
|
let artist = fileEntry.metadata?.artist || "";
|
||||||
if (artist == "" ) {
|
if (artist == "") {
|
||||||
artist = fileEntry.metadata?.albumArtist || "";
|
artist = fileEntry.metadata?.albumArtist || "";
|
||||||
}
|
}
|
||||||
let album = fileEntry.metadata?.album || "";
|
let album = fileEntry.metadata?.album || "";
|
||||||
@@ -814,6 +834,8 @@ export default withStyles(
|
|||||||
let needsDivider = canMove || canRename || canReorder;
|
let needsDivider = canMove || canRename || canReorder;
|
||||||
let trackPosition = 0;
|
let trackPosition = 0;
|
||||||
let compactVertical = this.state.windowHeight < 700;
|
let compactVertical = this.state.windowHeight < 700;
|
||||||
|
let canSelectFile = this.state.hasSelection && !this.isFolderArtwork(this.state.selectedFile);
|
||||||
|
|
||||||
|
|
||||||
return this.props.open &&
|
return this.props.open &&
|
||||||
(
|
(
|
||||||
@@ -886,7 +908,8 @@ export default withStyles(
|
|||||||
{this.props.fileProperty.label}
|
{this.props.fileProperty.label}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<IconButton
|
<IconButtonEx
|
||||||
|
tooltip="New folder"
|
||||||
aria-label="new folder"
|
aria-label="new folder"
|
||||||
edge="end"
|
edge="end"
|
||||||
color="inherit"
|
color="inherit"
|
||||||
@@ -895,10 +918,11 @@ export default withStyles(
|
|||||||
disabled={protectedDirectory}
|
disabled={protectedDirectory}
|
||||||
>
|
>
|
||||||
<CreateNewFolderIcon />
|
<CreateNewFolderIcon />
|
||||||
</IconButton>
|
</IconButtonEx>
|
||||||
|
|
||||||
|
|
||||||
<IconButton
|
<IconButtonEx
|
||||||
|
tooltip="More..."
|
||||||
aria-label="display more actions"
|
aria-label="display more actions"
|
||||||
edge="end"
|
edge="end"
|
||||||
color="inherit"
|
color="inherit"
|
||||||
@@ -908,7 +932,7 @@ export default withStyles(
|
|||||||
|
|
||||||
>
|
>
|
||||||
<MoreIcon />
|
<MoreIcon />
|
||||||
</IconButton>
|
</IconButtonEx>
|
||||||
<Menu
|
<Menu
|
||||||
id="menu-appbar"
|
id="menu-appbar"
|
||||||
anchorEl={this.state.menuAnchorEl}
|
anchorEl={this.state.menuAnchorEl}
|
||||||
@@ -927,6 +951,7 @@ export default withStyles(
|
|||||||
<MenuItem onClick={() => { this.handleMenuClose(); this.onNewFolder(); }}>New folder</MenuItem>
|
<MenuItem onClick={() => { this.handleMenuClose(); this.onNewFolder(); }}>New folder</MenuItem>
|
||||||
{(needsDivider) && (<Divider />)}
|
{(needsDivider) && (<Divider />)}
|
||||||
{canMove && (<MenuItem onClick={() => { this.handleMenuClose(); this.onMove(); }}>Move</MenuItem>)}
|
{canMove && (<MenuItem onClick={() => { this.handleMenuClose(); this.onMove(); }}>Move</MenuItem>)}
|
||||||
|
{canMove && (<MenuItem onClick={() => { this.handleMenuClose(); this.onCopy(); }}>Copy</MenuItem>)}
|
||||||
{canRename && this.hasSelectedFileOrFolder() && !protectedItem && (<MenuItem onClick={() => {
|
{canRename && this.hasSelectedFileOrFolder() && !protectedItem && (<MenuItem onClick={() => {
|
||||||
this.handleMenuClose(); this.onRename();
|
this.handleMenuClose(); this.onRename();
|
||||||
}}>Rename</MenuItem>)}
|
}}>Rename</MenuItem>)}
|
||||||
@@ -1013,7 +1038,7 @@ export default withStyles(
|
|||||||
<DraggableButtonBase key={value.pathname}
|
<DraggableButtonBase key={value.pathname}
|
||||||
longPressDelay={this.state.reordering ? 0 : -1}
|
longPressDelay={this.state.reordering ? 0 : -1}
|
||||||
data-position={dataPosition}
|
data-position={dataPosition}
|
||||||
data-fileName={
|
data-pathname={
|
||||||
value.metadata ? value.metadata.fileName : null
|
value.metadata ? value.metadata.fileName : null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1047,10 +1072,20 @@ export default withStyles(
|
|||||||
display: "flex", flexFlow: "row nowrap", textOverflow: "ellipsis",
|
display: "flex", flexFlow: "row nowrap", textOverflow: "ellipsis",
|
||||||
justifyContent: "start", alignItems: "center", width: "100%", height: "100%"
|
justifyContent: "start", alignItems: "center", width: "100%", height: "100%"
|
||||||
}}>
|
}}>
|
||||||
|
{
|
||||||
|
this.isFolderArtwork(value.pathname) ? (
|
||||||
|
<img
|
||||||
|
onDragStart={(e) => { e.preventDefault(); }}
|
||||||
|
src={this.getTrackThumbnail(value)}
|
||||||
|
style={{ width: 24, height: 24, margin: 20, borderRadius: 4 }} />
|
||||||
|
):(
|
||||||
<img
|
<img
|
||||||
onDragStart={(e) => { e.preventDefault(); }}
|
onDragStart={(e) => { e.preventDefault(); }}
|
||||||
src={this.getTrackThumbnail(value)}
|
src={this.getTrackThumbnail(value)}
|
||||||
style={{ width: 48, height: 48, margin: 8, borderRadius: 4 }} />
|
style={{ width: 48, height: 48, margin: 8, borderRadius: 4 }} />
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
||||||
<div style={{
|
<div style={{
|
||||||
flex: "1 1 1px", display: "flex", flexFlow: "column nowrap",
|
flex: "1 1 1px", display: "flex", flexFlow: "column nowrap",
|
||||||
marginLeft: 8,
|
marginLeft: 8,
|
||||||
@@ -1060,7 +1095,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)}
|
{this.getTrackTitle(value)}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography noWrap className={classes.secondaryText}
|
<Typography noWrap className={classes.secondaryText}
|
||||||
variant="body2"
|
variant="body2"
|
||||||
@@ -1115,23 +1150,24 @@ export default withStyles(
|
|||||||
<DialogActions style={{ justifyContent: "stretch" }}>
|
<DialogActions style={{ justifyContent: "stretch" }}>
|
||||||
{this.state.windowWidth > 500 ? (
|
{this.state.windowWidth > 500 ? (
|
||||||
<div style={{ display: "flex", width: "100%", alignItems: "center", flexFlow: "row nowrap" }}>
|
<div style={{ display: "flex", width: "100%", alignItems: "center", flexFlow: "row nowrap" }}>
|
||||||
<IconButton style={{ visibility: (this.state.hasSelection ? "visible" : "hidden") }} aria-label="delete" component="label" color="primary"
|
<IconButtonEx style={{ visibility: (this.state.hasSelection ? "visible" : "hidden") }} aria-label="delete" component="label" color="primary"
|
||||||
|
tooltip="Delete selected file or folder"
|
||||||
disabled={!this.state.hasSelection || this.state.selectedFile === "" || protectedItem}
|
disabled={!this.state.hasSelection || this.state.selectedFile === "" || protectedItem}
|
||||||
onClick={() => this.handleDelete()} >
|
onClick={() => this.handleDelete()} >
|
||||||
<OldDeleteIcon fontSize='small' />
|
<OldDeleteIcon fontSize='small' />
|
||||||
</IconButton>
|
</IconButtonEx>
|
||||||
|
|
||||||
<Button style={{ flex: "0 0 auto" }} aria-label="upload" variant="text" startIcon={<FileUploadIcon />}
|
<ButtonEx tooltip="Upload file" style={{ flex: "0 0 auto" }} aria-label="upload" variant="text" startIcon={<FileUploadIcon />}
|
||||||
onClick={() => { this.setState({ openUploadFileDialog: true }) }} disabled={protectedDirectory}
|
onClick={() => { this.setState({ openUploadFileDialog: true }) }} disabled={protectedDirectory}
|
||||||
>
|
>
|
||||||
<div>Upload</div>
|
<div>Upload</div>
|
||||||
</Button>
|
</ButtonEx>
|
||||||
|
|
||||||
<Button style={{ flex: "0 0 auto" }} aria-label="upload" variant="text" startIcon={<FileDownloadIcon />}
|
<ButtonEx tooltip="Download file" style={{ flex: "0 0 auto" }} aria-label="upload" variant="text" startIcon={<FileDownloadIcon />}
|
||||||
onClick={() => { this.handleDownloadFile(); }} disabled={protectedDirectory || !this.state.hasFileSelection}
|
onClick={() => { this.handleDownloadFile(); }} disabled={protectedDirectory || !this.state.hasFileSelection}
|
||||||
>
|
>
|
||||||
<div>Download</div>
|
<div>Download</div>
|
||||||
</Button>
|
</ButtonEx>
|
||||||
|
|
||||||
<div style={{ flex: "1 1 auto" }}> </div>
|
<div style={{ flex: "1 1 auto" }}> </div>
|
||||||
|
|
||||||
@@ -1144,7 +1180,8 @@ export default withStyles(
|
|||||||
<Button variant="dialogPrimary" style={{ flex: "0 0 auto" }}
|
<Button variant="dialogPrimary" style={{ flex: "0 0 auto" }}
|
||||||
onClick={() => { this.openSelectedFile(); }}
|
onClick={() => { this.openSelectedFile(); }}
|
||||||
|
|
||||||
disabled={(!this.state.hasSelection) && this.state.selectedFile !== ""} aria-label="select"
|
disabled={(!canSelectFile)
|
||||||
|
} aria-label="select"
|
||||||
>
|
>
|
||||||
{okButtonText}
|
{okButtonText}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -1182,7 +1219,8 @@ export default withStyles(
|
|||||||
<Button variant="dialogPrimary" style={{ flex: "0 0 auto" }}
|
<Button variant="dialogPrimary" style={{ flex: "0 0 auto" }}
|
||||||
onClick={() => { this.openSelectedFile(); }}
|
onClick={() => { this.openSelectedFile(); }}
|
||||||
|
|
||||||
disabled={(!this.state.hasSelection) && this.state.selectedFile !== ""} aria-label="select"
|
disabled={!canSelectFile}
|
||||||
|
aria-label="select"
|
||||||
>
|
>
|
||||||
{okButtonText}
|
{okButtonText}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -1200,6 +1238,7 @@ export default withStyles(
|
|||||||
this.setState({ openUploadFileDialog: false });
|
this.setState({ openUploadFileDialog: false });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
isTracksDirectory={this.isTracksDirectory()}
|
||||||
uploadPage={
|
uploadPage={
|
||||||
"uploadUserFile?directory=" + encodeURIComponent(this.state.currentDirectory)
|
"uploadUserFile?directory=" + encodeURIComponent(this.state.currentDirectory)
|
||||||
+ "&id=" + this.props.instanceId.toString()
|
+ "&id=" + this.props.instanceId.toString()
|
||||||
@@ -1224,6 +1263,17 @@ export default withStyles(
|
|||||||
}}
|
}}
|
||||||
|
|
||||||
/>
|
/>
|
||||||
|
<OkCancelDialog open={this.state.confirmCopyDialogState !== null}
|
||||||
|
text={
|
||||||
|
"The target file already exists. Would you like to overwrite it?"}
|
||||||
|
okButtonText="Overwrite"
|
||||||
|
onOk={() => { this.handleConfirmCopy(); }}
|
||||||
|
onClose={() => {
|
||||||
|
this.setState({ confirmCopyDialogState: null });
|
||||||
|
}}
|
||||||
|
|
||||||
|
/>
|
||||||
|
|
||||||
{
|
{
|
||||||
this.state.newFolderDialogOpen && (
|
this.state.newFolderDialogOpen && (
|
||||||
<RenameDialog open={this.state.newFolderDialogOpen} defaultName=""
|
<RenameDialog open={this.state.newFolderDialogOpen} defaultName=""
|
||||||
@@ -1244,23 +1294,28 @@ export default withStyles(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
this.state.moveDialogOpen && (
|
(this.state.moveDialogOpen || this.state.copyDialogOpen)
|
||||||
|
&& (
|
||||||
(
|
(
|
||||||
<FilePropertyDirectorySelectDialog
|
<FilePropertyDirectorySelectDialog
|
||||||
open={this.state.moveDialogOpen}
|
open={this.state.moveDialogOpen || this.state.copyDialogOpen}
|
||||||
dialogTitle="Move to"
|
dialogTitle={this.state.moveDialogOpen ? "Move to" : "Copy Link to"}
|
||||||
uiFileProperty={this.props.fileProperty}
|
uiFileProperty={this.props.fileProperty}
|
||||||
defaultPath={this.getDefaultPath()}
|
defaultPath={this.getDefaultPath()}
|
||||||
selectedFile={this.state.selectedFile}
|
selectedFile={this.state.selectedFile}
|
||||||
excludeDirectory={
|
excludeDirectory={
|
||||||
this.isDirectory(this.state.selectedFile)
|
this.isDirectory(this.state.selectedFile)
|
||||||
? this.state.selectedFile : ""}
|
? this.state.selectedFile : ""}
|
||||||
onClose={() => { this.setState({ moveDialogOpen: false }); }}
|
onClose={() => { this.setState({ moveDialogOpen: false, copyDialogOpen: false }); }}
|
||||||
onOk={
|
onOk={
|
||||||
(path) => {
|
(path) => {
|
||||||
this.setState({ moveDialogOpen: false });
|
this.setState({ moveDialogOpen: false });
|
||||||
this.setDefaultPath(path);
|
this.setDefaultPath(path);
|
||||||
this.onExecuteMove(path);
|
if (this.state.copyDialogOpen) {
|
||||||
|
this.onExecuteCopy(path);
|
||||||
|
} else {
|
||||||
|
this.onExecuteMove(path);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1272,6 +1327,9 @@ export default withStyles(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
openSelectedFile(): void {
|
openSelectedFile(): void {
|
||||||
|
if (this.isFolderArtwork(this.state.selectedFile)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (this.isDirectory(this.state.selectedFile)) {
|
if (this.isDirectory(this.state.selectedFile)) {
|
||||||
this.requestFiles(this.state.selectedFile);
|
this.requestFiles(this.state.selectedFile);
|
||||||
this.setState({ navDirectory: this.state.selectedFile });
|
this.setState({ navDirectory: this.state.selectedFile });
|
||||||
@@ -1292,6 +1350,9 @@ export default withStyles(
|
|||||||
private onMove(): void {
|
private onMove(): void {
|
||||||
this.setState({ moveDialogOpen: true });
|
this.setState({ moveDialogOpen: true });
|
||||||
}
|
}
|
||||||
|
private onCopy(): void {
|
||||||
|
this.setState({ copyDialogOpen: true });
|
||||||
|
}
|
||||||
private onExecuteMove(newDirectory: string) {
|
private onExecuteMove(newDirectory: string) {
|
||||||
let fileName = pathFileName(this.state.selectedFile);
|
let fileName = pathFileName(this.state.selectedFile);
|
||||||
let oldFilePath = pathConcat(this.state.navDirectory, fileName);
|
let oldFilePath = pathConcat(this.state.navDirectory, fileName);
|
||||||
@@ -1305,6 +1366,46 @@ export default withStyles(
|
|||||||
this.model.showAlert(e.toString());
|
this.model.showAlert(e.toString());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
private handleConfirmCopy() {
|
||||||
|
if (this.state.confirmCopyDialogState) {
|
||||||
|
this.model.copyFilePropertyFile(
|
||||||
|
this.state.confirmCopyDialogState.oldFilePath,
|
||||||
|
this.state.confirmCopyDialogState.newFilePath,
|
||||||
|
this.props.fileProperty, true)
|
||||||
|
.then((filename) => {
|
||||||
|
this.requestFiles(this.state.navDirectory);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
this.model.showAlert(e.toString());
|
||||||
|
});
|
||||||
|
this.setState({ confirmCopyDialogState: null });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private onExecuteCopy(newDirectory: string) {
|
||||||
|
this.setState({ copyDialogOpen: false });
|
||||||
|
let fileName = pathFileName(this.state.selectedFile);
|
||||||
|
let oldFilePath = pathConcat(this.state.navDirectory, fileName);
|
||||||
|
let newFilePath = pathConcat(newDirectory, fileName);
|
||||||
|
|
||||||
|
this.model.copyFilePropertyFile(oldFilePath, newFilePath, this.props.fileProperty, false)
|
||||||
|
.then((filename) => {
|
||||||
|
if (filename === "") {
|
||||||
|
// the file already exists. prompt for overrwrite.
|
||||||
|
this.setState({
|
||||||
|
confirmCopyDialogState: {
|
||||||
|
fileName: fileName,
|
||||||
|
oldFilePath: oldFilePath,
|
||||||
|
newFilePath
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
this.model.showAlert(e.toString());
|
||||||
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private onRename(): void {
|
private onRename(): void {
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import IconButton, {IconButtonProps} from '@mui/material/IconButton';
|
||||||
|
import Tooltip from "@mui/material/Tooltip";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
|
||||||
|
interface IconButtonExProps extends IconButtonProps {
|
||||||
|
tooltip: string;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
};
|
||||||
|
|
||||||
|
function IconButtonEx(props: IconButtonExProps) {
|
||||||
|
const { tooltip,style, ...extra } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip title={
|
||||||
|
(
|
||||||
|
<Typography variant="caption">{tooltip || extra['aria-label'] }</Typography>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
placement="top-start" arrow
|
||||||
|
enterDelay={1500} enterNextDelay={1500}
|
||||||
|
>
|
||||||
|
<IconButton {...extra} style={style} />
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default IconButtonEx;
|
||||||
+132
-114
@@ -36,17 +36,22 @@ import Slider, { SliderProps } from "@mui/material/Slider";
|
|||||||
import Checkbox from "@mui/material/Checkbox";
|
import Checkbox from "@mui/material/Checkbox";
|
||||||
import TimebaseSelectorDialog from "./TimebaseselectorDialog";
|
import TimebaseSelectorDialog from "./TimebaseselectorDialog";
|
||||||
import Timebase, { TimebaseUnits } from "./Timebase";
|
import Timebase, { TimebaseUnits } from "./Timebase";
|
||||||
|
import { useWindowHeight } from "@react-hook/window-size";
|
||||||
|
import Button from "@mui/material/Button";
|
||||||
|
import PlayArrow from "@mui/icons-material/PlayArrow";
|
||||||
|
import StopIcon from '@mui/icons-material/Stop';
|
||||||
|
import { LoopParameters } from './Timebase';
|
||||||
|
|
||||||
|
|
||||||
export interface LoopDialogProps {
|
export interface LoopDialogProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onSetLoop: (start: number, loopEnable: boolean, loopStart: number, loopEnd: number) => void;
|
onSetLoop: (value: LoopParameters) => void;
|
||||||
start: number;
|
value: LoopParameters;
|
||||||
loopEnable: boolean;
|
playing: boolean;
|
||||||
loopStart: number;
|
onPreview: () => void;
|
||||||
loopEnd: number;
|
onCancelPlaying: () => void;
|
||||||
duration: number;
|
duration: number;
|
||||||
fullScreen?: boolean;
|
|
||||||
timebase: Timebase;
|
timebase: Timebase;
|
||||||
onTimebaseChange: (timebase: Timebase) => void; // Optional callback for timebase changes
|
onTimebaseChange: (timebase: Timebase) => void; // Optional callback for timebase changes
|
||||||
sampleRate: number;
|
sampleRate: number;
|
||||||
@@ -74,9 +79,9 @@ function parseTime(timebase: Timebase, sampleRate: number, duration: number, tim
|
|||||||
if (isNaN(samples)) {
|
if (isNaN(samples)) {
|
||||||
return samples;
|
return samples;
|
||||||
}
|
}
|
||||||
let result = samples / sampleRate; // Convert samples to seconds
|
let result = samples / sampleRate; // Convert samples to seconds
|
||||||
if (result < 0) {
|
if (result < 0) {
|
||||||
result = 0;
|
result = 0;
|
||||||
}
|
}
|
||||||
if (result > duration) {
|
if (result > duration) {
|
||||||
result = duration;
|
result = duration;
|
||||||
@@ -118,13 +123,13 @@ function parseTime(timebase: Timebase, sampleRate: number, duration: number, tim
|
|||||||
|
|
||||||
let result = hours * 60 * 60 + minutes * 60 + seconds; // Convert total time to seconds
|
let result = hours * 60 * 60 + minutes * 60 + seconds; // Convert total time to seconds
|
||||||
if (result < 0) {
|
if (result < 0) {
|
||||||
result = 0;
|
result = 0;
|
||||||
}
|
}
|
||||||
if (result > duration) {
|
if (result > duration) {
|
||||||
result = duration;
|
result = duration;
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
|
|
||||||
}
|
}
|
||||||
case TimebaseUnits.Beats:
|
case TimebaseUnits.Beats:
|
||||||
{
|
{
|
||||||
@@ -141,9 +146,9 @@ function parseTime(timebase: Timebase, sampleRate: number, duration: number, tim
|
|||||||
if (isNaN(beat)) {
|
if (isNaN(beat)) {
|
||||||
throw new Error("Invalid seconds format. Use 'bar:beat' or 'bar:beat.fraction'.");
|
throw new Error("Invalid seconds format. Use 'bar:beat' or 'bar:beat.fraction'.");
|
||||||
}
|
}
|
||||||
if (beat < 1 || beat >= timebase.timeSignature.numerator+1) {
|
if (beat < 1 || beat >= timebase.timeSignature.numerator + 1) {
|
||||||
throw new Error(`Beat number must be between 1 and ${timebase.timeSignature.numerator+1}.`);
|
throw new Error(`Beat number must be between 1 and ${timebase.timeSignature.numerator + 1}.`);
|
||||||
}
|
}
|
||||||
} else if (parts.length === 2) {
|
} else if (parts.length === 2) {
|
||||||
bar = parseInt(parts[0], 10);
|
bar = parseInt(parts[0], 10);
|
||||||
if (bar < 1) {
|
if (bar < 1) {
|
||||||
@@ -153,16 +158,16 @@ function parseTime(timebase: Timebase, sampleRate: number, duration: number, tim
|
|||||||
if (isNaN(bar) || isNaN(beat)) {
|
if (isNaN(bar) || isNaN(beat)) {
|
||||||
throw new Error("Invalid bar or beat format. Use 'bar:beat' or 'bar:beat.fraction'.");
|
throw new Error("Invalid bar or beat format. Use 'bar:beat' or 'bar:beat.fraction'.");
|
||||||
}
|
}
|
||||||
if (beat < 1 || beat >= timebase.timeSignature.numerator+1) {
|
if (beat < 1 || beat >= timebase.timeSignature.numerator + 1) {
|
||||||
throw new Error(`Beat number must be between 1 and ${timebase.timeSignature.numerator+1}.`);
|
throw new Error(`Beat number must be between 1 and ${timebase.timeSignature.numerator + 1}.`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error("Invalid time format. Use 'hh:mm:ss' or 'mm:ss.mmmm'.");
|
throw new Error("Invalid time format. Use 'hh:mm:ss' or 'mm:ss.mmmm'.");
|
||||||
}
|
}
|
||||||
const actualBeat = (bar-1) * timebase.timeSignature.numerator + (beat-1); // Convert bar and beat to actual beat number
|
const actualBeat = (bar - 1) * timebase.timeSignature.numerator + (beat - 1); // Convert bar and beat to actual beat number
|
||||||
let result = actualBeat *60/ timebase.tempo; // Convert beats to seconds based on tempo
|
let result = actualBeat * 60 / timebase.tempo; // Convert beats to seconds based on tempo
|
||||||
if (result < 0) {
|
if (result < 0) {
|
||||||
result = 0;
|
result = 0;
|
||||||
}
|
}
|
||||||
if (result > duration) {
|
if (result > duration) {
|
||||||
result = duration;
|
result = duration;
|
||||||
@@ -172,7 +177,7 @@ function parseTime(timebase: Timebase, sampleRate: number, duration: number, tim
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatTime(timebase: Timebase, sampleRate: number, seconds: number): string {
|
export function formatTime(timebase: Timebase, sampleRate: number, seconds: number): string {
|
||||||
switch (timebase.units) {
|
switch (timebase.units) {
|
||||||
case TimebaseUnits.Samples:
|
case TimebaseUnits.Samples:
|
||||||
{
|
{
|
||||||
@@ -197,7 +202,7 @@ function formatTime(timebase: Timebase, sampleRate: number, seconds: number): st
|
|||||||
let beats = timebase.tempo / 60.0 * seconds;
|
let beats = timebase.tempo / 60.0 * seconds;
|
||||||
const bars = Math.floor(beats / timebase.timeSignature.numerator);
|
const bars = Math.floor(beats / timebase.timeSignature.numerator);
|
||||||
const beat = (beats - bars * timebase.timeSignature.numerator);
|
const beat = (beats - bars * timebase.timeSignature.numerator);
|
||||||
return `${bars + 1}:${(beat + 1).toFixed(4) }`;
|
return `${bars + 1}:${(beat + 1).toFixed(4)}`;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@@ -320,22 +325,23 @@ function SliderWithPreview(props: SliderWithPreviewProps) {
|
|||||||
|
|
||||||
|
|
||||||
function TimeEdit(props: TimeEditProps) {
|
function TimeEdit(props: TimeEditProps) {
|
||||||
let { value, onValueChange, onBlur, timebase,sampleRate,max, ...extra } = props;
|
let { value, onValueChange, onBlur, timebase, sampleRate, max, ...extra } = props;
|
||||||
const [text, setText] = React.useState(formatTime(timebase, props.sampleRate, props.value));
|
const [text, setText] = React.useState(formatTime(timebase, props.sampleRate, props.value));
|
||||||
const [error, setError] = React.useState(false);
|
const [error, setError] = React.useState(false);
|
||||||
const [editValue, setEditValue] = React.useState(props.value);
|
|
||||||
const [focus, setFocus] = React.useState(false);
|
const [focus, setFocus] = React.useState(false);
|
||||||
// slice props.
|
// slice props.
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!focus ) {
|
if (!focus) {
|
||||||
setText(formatTime(timebase, sampleRate, props.value));
|
setText(formatTime(timebase, sampleRate, props.value));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[props.value]);
|
[props.value]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
setText(formatTime(props.timebase, sampleRate, props.value));
|
if (!focus) {
|
||||||
|
setText(formatTime(props.timebase, sampleRate, props.value));
|
||||||
|
}
|
||||||
}, [props.timebase, sampleRate]);
|
}, [props.timebase, sampleRate]);
|
||||||
|
|
||||||
|
|
||||||
@@ -348,11 +354,9 @@ function TimeEdit(props: TimeEditProps) {
|
|||||||
error={error}
|
error={error}
|
||||||
value={text}
|
value={text}
|
||||||
onBlur={() => {
|
onBlur={() => {
|
||||||
console.log("onBlur", text, props.value, editValue,
|
|
||||||
formatTime(props.timebase, sampleRate, props.value));
|
|
||||||
console.log("onBlur", props.value);
|
|
||||||
setText(formatTime(props.timebase, sampleRate, props.value));
|
setText(formatTime(props.timebase, sampleRate, props.value));
|
||||||
setFocus(false);
|
setFocus(false);
|
||||||
|
onBlur();
|
||||||
}}
|
}}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
try {
|
try {
|
||||||
@@ -362,7 +366,6 @@ function TimeEdit(props: TimeEditProps) {
|
|||||||
if (val > max) {
|
if (val > max) {
|
||||||
val = max;
|
val = max;
|
||||||
}
|
}
|
||||||
setEditValue(val);
|
|
||||||
if (props.onValueChange) {
|
if (props.onValueChange) {
|
||||||
props.onValueChange(e, val);
|
props.onValueChange(e, val);
|
||||||
}
|
}
|
||||||
@@ -389,34 +392,64 @@ function TimeEdit(props: TimeEditProps) {
|
|||||||
autoCapitalize: "off",
|
autoCapitalize: "off",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export default function LoopDialog(props: LoopDialogProps) {
|
export default function LoopDialog(props: LoopDialogProps) {
|
||||||
const [previewStartValue, setPreviewStartValue] = React.useState<number | null>(null);
|
const [start, setStart] = React.useState(props.value.start);
|
||||||
const [previewLoopStart, setPreviewLoopStart] = React.useState<number | null>(null);
|
const [loopEnable, setLoopEnable] = React.useState(props.value.loopEnable);
|
||||||
const [previewLoopEnd, setPreviewLoopEnd] = React.useState<number | null>(null);
|
const [loopStart, setLoopStart] = React.useState(props.value.loopStart);
|
||||||
|
const [loopEnd, setLoopEnd] = React.useState(props.value.loopEnd);
|
||||||
|
|
||||||
|
|
||||||
|
const height = useWindowHeight();
|
||||||
|
const fullScreen = height < 500;
|
||||||
|
|
||||||
|
function cancelPlaying() {
|
||||||
|
props.onCancelPlaying();
|
||||||
|
}
|
||||||
|
function commitResults() {
|
||||||
|
let tLoopStart = loopStart;
|
||||||
|
let tLoopEnd = loopEnd;
|
||||||
|
if (tLoopEnd < tLoopStart) {
|
||||||
|
let tmp = tLoopStart;
|
||||||
|
tLoopStart = tLoopEnd;
|
||||||
|
tLoopEnd = tmp;
|
||||||
|
setLoopStart(tLoopStart);
|
||||||
|
setLoopEnd(tLoopEnd);
|
||||||
|
}
|
||||||
|
if (props.onSetLoop) {
|
||||||
|
props.onSetLoop({ start: start, loopEnable: loopEnable, loopStart: tLoopStart, loopEnd: tLoopEnd });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
React.useEffect(() => {
|
||||||
|
setStart(props.value.start);
|
||||||
|
setLoopEnable(props.value.loopEnable);
|
||||||
|
setLoopStart(props.value.loopStart);
|
||||||
|
setLoopEnd(props.value.loopEnd);
|
||||||
|
}, [props.value]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DialogEx
|
<DialogEx
|
||||||
tag="LoopDialog"
|
tag="LoopDialog"
|
||||||
onClose={props.onClose}
|
onClose={() => {
|
||||||
fullScreen={false}
|
commitResults();
|
||||||
|
props.onClose();
|
||||||
|
}}
|
||||||
|
|
||||||
maxWidth="xl"
|
maxWidth="xl"
|
||||||
open={props.isOpen}
|
open={props.isOpen}
|
||||||
onEnterKey={() => {
|
onEnterKey={() => {
|
||||||
}}
|
}}
|
||||||
|
fullScreen={height < 500}
|
||||||
sx={{
|
sx={{
|
||||||
"& .MuiDialog-container": {
|
"& .MuiDialog-container": {
|
||||||
"& .MuiPaper-root": {
|
"& .MuiPaper-root": {
|
||||||
width: "100%",
|
width: "100%",
|
||||||
maxWidth: "500px", // Set your width here
|
maxWidth: fullScreen ? undefined : "500px", // Set your width here
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
@@ -428,7 +461,10 @@ export default function LoopDialog(props: LoopDialogProps) {
|
|||||||
color="inherit"
|
color="inherit"
|
||||||
aria-label="back"
|
aria-label="back"
|
||||||
style={{ opacity: 0.6 }}
|
style={{ opacity: 0.6 }}
|
||||||
onClick={() => { props.onClose(); }}
|
onClick={() => {
|
||||||
|
commitResults();
|
||||||
|
props.onClose();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<ArrowBackIcon style={{ width: 24, height: 24 }} />
|
<ArrowBackIcon style={{ width: 24, height: 24 }} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
@@ -449,8 +485,8 @@ export default function LoopDialog(props: LoopDialogProps) {
|
|||||||
<DialogContent style={{}}>
|
<DialogContent style={{}}>
|
||||||
<div style={{
|
<div style={{
|
||||||
position: "relative",
|
position: "relative",
|
||||||
display: "flex", justifyContent: "stretch", flexFlow: "column nowrap", marginLeft: 32, marginRight: 32,
|
display: "flex", justifyContent: "stretch", flexFlow: "column nowrap", marginLeft: 24, marginRight: 24,
|
||||||
marginBottom: 16
|
marginBottom: 8
|
||||||
|
|
||||||
}}>
|
}}>
|
||||||
<Typography display="block" variant="body1" gutterBottom style={{}}>
|
<Typography display="block" variant="body1" gutterBottom style={{}}>
|
||||||
@@ -460,53 +496,44 @@ export default function LoopDialog(props: LoopDialogProps) {
|
|||||||
style={{
|
style={{
|
||||||
width: "100%"
|
width: "100%"
|
||||||
}}
|
}}
|
||||||
value={props.start}
|
value={start}
|
||||||
min={0}
|
min={0}
|
||||||
max={props.duration}
|
max={props.duration}
|
||||||
|
|
||||||
onChange={(e, v, thumb) => {
|
onChange={(e, v, thumb) => {
|
||||||
let val = Array.isArray(v) ? v[0] : v
|
let val = Array.isArray(v) ? v[0] : v
|
||||||
|
setStart(val);
|
||||||
if (props.onSetLoop) {
|
|
||||||
props.onSetLoop(val, props.loopEnable, props.loopStart, props.loopEnd);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
onPreview={(e, v, thumb) => {
|
onPreview={(e, v, thumb) => {
|
||||||
let val = Array.isArray(v) ? v[0] : v
|
let val = Array.isArray(v) ? v[0] : v
|
||||||
|
|
||||||
setPreviewStartValue(val);
|
setStart(val);
|
||||||
}}
|
}}
|
||||||
onCommitPreviewValue={(e, v, thumb) => {
|
onCommitPreviewValue={(e, v, thumb) => {
|
||||||
let val = Array.isArray(v) ? v[0] : v
|
let val = Array.isArray(v) ? v[0] : v
|
||||||
|
|
||||||
setPreviewStartValue(null);
|
setStart(val);
|
||||||
if (props.onSetLoop) {
|
cancelPlaying();
|
||||||
props.onSetLoop(val, props.loopEnable, props.loopStart, props.loopEnd);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
valueLabelFormat={(v) => formatTime(props.timebase, props.sampleRate, v)}
|
valueLabelFormat={(v) => formatTime(props.timebase, props.sampleRate, v)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TimeEdit variant="standard" spellCheck="false"
|
<TimeEdit variant="standard" spellCheck="false"
|
||||||
value={previewStartValue ? previewStartValue : props.start}
|
value={start}
|
||||||
max={props.duration}
|
max={props.duration}
|
||||||
timebase={props.timebase}
|
timebase={props.timebase}
|
||||||
sampleRate={props.sampleRate}
|
sampleRate={props.sampleRate}
|
||||||
|
|
||||||
|
|
||||||
onBlur={() => {
|
onBlur={() => {
|
||||||
let t = props.start;
|
if (start > props.duration) {
|
||||||
if (t > props.duration) {
|
setStart(props.duration);
|
||||||
t = props.duration;
|
|
||||||
if (props.onSetLoop) {
|
|
||||||
props.onSetLoop(t, props.loopEnable, props.loopStart, props.loopEnd);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onValueChange={(e, val) => {
|
onValueChange={(e, val) => {
|
||||||
if (props.onSetLoop) {
|
setStart(val);
|
||||||
props.onSetLoop(val, props.loopEnable, props.loopStart, props.loopEnd);
|
cancelPlaying();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
style={{ width: 120, alignSelf: "center", textAlign: "center" }} />
|
style={{ width: 120, alignSelf: "center", textAlign: "center" }} />
|
||||||
@@ -514,9 +541,10 @@ export default function LoopDialog(props: LoopDialogProps) {
|
|||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
labelPlacement="start"
|
labelPlacement="start"
|
||||||
style={{ marginLeft: 0 }}
|
style={{ marginLeft: 0 }}
|
||||||
control={<Checkbox checked={props.loopEnable}
|
control={<Checkbox checked={loopEnable}
|
||||||
onChange={(e, checked) => {
|
onChange={(e, checked) => {
|
||||||
props.onSetLoop(props.start, !props.loopEnable, props.loopStart, props.loopEnd);
|
setLoopEnable(checked);
|
||||||
|
cancelPlaying();
|
||||||
}}
|
}}
|
||||||
|
|
||||||
/>} label="Enable loop" />
|
/>} label="Enable loop" />
|
||||||
@@ -525,102 +553,92 @@ export default function LoopDialog(props: LoopDialogProps) {
|
|||||||
<SliderWithPreview
|
<SliderWithPreview
|
||||||
style={{
|
style={{
|
||||||
width: "100%",
|
width: "100%",
|
||||||
opacity: props.loopEnable ? 1 : 0.4,
|
opacity: loopEnable ? 1 : 0.4,
|
||||||
}}
|
}}
|
||||||
disabled={!props.loopEnable}
|
disabled={!loopEnable}
|
||||||
value={
|
value={
|
||||||
previewLoopStart != null ?
|
[loopStart, loopEnd]
|
||||||
[previewLoopStart as number, previewLoopEnd as number]
|
}
|
||||||
: [props.loopStart, props.loopEnd]}
|
|
||||||
min={0}
|
min={0}
|
||||||
max={props.duration}
|
max={props.duration}
|
||||||
onChange={(e, v, thumb) => {
|
onChange={(e, v, thumb) => {
|
||||||
if (!Array.isArray(v)) throw new Error("Expected array for loop start and end preview");
|
if (!Array.isArray(v)) throw new Error("Expected array for loop start and end");
|
||||||
if (props.onSetLoop) {
|
|
||||||
props.onSetLoop(props.start, props.loopEnable, v[0], v[1]);
|
setLoopStart(v[0]);
|
||||||
}
|
setLoopEnd(v[1]);
|
||||||
}}
|
}}
|
||||||
onPreview={(e, v, thumb) => {
|
onPreview={(e, v, thumb) => {
|
||||||
if (!Array.isArray(v)) throw new Error("Expected array for loop start and end preview");
|
if (!Array.isArray(v)) throw new Error("Expected array for loop start and end");
|
||||||
setPreviewLoopStart(v[0]);
|
|
||||||
setPreviewLoopEnd(v[1]);
|
setLoopStart(v[0]);
|
||||||
|
setLoopEnd(v[1]);
|
||||||
}}
|
}}
|
||||||
onCommitPreviewValue={(e, v, thumb) => {
|
onCommitPreviewValue={(e, v, thumb) => {
|
||||||
if (!Array.isArray(v)) throw new Error("Expected array for loop start and end preview");
|
if (!Array.isArray(v)) throw new Error("Expected array for loop start and end");
|
||||||
setPreviewLoopStart(null);
|
|
||||||
setPreviewLoopEnd(null);
|
setLoopStart(v[0]);
|
||||||
props.onSetLoop(props.start, props.loopEnable, v[0], v[1]);
|
setLoopEnd(v[1]);
|
||||||
|
cancelPlaying();
|
||||||
|
|
||||||
}}
|
}}
|
||||||
|
|
||||||
/>
|
/>
|
||||||
<div style={{ display: "flex", justifyContent: "space-evenly", columnGap: 8 }}>
|
<div style={{ display: "flex", justifyContent: "space-evenly", columnGap: 8 }}>
|
||||||
<TimeEdit variant="standard" spellCheck="false"
|
<TimeEdit variant="standard" spellCheck="false"
|
||||||
value={previewLoopStart ? previewLoopStart : props.loopStart}
|
value={loopStart}
|
||||||
disabled={!props.loopEnable}
|
disabled={!loopEnable}
|
||||||
max={props.duration}
|
max={props.duration}
|
||||||
timebase={props.timebase}
|
timebase={props.timebase}
|
||||||
sampleRate={props.sampleRate}
|
sampleRate={props.sampleRate}
|
||||||
|
|
||||||
style={{
|
style={{
|
||||||
maxWidth: 120, textAlign: "center", opacity: props.loopEnable ? 1 : 0.4,
|
maxWidth: 120, textAlign: "center", opacity: loopEnable ? 1 : 0.4,
|
||||||
flex: "1 1 auto"
|
flex: "1 1 auto"
|
||||||
}}
|
}}
|
||||||
onBlur={() => {
|
onBlur={() => {
|
||||||
let tStart = props.loopStart;
|
|
||||||
if (tStart > props.duration) {
|
|
||||||
tStart = props.duration;
|
|
||||||
}
|
|
||||||
let tEnd = props.loopEnd;
|
|
||||||
if (tStart > tEnd) {
|
|
||||||
let t = tStart;
|
|
||||||
tStart = tEnd;
|
|
||||||
tEnd = t;
|
|
||||||
if (props.onSetLoop) {
|
|
||||||
props.onSetLoop(props.start, props.loopEnable, tStart, tEnd);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
onValueChange={(e, val) => {
|
onValueChange={(e, val) => {
|
||||||
if (props.onSetLoop) {
|
setLoopStart(val);
|
||||||
props.onSetLoop(props.start, props.loopEnable, val, props.loopEnd);
|
cancelPlaying();
|
||||||
}
|
|
||||||
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<TimeEdit variant="standard" spellCheck="false"
|
<TimeEdit variant="standard" spellCheck="false"
|
||||||
max={props.duration}
|
max={props.duration}
|
||||||
|
|
||||||
value={previewLoopEnd ? previewLoopEnd : props.loopEnd}
|
value={loopEnd}
|
||||||
sampleRate={props.sampleRate}
|
sampleRate={props.sampleRate}
|
||||||
|
|
||||||
disabled={!props.loopEnable}
|
disabled={!loopEnable}
|
||||||
|
|
||||||
timebase={props.timebase}
|
timebase={props.timebase}
|
||||||
|
|
||||||
style={{
|
style={{
|
||||||
maxWidth: 120, textAlign: "center", opacity: props.loopEnable ? 1 : 0.4,
|
maxWidth: 120, textAlign: "center",
|
||||||
|
opacity: loopEnable ? 1 : 0.4,
|
||||||
flex: "1 1 auto"
|
flex: "1 1 auto"
|
||||||
}}
|
}}
|
||||||
|
|
||||||
onBlur={() => {
|
onBlur={() => {
|
||||||
let tStart = props.loopStart;
|
|
||||||
let tEnd = props.loopEnd;;
|
|
||||||
if (tEnd < tStart) {
|
|
||||||
let t = tEnd;
|
|
||||||
tEnd = tStart;
|
|
||||||
tStart = t;
|
|
||||||
if (props.onSetLoop) {
|
|
||||||
props.onSetLoop(props.start, props.loopEnable, tStart, tEnd);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
onValueChange={(e, val) => {
|
onValueChange={(e, val) => {
|
||||||
if (props.onSetLoop) {
|
setLoopEnd(val);
|
||||||
props.onSetLoop(props.start, props.loopEnable, props.loopStart, val);
|
cancelPlaying();
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<Button variant="dialogSecondary" style={{ alignSelf: "end", marginTop: 16, width: 120 }}
|
||||||
|
startIcon={
|
||||||
|
props.playing ? (<StopIcon />) : (<PlayArrow />)
|
||||||
|
}
|
||||||
|
onClick={() => {
|
||||||
|
commitResults();
|
||||||
|
props.onPreview();
|
||||||
|
}}>
|
||||||
|
Preview
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
@@ -21,12 +21,14 @@ import { SyntheticEvent } from 'react';
|
|||||||
import { Theme } from '@mui/material/styles';
|
import { Theme } from '@mui/material/styles';
|
||||||
import WithStyles, {withTheme} from './WithStyles';
|
import WithStyles, {withTheme} from './WithStyles';
|
||||||
import { withStyles } from "tss-react/mui";
|
import { withStyles } from "tss-react/mui";
|
||||||
|
import IconButtonEx from './IconButtonEx';
|
||||||
|
import ButtonEx from './ButtonEx';
|
||||||
|
import Tooltip from '@mui/material/Tooltip';
|
||||||
|
|
||||||
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
|
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
|
||||||
import {
|
import {
|
||||||
Pedalboard, PedalboardItem, PedalboardSplitItem, SplitType
|
Pedalboard, PedalboardItem, PedalboardSplitItem, SplitType
|
||||||
} from './Pedalboard';
|
} from './Pedalboard';
|
||||||
import Button from '@mui/material/Button';
|
|
||||||
import InputIcon from '@mui/icons-material/Input';
|
import InputIcon from '@mui/icons-material/Input';
|
||||||
import LoadPluginDialog from './LoadPluginDialog';
|
import LoadPluginDialog from './LoadPluginDialog';
|
||||||
import Switch from '@mui/material/Switch';
|
import Switch from '@mui/material/Switch';
|
||||||
@@ -34,7 +36,6 @@ import Typography from '@mui/material/Typography';
|
|||||||
|
|
||||||
import PedalboardView from './PedalboardView';
|
import PedalboardView from './PedalboardView';
|
||||||
import { PiPedalStateError } from './PiPedalError';
|
import { PiPedalStateError } from './PiPedalError';
|
||||||
import IconButton from '@mui/material/IconButton';
|
|
||||||
import AddIcon from '@mui/icons-material/Add';
|
import AddIcon from '@mui/icons-material/Add';
|
||||||
import Menu from '@mui/material/Menu';
|
import Menu from '@mui/material/Menu';
|
||||||
import MenuItem from '@mui/material/MenuItem';
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
@@ -529,7 +530,9 @@ export const MainPage =
|
|||||||
}} >
|
}} >
|
||||||
<div style={{ flex: "0 0 auto", width: this.state.splitControlBar? undefined: 80 }} >
|
<div style={{ flex: "0 0 auto", width: this.state.splitControlBar? undefined: 80 }} >
|
||||||
<div style={{ display: bypassVisible ? "block" : "none", width: this.state.splitControlBar? undefined: 80 }} >
|
<div style={{ display: bypassVisible ? "block" : "none", width: this.state.splitControlBar? undefined: 80 }} >
|
||||||
<Switch color="secondary" checked={bypassChecked} onChange={this.handleEnableCurrentItemChanged} />
|
<Tooltip title="Bypass" placement="top-start" arrow enterDelay={1500} enterNextDelay={1500}>
|
||||||
|
<Switch color="secondary" checked={bypassChecked} onChange={this.handleEnableCurrentItemChanged} />
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{
|
{
|
||||||
@@ -541,9 +544,9 @@ export const MainPage =
|
|||||||
{this.props.enableStructureEditing && (
|
{this.props.enableStructureEditing && (
|
||||||
<div style={{ flex: "0 0 auto", display: "flex", flexFlow: "row nowrap",alignItems: "center" }}>
|
<div style={{ flex: "0 0 auto", display: "flex", flexFlow: "row nowrap",alignItems: "center" }}>
|
||||||
<div style={{ flex: "0 0 auto", display: (canInsert || canAppend) ? "block" : "none", paddingRight: 8 }}>
|
<div style={{ flex: "0 0 auto", display: (canInsert || canAppend) ? "block" : "none", paddingRight: 8 }}>
|
||||||
<IconButton onClick={(e) => { this.onAddClick(e) }} size="large">
|
<IconButtonEx tooltip="Add pedal slot" onClick={(e) => { this.onAddClick(e) }} size="large">
|
||||||
<AddIcon style={{ height: 24, width: 24, fill: this.props.theme.palette.text.primary, opacity: 0.6 }} />
|
<AddIcon style={{ height: 24, width: 24, fill: this.props.theme.palette.text.primary, opacity: 0.6 }} />
|
||||||
</IconButton>
|
</IconButtonEx>
|
||||||
<Menu
|
<Menu
|
||||||
id="add-menu"
|
id="add-menu"
|
||||||
anchorEl={this.state.addMenuAnchorEl}
|
anchorEl={this.state.addMenuAnchorEl}
|
||||||
@@ -560,17 +563,18 @@ export const MainPage =
|
|||||||
</Menu>
|
</Menu>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ flex: "0 0 auto", display: canDelete ? "block" : "none", paddingRight: 8 }}>
|
<div style={{ flex: "0 0 auto", display: canDelete ? "block" : "none", paddingRight: 8 }}>
|
||||||
<IconButton
|
<IconButtonEx tooltip="Delete pedal"
|
||||||
onClick={() => { this.onDeletePedal(pedalboardItem?.instanceId ?? -1) }}
|
onClick={() => { this.onDeletePedal(pedalboardItem?.instanceId ?? -1) }}
|
||||||
size="large">
|
size="large">
|
||||||
<OldDeleteIcon style={{ height: 24, width: 24, fill: this.props.theme.palette.text.primary, opacity: 0.6 }} />
|
<OldDeleteIcon style={{ height: 24, width: 24, fill: this.props.theme.palette.text.primary, opacity: 0.6 }} />
|
||||||
</IconButton>
|
</IconButtonEx>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ flex: "0 0 auto" }}>
|
<div style={{ flex: "0 0 auto" }}>
|
||||||
<Button
|
<ButtonEx
|
||||||
variant="contained"
|
variant="contained"
|
||||||
color="primary"
|
color="primary"
|
||||||
size="small"
|
size="small"
|
||||||
|
tooltip="Load plugin"
|
||||||
onClick={this.onLoadClick}
|
onClick={this.onLoadClick}
|
||||||
disabled={this.state.selectedPedal === -1 || (!canLoad) || (this.getSelectedPedalboardItem()?.isSplit() ?? true)}
|
disabled={this.state.selectedPedal === -1 || (!canLoad) || (this.getSelectedPedalboardItem()?.isSplit() ?? true)}
|
||||||
startIcon={<InputIcon />}
|
startIcon={<InputIcon />}
|
||||||
@@ -580,21 +584,21 @@ export const MainPage =
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Load
|
Load
|
||||||
</Button>
|
</ButtonEx>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ flex: "0 0 auto" }}>
|
<div style={{ flex: "0 0 auto" }}>
|
||||||
<IconButton
|
<IconButtonEx tooltip="MIDI bindings"
|
||||||
onClick={(e) => { this.handleMidiConfiguration(instanceId); }}
|
onClick={(e) => { this.handleMidiConfiguration(instanceId); }}
|
||||||
size="large">
|
size="large">
|
||||||
<MidiIcon style={{ height: 24, width: 24, fill: this.props.theme.palette.text.primary, opacity: 0.6 }} />
|
<MidiIcon style={{ height: 24, width: 24, fill: this.props.theme.palette.text.primary, opacity: 0.6 }} />
|
||||||
</IconButton>
|
</IconButtonEx>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ flex: "0 0 auto" }}>
|
<div style={{ flex: "0 0 auto" }}>
|
||||||
<IconButton
|
<IconButtonEx tooltip="Snapshots"
|
||||||
onClick={(e) => { this.setState({ snapshotDialogOpen: true }); }}
|
onClick={(e) => { this.setState({ snapshotDialogOpen: true }); }}
|
||||||
size="large">
|
size="large">
|
||||||
{this.snapshotIcon(this.props.theme, this.state.selectedSnapshot)}
|
{this.snapshotIcon(this.props.theme, this.state.selectedSnapshot)}
|
||||||
</IconButton>
|
</IconButtonEx>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -2697,6 +2697,36 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
copyFilePropertyFile(
|
||||||
|
oldRelativePath: string,
|
||||||
|
newRelativePath: string,
|
||||||
|
uiFileProperty: UiFileProperty,
|
||||||
|
overwrite: boolean
|
||||||
|
): Promise<string> {
|
||||||
|
return new Promise<string>((resolve, reject) => {
|
||||||
|
|
||||||
|
let ws = this.webSocket;
|
||||||
|
if (!ws) {
|
||||||
|
resolve("");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ws.request<string>(
|
||||||
|
"copyFilePropertyFile",
|
||||||
|
{
|
||||||
|
oldRelativePath: oldRelativePath,
|
||||||
|
newRelativePath: newRelativePath,
|
||||||
|
uiFileProperty: uiFileProperty,
|
||||||
|
overwrite: overwrite
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then((newPath) => {
|
||||||
|
resolve(newPath);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
setWifiConfigSettings(wifiConfigSettings: WifiConfigSettings): Promise<void> {
|
setWifiConfigSettings(wifiConfigSettings: WifiConfigSettings): Promise<void> {
|
||||||
let result = new Promise<void>((resolve, reject) => {
|
let result = new Promise<void>((resolve, reject) => {
|
||||||
|
|||||||
+138
-124
@@ -23,6 +23,7 @@ import Button from '@mui/material/Button';
|
|||||||
import { Theme } from '@mui/material/styles';
|
import { Theme } from '@mui/material/styles';
|
||||||
import WithStyles, { withTheme } from './WithStyles';
|
import WithStyles, { withTheme } from './WithStyles';
|
||||||
import { createStyles } from './WithStyles';
|
import { createStyles } from './WithStyles';
|
||||||
|
import ControlTooltip from './ControlTooltip';
|
||||||
|
|
||||||
import { withStyles } from "tss-react/mui";
|
import { withStyles } from "tss-react/mui";
|
||||||
import { UiControl, ScalePoint } from './Lv2Plugin';
|
import { UiControl, ScalePoint } from './Lv2Plugin';
|
||||||
@@ -35,7 +36,7 @@ import MenuItem from '@mui/material/MenuItem';
|
|||||||
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
|
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
|
||||||
import DialIcon from './svg/fx_dial.svg?react';
|
import DialIcon from './svg/fx_dial.svg?react';
|
||||||
import { isDarkMode } from './DarkMode';
|
import { isDarkMode } from './DarkMode';
|
||||||
import ControlTooltip from './ControlTooltip';
|
|
||||||
|
|
||||||
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
|
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
|
||||||
import StopIcon from '@mui/icons-material/Stop';
|
import StopIcon from '@mui/icons-material/Stop';
|
||||||
@@ -699,45 +700,51 @@ const PluginControl =
|
|||||||
if (control.isOnOffSwitch()) {
|
if (control.isOnOffSwitch()) {
|
||||||
// normal gray unchecked state.
|
// normal gray unchecked state.
|
||||||
return (
|
return (
|
||||||
<Switch checked={value !== 0} color="primary"
|
<ControlTooltip uiControl={control}>
|
||||||
onChange={(event) => {
|
<Switch checked={value !== 0} color="primary"
|
||||||
this.onCheckChanged(event.target.checked);
|
onChange={(event) => {
|
||||||
}}
|
this.onCheckChanged(event.target.checked);
|
||||||
/>
|
}}
|
||||||
|
/>
|
||||||
|
</ControlTooltip>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (control.isAbToggle()) {
|
if (control.isAbToggle()) {
|
||||||
let classes = withStyles.getClasses(this.props);
|
let classes = withStyles.getClasses(this.props);
|
||||||
// unchecked color is not gray.
|
// unchecked color is not gray.
|
||||||
return (
|
return (
|
||||||
<Switch checked={value !== 0} color="primary"
|
<ControlTooltip uiControl={control}>
|
||||||
onChange={(event) => {
|
<Switch checked={value !== 0} color="primary"
|
||||||
this.onCheckChanged(event.target.checked);
|
onChange={(event) => {
|
||||||
}}
|
this.onCheckChanged(event.target.checked);
|
||||||
classes={{
|
}}
|
||||||
track: classes.switchTrack
|
classes={{
|
||||||
}}
|
track: classes.switchTrack
|
||||||
style={{ color: this.props.theme.palette.primary.main }}
|
}}
|
||||||
/>
|
style={{ color: this.props.theme.palette.primary.main }}
|
||||||
|
/>
|
||||||
|
</ControlTooltip>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<Select variant="standard"
|
<ControlTooltip uiControl={control}>
|
||||||
ref={this.selectRef}
|
<Select variant="standard"
|
||||||
value={control.clampSelectValue(value)}
|
ref={this.selectRef}
|
||||||
onChange={this.onSelectChanged}
|
value={control.clampSelectValue(value)}
|
||||||
inputProps={{
|
onChange={this.onSelectChanged}
|
||||||
name: control.name,
|
inputProps={{
|
||||||
id: 'id' + control.symbol,
|
name: control.name,
|
||||||
style: { fontSize: FONT_SIZE }
|
id: 'id' + control.symbol,
|
||||||
}}
|
style: { fontSize: FONT_SIZE }
|
||||||
style={{ marginLeft: 4, marginRight: 4, width: 140, fontSize: 14 }}
|
}}
|
||||||
>
|
style={{ marginLeft: 4, marginRight: 4, width: 140, fontSize: 14 }}
|
||||||
{control.scale_points.map((scale_point: ScalePoint) => (
|
>
|
||||||
<MenuItem key={scale_point.value} value={scale_point.value}>{scale_point.label}</MenuItem>
|
{control.scale_points.map((scale_point: ScalePoint) => (
|
||||||
|
<MenuItem key={scale_point.value} value={scale_point.value}>{scale_point.label}</MenuItem>
|
||||||
|
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
|
</ControlTooltip>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -919,12 +926,10 @@ const PluginControl =
|
|||||||
alignSelf: "stretch", marginBottom: 8, marginLeft: isSelect ? 8 : 0, marginRight: 0
|
alignSelf: "stretch", marginBottom: 8, marginLeft: isSelect ? 8 : 0, marginRight: 0
|
||||||
|
|
||||||
}}>
|
}}>
|
||||||
<ControlTooltip uiControl={control} >
|
<Typography variant="caption" display="block" noWrap style={{
|
||||||
<Typography variant="caption" display="block" noWrap style={{
|
width: "100%",
|
||||||
width: "100%",
|
textAlign: isSelect ? "left" : "center"
|
||||||
textAlign: isSelect ? "left" : "center"
|
}}> {isButton ? "\u00A0" : control.name}</Typography>
|
||||||
}}> {isButton ? "\u00A0" : control.name}</Typography>
|
|
||||||
</ControlTooltip>
|
|
||||||
</div>
|
</div>
|
||||||
{/* CONTROL SECTION */}
|
{/* CONTROL SECTION */}
|
||||||
|
|
||||||
@@ -933,81 +938,86 @@ const PluginControl =
|
|||||||
{isButton ?
|
{isButton ?
|
||||||
(
|
(
|
||||||
control.name.length !== 1 ? (
|
control.name.length !== 1 ? (
|
||||||
<Button variant="contained" color="primary" size="small"
|
<ControlTooltip uiControl={control}>
|
||||||
onMouseDown={
|
|
||||||
(evt) => { this.handleButtonMouseDown(buttonStyle); }
|
<Button variant="contained" color="primary" size="small"
|
||||||
}
|
onMouseDown={
|
||||||
onMouseUp={
|
(evt) => { this.handleButtonMouseDown(buttonStyle); }
|
||||||
(evt) => { this.handleButtonMouseUp(buttonStyle); }
|
|
||||||
}
|
|
||||||
onTouchStart={
|
|
||||||
(evt) => {
|
|
||||||
evt.preventDefault();
|
|
||||||
this.handleButtonMouseDown(buttonStyle);
|
|
||||||
}
|
}
|
||||||
}
|
onMouseUp={
|
||||||
onTouchEnd={
|
(evt) => { this.handleButtonMouseUp(buttonStyle); }
|
||||||
(evt) => {
|
|
||||||
evt.preventDefault();
|
|
||||||
this.handleButtonMouseUp(buttonStyle);
|
|
||||||
}
|
}
|
||||||
}
|
onTouchStart={
|
||||||
onMouseLeave={(
|
(evt) => {
|
||||||
(evet) => { this.handleButtonMouseLeave(buttonStyle); }
|
evt.preventDefault();
|
||||||
)}
|
this.handleButtonMouseDown(buttonStyle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onTouchEnd={
|
||||||
|
(evt) => {
|
||||||
|
evt.preventDefault();
|
||||||
|
this.handleButtonMouseUp(buttonStyle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onMouseLeave={(
|
||||||
|
(evet) => { this.handleButtonMouseLeave(buttonStyle); }
|
||||||
|
)}
|
||||||
|
|
||||||
style={{
|
style={{
|
||||||
textTransform: "none",
|
textTransform: "none",
|
||||||
background: (isDarkMode() ? "#6750A4" : undefined),
|
background: (isDarkMode() ? "#6750A4" : undefined),
|
||||||
marginLeft: 8, marginRight: 8, minWidth: 60,
|
marginLeft: 8, marginRight: 8, minWidth: 60,
|
||||||
marginTop: 0
|
marginTop: 0
|
||||||
|
|
||||||
}}
|
}}
|
||||||
|
|
||||||
>
|
>
|
||||||
{control.name}
|
{control.name}
|
||||||
</Button>
|
</Button>
|
||||||
|
</ControlTooltip>
|
||||||
|
|
||||||
) : (
|
) : (
|
||||||
<Button variant="contained" color="primary" size="small"
|
<ControlTooltip uiControl={control}>
|
||||||
onMouseDown={
|
<Button variant="contained" color="primary" size="small"
|
||||||
(evt) => { this.handleButtonMouseDown(buttonStyle); }
|
onMouseDown={
|
||||||
}
|
(evt) => { this.handleButtonMouseDown(buttonStyle); }
|
||||||
onMouseUp={
|
|
||||||
(evt) => { this.handleButtonMouseUp(buttonStyle); }
|
|
||||||
}
|
|
||||||
onTouchStart={
|
|
||||||
(evt) => {
|
|
||||||
evt.preventDefault();
|
|
||||||
this.handleButtonMouseDown(buttonStyle);
|
|
||||||
}
|
}
|
||||||
}
|
onMouseUp={
|
||||||
onTouchEnd={
|
(evt) => { this.handleButtonMouseUp(buttonStyle); }
|
||||||
(evt) => {
|
|
||||||
evt.preventDefault();
|
|
||||||
this.handleButtonMouseUp(buttonStyle);
|
|
||||||
}
|
}
|
||||||
}
|
onTouchStart={
|
||||||
onMouseLeave={(
|
(evt) => {
|
||||||
(evet) => { this.handleButtonMouseLeave(buttonStyle); }
|
evt.preventDefault();
|
||||||
)}
|
this.handleButtonMouseDown(buttonStyle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onTouchEnd={
|
||||||
|
(evt) => {
|
||||||
|
evt.preventDefault();
|
||||||
|
this.handleButtonMouseUp(buttonStyle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onMouseLeave={(
|
||||||
|
(evet) => { this.handleButtonMouseLeave(buttonStyle); }
|
||||||
|
)}
|
||||||
|
|
||||||
style={{
|
style={{
|
||||||
textTransform: "none",
|
textTransform: "none",
|
||||||
background: (isDarkMode() ? "#6750A4" : undefined),
|
background: (isDarkMode() ? "#6750A4" : undefined),
|
||||||
marginLeft: 8, marginRight: 8,
|
marginLeft: 8, marginRight: 8,
|
||||||
paddingLeft: 0, paddingRight: 0,
|
paddingLeft: 0, paddingRight: 0,
|
||||||
width: 36, height: 36,
|
width: 36, height: 36,
|
||||||
marginTop: 0,
|
marginTop: 0,
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
fontSize: "1.2em"
|
fontSize: "1.2em"
|
||||||
|
|
||||||
}}
|
}}
|
||||||
|
|
||||||
>
|
>
|
||||||
{androidEmoji(control.name)}
|
{androidEmoji(control.name)}
|
||||||
</Button>
|
</Button>
|
||||||
|
</ControlTooltip>
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1016,34 +1026,38 @@ const PluginControl =
|
|||||||
)
|
)
|
||||||
: (isGraphicEq) ? (
|
: (isGraphicEq) ? (
|
||||||
<div style={{ flex: "0 1 auto" }}>
|
<div style={{ flex: "0 1 auto" }}>
|
||||||
<GraphicEqCtl
|
<ControlTooltip uiControl={control}>
|
||||||
imgRef={this.imgRef}
|
<GraphicEqCtl
|
||||||
position={this.getEqPosition()}
|
imgRef={this.imgRef}
|
||||||
dialColor={dialColor}
|
position={this.getEqPosition()}
|
||||||
opacity={DEFAULT_OPACITY}
|
dialColor={dialColor}
|
||||||
|
opacity={DEFAULT_OPACITY}
|
||||||
|
|
||||||
onTouchStart={this.onTouchStart}
|
onTouchStart={this.onTouchStart}
|
||||||
onTouchMove={this.onTouchMove}
|
onTouchMove={this.onTouchMove}
|
||||||
onPointerDown={this.onPointerDown} onPointerUp={this.onPointerUp}
|
onPointerDown={this.onPointerDown} onPointerUp={this.onPointerUp}
|
||||||
onPointerMoveCapture={this.onPointerMove}
|
onPointerMoveCapture={this.onPointerMove}
|
||||||
onDrag={this.onDrag}
|
onDrag={this.onDrag}
|
||||||
|
|
||||||
|
|
||||||
/>
|
/>
|
||||||
|
</ControlTooltip>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div style={{ flex: "0 1 auto" }}>
|
<div style={{ flex: "0 1 auto" }}>
|
||||||
<DialIcon ref={this.imgRef}
|
<ControlTooltip uiControl={control}>
|
||||||
style={{
|
<DialIcon ref={this.imgRef}
|
||||||
overscrollBehavior: "none", touchAction: "none", fill: dialColor,
|
style={{
|
||||||
width: 36, height: 36, opacity: DEFAULT_OPACITY, transform: this.getRotationTransform()
|
overscrollBehavior: "none", touchAction: "none", fill: dialColor,
|
||||||
}}
|
width: 36, height: 36, opacity: DEFAULT_OPACITY, transform: this.getRotationTransform()
|
||||||
onTouchStart={this.onTouchStart} onTouchMove={this.onTouchMove}
|
}}
|
||||||
onPointerDown={this.onPointerDown} onPointerUp={this.onPointerUp}
|
onTouchStart={this.onTouchStart} onTouchMove={this.onTouchMove}
|
||||||
onPointerMoveCapture={this.onPointerMove}
|
onPointerDown={this.onPointerDown} onPointerUp={this.onPointerUp}
|
||||||
onDrag={this.onDrag}
|
onPointerMoveCapture={this.onPointerMove}
|
||||||
|
onDrag={this.onDrag}
|
||||||
|
|
||||||
/>
|
/>
|
||||||
|
</ControlTooltip>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1079,11 +1093,11 @@ const PluginControl =
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
|
|
||||||
inputRef={this.inputRef} onChange={this.onInputChange}
|
inputRef={this.inputRef} onChange={this.onInputChange}
|
||||||
onBlur={this.onInputLostFocus}
|
onBlur={this.onInputLostFocus}
|
||||||
onFocus={this.onInputFocus}
|
onFocus={this.onInputFocus}
|
||||||
|
|
||||||
onKeyPress={this.onInputKeyPress} />
|
onKeyPress={this.onInputKeyPress} />
|
||||||
<div className={classes.displayValue}
|
<div className={classes.displayValue}
|
||||||
ref={this.displayValueRef} onClick={(e) => { this.inputRef.current!.focus(); }}
|
ref={this.displayValueRef} onClick={(e) => { this.inputRef.current!.focus(); }}
|
||||||
style={{ display: this.state.editFocused ? "none" : "block" }}
|
style={{ display: this.state.editFocused ? "none" : "block" }}
|
||||||
|
|||||||
@@ -472,7 +472,6 @@ 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={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) }}
|
||||||
@@ -480,6 +479,14 @@ const PluginControlView =
|
|||||||
|
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
|
// return ((
|
||||||
|
// <PluginControl key={uiControl.symbol} instanceId={this.props.instanceId} uiControl={uiControl} value={controlValue.value}
|
||||||
|
// onChange={(value: number) => { this.onControlValueChanged(controlValue!.key, value) }}
|
||||||
|
// onPreviewChange={(value: number) => { this.onPreviewChange(controlValue!.key, value) }}
|
||||||
|
// requestIMEEdit={(uiControl: any, value: any) => this.requestImeEdit(uiControl, value)}
|
||||||
|
|
||||||
|
// />
|
||||||
|
// ));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +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 from 'react';
|
import React from 'react';
|
||||||
|
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||||
import { Theme } from '@mui/material/styles';
|
import { Theme } from '@mui/material/styles';
|
||||||
import WithStyles from './WithStyles';
|
import WithStyles from './WithStyles';
|
||||||
import { createStyles } from './WithStyles';
|
import { createStyles } from './WithStyles';
|
||||||
@@ -27,8 +28,7 @@ import DialogEx from './DialogEx';
|
|||||||
import MuiDialogTitle from '@mui/material/DialogTitle';
|
import MuiDialogTitle from '@mui/material/DialogTitle';
|
||||||
import MuiDialogContent from '@mui/material/DialogContent';
|
import MuiDialogContent from '@mui/material/DialogContent';
|
||||||
import MuiDialogActions from '@mui/material/DialogActions';
|
import MuiDialogActions from '@mui/material/DialogActions';
|
||||||
import IconButton from '@mui/material/IconButton';
|
import IconButtonEx from './IconButtonEx';
|
||||||
import CloseIcon from '@mui/icons-material/Close';
|
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import Grid from '@mui/material/Grid';
|
import Grid from '@mui/material/Grid';
|
||||||
import { PiPedalModelFactory } from "./PiPedalModel";
|
import { PiPedalModelFactory } from "./PiPedalModel";
|
||||||
@@ -255,32 +255,36 @@ const PluginInfoDialog = withStyles((props: PluginInfoProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<IconButton
|
<IconButtonEx tooltip="Plugin info"
|
||||||
style={{ display: (props.plugin_uri !== "") ? "inline-flex" : "none" }}
|
style={{ display: (props.plugin_uri !== "") ? "inline-flex" : "none" }}
|
||||||
onClick={handleClickOpen}
|
onClick={handleClickOpen}
|
||||||
size="large">
|
size="large">
|
||||||
<InfoOutlinedIcon className={classes.icon} color='inherit' />
|
<InfoOutlinedIcon className={classes.icon} color='inherit' />
|
||||||
</IconButton>
|
</IconButtonEx>
|
||||||
{open && (
|
{open && (
|
||||||
<DialogEx tag="info" onClose={handleClose} open={open} fullWidth maxWidth="md"
|
<DialogEx tag="info" onClose={handleClose} open={open} fullWidth maxWidth="md"
|
||||||
onEnterKey={handleClose}
|
onEnterKey={handleClose}
|
||||||
>
|
>
|
||||||
<MuiDialogTitle >
|
<MuiDialogTitle >
|
||||||
<div style={{ display: "flex", flexDirection: "row", alignItems: "start", flexWrap: "nowrap" }}>
|
<div style={{ display: "flex", flexDirection: "row", alignItems: "center", flexWrap: "nowrap" }}>
|
||||||
<div style={{ flex: "0 0 auto", marginRight: 16 }}>
|
|
||||||
<PluginIcon pluginType={plugin.plugin_type} offsetY={3} />
|
<IconButtonEx
|
||||||
|
edge="start"
|
||||||
|
color="inherit"
|
||||||
|
tooltip="Back"
|
||||||
|
aria-label="back"
|
||||||
|
style={{ opacity: 0.6 }}
|
||||||
|
onClick={() => { handleClose()}}
|
||||||
|
>
|
||||||
|
<ArrowBackIcon style={{ width: 24, height: 24 }} />
|
||||||
|
</IconButtonEx>
|
||||||
|
|
||||||
|
<div style={{ flex: "0 0 auto", marginLeft: 16,marginRight: 8, position: "relative", top: 3 }}>
|
||||||
|
<PluginIcon pluginType={plugin.plugin_type} />
|
||||||
</div>
|
</div>
|
||||||
<div style={{ flex: "1 1 auto" }}>
|
<div style={{ flex: "1 1 auto" }}>
|
||||||
<Typography variant="h6">{plugin.name}</Typography>
|
<Typography variant="h6">{plugin.name}</Typography>
|
||||||
</div>
|
</div>
|
||||||
<IconButton
|
|
||||||
aria-label="close"
|
|
||||||
className={classes.closeButton}
|
|
||||||
onClick={() => handleClose()}
|
|
||||||
style={{ flex: "0 0 auto" }}
|
|
||||||
size="large">
|
|
||||||
<CloseIcon />
|
|
||||||
</IconButton>
|
|
||||||
</div>
|
</div>
|
||||||
</MuiDialogTitle>
|
</MuiDialogTitle>
|
||||||
<PluginInfoDialogContent dividers style={{ width: "100%", maxHeight: "80%", overflowX: "hidden" }}>
|
<PluginInfoDialogContent dividers style={{ width: "100%", maxHeight: "80%", overflowX: "hidden" }}>
|
||||||
|
|||||||
@@ -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 { SyntheticEvent, Component } from 'react';
|
import { SyntheticEvent, Component } from 'react';
|
||||||
import IconButton from '@mui/material/IconButton';
|
import IconButtonEx from './IconButtonEx';
|
||||||
import { PiPedalModel, PiPedalModelFactory, PluginPresetsChangedHandle } from './PiPedalModel';
|
import { PiPedalModel, PiPedalModelFactory, PluginPresetsChangedHandle } from './PiPedalModel';
|
||||||
import { Theme } from '@mui/material/styles';
|
import { Theme } from '@mui/material/styles';
|
||||||
import WithStyles from './WithStyles';
|
import WithStyles from './WithStyles';
|
||||||
@@ -293,9 +293,11 @@ const PluginPresetSelector =
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div >
|
<div >
|
||||||
<IconButton onClick={(e)=> this.handlePresetMenuClick(e)} size="large">
|
<IconButtonEx
|
||||||
|
tooltip="Plugin presets"
|
||||||
|
onClick={(e)=> this.handlePresetMenuClick(e)} size="large">
|
||||||
<PluginPresetsIcon className={classes.pluginIcon}/>
|
<PluginPresetsIcon className={classes.pluginIcon}/>
|
||||||
</IconButton>
|
</IconButtonEx>
|
||||||
<Menu
|
<Menu
|
||||||
id="edit-plugin-presets-menu"
|
id="edit-plugin-presets-menu"
|
||||||
anchorEl={this.state.presetsMenuAnchorRef}
|
anchorEl={this.state.presetsMenuAnchorRef}
|
||||||
|
|||||||
@@ -26,8 +26,7 @@ import { styled } from '@mui/material/styles';
|
|||||||
import LoopDialog from './LoopDialog';
|
import LoopDialog from './LoopDialog';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import Button from '@mui/material/Button';
|
import ButtonEx from './ButtonEx';
|
||||||
import Slider from '@mui/material/Slider';
|
|
||||||
import IconButton from '@mui/material/IconButton';
|
import IconButton from '@mui/material/IconButton';
|
||||||
import Pause from '@mui/icons-material/Pause';
|
import Pause from '@mui/icons-material/Pause';
|
||||||
import PlayArrow from '@mui/icons-material/PlayArrow';
|
import PlayArrow from '@mui/icons-material/PlayArrow';
|
||||||
@@ -44,19 +43,23 @@ import { Divider } from '@mui/material';
|
|||||||
import useWindowSize from './UseWindowSize';
|
import useWindowSize from './UseWindowSize';
|
||||||
import { getAlbumArtUri } from './AudioFileMetadata';
|
import { getAlbumArtUri } from './AudioFileMetadata';
|
||||||
import RepeatIcon from '@mui/icons-material/Repeat';
|
import RepeatIcon from '@mui/icons-material/Repeat';
|
||||||
import { LoopParameters, TimebaseUnits } from './Timebase';
|
import Timebase, { LoopParameters, TimebaseUnits } from './Timebase';
|
||||||
|
import ControlSlider from './ControlSlider';
|
||||||
|
|
||||||
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";
|
||||||
|
const LOOP_PROPERTY_URI = "http://two-play.com/plugins/toob-player#loop";
|
||||||
class PluginState {
|
class PluginState {
|
||||||
// Must match PluginState values in ToobPlayer.hpp
|
// Must match ProcessorState values in Lv2AudioFileProcessor.hpp
|
||||||
static Idle = 0.0;
|
// Not an enum because it has to be json serializable.
|
||||||
static CuePlaying = 1;
|
static Idle = 0;
|
||||||
static CuePlayPaused = 2.0;
|
static Recording = 1;
|
||||||
static Pausing = 3;
|
static StoppingRecording = 2;
|
||||||
static Paused = 4;
|
static CuePlayingThenPlay = 3;
|
||||||
static Playing = 5;
|
static CuePlayingThenPause = 4;
|
||||||
static Error = 6;
|
static Paused = 5;
|
||||||
|
static Playing = 6;
|
||||||
|
static Error = 7;
|
||||||
};
|
};
|
||||||
|
|
||||||
const useWallpaper = false;
|
const useWallpaper = false;
|
||||||
@@ -92,7 +95,72 @@ const WallPaper = styled('div')({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
function getAlbumLine(album: string, artist: string, albumArtist: string): string {
|
function tidyHundredths(value: number): string {
|
||||||
|
if (value === 0) {
|
||||||
|
return "";
|
||||||
|
} else if (value % 10 == 0) {
|
||||||
|
return `.${(value / 10).toString()}`;
|
||||||
|
} else {
|
||||||
|
return `.${value.toString()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
export function formatTimeCompact(timebase: Timebase, sampleRate: number, seconds: number): string {
|
||||||
|
switch (timebase.units) {
|
||||||
|
case TimebaseUnits.Samples:
|
||||||
|
{
|
||||||
|
return Math.round(seconds * sampleRate).toString();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case TimebaseUnits.Seconds:
|
||||||
|
{
|
||||||
|
const t = Math.round(seconds * 100);
|
||||||
|
let hundredths = t % 100;
|
||||||
|
let secs = Math.floor(t / 100);
|
||||||
|
let minutes = Math.floor(secs / 60);
|
||||||
|
let hours = Math.floor(minutes / 60);
|
||||||
|
minutes = minutes % 60;
|
||||||
|
secs = secs % 60;
|
||||||
|
|
||||||
|
if (hours == 0) {
|
||||||
|
return `${minutes}:${secs.toString().padStart(2, '0')}${tidyHundredths(hundredths)}`;
|
||||||
|
} else {
|
||||||
|
return `${hours}:${minutes}:${secs.toString().padStart(2, '0')}${tidyHundredths(hundredths)}`;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case TimebaseUnits.Beats:
|
||||||
|
{
|
||||||
|
let t = Math.round(seconds * timebase.tempo / 60.0 * 100.0);
|
||||||
|
let hundredths = t % 100;
|
||||||
|
let beats = Math.floor(t / 100.0);
|
||||||
|
let bars = Math.floor(beats / timebase.timeSignature.numerator);
|
||||||
|
let beat = (beats - bars * timebase.timeSignature.numerator);
|
||||||
|
|
||||||
|
return `${bars + 1}:${(beat + 1)}${tidyHundredths(hundredths)}`;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error("Unsupported timebase units");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function timebaseEqual(a: Timebase, b: Timebase): boolean {
|
||||||
|
if (a.units !== b.units) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (a.tempo !== b.tempo) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (a.timeSignature.numerator !== b.timeSignature.numerator) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (a.timeSignature.denominator !== b.timeSignature.denominator) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
function getAlbumLine(album: string, artist: string, albumArtist: string): string {
|
||||||
if (artist === "") {
|
if (artist === "") {
|
||||||
artist = albumArtist;
|
artist = albumArtist;
|
||||||
}
|
}
|
||||||
@@ -119,7 +187,7 @@ const WidgetBorders = styled('div')(({ theme }) => ({
|
|||||||
backgroundColor: 'rgba(255,255,255,0.4)',
|
backgroundColor: 'rgba(255,255,255,0.4)',
|
||||||
boxShadow: "1px 4px 12px rgba(0,0,0,0.2)",
|
boxShadow: "1px 4px 12px rgba(0,0,0,0.2)",
|
||||||
...theme.applyStyles('dark', {
|
...theme.applyStyles('dark', {
|
||||||
backgroundColor: useWallpaper? 'rgba(0,0,0,0.6)' : '#282828',
|
backgroundColor: useWallpaper ? 'rgba(0,0,0,0.6)' : '#282828',
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -174,12 +242,6 @@ const CoverImage = styled('div')({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const TinyText = styled(Typography)({
|
|
||||||
fontSize: '0.85rem',
|
|
||||||
opacity: 0.9,
|
|
||||||
fontWeight: 400,
|
|
||||||
letterSpacing: 0.2,
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
export interface ToobPlayerControlProps {
|
export interface ToobPlayerControlProps {
|
||||||
@@ -192,32 +254,41 @@ export default function ToobPlayerControl(
|
|||||||
) {
|
) {
|
||||||
|
|
||||||
const model = PiPedalModelFactory.getInstance();
|
const model = PiPedalModelFactory.getInstance();
|
||||||
|
const sampleRate = model.jackConfiguration.get().sampleRate == 0 ?
|
||||||
|
48000 :
|
||||||
|
model.jackConfiguration.get().sampleRate;
|
||||||
|
|
||||||
|
|
||||||
const defaultCoverArt = "/img/default_album.jpg";
|
const defaultCoverArt = "/img/default_album.jpg";
|
||||||
const [serverConnected, setServerConnected] = React.useState(model.state.get() == State.Ready);
|
const [serverConnected, setServerConnected] = React.useState(model.state.get() == State.Ready);
|
||||||
const [duration, setDuration] = React.useState(0.0);
|
const [duration, setDuration] = React.useState(0.0);
|
||||||
const [position, setPosition] = React.useState(0.0);
|
|
||||||
const [start, setStart] = React.useState(0.0);
|
const [start, setStart] = React.useState(0.0);
|
||||||
const [loopStart, setLoopStart] = React.useState(0.0);
|
const [loopStart, setLoopStart] = React.useState(0.0);
|
||||||
const [loopEnable, setLoopEnable] = React.useState(false);
|
const [loopEnable, setLoopEnable] = React.useState(false);
|
||||||
const [loopEnd, setLoopEnd] = React.useState(0.0);
|
const [loopEnd, setLoopEnd] = React.useState(0.0);
|
||||||
const [dragging, setDragging] = React.useState(false);
|
|
||||||
const [pluginState, setPluginState] = React.useState(0.0);
|
const [pluginState, setPluginState] = React.useState(0.0);
|
||||||
const [sliderValue, setSliderValue] = React.useState(0.0);
|
|
||||||
const [coverArt, setCoverArt] = React.useState(defaultCoverArt);
|
const [coverArt, setCoverArt] = React.useState(defaultCoverArt);
|
||||||
const [audioFile, setAudioFile] = React.useState("");
|
const [audioFile, setAudioFile] = React.useState("");
|
||||||
|
const [position, setPosition] = React.useState(0.0);
|
||||||
|
//let position = 0;
|
||||||
const [title, setTitle] = React.useState("");
|
const [title, setTitle] = React.useState("");
|
||||||
const [album, setAlbum] = React.useState("");
|
const [album, setAlbum] = React.useState("");
|
||||||
const [artist, setArtist] = React.useState("");
|
const [artist, setArtist] = React.useState("");
|
||||||
const [albumArtist, setAlbumArtist] = React.useState("");
|
const [albumArtist, setAlbumArtist] = React.useState("");
|
||||||
const [showFileDialog, setShowFileDialog] = React.useState(false);
|
const [showFileDialog, setShowFileDialog] = React.useState(false);
|
||||||
const [showLoopDialog, setShowLoopDialog] = React.useState(false);
|
const [showLoopDialog, setShowLoopDialog] = React.useState(false);
|
||||||
const [timebase,setTimebase] = React.useState(
|
const [timebase, setTimebase] = React.useState<Timebase>(
|
||||||
{
|
{
|
||||||
units: TimebaseUnits.Seconds,
|
units: TimebaseUnits.Seconds,
|
||||||
tempo: 120.0,
|
tempo: 120.0,
|
||||||
timeSignature: { numerator: 4, denominator: 4 }
|
timeSignature: { numerator: 4, denominator: 4 }
|
||||||
});
|
});
|
||||||
|
const [loopParameters, setLoopParameters] = React.useState<LoopParameters>({
|
||||||
|
start: 0.0,
|
||||||
|
loopEnable: false,
|
||||||
|
loopStart: 0.0,
|
||||||
|
loopEnd: 0.0
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
const [size] = useWindowSize();
|
const [size] = useWindowSize();
|
||||||
@@ -237,12 +308,6 @@ export default function ToobPlayerControl(
|
|||||||
function SelectFile() {
|
function SelectFile() {
|
||||||
setShowFileDialog(true);
|
setShowFileDialog(true);
|
||||||
}
|
}
|
||||||
function formatDuration(value_: number) {
|
|
||||||
let value = Math.ceil(value_);
|
|
||||||
const minute = Math.floor(value / 60);
|
|
||||||
const secondLeft = value - minute * 60;
|
|
||||||
return `${minute}:${secondLeft < 10 ? `0${secondLeft}` : secondLeft}`;
|
|
||||||
}
|
|
||||||
function onAudioFileChanged(path: string) {
|
function onAudioFileChanged(path: string) {
|
||||||
setAudioFile(path);
|
setAudioFile(path);
|
||||||
if (path === "") {
|
if (path === "") {
|
||||||
@@ -257,7 +322,7 @@ export default function ToobPlayerControl(
|
|||||||
.then((metadata) => {
|
.then((metadata) => {
|
||||||
let strTrack: string = "";
|
let strTrack: string = "";
|
||||||
if (metadata.track > 0) {
|
if (metadata.track > 0) {
|
||||||
let disc = (metadata.track /1000);
|
let disc = (metadata.track / 1000);
|
||||||
if (disc >= 1) {
|
if (disc >= 1) {
|
||||||
strTrack = (metadata.track % 1000).toString() + '/' + Math.floor(disc).toString();
|
strTrack = (metadata.track % 1000).toString() + '/' + Math.floor(disc).toString();
|
||||||
} else {
|
} else {
|
||||||
@@ -265,14 +330,14 @@ export default function ToobPlayerControl(
|
|||||||
}
|
}
|
||||||
strTrack += ". ";
|
strTrack += ". ";
|
||||||
}
|
}
|
||||||
let coverArtUri = getAlbumArtUri(model,metadata,path);
|
let coverArtUri = getAlbumArtUri(model, metadata, path);
|
||||||
setCoverArt(coverArtUri);
|
setCoverArt(coverArtUri);
|
||||||
setTitle(strTrack + metadata.title);
|
setTitle(strTrack + metadata.title);
|
||||||
setAlbum(metadata.album);
|
setAlbum(metadata.album);
|
||||||
setArtist(metadata.artist);
|
setArtist(metadata.artist);
|
||||||
setAlbumArtist(metadata.albumArtist);
|
setAlbumArtist(metadata.albumArtist);
|
||||||
})
|
})
|
||||||
.catch((e)=>{
|
.catch((e) => {
|
||||||
setTitle("#error" + e.message);
|
setTitle("#error" + e.message);
|
||||||
setAlbum("");
|
setAlbum("");
|
||||||
});
|
});
|
||||||
@@ -298,7 +363,7 @@ export default function ToobPlayerControl(
|
|||||||
<IconButton
|
<IconButton
|
||||||
style={{ opacity: (pluginState === PluginState.Idle ? 0.2 : 0.7) }}
|
style={{ opacity: (pluginState === PluginState.Idle ? 0.2 : 0.7) }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (position > 3) {
|
if (position > start + 3.0) {
|
||||||
model.sendPedalboardControlTrigger(
|
model.sendPedalboardControlTrigger(
|
||||||
props.instanceId,
|
props.instanceId,
|
||||||
"stop",
|
"stop",
|
||||||
@@ -394,12 +459,12 @@ export default function ToobPlayerControl(
|
|||||||
}
|
}
|
||||||
function loopButtonText() {
|
function loopButtonText() {
|
||||||
if (start === 0 && !loopEnable) {
|
if (start === 0 && !loopEnable) {
|
||||||
return "Set loop";
|
return "Set loop";
|
||||||
|
|
||||||
} else if (loopEnable) {
|
} else if (loopEnable) {
|
||||||
return `${formatDuration(start)} [${formatDuration(loopStart)} - ${formatDuration(loopEnd)}]`;
|
return `${formatTimeCompact(timebase, sampleRate, start)} [${formatTimeCompact(timebase, sampleRate, loopStart)} - ${formatTimeCompact(timebase, sampleRate, loopEnd)}]`;
|
||||||
} else {
|
} else {
|
||||||
return `Start: ${formatDuration(start)}`;
|
return `Start: ${formatTimeCompact(timebase, sampleRate, start)}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function getUiFileProperty(uri: string): UiFileProperty {
|
function getUiFileProperty(uri: string): UiFileProperty {
|
||||||
@@ -415,6 +480,39 @@ export default function ToobPlayerControl(
|
|||||||
}
|
}
|
||||||
throw "FileProperty not found.";
|
throw "FileProperty not found.";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onLoopPropertyChanged(loopSettingsJson: string) {
|
||||||
|
try {
|
||||||
|
let atomObject = JSON.parse(loopSettingsJson);
|
||||||
|
let loopParameters: LoopParameters = atomObject.loopParameters as LoopParameters;
|
||||||
|
let newTimebase: Timebase | undefined = atomObject.timebase as (Timebase | undefined);;
|
||||||
|
if (newTimebase !== undefined) {
|
||||||
|
if (!timebaseEqual(timebase, newTimebase)) {
|
||||||
|
setTimebase(newTimebase);
|
||||||
|
}
|
||||||
|
setLoopParameters(loopParameters);
|
||||||
|
setLoopEnable(loopParameters.loopEnable);
|
||||||
|
setStart(loopParameters.start);
|
||||||
|
setLoopStart(loopParameters.loopStart);
|
||||||
|
setLoopEnd(loopParameters.loopEnd);
|
||||||
|
} else {
|
||||||
|
throw new Error("Invalid loop settings.");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Unable to parse loop settings.");
|
||||||
|
setTimebase(
|
||||||
|
{
|
||||||
|
units: TimebaseUnits.Seconds,
|
||||||
|
tempo: 120.0,
|
||||||
|
timeSignature: { numerator: 4, denominator: 4 }
|
||||||
|
})
|
||||||
|
setLoopEnable(false);
|
||||||
|
setStart(0.0);
|
||||||
|
setLoopStart(0.0);
|
||||||
|
setLoopEnd(0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
function onNextTrack() {
|
function onNextTrack() {
|
||||||
model.getNextAudioFile(audioFile)
|
model.getNextAudioFile(audioFile)
|
||||||
.then((file) => {
|
.then((file) => {
|
||||||
@@ -441,17 +539,11 @@ export default function ToobPlayerControl(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
function OnSeek(value: number) {
|
function OnSeek(value: number) {
|
||||||
setDragging(true);
|
|
||||||
model.setPatchProperty(props.instanceId, Player__seek, value)
|
model.setPatchProperty(props.instanceId, Player__seek, value)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setPosition(value);
|
|
||||||
setDragging(false);
|
|
||||||
}).catch((e) => {
|
}).catch((e) => {
|
||||||
console.warn("Seek error. " + e.toString());
|
console.warn("Seek error. " + e.toString());
|
||||||
setPosition(value);
|
|
||||||
setDragging(false);
|
|
||||||
});
|
});
|
||||||
setSliderValue(value);
|
|
||||||
}
|
}
|
||||||
function onStateChanged(value: State) {
|
function onStateChanged(value: State) {
|
||||||
setServerConnected(value === State.Ready);
|
setServerConnected(value === State.Ready);
|
||||||
@@ -461,7 +553,7 @@ export default function ToobPlayerControl(
|
|||||||
if (model.state.get() !== State.Ready) {
|
if (model.state.get() !== State.Ready) {
|
||||||
// wait for it.
|
// wait for it.
|
||||||
return () => {
|
return () => {
|
||||||
model.state.removeOnChangedHandler
|
model.state.removeOnChangedHandler(onStateChanged);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let durationHandle = model.monitorPort(props.instanceId, "duration", 1.0 / 15,
|
let durationHandle = model.monitorPort(props.instanceId, "duration", 1.0 / 15,
|
||||||
@@ -474,27 +566,7 @@ export default function ToobPlayerControl(
|
|||||||
setPosition(value);
|
setPosition(value);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
// let startHandle = model.monitorPort(props.instanceId, "start", 1.0,
|
|
||||||
// (value) => {
|
|
||||||
// setStart(value);
|
|
||||||
// }
|
|
||||||
// );
|
|
||||||
// let loopStartHandle = model.monitorPort(props.instanceId, "loopStart", 1.0,
|
|
||||||
// (value) => {
|
|
||||||
// setLoopStart(value);
|
|
||||||
// }
|
|
||||||
// );
|
|
||||||
// let loopEnableHandle = model.monitorPort(props.instanceId, "loopEnable", 1.0,
|
|
||||||
// (value) => {
|
|
||||||
// setLoopEnable(value != 0);
|
|
||||||
// }
|
|
||||||
// );
|
|
||||||
// let loopEndHandle = model.monitorPort(props.instanceId, "loopEnd", 1.0,
|
|
||||||
// (value) => {
|
|
||||||
// setLoopEnd(value);
|
|
||||||
// }
|
|
||||||
// );
|
|
||||||
|
|
||||||
let pluginStateHandle = model.monitorPort(props.instanceId, "state", 1.0 / 1000.0,
|
let pluginStateHandle = model.monitorPort(props.instanceId, "state", 1.0 / 1000.0,
|
||||||
(value) => {
|
(value) => {
|
||||||
setPluginState(value);
|
setPluginState(value);
|
||||||
@@ -507,44 +579,81 @@ export default function ToobPlayerControl(
|
|||||||
if (typeof (atomObject) === "object") {
|
if (typeof (atomObject) === "object") {
|
||||||
let path = atomObject.value;
|
let path = atomObject.value;
|
||||||
onAudioFileChanged(path);
|
onAudioFileChanged(path);
|
||||||
} else if (typeof(atomObject) === "string")
|
} else if (typeof (atomObject) === "string") {
|
||||||
{
|
|
||||||
onAudioFileChanged(atomObject as string);
|
onAudioFileChanged(atomObject as string);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
);
|
||||||
|
let loopPropertyHandle = model.monitorPatchProperty(
|
||||||
|
props.instanceId,
|
||||||
|
LOOP_PROPERTY_URI,
|
||||||
|
(instanceId: number, propertyUri: string, atomObject: any) => {
|
||||||
|
if (typeof (atomObject) === "string") {
|
||||||
|
onLoopPropertyChanged(atomObject as string);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
);
|
);
|
||||||
|
|
||||||
model.getPatchProperty(props.instanceId, AUDIO_FILE_PROPERTY_URI)
|
model.getPatchProperty(props.instanceId, AUDIO_FILE_PROPERTY_URI)
|
||||||
.then((o) => {
|
.then((o) => {
|
||||||
let path = o.value;
|
let path = o.value;
|
||||||
onAudioFileChanged(path);
|
onAudioFileChanged(path);
|
||||||
|
});
|
||||||
|
model.getPatchProperty(props.instanceId, LOOP_PROPERTY_URI)
|
||||||
|
.then((o) => {
|
||||||
|
onLoopPropertyChanged(o as string);
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
model.state.removeOnChangedHandler(onStateChanged);
|
model.state.removeOnChangedHandler(onStateChanged);
|
||||||
model.unmonitorPort(durationHandle);
|
model.unmonitorPort(durationHandle);
|
||||||
model.unmonitorPort(positionHandle);
|
|
||||||
model.unmonitorPort(pluginStateHandle);
|
model.unmonitorPort(pluginStateHandle);
|
||||||
|
model.unmonitorPort(positionHandle);
|
||||||
// model.unmonitorPort(loopEnableHandle);
|
// model.unmonitorPort(loopEnableHandle);
|
||||||
// model.unmonitorPort(startHandle);
|
// model.unmonitorPort(startHandle);
|
||||||
// model.unmonitorPort(loopStartHandle);
|
// model.unmonitorPort(loopStartHandle);
|
||||||
// model.unmonitorPort(loopEndHandle);
|
// model.unmonitorPort(loopEndHandle);
|
||||||
model.cancelMonitorPatchProperty(filePropertyHandle);
|
model.cancelMonitorPatchProperty(filePropertyHandle);
|
||||||
|
model.cancelMonitorPatchProperty(loopPropertyHandle);
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[serverConnected]
|
[serverConnected]
|
||||||
);
|
);
|
||||||
const effectivePosition =
|
|
||||||
(pluginState !== PluginState.Idle) ? Math.min(position, duration) : 0.0;
|
|
||||||
const titleLine = title !== "" ? title : pathFileNameOnly(audioFile);
|
const titleLine = title !== "" ? title : pathFileNameOnly(audioFile);
|
||||||
const albumLine = getAlbumLine(album,artist,albumArtist);
|
const albumLine = getAlbumLine(album, artist, albumArtist);
|
||||||
|
|
||||||
const paused = (pluginState === PluginState.Idle
|
const paused = (pluginState === PluginState.Idle
|
||||||
|| pluginState === PluginState.CuePlayPaused
|
|| pluginState === PluginState.CuePlayingThenPause
|
||||||
|| pluginState === PluginState.Paused);
|
|| pluginState === PluginState.Paused
|
||||||
|
|| pluginState === PluginState.Error);
|
||||||
|
|
||||||
|
function handlePreview() {
|
||||||
|
if (paused) {
|
||||||
|
model.sendPedalboardControlTrigger(
|
||||||
|
props.instanceId,
|
||||||
|
"play",
|
||||||
|
1
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
model.sendPedalboardControlTrigger(
|
||||||
|
props.instanceId,
|
||||||
|
"stop",
|
||||||
|
1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function handleCancelPlaying() {
|
||||||
|
if (!paused) {
|
||||||
|
model.sendPedalboardControlTrigger(
|
||||||
|
props.instanceId,
|
||||||
|
"stop",
|
||||||
|
1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
function LinearMixPanel() {
|
function LinearMixPanel() {
|
||||||
return (
|
return (
|
||||||
<Box style={{ width: "100%" }} >
|
<Box style={{ width: "100%" }} >
|
||||||
@@ -573,84 +682,55 @@ export default function ToobPlayerControl(
|
|||||||
function SliderCluster() {
|
function SliderCluster() {
|
||||||
return (
|
return (
|
||||||
|
|
||||||
<div style={{ display: "flex", flexFlow: "column nowrap" }}>
|
<div style={{ position: "relative", display: "flex", flexFlow: "column nowrap" }}>
|
||||||
<Slider
|
<div style={{ marginBottom: 16 }}>
|
||||||
value={dragging ? sliderValue : effectivePosition}
|
<ControlSlider
|
||||||
min={0}
|
instanceId={props.instanceId}
|
||||||
step={1}
|
controlKey="position"
|
||||||
max={duration}
|
duration={duration}
|
||||||
onChange={(_, val) => {
|
onPreviewValue={(value) => {
|
||||||
let v = val as number;
|
}}
|
||||||
setPosition(v);
|
onValueChanged={(value) => {
|
||||||
setSliderValue(v);
|
OnSeek(value);
|
||||||
}}
|
}}
|
||||||
onChangeCommitted={(e, value) => {
|
/>
|
||||||
let v = value as number;
|
</div>
|
||||||
OnSeek(v);
|
|
||||||
}}
|
|
||||||
onPointerDown={(e) => {
|
|
||||||
setDragging(true);
|
|
||||||
}}
|
|
||||||
onPointerUp={(e) => {
|
|
||||||
// setDragging(false);
|
|
||||||
}}
|
|
||||||
disabled={duration == 0}
|
|
||||||
sx={(t) => ({
|
|
||||||
width: "100%",
|
|
||||||
color: 'rgba(0,0,0,0.87)',
|
|
||||||
height: 4,
|
|
||||||
'& .MuiSlider-thumb': {
|
|
||||||
width: 8,
|
|
||||||
height: 8,
|
|
||||||
transition: '0.3s cubic-bezier(.47,1.64,.41,.8)',
|
|
||||||
'&::before': {
|
|
||||||
boxShadow: '0 2px 12px 0 rgba(0,0,0,0.4)',
|
|
||||||
},
|
|
||||||
'&:hover, &.Mui-focusVisible': {
|
|
||||||
boxShadow: `0px 0px 0px 8px ${'rgb(0 0 0 / 16%)'}`,
|
|
||||||
...t.applyStyles('dark', {
|
|
||||||
boxShadow: `0px 0px 0px 8px ${'rgb(255 255 255 / 16%)'}`,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
'&.Mui-active': {
|
|
||||||
width: 20,
|
|
||||||
height: 20,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'& .MuiSlider-rail': {
|
|
||||||
opacity: 0.28,
|
|
||||||
},
|
|
||||||
...t.applyStyles('dark', {
|
|
||||||
color: '#fff',
|
|
||||||
}),
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
flex: "0 0 auto",
|
position: "absolute",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
display: 'flex',
|
display: "flex", flexFlow: "row nowrap", alignItems: "center",
|
||||||
alignItems: 'top',
|
justifyContent: "center",
|
||||||
justifyContent: 'space-between',
|
bottom: 0
|
||||||
marginTop: -4,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TinyText>{formatDuration(dragging ? sliderValue : effectivePosition)}</TinyText>
|
{/**
|
||||||
<Button title="Loop" variant="dialogSecondary"
|
*
|
||||||
style={{flex: "0 1 auto",width: 200,
|
sx={{
|
||||||
textTransform: "none", fontSize: "0.9rem", fontWeight: 500, padding: "4px 8px"
|
'& .MuiTouchRipple-root': {
|
||||||
|
},
|
||||||
|
'& .MuiTouchRipple-ripple': {
|
||||||
|
transform: 'scale(1.9) !important',
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
startIcon= {(<RepeatIcon />)}
|
*/}
|
||||||
|
<ButtonEx tooltip="Loop Settings" variant="dialogSecondary"
|
||||||
|
style={{
|
||||||
|
flex: "0 1 auto", maxWidth: 200,
|
||||||
|
textTransform: "none", padding: "4px 8px"
|
||||||
|
}}
|
||||||
|
startIcon={(<RepeatIcon />)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowLoopDialog(true);
|
setShowLoopDialog(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{loopButtonText()
|
<Typography noWrap variant="caption">
|
||||||
}
|
{loopButtonText()
|
||||||
</Button>
|
}
|
||||||
<TinyText>{formatDuration(duration)}</TinyText>
|
</Typography>
|
||||||
|
</ButtonEx>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div >
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -742,10 +822,12 @@ export default function ToobPlayerControl(
|
|||||||
{showLoopDialog && (
|
{showLoopDialog && (
|
||||||
<LoopDialog isOpen={showLoopDialog}
|
<LoopDialog isOpen={showLoopDialog}
|
||||||
sampleRate={
|
sampleRate={
|
||||||
model.jackConfiguration.get().sampleRate == 0?
|
sampleRate
|
||||||
96000 :
|
|
||||||
model.jackConfiguration.get().sampleRate
|
|
||||||
}
|
}
|
||||||
|
playing={!paused}
|
||||||
|
onPreview={() => { handlePreview(); }}
|
||||||
|
value={loopParameters}
|
||||||
|
onCancelPlaying= {()=> { handleCancelPlaying();}}
|
||||||
timebase={timebase}
|
timebase={timebase}
|
||||||
onTimebaseChange={(newTimebase) => {
|
onTimebaseChange={(newTimebase) => {
|
||||||
setTimebase(newTimebase);
|
setTimebase(newTimebase);
|
||||||
@@ -759,50 +841,23 @@ export default function ToobPlayerControl(
|
|||||||
onClose={() => {
|
onClose={() => {
|
||||||
setShowLoopDialog(false);
|
setShowLoopDialog(false);
|
||||||
}}
|
}}
|
||||||
onSetLoop={(start, loopEnable,loopStart, loopEnd) => {
|
onSetLoop={(loop: LoopParameters) => {
|
||||||
|
|
||||||
let loop: LoopParameters = { start: start,
|
|
||||||
loopEnable: loopEnable,
|
|
||||||
loopStart: loopStart,
|
|
||||||
loopEnd: loopEnd };
|
|
||||||
|
|
||||||
let loopSettings = {
|
let loopSettings = {
|
||||||
timebase: timebase,
|
timebase: timebase,
|
||||||
loopParameters: loop
|
loopParameters: loop
|
||||||
};
|
};
|
||||||
model.setPatchProperty(
|
model.setPatchProperty(
|
||||||
props.instanceId,
|
props.instanceId,
|
||||||
"http://two-play.com/plugins/toob-player#loopSettings",
|
LOOP_PROPERTY_URI,
|
||||||
loopSettings.toString());
|
JSON.stringify(loopSettings));
|
||||||
|
|
||||||
setStart(start);
|
setStart(loop.start);
|
||||||
// model.setPedalboardControl(
|
setLoopEnable(loop.loopEnable);
|
||||||
// props.instanceId,
|
setLoopStart(loop.loopStart);
|
||||||
// "start",
|
setLoopEnd(loop.loopEnd);
|
||||||
// start);
|
|
||||||
setLoopEnable(loopEnable);
|
|
||||||
// model.setPedalboardControl(
|
|
||||||
// props.instanceId,
|
|
||||||
// "loopEnable",
|
|
||||||
// loopEnable? 1.0: 0.0);
|
|
||||||
setLoopStart(loopStart);
|
|
||||||
// model.setPedalboardControl(
|
|
||||||
// props.instanceId,
|
|
||||||
// "loopStart",
|
|
||||||
// loopStart);
|
|
||||||
setLoopEnd(loopEnd);
|
|
||||||
// model.setPedalboardControl(
|
|
||||||
// props.instanceId,
|
|
||||||
// "loopEnd",
|
|
||||||
// loopEnd
|
|
||||||
// );
|
|
||||||
}}
|
}}
|
||||||
start={start}
|
|
||||||
loopStart={loopStart}
|
|
||||||
loopEnable={loopEnable}
|
|
||||||
loopEnd={loopEnd}
|
|
||||||
duration={duration}
|
duration={duration}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{showFileDialog && (
|
{showFileDialog && (
|
||||||
<FilePropertyDialog open={showFileDialog}
|
<FilePropertyDialog open={showFileDialog}
|
||||||
|
|||||||
@@ -41,12 +41,38 @@ import CircularProgress from '@mui/material/CircularProgress';
|
|||||||
// const BANK_EXTENSION = ".piBank";
|
// const BANK_EXTENSION = ".piBank";
|
||||||
// const PLUGIN_PRESETS_EXTENSION = ".piPluginPresets";
|
// const PLUGIN_PRESETS_EXTENSION = ".piPluginPresets";
|
||||||
|
|
||||||
|
let COVER_ART_FILES = [
|
||||||
|
"Folder.jpg", // window media player/explorer.
|
||||||
|
"Cover.jpg", // itunes.
|
||||||
|
"folder.jpg", // Linux audio players plex, kodi, Rythmbox, Amorok, Clementine.
|
||||||
|
"cover.jpg", // Linux audio players plex, kodi, Rythmbox, Amorok, Clementine.
|
||||||
|
"Artwork.jpg", // itunes.
|
||||||
|
"Front.jpg",
|
||||||
|
"front.jpg", // linux.
|
||||||
|
"AlbumArt.jpg",
|
||||||
|
"albumArt.jpg",
|
||||||
|
"Frontcover.jpg",
|
||||||
|
"AlbumArtSmall.jpg" // windows media player.
|
||||||
|
];
|
||||||
|
|
||||||
|
function isCoverArtFile(fileName: string) : boolean {
|
||||||
|
// does COVER_ART_FILES contain the file name?
|
||||||
|
for (let coverArtFile of COVER_ART_FILES) {
|
||||||
|
if (fileName === coverArtFile) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface UploadFileDialogProps {
|
export interface UploadFileDialogProps {
|
||||||
open: boolean,
|
open: boolean,
|
||||||
onClose: () => void,
|
onClose: () => void,
|
||||||
onUploaded: (fileName: string) => void,
|
onUploaded: (fileName: string) => void,
|
||||||
uploadPage: string,
|
uploadPage: string,
|
||||||
fileProperty: UiFileProperty
|
fileProperty: UiFileProperty,
|
||||||
|
isTracksDirectory: boolean
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -251,7 +277,15 @@ export default class UploadFileDialog extends ResizeResponsiveComponent<UploadFi
|
|||||||
|
|
||||||
private wantsFile(file: File | null) {
|
private wantsFile(file: File | null) {
|
||||||
if (file === null) return false;
|
if (file === null) return false;
|
||||||
return this.props.fileProperty.wantsFile(file.name);
|
if (this.props.fileProperty.wantsFile(file.name))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (this.props.isTracksDirectory) {
|
||||||
|
if (isCoverArtFile(file.name)) {
|
||||||
|
return true;
|
||||||
|
} }
|
||||||
|
return false;
|
||||||
|
|
||||||
}
|
}
|
||||||
private wantsTransfer(fileList: DataTransfer): boolean {
|
private wantsTransfer(fileList: DataTransfer): boolean {
|
||||||
|
|||||||
Reference in New Issue
Block a user