Toob Player
This commit is contained in:
Vendored
+21
-1
@@ -135,5 +135,25 @@
|
||||
"cSpell.ignoreWords": [
|
||||
"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();
|
||||
|
||||
|
||||
// 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)
|
||||
{
|
||||
temporaryDirectory = path;
|
||||
@@ -96,10 +131,9 @@ namespace
|
||||
class AudioDirectoryInfoImpl : public AudioDirectoryInfo
|
||||
{
|
||||
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(
|
||||
(indexPath.empty() ? path: indexPath) / ".index.pipedal"
|
||||
)
|
||||
(indexPath.empty() ? path : indexPath) / ".index.pipedal")
|
||||
{
|
||||
if (this->indexPath.parent_path() != path)
|
||||
{
|
||||
@@ -129,7 +163,6 @@ namespace
|
||||
private:
|
||||
std::vector<DbFileInfo> QueryTracks();
|
||||
|
||||
|
||||
bool opened = false;
|
||||
std::filesystem::path indexPath;
|
||||
void OpenAudioDb();
|
||||
@@ -139,7 +172,6 @@ namespace
|
||||
fs::path path;
|
||||
using id_t = int64_t;
|
||||
|
||||
|
||||
std::vector<DbFileInfo> UpdateDbFiles();
|
||||
void DbSetThumbnailType(
|
||||
int64_t idFile,
|
||||
@@ -156,16 +188,14 @@ namespace
|
||||
void UpdateMetadata(DbFileInfo *dbFile);
|
||||
static constexpr int DB_VERSION = 1;
|
||||
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()
|
||||
{
|
||||
if (audioFilesDb)
|
||||
@@ -264,7 +294,18 @@ std::vector<DbFileInfo> AudioDirectoryInfoImpl::UpdateDbFiles()
|
||||
auto path = dirEntry.path();
|
||||
std::string name = path.filename();
|
||||
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());
|
||||
|
||||
@@ -322,7 +363,7 @@ std::vector<DbFileInfo> AudioDirectoryInfoImpl::UpdateDbFiles()
|
||||
}
|
||||
for (auto &dbFile : dbFiles)
|
||||
{
|
||||
if (dbFile.thumbnailType() == ThumbnailType::Unknown)
|
||||
if (dbFile.thumbnailType() == ThumbnailType::None)
|
||||
{
|
||||
if (!folderFileName.empty())
|
||||
{
|
||||
@@ -410,27 +451,67 @@ void AudioDirectoryInfoImpl::DbDeleteFile(DbFileInfo *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.
|
||||
// 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.
|
||||
ThumbnailTemporaryFile AudioDirectoryInfoImpl::GetUnindexedThunbnail(
|
||||
const std::string &fileNameOnly, int32_t width, int32_t height)
|
||||
{
|
||||
fs::path file = this->path / fileNameOnly;
|
||||
if (!fs::exists(file))
|
||||
{
|
||||
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 = {
|
||||
"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.
|
||||
};
|
||||
// Read the thumbnail data from the temporary file.
|
||||
std::ifstream thumbnailStream(thumbnailPath, std::ios::binary);
|
||||
if (!thumbnailStream)
|
||||
{
|
||||
throw std::runtime_error("Failed to open thumbnail file: " + thumbnailPath.string());
|
||||
}
|
||||
std::vector<uint8_t> thumbnailData(
|
||||
(std::istreambuf_iterator<char>(thumbnailStream)),
|
||||
std::istreambuf_iterator<char>());
|
||||
|
||||
if (!thumbnailData.empty())
|
||||
{
|
||||
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
|
||||
{
|
||||
@@ -604,22 +685,6 @@ ThumbnailTemporaryFile AudioDirectoryInfoImpl::GetThumbnail(const std::string &f
|
||||
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)
|
||||
{
|
||||
@@ -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)
|
||||
{
|
||||
@@ -801,8 +859,8 @@ void AudioDirectoryInfoImpl::MoveAudioFile(
|
||||
transaction->commit();
|
||||
}
|
||||
|
||||
|
||||
namespace pipedal {
|
||||
namespace pipedal
|
||||
{
|
||||
std::filesystem::path IndexPathToShadowIndexPath(const std::filesystem::path &audioRootDirectory, const std::filesystem::path &path)
|
||||
{
|
||||
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 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);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(mutex);
|
||||
|
||||
@@ -454,6 +454,7 @@ namespace pipedal
|
||||
void DeleteSampleFile(const std::filesystem::path &fileName);
|
||||
std::string CreateNewSampleDirectory(const std::string &relativePath, const UiFileProperty &uiFileProperty);
|
||||
std::string RenameFilePropertyFile(const std::string &oldRelativePath, const std::string &newRelativePath, const UiFileProperty &uiFileProperty);
|
||||
std::string CopyFilePropertyFile(const std::string &oldRelativePath, const std::string &newRelativePath, const UiFileProperty &uiFileProperty,bool overwrite);
|
||||
FilePropertyDirectoryTree::ptr GetFilePropertydirectoryTree(const UiFileProperty &uiFileProperty,const std::string&selectedPath);
|
||||
|
||||
bool IsInUploadsDirectory(const std::string &path);
|
||||
|
||||
@@ -115,6 +115,23 @@ JSON_MAP_REFERENCE(RenameSampleFileArgs, newRelativePath)
|
||||
JSON_MAP_REFERENCE(RenameSampleFileArgs, uiFileProperty)
|
||||
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
|
||||
{
|
||||
public:
|
||||
@@ -1645,6 +1662,14 @@ public:
|
||||
std::string newFileName = this->model.RenameFilePropertyFile(args.oldRelativePath_, args.newRelativePath_, args.uiFileProperty_);
|
||||
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")
|
||||
{
|
||||
GetFilePropertyDirectoryTreeArgs args;
|
||||
|
||||
+68
-2
@@ -1705,6 +1705,13 @@ static void AddTracksToResult(
|
||||
std::set<std::string> validExtensions = fileProperty.GetPermittedFileExtensions(
|
||||
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
|
||||
{
|
||||
// Add directories first.
|
||||
@@ -1828,9 +1835,11 @@ FileRequestResult Storage::GetModFileList2(const std::string &relativePath, cons
|
||||
++iRp;
|
||||
}
|
||||
}
|
||||
fs::path cumulativePath = modDirectoryPath;
|
||||
while (iRp != rp.end())
|
||||
{
|
||||
result.breadcrumbs_.push_back({*iRp, *iRp});
|
||||
cumulativePath /= (*iRp);
|
||||
result.breadcrumbs_.push_back({cumulativePath, *iRp});
|
||||
++iRp;
|
||||
}
|
||||
}
|
||||
@@ -2036,6 +2045,17 @@ std::filesystem::path Storage::MakeUserFilePath(const std::string &directory, co
|
||||
}
|
||||
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,
|
||||
UiFileProperty::ptr uiFileProperty,
|
||||
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.");
|
||||
}
|
||||
|
||||
if (!uiFileProperty->IsValidExtension(relativePath))
|
||||
if (!(uiFileProperty->IsValidExtension(relativePath) || IsValidArtworkFile(path)))
|
||||
{
|
||||
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);
|
||||
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
|
||||
{
|
||||
|
||||
@@ -156,6 +156,7 @@ public:
|
||||
|
||||
bool IsInUploadsDirectory(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);
|
||||
|
||||
@@ -232,6 +233,11 @@ public:
|
||||
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 = false);
|
||||
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?
|
||||
|
||||
libsqlite3-dev install
|
||||
libsqlitecpp-dev install
|
||||
|
||||
Check that OnNotifyPatchProperty still worsk for
|
||||
- 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,
|
||||
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: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
'& .MuiTouchRipple-ripple': {
|
||||
transform: 'scale(1.9)',
|
||||
}
|
||||
},
|
||||
containedPrimary: {
|
||||
borderRadius: '9999px',
|
||||
paddingLeft: "16px", paddingRight: "16px",
|
||||
@@ -80,7 +122,7 @@ const theme = createTheme(
|
||||
}
|
||||
},
|
||||
{
|
||||
props: { variant: 'dialogSecondary', },
|
||||
props: { variant: 'dialogSecondary', },
|
||||
style: {
|
||||
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) */
|
||||
MuiListItemButton: {
|
||||
styleOverrides: {
|
||||
root: ({ theme }) => ({
|
||||
'&.Mui-selected': {
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.2)', // Adjust for desired darkness
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.25)', // Slightly darker on hover
|
||||
},
|
||||
},
|
||||
}),
|
||||
root: ({ theme }) => ({
|
||||
'&.Mui-selected': {
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.2)', // Adjust for desired darkness
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.25)', // Slightly darker on hover
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
MuiButton: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
'& .MuiTouchRipple-root': {
|
||||
borderRadius: 'inherit',
|
||||
},
|
||||
'& .MuiTouchRipple-ripple': {
|
||||
transform: 'scale(1.9)!important',
|
||||
}
|
||||
},
|
||||
containedPrimary: {
|
||||
borderRadius: '9999px',
|
||||
paddingLeft: "16px", paddingRight: "16px",
|
||||
@@ -140,7 +190,7 @@ const theme = createTheme(
|
||||
}
|
||||
},
|
||||
{
|
||||
props: { variant: 'dialogSecondary', },
|
||||
props: { variant: 'dialogSecondary', },
|
||||
style: {
|
||||
color: "rgb(0,0,0,0.6)"
|
||||
},
|
||||
@@ -178,8 +228,7 @@ const App = (class extends React.Component {
|
||||
super(props);
|
||||
this.state = {
|
||||
};
|
||||
if (!App.virtualKeyboardHandler)
|
||||
{
|
||||
if (!App.virtualKeyboardHandler) {
|
||||
App.virtualKeyboardHandler = new VirtualKeyboardHandler();
|
||||
}
|
||||
}
|
||||
@@ -192,7 +241,7 @@ const App = (class extends React.Component {
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
|
||||
<AppThemed />
|
||||
<AppThemed />
|
||||
</ThemeProvider>
|
||||
</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 { isDarkMode } from './DarkMode';
|
||||
import Button from '@mui/material/Button';
|
||||
import ButtonEx from './ButtonEx';
|
||||
import FileUploadIcon from '@mui/icons-material/FileUpload';
|
||||
import FileDownloadIcon from '@mui/icons-material/FileDownload';
|
||||
import AudioFileIcon from '@mui/icons-material/AudioFile';
|
||||
@@ -44,12 +45,13 @@ import DialogActions from '@mui/material/DialogActions';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import IconButtonEx from './IconButtonEx';
|
||||
import OldDeleteIcon from './OldDeleteIcon';
|
||||
import Toolbar from '@mui/material/Toolbar';
|
||||
import WithStyles from './WithStyles';
|
||||
import { withStyles } from "tss-react/mui";
|
||||
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';
|
||||
@@ -77,13 +79,14 @@ const styles = (theme: Theme) => createStyles({
|
||||
const audioFileExtensions: { [name: string]: boolean } = {
|
||||
".wav": true,
|
||||
".flac": true,
|
||||
".mp3": true,
|
||||
".m4a": true,
|
||||
".ogg": true,
|
||||
".aac": true,
|
||||
".au": true,
|
||||
".snd": true,
|
||||
".mid": true,
|
||||
".rmi": true,
|
||||
".mp3": true,
|
||||
".mp4": true,
|
||||
".aif": true,
|
||||
".aifc": true,
|
||||
@@ -146,10 +149,16 @@ export interface FilePropertyDialogState {
|
||||
columnWidth: number;
|
||||
openUploadFileDialog: boolean;
|
||||
openConfirmDeleteDialog: boolean;
|
||||
confirmCopyDialogState: {
|
||||
fileName: string;
|
||||
oldFilePath: string;
|
||||
newFilePath: string;
|
||||
} | null;
|
||||
menuAnchorEl: null | HTMLElement;
|
||||
newFolderDialogOpen: boolean;
|
||||
renameDialogOpen: boolean;
|
||||
moveDialogOpen: boolean;
|
||||
copyDialogOpen: boolean;
|
||||
initialSelection: string;
|
||||
};
|
||||
|
||||
@@ -173,6 +182,12 @@ export default withStyles(
|
||||
isTracksDirectory(): boolean {
|
||||
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) {
|
||||
super(props);
|
||||
|
||||
@@ -202,10 +217,12 @@ export default withStyles(
|
||||
isProtectedDirectory: true,
|
||||
openUploadFileDialog: false,
|
||||
openConfirmDeleteDialog: false,
|
||||
confirmCopyDialogState: null,
|
||||
menuAnchorEl: null,
|
||||
newFolderDialogOpen: false,
|
||||
renameDialogOpen: false,
|
||||
moveDialogOpen: false,
|
||||
copyDialogOpen: false,
|
||||
initialSelection: this.props.selectedFile
|
||||
};
|
||||
this.requestScroll = true;
|
||||
@@ -485,6 +502,7 @@ export default withStyles(
|
||||
newFolderDialogOpen: false,
|
||||
renameDialogOpen: false,
|
||||
moveDialogOpen: false,
|
||||
copyDialogOpen: false,
|
||||
navDirectory: navDirectory
|
||||
});
|
||||
this.requestFiles(navDirectory)
|
||||
@@ -531,7 +549,9 @@ export default withStyles(
|
||||
}
|
||||
this.requestScroll = true;
|
||||
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({
|
||||
selectedFile: fileEntry.pathname,
|
||||
@@ -641,7 +661,7 @@ export default withStyles(
|
||||
}
|
||||
getAlbumTitle(fileEntry: FileEntry): string {
|
||||
let artist = fileEntry.metadata?.artist || "";
|
||||
if (artist == "" ) {
|
||||
if (artist == "") {
|
||||
artist = fileEntry.metadata?.albumArtist || "";
|
||||
}
|
||||
let album = fileEntry.metadata?.album || "";
|
||||
@@ -814,6 +834,8 @@ export default withStyles(
|
||||
let needsDivider = canMove || canRename || canReorder;
|
||||
let trackPosition = 0;
|
||||
let compactVertical = this.state.windowHeight < 700;
|
||||
let canSelectFile = this.state.hasSelection && !this.isFolderArtwork(this.state.selectedFile);
|
||||
|
||||
|
||||
return this.props.open &&
|
||||
(
|
||||
@@ -886,7 +908,8 @@ export default withStyles(
|
||||
{this.props.fileProperty.label}
|
||||
</Typography>
|
||||
|
||||
<IconButton
|
||||
<IconButtonEx
|
||||
tooltip="New folder"
|
||||
aria-label="new folder"
|
||||
edge="end"
|
||||
color="inherit"
|
||||
@@ -895,10 +918,11 @@ export default withStyles(
|
||||
disabled={protectedDirectory}
|
||||
>
|
||||
<CreateNewFolderIcon />
|
||||
</IconButton>
|
||||
</IconButtonEx>
|
||||
|
||||
|
||||
<IconButton
|
||||
<IconButtonEx
|
||||
tooltip="More..."
|
||||
aria-label="display more actions"
|
||||
edge="end"
|
||||
color="inherit"
|
||||
@@ -908,7 +932,7 @@ export default withStyles(
|
||||
|
||||
>
|
||||
<MoreIcon />
|
||||
</IconButton>
|
||||
</IconButtonEx>
|
||||
<Menu
|
||||
id="menu-appbar"
|
||||
anchorEl={this.state.menuAnchorEl}
|
||||
@@ -927,6 +951,7 @@ export default withStyles(
|
||||
<MenuItem onClick={() => { this.handleMenuClose(); this.onNewFolder(); }}>New folder</MenuItem>
|
||||
{(needsDivider) && (<Divider />)}
|
||||
{canMove && (<MenuItem onClick={() => { this.handleMenuClose(); this.onMove(); }}>Move</MenuItem>)}
|
||||
{canMove && (<MenuItem onClick={() => { this.handleMenuClose(); this.onCopy(); }}>Copy</MenuItem>)}
|
||||
{canRename && this.hasSelectedFileOrFolder() && !protectedItem && (<MenuItem onClick={() => {
|
||||
this.handleMenuClose(); this.onRename();
|
||||
}}>Rename</MenuItem>)}
|
||||
@@ -1013,7 +1038,7 @@ export default withStyles(
|
||||
<DraggableButtonBase key={value.pathname}
|
||||
longPressDelay={this.state.reordering ? 0 : -1}
|
||||
data-position={dataPosition}
|
||||
data-fileName={
|
||||
data-pathname={
|
||||
value.metadata ? value.metadata.fileName : null
|
||||
}
|
||||
|
||||
@@ -1047,10 +1072,20 @@ export default withStyles(
|
||||
display: "flex", flexFlow: "row nowrap", textOverflow: "ellipsis",
|
||||
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
|
||||
onDragStart={(e) => { e.preventDefault(); }}
|
||||
src={this.getTrackThumbnail(value)}
|
||||
style={{ width: 48, height: 48, margin: 8, borderRadius: 4 }} />
|
||||
|
||||
)
|
||||
}
|
||||
<div style={{
|
||||
flex: "1 1 1px", display: "flex", flexFlow: "column nowrap",
|
||||
marginLeft: 8,
|
||||
@@ -1060,7 +1095,7 @@ export default withStyles(
|
||||
className={classes.secondaryText}
|
||||
variant="body2"
|
||||
style={{ flex: "0 0 auto", textOverflow: "ellipsis", textAlign: "left", marginBottom: 4 }}>
|
||||
{this.getTrackTitle(value)}
|
||||
{this.getTrackTitle(value)}
|
||||
</Typography>
|
||||
<Typography noWrap className={classes.secondaryText}
|
||||
variant="body2"
|
||||
@@ -1115,23 +1150,24 @@ export default withStyles(
|
||||
<DialogActions style={{ justifyContent: "stretch" }}>
|
||||
{this.state.windowWidth > 500 ? (
|
||||
<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}
|
||||
onClick={() => this.handleDelete()} >
|
||||
<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}
|
||||
>
|
||||
<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}
|
||||
>
|
||||
<div>Download</div>
|
||||
</Button>
|
||||
</ButtonEx>
|
||||
|
||||
<div style={{ flex: "1 1 auto" }}> </div>
|
||||
|
||||
@@ -1144,7 +1180,8 @@ export default withStyles(
|
||||
<Button variant="dialogPrimary" style={{ flex: "0 0 auto" }}
|
||||
onClick={() => { this.openSelectedFile(); }}
|
||||
|
||||
disabled={(!this.state.hasSelection) && this.state.selectedFile !== ""} aria-label="select"
|
||||
disabled={(!canSelectFile)
|
||||
} aria-label="select"
|
||||
>
|
||||
{okButtonText}
|
||||
</Button>
|
||||
@@ -1182,7 +1219,8 @@ export default withStyles(
|
||||
<Button variant="dialogPrimary" style={{ flex: "0 0 auto" }}
|
||||
onClick={() => { this.openSelectedFile(); }}
|
||||
|
||||
disabled={(!this.state.hasSelection) && this.state.selectedFile !== ""} aria-label="select"
|
||||
disabled={!canSelectFile}
|
||||
aria-label="select"
|
||||
>
|
||||
{okButtonText}
|
||||
</Button>
|
||||
@@ -1200,6 +1238,7 @@ export default withStyles(
|
||||
this.setState({ openUploadFileDialog: false });
|
||||
}
|
||||
}
|
||||
isTracksDirectory={this.isTracksDirectory()}
|
||||
uploadPage={
|
||||
"uploadUserFile?directory=" + encodeURIComponent(this.state.currentDirectory)
|
||||
+ "&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 && (
|
||||
<RenameDialog open={this.state.newFolderDialogOpen} defaultName=""
|
||||
@@ -1244,23 +1294,28 @@ export default withStyles(
|
||||
)
|
||||
}
|
||||
{
|
||||
this.state.moveDialogOpen && (
|
||||
(this.state.moveDialogOpen || this.state.copyDialogOpen)
|
||||
&& (
|
||||
(
|
||||
<FilePropertyDirectorySelectDialog
|
||||
open={this.state.moveDialogOpen}
|
||||
dialogTitle="Move to"
|
||||
open={this.state.moveDialogOpen || this.state.copyDialogOpen}
|
||||
dialogTitle={this.state.moveDialogOpen ? "Move to" : "Copy Link to"}
|
||||
uiFileProperty={this.props.fileProperty}
|
||||
defaultPath={this.getDefaultPath()}
|
||||
selectedFile={this.state.selectedFile}
|
||||
excludeDirectory={
|
||||
this.isDirectory(this.state.selectedFile)
|
||||
? this.state.selectedFile : ""}
|
||||
onClose={() => { this.setState({ moveDialogOpen: false }); }}
|
||||
onClose={() => { this.setState({ moveDialogOpen: false, copyDialogOpen: false }); }}
|
||||
onOk={
|
||||
(path) => {
|
||||
this.setState({ moveDialogOpen: false });
|
||||
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 {
|
||||
if (this.isFolderArtwork(this.state.selectedFile)) {
|
||||
return;
|
||||
}
|
||||
if (this.isDirectory(this.state.selectedFile)) {
|
||||
this.requestFiles(this.state.selectedFile);
|
||||
this.setState({ navDirectory: this.state.selectedFile });
|
||||
@@ -1292,6 +1350,9 @@ export default withStyles(
|
||||
private onMove(): void {
|
||||
this.setState({ moveDialogOpen: true });
|
||||
}
|
||||
private onCopy(): void {
|
||||
this.setState({ copyDialogOpen: true });
|
||||
}
|
||||
private onExecuteMove(newDirectory: string) {
|
||||
let fileName = pathFileName(this.state.selectedFile);
|
||||
let oldFilePath = pathConcat(this.state.navDirectory, fileName);
|
||||
@@ -1305,6 +1366,46 @@ export default withStyles(
|
||||
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 {
|
||||
|
||||
@@ -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 TimebaseSelectorDialog from "./TimebaseselectorDialog";
|
||||
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 {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSetLoop: (start: number, loopEnable: boolean, loopStart: number, loopEnd: number) => void;
|
||||
start: number;
|
||||
loopEnable: boolean;
|
||||
loopStart: number;
|
||||
loopEnd: number;
|
||||
onSetLoop: (value: LoopParameters) => void;
|
||||
value: LoopParameters;
|
||||
playing: boolean;
|
||||
onPreview: () => void;
|
||||
onCancelPlaying: () => void;
|
||||
duration: number;
|
||||
fullScreen?: boolean;
|
||||
timebase: Timebase;
|
||||
onTimebaseChange: (timebase: Timebase) => void; // Optional callback for timebase changes
|
||||
sampleRate: number;
|
||||
@@ -74,9 +79,9 @@ function parseTime(timebase: Timebase, sampleRate: number, duration: number, tim
|
||||
if (isNaN(samples)) {
|
||||
return samples;
|
||||
}
|
||||
let result = samples / sampleRate; // Convert samples to seconds
|
||||
let result = samples / sampleRate; // Convert samples to seconds
|
||||
if (result < 0) {
|
||||
result = 0;
|
||||
result = 0;
|
||||
}
|
||||
if (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
|
||||
if (result < 0) {
|
||||
result = 0;
|
||||
result = 0;
|
||||
}
|
||||
if (result > duration) {
|
||||
result = duration;
|
||||
}
|
||||
return result;
|
||||
|
||||
|
||||
}
|
||||
case TimebaseUnits.Beats:
|
||||
{
|
||||
@@ -141,9 +146,9 @@ function parseTime(timebase: Timebase, sampleRate: number, duration: number, tim
|
||||
if (isNaN(beat)) {
|
||||
throw new Error("Invalid seconds format. Use 'bar:beat' or 'bar:beat.fraction'.");
|
||||
}
|
||||
if (beat < 1 || beat >= timebase.timeSignature.numerator+1) {
|
||||
throw new Error(`Beat number must be between 1 and ${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}.`);
|
||||
}
|
||||
} else if (parts.length === 2) {
|
||||
bar = parseInt(parts[0], 10);
|
||||
if (bar < 1) {
|
||||
@@ -153,16 +158,16 @@ function parseTime(timebase: Timebase, sampleRate: number, duration: number, tim
|
||||
if (isNaN(bar) || isNaN(beat)) {
|
||||
throw new Error("Invalid bar or beat format. Use 'bar:beat' or 'bar:beat.fraction'.");
|
||||
}
|
||||
if (beat < 1 || beat >= timebase.timeSignature.numerator+1) {
|
||||
throw new Error(`Beat number must be between 1 and ${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}.`);
|
||||
}
|
||||
} else {
|
||||
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
|
||||
let result = actualBeat *60/ timebase.tempo; // Convert beats to seconds based on tempo
|
||||
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
|
||||
if (result < 0) {
|
||||
result = 0;
|
||||
result = 0;
|
||||
}
|
||||
if (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) {
|
||||
case TimebaseUnits.Samples:
|
||||
{
|
||||
@@ -197,7 +202,7 @@ function formatTime(timebase: Timebase, sampleRate: number, seconds: number): st
|
||||
let beats = timebase.tempo / 60.0 * seconds;
|
||||
const bars = Math.floor(beats / 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;
|
||||
default:
|
||||
@@ -320,22 +325,23 @@ function SliderWithPreview(props: SliderWithPreviewProps) {
|
||||
|
||||
|
||||
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 [error, setError] = React.useState(false);
|
||||
const [editValue, setEditValue] = React.useState(props.value);
|
||||
const [focus, setFocus] = React.useState(false);
|
||||
// slice props.
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!focus ) {
|
||||
if (!focus) {
|
||||
setText(formatTime(timebase, sampleRate, props.value));
|
||||
}
|
||||
}
|
||||
},
|
||||
[props.value]);
|
||||
[props.value]);
|
||||
|
||||
React.useEffect(() => {
|
||||
setText(formatTime(props.timebase, sampleRate, props.value));
|
||||
if (!focus) {
|
||||
setText(formatTime(props.timebase, sampleRate, props.value));
|
||||
}
|
||||
}, [props.timebase, sampleRate]);
|
||||
|
||||
|
||||
@@ -348,11 +354,9 @@ function TimeEdit(props: TimeEditProps) {
|
||||
error={error}
|
||||
value={text}
|
||||
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));
|
||||
setFocus(false);
|
||||
onBlur();
|
||||
}}
|
||||
onChange={(e) => {
|
||||
try {
|
||||
@@ -362,7 +366,6 @@ function TimeEdit(props: TimeEditProps) {
|
||||
if (val > max) {
|
||||
val = max;
|
||||
}
|
||||
setEditValue(val);
|
||||
if (props.onValueChange) {
|
||||
props.onValueChange(e, val);
|
||||
}
|
||||
@@ -389,34 +392,64 @@ function TimeEdit(props: TimeEditProps) {
|
||||
autoCapitalize: "off",
|
||||
}}
|
||||
/>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
export default function LoopDialog(props: LoopDialogProps) {
|
||||
const [previewStartValue, setPreviewStartValue] = React.useState<number | null>(null);
|
||||
const [previewLoopStart, setPreviewLoopStart] = React.useState<number | null>(null);
|
||||
const [previewLoopEnd, setPreviewLoopEnd] = React.useState<number | null>(null);
|
||||
const [start, setStart] = React.useState(props.value.start);
|
||||
const [loopEnable, setLoopEnable] = React.useState(props.value.loopEnable);
|
||||
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 (
|
||||
<DialogEx
|
||||
tag="LoopDialog"
|
||||
onClose={props.onClose}
|
||||
fullScreen={false}
|
||||
onClose={() => {
|
||||
commitResults();
|
||||
props.onClose();
|
||||
}}
|
||||
|
||||
maxWidth="xl"
|
||||
open={props.isOpen}
|
||||
onEnterKey={() => {
|
||||
}}
|
||||
|
||||
fullScreen={height < 500}
|
||||
sx={{
|
||||
"& .MuiDialog-container": {
|
||||
"& .MuiPaper-root": {
|
||||
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"
|
||||
aria-label="back"
|
||||
style={{ opacity: 0.6 }}
|
||||
onClick={() => { props.onClose(); }}
|
||||
onClick={() => {
|
||||
commitResults();
|
||||
props.onClose();
|
||||
}}
|
||||
>
|
||||
<ArrowBackIcon style={{ width: 24, height: 24 }} />
|
||||
</IconButton>
|
||||
@@ -449,8 +485,8 @@ export default function LoopDialog(props: LoopDialogProps) {
|
||||
<DialogContent style={{}}>
|
||||
<div style={{
|
||||
position: "relative",
|
||||
display: "flex", justifyContent: "stretch", flexFlow: "column nowrap", marginLeft: 32, marginRight: 32,
|
||||
marginBottom: 16
|
||||
display: "flex", justifyContent: "stretch", flexFlow: "column nowrap", marginLeft: 24, marginRight: 24,
|
||||
marginBottom: 8
|
||||
|
||||
}}>
|
||||
<Typography display="block" variant="body1" gutterBottom style={{}}>
|
||||
@@ -460,53 +496,44 @@ export default function LoopDialog(props: LoopDialogProps) {
|
||||
style={{
|
||||
width: "100%"
|
||||
}}
|
||||
value={props.start}
|
||||
value={start}
|
||||
min={0}
|
||||
max={props.duration}
|
||||
|
||||
onChange={(e, v, thumb) => {
|
||||
let val = Array.isArray(v) ? v[0] : v
|
||||
|
||||
if (props.onSetLoop) {
|
||||
props.onSetLoop(val, props.loopEnable, props.loopStart, props.loopEnd);
|
||||
}
|
||||
setStart(val);
|
||||
}}
|
||||
onPreview={(e, v, thumb) => {
|
||||
let val = Array.isArray(v) ? v[0] : v
|
||||
|
||||
setPreviewStartValue(val);
|
||||
setStart(val);
|
||||
}}
|
||||
onCommitPreviewValue={(e, v, thumb) => {
|
||||
let val = Array.isArray(v) ? v[0] : v
|
||||
|
||||
setPreviewStartValue(null);
|
||||
if (props.onSetLoop) {
|
||||
props.onSetLoop(val, props.loopEnable, props.loopStart, props.loopEnd);
|
||||
}
|
||||
setStart(val);
|
||||
cancelPlaying();
|
||||
|
||||
}}
|
||||
valueLabelFormat={(v) => formatTime(props.timebase, props.sampleRate, v)}
|
||||
/>
|
||||
|
||||
<TimeEdit variant="standard" spellCheck="false"
|
||||
value={previewStartValue ? previewStartValue : props.start}
|
||||
value={start}
|
||||
max={props.duration}
|
||||
timebase={props.timebase}
|
||||
sampleRate={props.sampleRate}
|
||||
|
||||
|
||||
onBlur={() => {
|
||||
let t = props.start;
|
||||
if (t > props.duration) {
|
||||
t = props.duration;
|
||||
if (props.onSetLoop) {
|
||||
props.onSetLoop(t, props.loopEnable, props.loopStart, props.loopEnd);
|
||||
}
|
||||
if (start > props.duration) {
|
||||
setStart(props.duration);
|
||||
}
|
||||
}}
|
||||
onValueChange={(e, val) => {
|
||||
if (props.onSetLoop) {
|
||||
props.onSetLoop(val, props.loopEnable, props.loopStart, props.loopEnd);
|
||||
}
|
||||
setStart(val);
|
||||
cancelPlaying();
|
||||
}
|
||||
}
|
||||
style={{ width: 120, alignSelf: "center", textAlign: "center" }} />
|
||||
@@ -514,9 +541,10 @@ export default function LoopDialog(props: LoopDialogProps) {
|
||||
<FormControlLabel
|
||||
labelPlacement="start"
|
||||
style={{ marginLeft: 0 }}
|
||||
control={<Checkbox checked={props.loopEnable}
|
||||
control={<Checkbox checked={loopEnable}
|
||||
onChange={(e, checked) => {
|
||||
props.onSetLoop(props.start, !props.loopEnable, props.loopStart, props.loopEnd);
|
||||
setLoopEnable(checked);
|
||||
cancelPlaying();
|
||||
}}
|
||||
|
||||
/>} label="Enable loop" />
|
||||
@@ -525,102 +553,92 @@ export default function LoopDialog(props: LoopDialogProps) {
|
||||
<SliderWithPreview
|
||||
style={{
|
||||
width: "100%",
|
||||
opacity: props.loopEnable ? 1 : 0.4,
|
||||
opacity: loopEnable ? 1 : 0.4,
|
||||
}}
|
||||
disabled={!props.loopEnable}
|
||||
disabled={!loopEnable}
|
||||
value={
|
||||
previewLoopStart != null ?
|
||||
[previewLoopStart as number, previewLoopEnd as number]
|
||||
: [props.loopStart, props.loopEnd]}
|
||||
[loopStart, loopEnd]
|
||||
}
|
||||
min={0}
|
||||
max={props.duration}
|
||||
onChange={(e, v, thumb) => {
|
||||
if (!Array.isArray(v)) throw new Error("Expected array for loop start and end preview");
|
||||
if (props.onSetLoop) {
|
||||
props.onSetLoop(props.start, props.loopEnable, v[0], v[1]);
|
||||
}
|
||||
if (!Array.isArray(v)) throw new Error("Expected array for loop start and end");
|
||||
|
||||
setLoopStart(v[0]);
|
||||
setLoopEnd(v[1]);
|
||||
}}
|
||||
onPreview={(e, v, thumb) => {
|
||||
if (!Array.isArray(v)) throw new Error("Expected array for loop start and end preview");
|
||||
setPreviewLoopStart(v[0]);
|
||||
setPreviewLoopEnd(v[1]);
|
||||
if (!Array.isArray(v)) throw new Error("Expected array for loop start and end");
|
||||
|
||||
setLoopStart(v[0]);
|
||||
setLoopEnd(v[1]);
|
||||
}}
|
||||
onCommitPreviewValue={(e, v, thumb) => {
|
||||
if (!Array.isArray(v)) throw new Error("Expected array for loop start and end preview");
|
||||
setPreviewLoopStart(null);
|
||||
setPreviewLoopEnd(null);
|
||||
props.onSetLoop(props.start, props.loopEnable, v[0], v[1]);
|
||||
if (!Array.isArray(v)) throw new Error("Expected array for loop start and end");
|
||||
|
||||
setLoopStart(v[0]);
|
||||
setLoopEnd(v[1]);
|
||||
cancelPlaying();
|
||||
|
||||
}}
|
||||
|
||||
/>
|
||||
<div style={{ display: "flex", justifyContent: "space-evenly", columnGap: 8 }}>
|
||||
<TimeEdit variant="standard" spellCheck="false"
|
||||
value={previewLoopStart ? previewLoopStart : props.loopStart}
|
||||
disabled={!props.loopEnable}
|
||||
value={loopStart}
|
||||
disabled={!loopEnable}
|
||||
max={props.duration}
|
||||
timebase={props.timebase}
|
||||
sampleRate={props.sampleRate}
|
||||
|
||||
style={{
|
||||
maxWidth: 120, textAlign: "center", opacity: props.loopEnable ? 1 : 0.4,
|
||||
maxWidth: 120, textAlign: "center", opacity: loopEnable ? 1 : 0.4,
|
||||
flex: "1 1 auto"
|
||||
}}
|
||||
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) => {
|
||||
if (props.onSetLoop) {
|
||||
props.onSetLoop(props.start, props.loopEnable, val, props.loopEnd);
|
||||
}
|
||||
setLoopStart(val);
|
||||
cancelPlaying();
|
||||
|
||||
|
||||
}}
|
||||
/>
|
||||
<TimeEdit variant="standard" spellCheck="false"
|
||||
max={props.duration}
|
||||
|
||||
value={previewLoopEnd ? previewLoopEnd : props.loopEnd}
|
||||
value={loopEnd}
|
||||
sampleRate={props.sampleRate}
|
||||
|
||||
disabled={!props.loopEnable}
|
||||
disabled={!loopEnable}
|
||||
|
||||
timebase={props.timebase}
|
||||
|
||||
style={{
|
||||
maxWidth: 120, textAlign: "center", opacity: props.loopEnable ? 1 : 0.4,
|
||||
maxWidth: 120, textAlign: "center",
|
||||
opacity: loopEnable ? 1 : 0.4,
|
||||
flex: "1 1 auto"
|
||||
}}
|
||||
|
||||
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) => {
|
||||
if (props.onSetLoop) {
|
||||
props.onSetLoop(props.start, props.loopEnable, props.loopStart, val);
|
||||
}
|
||||
setLoopEnd(val);
|
||||
cancelPlaying();
|
||||
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Button variant="dialogSecondary" style={{ alignSelf: "end", marginTop: 16, width: 120 }}
|
||||
startIcon={
|
||||
props.playing ? (<StopIcon />) : (<PlayArrow />)
|
||||
}
|
||||
onClick={() => {
|
||||
commitResults();
|
||||
props.onPreview();
|
||||
}}>
|
||||
Preview
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
</DialogContent>
|
||||
|
||||
@@ -21,12 +21,14 @@ import { SyntheticEvent } from 'react';
|
||||
import { Theme } from '@mui/material/styles';
|
||||
import WithStyles, {withTheme} from './WithStyles';
|
||||
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 {
|
||||
Pedalboard, PedalboardItem, PedalboardSplitItem, SplitType
|
||||
} from './Pedalboard';
|
||||
import Button from '@mui/material/Button';
|
||||
import InputIcon from '@mui/icons-material/Input';
|
||||
import LoadPluginDialog from './LoadPluginDialog';
|
||||
import Switch from '@mui/material/Switch';
|
||||
@@ -34,7 +36,6 @@ import Typography from '@mui/material/Typography';
|
||||
|
||||
import PedalboardView from './PedalboardView';
|
||||
import { PiPedalStateError } from './PiPedalError';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import Menu from '@mui/material/Menu';
|
||||
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={{ 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>
|
||||
{
|
||||
@@ -541,9 +544,9 @@ export const MainPage =
|
||||
{this.props.enableStructureEditing && (
|
||||
<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 }}>
|
||||
<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 }} />
|
||||
</IconButton>
|
||||
</IconButtonEx>
|
||||
<Menu
|
||||
id="add-menu"
|
||||
anchorEl={this.state.addMenuAnchorEl}
|
||||
@@ -560,17 +563,18 @@ export const MainPage =
|
||||
</Menu>
|
||||
</div>
|
||||
<div style={{ flex: "0 0 auto", display: canDelete ? "block" : "none", paddingRight: 8 }}>
|
||||
<IconButton
|
||||
<IconButtonEx tooltip="Delete pedal"
|
||||
onClick={() => { this.onDeletePedal(pedalboardItem?.instanceId ?? -1) }}
|
||||
size="large">
|
||||
<OldDeleteIcon style={{ height: 24, width: 24, fill: this.props.theme.palette.text.primary, opacity: 0.6 }} />
|
||||
</IconButton>
|
||||
</IconButtonEx>
|
||||
</div>
|
||||
<div style={{ flex: "0 0 auto" }}>
|
||||
<Button
|
||||
<ButtonEx
|
||||
variant="contained"
|
||||
color="primary"
|
||||
size="small"
|
||||
tooltip="Load plugin"
|
||||
onClick={this.onLoadClick}
|
||||
disabled={this.state.selectedPedal === -1 || (!canLoad) || (this.getSelectedPedalboardItem()?.isSplit() ?? true)}
|
||||
startIcon={<InputIcon />}
|
||||
@@ -580,21 +584,21 @@ export const MainPage =
|
||||
}}
|
||||
>
|
||||
Load
|
||||
</Button>
|
||||
</ButtonEx>
|
||||
</div>
|
||||
<div style={{ flex: "0 0 auto" }}>
|
||||
<IconButton
|
||||
<IconButtonEx tooltip="MIDI bindings"
|
||||
onClick={(e) => { this.handleMidiConfiguration(instanceId); }}
|
||||
size="large">
|
||||
<MidiIcon style={{ height: 24, width: 24, fill: this.props.theme.palette.text.primary, opacity: 0.6 }} />
|
||||
</IconButton>
|
||||
</IconButtonEx>
|
||||
</div>
|
||||
<div style={{ flex: "0 0 auto" }}>
|
||||
<IconButton
|
||||
<IconButtonEx tooltip="Snapshots"
|
||||
onClick={(e) => { this.setState({ snapshotDialogOpen: true }); }}
|
||||
size="large">
|
||||
{this.snapshotIcon(this.props.theme, this.state.selectedSnapshot)}
|
||||
</IconButton>
|
||||
</IconButtonEx>
|
||||
</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> {
|
||||
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 WithStyles, { withTheme } from './WithStyles';
|
||||
import { createStyles } from './WithStyles';
|
||||
import ControlTooltip from './ControlTooltip';
|
||||
|
||||
import { withStyles } from "tss-react/mui";
|
||||
import { UiControl, ScalePoint } from './Lv2Plugin';
|
||||
@@ -35,7 +36,7 @@ import MenuItem from '@mui/material/MenuItem';
|
||||
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
|
||||
import DialIcon from './svg/fx_dial.svg?react';
|
||||
import { isDarkMode } from './DarkMode';
|
||||
import ControlTooltip from './ControlTooltip';
|
||||
|
||||
|
||||
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
|
||||
import StopIcon from '@mui/icons-material/Stop';
|
||||
@@ -699,45 +700,51 @@ const PluginControl =
|
||||
if (control.isOnOffSwitch()) {
|
||||
// normal gray unchecked state.
|
||||
return (
|
||||
<Switch checked={value !== 0} color="primary"
|
||||
onChange={(event) => {
|
||||
this.onCheckChanged(event.target.checked);
|
||||
}}
|
||||
/>
|
||||
<ControlTooltip uiControl={control}>
|
||||
<Switch checked={value !== 0} color="primary"
|
||||
onChange={(event) => {
|
||||
this.onCheckChanged(event.target.checked);
|
||||
}}
|
||||
/>
|
||||
</ControlTooltip>
|
||||
);
|
||||
}
|
||||
if (control.isAbToggle()) {
|
||||
let classes = withStyles.getClasses(this.props);
|
||||
// unchecked color is not gray.
|
||||
return (
|
||||
<Switch checked={value !== 0} color="primary"
|
||||
onChange={(event) => {
|
||||
this.onCheckChanged(event.target.checked);
|
||||
}}
|
||||
classes={{
|
||||
track: classes.switchTrack
|
||||
}}
|
||||
style={{ color: this.props.theme.palette.primary.main }}
|
||||
/>
|
||||
<ControlTooltip uiControl={control}>
|
||||
<Switch checked={value !== 0} color="primary"
|
||||
onChange={(event) => {
|
||||
this.onCheckChanged(event.target.checked);
|
||||
}}
|
||||
classes={{
|
||||
track: classes.switchTrack
|
||||
}}
|
||||
style={{ color: this.props.theme.palette.primary.main }}
|
||||
/>
|
||||
</ControlTooltip>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Select variant="standard"
|
||||
ref={this.selectRef}
|
||||
value={control.clampSelectValue(value)}
|
||||
onChange={this.onSelectChanged}
|
||||
inputProps={{
|
||||
name: control.name,
|
||||
id: 'id' + control.symbol,
|
||||
style: { fontSize: FONT_SIZE }
|
||||
}}
|
||||
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>
|
||||
<ControlTooltip uiControl={control}>
|
||||
<Select variant="standard"
|
||||
ref={this.selectRef}
|
||||
value={control.clampSelectValue(value)}
|
||||
onChange={this.onSelectChanged}
|
||||
inputProps={{
|
||||
name: control.name,
|
||||
id: 'id' + control.symbol,
|
||||
style: { fontSize: FONT_SIZE }
|
||||
}}
|
||||
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>
|
||||
|
||||
))}
|
||||
</Select>
|
||||
))}
|
||||
</Select>
|
||||
</ControlTooltip>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -919,12 +926,10 @@ const PluginControl =
|
||||
alignSelf: "stretch", marginBottom: 8, marginLeft: isSelect ? 8 : 0, marginRight: 0
|
||||
|
||||
}}>
|
||||
<ControlTooltip uiControl={control} >
|
||||
<Typography variant="caption" display="block" noWrap style={{
|
||||
width: "100%",
|
||||
textAlign: isSelect ? "left" : "center"
|
||||
}}> {isButton ? "\u00A0" : control.name}</Typography>
|
||||
</ControlTooltip>
|
||||
<Typography variant="caption" display="block" noWrap style={{
|
||||
width: "100%",
|
||||
textAlign: isSelect ? "left" : "center"
|
||||
}}> {isButton ? "\u00A0" : control.name}</Typography>
|
||||
</div>
|
||||
{/* CONTROL SECTION */}
|
||||
|
||||
@@ -933,81 +938,86 @@ const PluginControl =
|
||||
{isButton ?
|
||||
(
|
||||
control.name.length !== 1 ? (
|
||||
<Button variant="contained" color="primary" size="small"
|
||||
onMouseDown={
|
||||
(evt) => { this.handleButtonMouseDown(buttonStyle); }
|
||||
}
|
||||
onMouseUp={
|
||||
(evt) => { this.handleButtonMouseUp(buttonStyle); }
|
||||
}
|
||||
onTouchStart={
|
||||
(evt) => {
|
||||
evt.preventDefault();
|
||||
this.handleButtonMouseDown(buttonStyle);
|
||||
<ControlTooltip uiControl={control}>
|
||||
|
||||
<Button variant="contained" color="primary" size="small"
|
||||
onMouseDown={
|
||||
(evt) => { this.handleButtonMouseDown(buttonStyle); }
|
||||
}
|
||||
}
|
||||
onTouchEnd={
|
||||
(evt) => {
|
||||
evt.preventDefault();
|
||||
this.handleButtonMouseUp(buttonStyle);
|
||||
onMouseUp={
|
||||
(evt) => { this.handleButtonMouseUp(buttonStyle); }
|
||||
}
|
||||
}
|
||||
onMouseLeave={(
|
||||
(evet) => { this.handleButtonMouseLeave(buttonStyle); }
|
||||
)}
|
||||
onTouchStart={
|
||||
(evt) => {
|
||||
evt.preventDefault();
|
||||
this.handleButtonMouseDown(buttonStyle);
|
||||
}
|
||||
}
|
||||
onTouchEnd={
|
||||
(evt) => {
|
||||
evt.preventDefault();
|
||||
this.handleButtonMouseUp(buttonStyle);
|
||||
}
|
||||
}
|
||||
onMouseLeave={(
|
||||
(evet) => { this.handleButtonMouseLeave(buttonStyle); }
|
||||
)}
|
||||
|
||||
style={{
|
||||
textTransform: "none",
|
||||
background: (isDarkMode() ? "#6750A4" : undefined),
|
||||
marginLeft: 8, marginRight: 8, minWidth: 60,
|
||||
marginTop: 0
|
||||
style={{
|
||||
textTransform: "none",
|
||||
background: (isDarkMode() ? "#6750A4" : undefined),
|
||||
marginLeft: 8, marginRight: 8, minWidth: 60,
|
||||
marginTop: 0
|
||||
|
||||
}}
|
||||
}}
|
||||
|
||||
>
|
||||
{control.name}
|
||||
</Button>
|
||||
>
|
||||
{control.name}
|
||||
</Button>
|
||||
</ControlTooltip>
|
||||
|
||||
) : (
|
||||
<Button variant="contained" color="primary" size="small"
|
||||
onMouseDown={
|
||||
(evt) => { this.handleButtonMouseDown(buttonStyle); }
|
||||
}
|
||||
onMouseUp={
|
||||
(evt) => { this.handleButtonMouseUp(buttonStyle); }
|
||||
}
|
||||
onTouchStart={
|
||||
(evt) => {
|
||||
evt.preventDefault();
|
||||
this.handleButtonMouseDown(buttonStyle);
|
||||
<ControlTooltip uiControl={control}>
|
||||
<Button variant="contained" color="primary" size="small"
|
||||
onMouseDown={
|
||||
(evt) => { this.handleButtonMouseDown(buttonStyle); }
|
||||
}
|
||||
}
|
||||
onTouchEnd={
|
||||
(evt) => {
|
||||
evt.preventDefault();
|
||||
this.handleButtonMouseUp(buttonStyle);
|
||||
onMouseUp={
|
||||
(evt) => { this.handleButtonMouseUp(buttonStyle); }
|
||||
}
|
||||
}
|
||||
onMouseLeave={(
|
||||
(evet) => { this.handleButtonMouseLeave(buttonStyle); }
|
||||
)}
|
||||
onTouchStart={
|
||||
(evt) => {
|
||||
evt.preventDefault();
|
||||
this.handleButtonMouseDown(buttonStyle);
|
||||
}
|
||||
}
|
||||
onTouchEnd={
|
||||
(evt) => {
|
||||
evt.preventDefault();
|
||||
this.handleButtonMouseUp(buttonStyle);
|
||||
}
|
||||
}
|
||||
onMouseLeave={(
|
||||
(evet) => { this.handleButtonMouseLeave(buttonStyle); }
|
||||
)}
|
||||
|
||||
style={{
|
||||
textTransform: "none",
|
||||
background: (isDarkMode() ? "#6750A4" : undefined),
|
||||
marginLeft: 8, marginRight: 8,
|
||||
paddingLeft: 0, paddingRight: 0,
|
||||
width: 36, height: 36,
|
||||
marginTop: 0,
|
||||
borderRadius: 8,
|
||||
minWidth: 0,
|
||||
fontSize: "1.2em"
|
||||
style={{
|
||||
textTransform: "none",
|
||||
background: (isDarkMode() ? "#6750A4" : undefined),
|
||||
marginLeft: 8, marginRight: 8,
|
||||
paddingLeft: 0, paddingRight: 0,
|
||||
width: 36, height: 36,
|
||||
marginTop: 0,
|
||||
borderRadius: 8,
|
||||
minWidth: 0,
|
||||
fontSize: "1.2em"
|
||||
|
||||
}}
|
||||
}}
|
||||
|
||||
>
|
||||
{androidEmoji(control.name)}
|
||||
</Button>
|
||||
>
|
||||
{androidEmoji(control.name)}
|
||||
</Button>
|
||||
</ControlTooltip>
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1016,34 +1026,38 @@ const PluginControl =
|
||||
)
|
||||
: (isGraphicEq) ? (
|
||||
<div style={{ flex: "0 1 auto" }}>
|
||||
<GraphicEqCtl
|
||||
imgRef={this.imgRef}
|
||||
position={this.getEqPosition()}
|
||||
dialColor={dialColor}
|
||||
opacity={DEFAULT_OPACITY}
|
||||
<ControlTooltip uiControl={control}>
|
||||
<GraphicEqCtl
|
||||
imgRef={this.imgRef}
|
||||
position={this.getEqPosition()}
|
||||
dialColor={dialColor}
|
||||
opacity={DEFAULT_OPACITY}
|
||||
|
||||
onTouchStart={this.onTouchStart}
|
||||
onTouchMove={this.onTouchMove}
|
||||
onPointerDown={this.onPointerDown} onPointerUp={this.onPointerUp}
|
||||
onPointerMoveCapture={this.onPointerMove}
|
||||
onDrag={this.onDrag}
|
||||
onTouchStart={this.onTouchStart}
|
||||
onTouchMove={this.onTouchMove}
|
||||
onPointerDown={this.onPointerDown} onPointerUp={this.onPointerUp}
|
||||
onPointerMoveCapture={this.onPointerMove}
|
||||
onDrag={this.onDrag}
|
||||
|
||||
|
||||
/>
|
||||
/>
|
||||
</ControlTooltip>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ flex: "0 1 auto" }}>
|
||||
<DialIcon ref={this.imgRef}
|
||||
style={{
|
||||
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}
|
||||
onPointerMoveCapture={this.onPointerMove}
|
||||
onDrag={this.onDrag}
|
||||
<ControlTooltip uiControl={control}>
|
||||
<DialIcon ref={this.imgRef}
|
||||
style={{
|
||||
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}
|
||||
onPointerMoveCapture={this.onPointerMove}
|
||||
onDrag={this.onDrag}
|
||||
|
||||
/>
|
||||
/>
|
||||
</ControlTooltip>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1079,11 +1093,11 @@ const PluginControl =
|
||||
},
|
||||
}}
|
||||
|
||||
inputRef={this.inputRef} onChange={this.onInputChange}
|
||||
onBlur={this.onInputLostFocus}
|
||||
onFocus={this.onInputFocus}
|
||||
inputRef={this.inputRef} onChange={this.onInputChange}
|
||||
onBlur={this.onInputLostFocus}
|
||||
onFocus={this.onInputFocus}
|
||||
|
||||
onKeyPress={this.onInputKeyPress} />
|
||||
onKeyPress={this.onInputKeyPress} />
|
||||
<div className={classes.displayValue}
|
||||
ref={this.displayValueRef} onClick={(e) => { this.inputRef.current!.focus(); }}
|
||||
style={{ display: this.state.editFocused ? "none" : "block" }}
|
||||
|
||||
@@ -472,7 +472,6 @@ const PluginControlView =
|
||||
throw new PiPedalStateError("Missing control value.");
|
||||
}
|
||||
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) }}
|
||||
@@ -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.
|
||||
|
||||
import React from 'react';
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
import { Theme } from '@mui/material/styles';
|
||||
import WithStyles from './WithStyles';
|
||||
import { createStyles } from './WithStyles';
|
||||
@@ -27,8 +28,7 @@ import DialogEx from './DialogEx';
|
||||
import MuiDialogTitle from '@mui/material/DialogTitle';
|
||||
import MuiDialogContent from '@mui/material/DialogContent';
|
||||
import MuiDialogActions from '@mui/material/DialogActions';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import IconButtonEx from './IconButtonEx';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Grid from '@mui/material/Grid';
|
||||
import { PiPedalModelFactory } from "./PiPedalModel";
|
||||
@@ -255,32 +255,36 @@ const PluginInfoDialog = withStyles((props: PluginInfoProps) => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<IconButton
|
||||
<IconButtonEx tooltip="Plugin info"
|
||||
style={{ display: (props.plugin_uri !== "") ? "inline-flex" : "none" }}
|
||||
onClick={handleClickOpen}
|
||||
size="large">
|
||||
<InfoOutlinedIcon className={classes.icon} color='inherit' />
|
||||
</IconButton>
|
||||
</IconButtonEx>
|
||||
{open && (
|
||||
<DialogEx tag="info" onClose={handleClose} open={open} fullWidth maxWidth="md"
|
||||
onEnterKey={handleClose}
|
||||
>
|
||||
<MuiDialogTitle >
|
||||
<div style={{ display: "flex", flexDirection: "row", alignItems: "start", flexWrap: "nowrap" }}>
|
||||
<div style={{ flex: "0 0 auto", marginRight: 16 }}>
|
||||
<PluginIcon pluginType={plugin.plugin_type} offsetY={3} />
|
||||
<div style={{ display: "flex", flexDirection: "row", alignItems: "center", flexWrap: "nowrap" }}>
|
||||
|
||||
<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 style={{ flex: "1 1 auto" }}>
|
||||
<Typography variant="h6">{plugin.name}</Typography>
|
||||
</div>
|
||||
<IconButton
|
||||
aria-label="close"
|
||||
className={classes.closeButton}
|
||||
onClick={() => handleClose()}
|
||||
style={{ flex: "0 0 auto" }}
|
||||
size="large">
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</div>
|
||||
</MuiDialogTitle>
|
||||
<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.
|
||||
|
||||
import { SyntheticEvent, Component } from 'react';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import IconButtonEx from './IconButtonEx';
|
||||
import { PiPedalModel, PiPedalModelFactory, PluginPresetsChangedHandle } from './PiPedalModel';
|
||||
import { Theme } from '@mui/material/styles';
|
||||
import WithStyles from './WithStyles';
|
||||
@@ -293,9 +293,11 @@ const PluginPresetSelector =
|
||||
}
|
||||
return (
|
||||
<div >
|
||||
<IconButton onClick={(e)=> this.handlePresetMenuClick(e)} size="large">
|
||||
<IconButtonEx
|
||||
tooltip="Plugin presets"
|
||||
onClick={(e)=> this.handlePresetMenuClick(e)} size="large">
|
||||
<PluginPresetsIcon className={classes.pluginIcon}/>
|
||||
</IconButton>
|
||||
</IconButtonEx>
|
||||
<Menu
|
||||
id="edit-plugin-presets-menu"
|
||||
anchorEl={this.state.presetsMenuAnchorRef}
|
||||
|
||||
@@ -26,8 +26,7 @@ import { styled } from '@mui/material/styles';
|
||||
import LoopDialog from './LoopDialog';
|
||||
import Box from '@mui/material/Box';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Button from '@mui/material/Button';
|
||||
import Slider from '@mui/material/Slider';
|
||||
import ButtonEx from './ButtonEx';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Pause from '@mui/icons-material/Pause';
|
||||
import PlayArrow from '@mui/icons-material/PlayArrow';
|
||||
@@ -44,19 +43,23 @@ import { Divider } from '@mui/material';
|
||||
import useWindowSize from './UseWindowSize';
|
||||
import { getAlbumArtUri } from './AudioFileMetadata';
|
||||
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"
|
||||
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 {
|
||||
// Must match PluginState values in ToobPlayer.hpp
|
||||
static Idle = 0.0;
|
||||
static CuePlaying = 1;
|
||||
static CuePlayPaused = 2.0;
|
||||
static Pausing = 3;
|
||||
static Paused = 4;
|
||||
static Playing = 5;
|
||||
static Error = 6;
|
||||
// Must match ProcessorState values in Lv2AudioFileProcessor.hpp
|
||||
// Not an enum because it has to be json serializable.
|
||||
static Idle = 0;
|
||||
static Recording = 1;
|
||||
static StoppingRecording = 2;
|
||||
static CuePlayingThenPlay = 3;
|
||||
static CuePlayingThenPause = 4;
|
||||
static Paused = 5;
|
||||
static Playing = 6;
|
||||
static Error = 7;
|
||||
};
|
||||
|
||||
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 === "") {
|
||||
artist = albumArtist;
|
||||
}
|
||||
@@ -119,7 +187,7 @@ const WidgetBorders = styled('div')(({ theme }) => ({
|
||||
backgroundColor: 'rgba(255,255,255,0.4)',
|
||||
boxShadow: "1px 4px 12px rgba(0,0,0,0.2)",
|
||||
...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 {
|
||||
@@ -192,32 +254,41 @@ export default function ToobPlayerControl(
|
||||
) {
|
||||
|
||||
const model = PiPedalModelFactory.getInstance();
|
||||
const sampleRate = model.jackConfiguration.get().sampleRate == 0 ?
|
||||
48000 :
|
||||
model.jackConfiguration.get().sampleRate;
|
||||
|
||||
|
||||
const defaultCoverArt = "/img/default_album.jpg";
|
||||
const [serverConnected, setServerConnected] = React.useState(model.state.get() == State.Ready);
|
||||
const [duration, setDuration] = React.useState(0.0);
|
||||
const [position, setPosition] = React.useState(0.0);
|
||||
const [start, setStart] = React.useState(0.0);
|
||||
const [loopStart, setLoopStart] = React.useState(0.0);
|
||||
const [loopEnable, setLoopEnable] = React.useState(false);
|
||||
const [loopEnd, setLoopEnd] = React.useState(0.0);
|
||||
const [dragging, setDragging] = React.useState(false);
|
||||
const [pluginState, setPluginState] = React.useState(0.0);
|
||||
const [sliderValue, setSliderValue] = React.useState(0.0);
|
||||
const [coverArt, setCoverArt] = React.useState(defaultCoverArt);
|
||||
const [audioFile, setAudioFile] = React.useState("");
|
||||
const [position, setPosition] = React.useState(0.0);
|
||||
//let position = 0;
|
||||
const [title, setTitle] = React.useState("");
|
||||
const [album, setAlbum] = React.useState("");
|
||||
const [artist, setArtist] = React.useState("");
|
||||
const [albumArtist, setAlbumArtist] = React.useState("");
|
||||
const [showFileDialog, setShowFileDialog] = React.useState(false);
|
||||
const [showLoopDialog, setShowLoopDialog] = React.useState(false);
|
||||
const [timebase,setTimebase] = React.useState(
|
||||
const [timebase, setTimebase] = React.useState<Timebase>(
|
||||
{
|
||||
units: TimebaseUnits.Seconds,
|
||||
tempo: 120.0,
|
||||
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();
|
||||
@@ -237,12 +308,6 @@ export default function ToobPlayerControl(
|
||||
function SelectFile() {
|
||||
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) {
|
||||
setAudioFile(path);
|
||||
if (path === "") {
|
||||
@@ -257,7 +322,7 @@ export default function ToobPlayerControl(
|
||||
.then((metadata) => {
|
||||
let strTrack: string = "";
|
||||
if (metadata.track > 0) {
|
||||
let disc = (metadata.track /1000);
|
||||
let disc = (metadata.track / 1000);
|
||||
if (disc >= 1) {
|
||||
strTrack = (metadata.track % 1000).toString() + '/' + Math.floor(disc).toString();
|
||||
} else {
|
||||
@@ -265,14 +330,14 @@ export default function ToobPlayerControl(
|
||||
}
|
||||
strTrack += ". ";
|
||||
}
|
||||
let coverArtUri = getAlbumArtUri(model,metadata,path);
|
||||
let coverArtUri = getAlbumArtUri(model, metadata, path);
|
||||
setCoverArt(coverArtUri);
|
||||
setTitle(strTrack + metadata.title);
|
||||
setAlbum(metadata.album);
|
||||
setArtist(metadata.artist);
|
||||
setAlbumArtist(metadata.albumArtist);
|
||||
})
|
||||
.catch((e)=>{
|
||||
.catch((e) => {
|
||||
setTitle("#error" + e.message);
|
||||
setAlbum("");
|
||||
});
|
||||
@@ -298,7 +363,7 @@ export default function ToobPlayerControl(
|
||||
<IconButton
|
||||
style={{ opacity: (pluginState === PluginState.Idle ? 0.2 : 0.7) }}
|
||||
onClick={() => {
|
||||
if (position > 3) {
|
||||
if (position > start + 3.0) {
|
||||
model.sendPedalboardControlTrigger(
|
||||
props.instanceId,
|
||||
"stop",
|
||||
@@ -394,12 +459,12 @@ export default function ToobPlayerControl(
|
||||
}
|
||||
function loopButtonText() {
|
||||
if (start === 0 && !loopEnable) {
|
||||
return "Set loop";
|
||||
return "Set loop";
|
||||
|
||||
} else if (loopEnable) {
|
||||
return `${formatDuration(start)} [${formatDuration(loopStart)} - ${formatDuration(loopEnd)}]`;
|
||||
return `${formatTimeCompact(timebase, sampleRate, start)} [${formatTimeCompact(timebase, sampleRate, loopStart)} - ${formatTimeCompact(timebase, sampleRate, loopEnd)}]`;
|
||||
} else {
|
||||
return `Start: ${formatDuration(start)}`;
|
||||
return `Start: ${formatTimeCompact(timebase, sampleRate, start)}`;
|
||||
}
|
||||
}
|
||||
function getUiFileProperty(uri: string): UiFileProperty {
|
||||
@@ -415,6 +480,39 @@ export default function ToobPlayerControl(
|
||||
}
|
||||
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() {
|
||||
model.getNextAudioFile(audioFile)
|
||||
.then((file) => {
|
||||
@@ -441,17 +539,11 @@ export default function ToobPlayerControl(
|
||||
});
|
||||
}
|
||||
function OnSeek(value: number) {
|
||||
setDragging(true);
|
||||
model.setPatchProperty(props.instanceId, Player__seek, value)
|
||||
.then(() => {
|
||||
setPosition(value);
|
||||
setDragging(false);
|
||||
}).catch((e) => {
|
||||
console.warn("Seek error. " + e.toString());
|
||||
setPosition(value);
|
||||
setDragging(false);
|
||||
});
|
||||
setSliderValue(value);
|
||||
}
|
||||
function onStateChanged(value: State) {
|
||||
setServerConnected(value === State.Ready);
|
||||
@@ -461,7 +553,7 @@ export default function ToobPlayerControl(
|
||||
if (model.state.get() !== State.Ready) {
|
||||
// wait for it.
|
||||
return () => {
|
||||
model.state.removeOnChangedHandler
|
||||
model.state.removeOnChangedHandler(onStateChanged);
|
||||
}
|
||||
}
|
||||
let durationHandle = model.monitorPort(props.instanceId, "duration", 1.0 / 15,
|
||||
@@ -474,27 +566,7 @@ export default function ToobPlayerControl(
|
||||
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,
|
||||
(value) => {
|
||||
setPluginState(value);
|
||||
@@ -507,44 +579,81 @@ export default function ToobPlayerControl(
|
||||
if (typeof (atomObject) === "object") {
|
||||
let path = atomObject.value;
|
||||
onAudioFileChanged(path);
|
||||
} else if (typeof(atomObject) === "string")
|
||||
{
|
||||
} else if (typeof (atomObject) === "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)
|
||||
.then((o) => {
|
||||
let path = o.value;
|
||||
onAudioFileChanged(path);
|
||||
});
|
||||
model.getPatchProperty(props.instanceId, LOOP_PROPERTY_URI)
|
||||
.then((o) => {
|
||||
onLoopPropertyChanged(o as string);
|
||||
})
|
||||
|
||||
|
||||
return () => {
|
||||
model.state.removeOnChangedHandler(onStateChanged);
|
||||
model.unmonitorPort(durationHandle);
|
||||
model.unmonitorPort(positionHandle);
|
||||
model.unmonitorPort(pluginStateHandle);
|
||||
model.unmonitorPort(positionHandle);
|
||||
// model.unmonitorPort(loopEnableHandle);
|
||||
// model.unmonitorPort(startHandle);
|
||||
// model.unmonitorPort(loopStartHandle);
|
||||
// model.unmonitorPort(loopEndHandle);
|
||||
model.cancelMonitorPatchProperty(filePropertyHandle);
|
||||
model.cancelMonitorPatchProperty(loopPropertyHandle);
|
||||
};
|
||||
},
|
||||
[serverConnected]
|
||||
);
|
||||
const effectivePosition =
|
||||
(pluginState !== PluginState.Idle) ? Math.min(position, duration) : 0.0;
|
||||
const titleLine = title !== "" ? title : pathFileNameOnly(audioFile);
|
||||
const albumLine = getAlbumLine(album,artist,albumArtist);
|
||||
const albumLine = getAlbumLine(album, artist, albumArtist);
|
||||
|
||||
const paused = (pluginState === PluginState.Idle
|
||||
|| pluginState === PluginState.CuePlayPaused
|
||||
|| pluginState === PluginState.Paused);
|
||||
|| pluginState === PluginState.CuePlayingThenPause
|
||||
|| 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() {
|
||||
return (
|
||||
<Box style={{ width: "100%" }} >
|
||||
@@ -573,84 +682,55 @@ export default function ToobPlayerControl(
|
||||
function SliderCluster() {
|
||||
return (
|
||||
|
||||
<div style={{ display: "flex", flexFlow: "column nowrap" }}>
|
||||
<Slider
|
||||
value={dragging ? sliderValue : effectivePosition}
|
||||
min={0}
|
||||
step={1}
|
||||
max={duration}
|
||||
onChange={(_, val) => {
|
||||
let v = val as number;
|
||||
setPosition(v);
|
||||
setSliderValue(v);
|
||||
}}
|
||||
onChangeCommitted={(e, value) => {
|
||||
let v = value as number;
|
||||
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 style={{ position: "relative", display: "flex", flexFlow: "column nowrap" }}>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<ControlSlider
|
||||
instanceId={props.instanceId}
|
||||
controlKey="position"
|
||||
duration={duration}
|
||||
onPreviewValue={(value) => {
|
||||
}}
|
||||
onValueChanged={(value) => {
|
||||
OnSeek(value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
flex: "0 0 auto",
|
||||
position: "absolute",
|
||||
width: "100%",
|
||||
display: 'flex',
|
||||
alignItems: 'top',
|
||||
justifyContent: 'space-between',
|
||||
marginTop: -4,
|
||||
display: "flex", flexFlow: "row nowrap", alignItems: "center",
|
||||
justifyContent: "center",
|
||||
bottom: 0
|
||||
}}
|
||||
>
|
||||
<TinyText>{formatDuration(dragging ? sliderValue : effectivePosition)}</TinyText>
|
||||
<Button title="Loop" variant="dialogSecondary"
|
||||
style={{flex: "0 1 auto",width: 200,
|
||||
textTransform: "none", fontSize: "0.9rem", fontWeight: 500, padding: "4px 8px"
|
||||
{/**
|
||||
*
|
||||
sx={{
|
||||
'& .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={() => {
|
||||
setShowLoopDialog(true);
|
||||
}}
|
||||
>
|
||||
{loopButtonText()
|
||||
}
|
||||
</Button>
|
||||
<TinyText>{formatDuration(duration)}</TinyText>
|
||||
>
|
||||
<Typography noWrap variant="caption">
|
||||
{loopButtonText()
|
||||
}
|
||||
</Typography>
|
||||
</ButtonEx>
|
||||
</div>
|
||||
</div>
|
||||
</div >
|
||||
);
|
||||
}
|
||||
|
||||
@@ -742,10 +822,12 @@ export default function ToobPlayerControl(
|
||||
{showLoopDialog && (
|
||||
<LoopDialog isOpen={showLoopDialog}
|
||||
sampleRate={
|
||||
model.jackConfiguration.get().sampleRate == 0?
|
||||
96000 :
|
||||
model.jackConfiguration.get().sampleRate
|
||||
sampleRate
|
||||
}
|
||||
playing={!paused}
|
||||
onPreview={() => { handlePreview(); }}
|
||||
value={loopParameters}
|
||||
onCancelPlaying= {()=> { handleCancelPlaying();}}
|
||||
timebase={timebase}
|
||||
onTimebaseChange={(newTimebase) => {
|
||||
setTimebase(newTimebase);
|
||||
@@ -759,50 +841,23 @@ export default function ToobPlayerControl(
|
||||
onClose={() => {
|
||||
setShowLoopDialog(false);
|
||||
}}
|
||||
onSetLoop={(start, loopEnable,loopStart, loopEnd) => {
|
||||
|
||||
let loop: LoopParameters = { start: start,
|
||||
loopEnable: loopEnable,
|
||||
loopStart: loopStart,
|
||||
loopEnd: loopEnd };
|
||||
|
||||
onSetLoop={(loop: LoopParameters) => {
|
||||
let loopSettings = {
|
||||
timebase: timebase,
|
||||
loopParameters: loop
|
||||
};
|
||||
model.setPatchProperty(
|
||||
props.instanceId,
|
||||
"http://two-play.com/plugins/toob-player#loopSettings",
|
||||
loopSettings.toString());
|
||||
LOOP_PROPERTY_URI,
|
||||
JSON.stringify(loopSettings));
|
||||
|
||||
setStart(start);
|
||||
// model.setPedalboardControl(
|
||||
// props.instanceId,
|
||||
// "start",
|
||||
// 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
|
||||
// );
|
||||
setStart(loop.start);
|
||||
setLoopEnable(loop.loopEnable);
|
||||
setLoopStart(loop.loopStart);
|
||||
setLoopEnd(loop.loopEnd);
|
||||
}}
|
||||
start={start}
|
||||
loopStart={loopStart}
|
||||
loopEnable={loopEnable}
|
||||
loopEnd={loopEnd}
|
||||
duration={duration}
|
||||
/>
|
||||
/>
|
||||
)}
|
||||
{showFileDialog && (
|
||||
<FilePropertyDialog open={showFileDialog}
|
||||
|
||||
@@ -41,12 +41,38 @@ import CircularProgress from '@mui/material/CircularProgress';
|
||||
// const BANK_EXTENSION = ".piBank";
|
||||
// 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 {
|
||||
open: boolean,
|
||||
onClose: () => void,
|
||||
onUploaded: (fileName: string) => void,
|
||||
uploadPage: string,
|
||||
fileProperty: UiFileProperty
|
||||
fileProperty: UiFileProperty,
|
||||
isTracksDirectory: boolean
|
||||
|
||||
};
|
||||
|
||||
@@ -251,7 +277,15 @@ export default class UploadFileDialog extends ResizeResponsiveComponent<UploadFi
|
||||
|
||||
private wantsFile(file: File | null) {
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user