From 13883f91abfa6a32d86323d77da3b90841c1b287 Mon Sep 17 00:00:00 2001 From: "Robin E. R. Davies" Date: Sun, 15 Jun 2025 16:41:11 -0400 Subject: [PATCH] Toob Player --- .vscode/settings.json | 22 +- artifacts/missing_thumbnail.svgz | Bin 2106 -> 2357 bytes src/AudioFiles.cpp | 168 ++++++--- src/AudioFiles.hpp | 1 + src/PiPedalModel.cpp | 11 + src/PiPedalModel.hpp | 1 + src/PiPedalSocket.cpp | 25 ++ src/Storage.cpp | 70 +++- src/Storage.hpp | 6 + todo.txt | 11 +- vite/public/img/missing_thumbnail.jpg | Bin 12741 -> 11521 bytes vite/src/pipedal/App.tsx | 75 +++- vite/src/pipedal/AppThemed.css | 1 + vite/src/pipedal/ButtonEx.tsx | 49 +++ vite/src/pipedal/ControlSlider.tsx | 156 ++++++++ vite/src/pipedal/FilePropertyDialog.tsx | 147 ++++++-- vite/src/pipedal/IconButtonEx.tsx | 50 +++ vite/src/pipedal/LoopDialog.tsx | 246 +++++++------ vite/src/pipedal/MainPage.tsx | 30 +- vite/src/pipedal/PiPedalModel.tsx | 30 ++ vite/src/pipedal/PluginControl.tsx | 262 +++++++------- vite/src/pipedal/PluginControlView.tsx | 9 +- vite/src/pipedal/PluginInfoDialog.tsx | 34 +- vite/src/pipedal/PluginPresetSelector.tsx | 8 +- vite/src/pipedal/ToobPlayerControl.tsx | 411 ++++++++++++---------- vite/src/pipedal/UploadFileDialog.tsx | 38 +- 26 files changed, 1314 insertions(+), 547 deletions(-) create mode 100644 vite/src/pipedal/ButtonEx.tsx create mode 100644 vite/src/pipedal/ControlSlider.tsx create mode 100644 vite/src/pipedal/IconButtonEx.tsx diff --git a/.vscode/settings.json b/.vscode/settings.json index f7d1460..7f85420 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -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, + } \ No newline at end of file diff --git a/artifacts/missing_thumbnail.svgz b/artifacts/missing_thumbnail.svgz index ae3a1068e705cf480f813ffb8f8972e2f43d1981..450410ba8eae470d17d2dc092180a5e3864b3b4e 100644 GIT binary patch literal 2357 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 diff --git a/src/AudioFiles.cpp b/src/AudioFiles.cpp index 0043925..b9cc1ad 100644 --- a/src/AudioFiles.cpp +++ b/src/AudioFiles.cpp @@ -43,6 +43,41 @@ namespace fs = std::filesystem; static constexpr size_t INVALID_INDEX = std::numeric_limits::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 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 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 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(path,indexPath); + return std::make_shared(path, indexPath); } - std::vector AudioDirectoryInfoImpl::QueryTracks() { if (audioFilesDb) @@ -264,7 +294,18 @@ std::vector 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 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 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 thumbnailData( + (std::istreambuf_iterator(thumbnailStream)), + std::istreambuf_iterator()); + + 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); diff --git a/src/AudioFiles.hpp b/src/AudioFiles.hpp index f3c1945..2d9fabe 100644 --- a/src/AudioFiles.hpp +++ b/src/AudioFiles.hpp @@ -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); } \ No newline at end of file diff --git a/src/PiPedalModel.cpp b/src/PiPedalModel.cpp index 2c13596..ab37917 100644 --- a/src/PiPedalModel.cpp +++ b/src/PiPedalModel.cpp @@ -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 lock(mutex); + return storage.CopyFilePropertyFile(oldRelativePath, newRelativePath, uiFileProperty,overwrite); +} + + void PiPedalModel::DeleteSampleFile(const std::filesystem::path &fileName) { std::lock_guard lock(mutex); diff --git a/src/PiPedalModel.hpp b/src/PiPedalModel.hpp index a7de722..1fba8e2 100644 --- a/src/PiPedalModel.hpp +++ b/src/PiPedalModel.hpp @@ -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); diff --git a/src/PiPedalSocket.cpp b/src/PiPedalSocket.cpp index 3f85297..ae4f965 100644 --- a/src/PiPedalSocket.cpp +++ b/src/PiPedalSocket.cpp @@ -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; diff --git a/src/Storage.cpp b/src/Storage.cpp index 1e959fe..217c782 100644 --- a/src/Storage.cpp +++ b/src/Storage.cpp @@ -1705,6 +1705,13 @@ static void AddTracksToResult( std::set 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 { diff --git a/src/Storage.hpp b/src/Storage.hpp index 41c4640..440ca17 100644 --- a/src/Storage.hpp +++ b/src/Storage.hpp @@ -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); }; diff --git a/todo.txt b/todo.txt index 8c66338..ac43064 100644 --- a/todo.txt +++ b/todo.txt @@ -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 diff --git a/vite/public/img/missing_thumbnail.jpg b/vite/public/img/missing_thumbnail.jpg index 3fff7247188d9799dfd2ddcf5326cccee66040ca..1083840cbe2e49d4e6753bf01f08ed054397526f 100644 GIT binary patch delta 11017 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 JtYEZ>{|D}dpX&es delta 12247 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 VAP3(d6aT?=t)Y=s?j-8e{{f8G`^W$Q diff --git a/vite/src/pipedal/App.tsx b/vite/src/pipedal/App.tsx index cb08a14..d1ab4dd 100644 --- a/vite/src/pipedal/App.tsx +++ b/vite/src/pipedal/App.tsx @@ -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 { - + ); diff --git a/vite/src/pipedal/AppThemed.css b/vite/src/pipedal/AppThemed.css index da48566..9300251 100644 --- a/vite/src/pipedal/AppThemed.css +++ b/vite/src/pipedal/AppThemed.css @@ -43,3 +43,4 @@ input[type=number] { + diff --git a/vite/src/pipedal/ButtonEx.tsx b/vite/src/pipedal/ButtonEx.tsx new file mode 100644 index 0000000..92d3b6f --- /dev/null +++ b/vite/src/pipedal/ButtonEx.tsx @@ -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 || extra['aria-label']} + ) + } + placement="top-start" arrow + enterDelay={1500} enterNextDelay={1500} + > + + - +
 
@@ -1144,7 +1180,8 @@ export default withStyles( @@ -1182,7 +1219,8 @@ export default withStyles( @@ -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( }} /> + { this.handleConfirmCopy(); }} + onClose={() => { + this.setState({ confirmCopyDialogState: null }); + }} + + /> + { this.state.newFolderDialogOpen && ( { 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 { diff --git a/vite/src/pipedal/IconButtonEx.tsx b/vite/src/pipedal/IconButtonEx.tsx new file mode 100644 index 0000000..e54692f --- /dev/null +++ b/vite/src/pipedal/IconButtonEx.tsx @@ -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 || extra['aria-label'] } + ) + } + placement="top-start" arrow + enterDelay={1500} enterNextDelay={1500} + > + + + ); +} + +export default IconButtonEx; \ No newline at end of file diff --git a/vite/src/pipedal/LoopDialog.tsx b/vite/src/pipedal/LoopDialog.tsx index a177ac2..66c9ab9 100644 --- a/vite/src/pipedal/LoopDialog.tsx +++ b/vite/src/pipedal/LoopDialog.tsx @@ -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(null); - const [previewLoopStart, setPreviewLoopStart] = React.useState(null); - const [previewLoopEnd, setPreviewLoopEnd] = React.useState(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 ( { + 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(); + }} > @@ -449,8 +485,8 @@ export default function LoopDialog(props: LoopDialogProps) {
@@ -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)} /> { - 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) { { - 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) { { - 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(); + }} />
{ - 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(); + + }} /> { - 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(); + }} />
+
diff --git a/vite/src/pipedal/MainPage.tsx b/vite/src/pipedal/MainPage.tsx index e308119..f618bc7 100644 --- a/vite/src/pipedal/MainPage.tsx +++ b/vite/src/pipedal/MainPage.tsx @@ -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 = }} >
- + + +
{ @@ -541,9 +544,9 @@ export const MainPage = {this.props.enableStructureEditing && (
- { this.onAddClick(e) }} size="large"> + { this.onAddClick(e) }} size="large"> - +
- { this.onDeletePedal(pedalboardItem?.instanceId ?? -1) }} size="large"> - +
- +
- { this.handleMidiConfiguration(instanceId); }} size="large"> - +
- { this.setState({ snapshotDialogOpen: true }); }} size="large"> {this.snapshotIcon(this.props.theme, this.state.selectedSnapshot)} - +
)} diff --git a/vite/src/pipedal/PiPedalModel.tsx b/vite/src/pipedal/PiPedalModel.tsx index f5fd124..dd9e011 100644 --- a/vite/src/pipedal/PiPedalModel.tsx +++ b/vite/src/pipedal/PiPedalModel.tsx @@ -2697,6 +2697,36 @@ export class PiPedalModel //implements PiPedalModel }); }); } + copyFilePropertyFile( + oldRelativePath: string, + newRelativePath: string, + uiFileProperty: UiFileProperty, + overwrite: boolean + ): Promise { + return new Promise((resolve, reject) => { + + let ws = this.webSocket; + if (!ws) { + resolve(""); + return; + } + ws.request( + "copyFilePropertyFile", + { + oldRelativePath: oldRelativePath, + newRelativePath: newRelativePath, + uiFileProperty: uiFileProperty, + overwrite: overwrite + } + ) + .then((newPath) => { + resolve(newPath); + }) + .catch((err) => { + reject(err); + }); + }); + } setWifiConfigSettings(wifiConfigSettings: WifiConfigSettings): Promise { let result = new Promise((resolve, reject) => { diff --git a/vite/src/pipedal/PluginControl.tsx b/vite/src/pipedal/PluginControl.tsx index eeb103d..1d1b86a 100644 --- a/vite/src/pipedal/PluginControl.tsx +++ b/vite/src/pipedal/PluginControl.tsx @@ -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 ( - { - this.onCheckChanged(event.target.checked); - }} - /> + + { + this.onCheckChanged(event.target.checked); + }} + /> + ); } if (control.isAbToggle()) { let classes = withStyles.getClasses(this.props); // unchecked color is not gray. return ( - { - this.onCheckChanged(event.target.checked); - }} - classes={{ - track: classes.switchTrack - }} - style={{ color: this.props.theme.palette.primary.main }} - /> + + { + this.onCheckChanged(event.target.checked); + }} + classes={{ + track: classes.switchTrack + }} + style={{ color: this.props.theme.palette.primary.main }} + /> + ); } else { return ( - + {control.scale_points.map((scale_point: ScalePoint) => ( + {scale_point.label} - ))} - + ))} + + ); } } @@ -919,12 +926,10 @@ const PluginControl = alignSelf: "stretch", marginBottom: 8, marginLeft: isSelect ? 8 : 0, marginRight: 0 }}> - - {isButton ? "\u00A0" : control.name} - + {isButton ? "\u00A0" : control.name} {/* CONTROL SECTION */} @@ -933,81 +938,86 @@ const PluginControl = {isButton ? ( control.name.length !== 1 ? ( - + > + {control.name} + + ) : ( - + > + {androidEmoji(control.name)} + + ) ) @@ -1016,34 +1026,38 @@ const PluginControl = ) : (isGraphicEq) ? (
- + + /> +
) : (
- + + /> +
) } @@ -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} />
{ this.inputRef.current!.focus(); }} style={{ display: this.state.editFocused ? "none" : "block" }} diff --git a/vite/src/pipedal/PluginControlView.tsx b/vite/src/pipedal/PluginControlView.tsx index 285fcf1..f6d597d 100644 --- a/vite/src/pipedal/PluginControlView.tsx +++ b/vite/src/pipedal/PluginControlView.tsx @@ -472,7 +472,6 @@ const PluginControlView = throw new PiPedalStateError("Missing control value."); } return (( - { this.onControlValueChanged(controlValue!.key, value) }} onPreviewChange={(value: number) => { this.onPreviewChange(controlValue!.key, value) }} @@ -480,6 +479,14 @@ const PluginControlView = /> )); + // return (( + // { this.onControlValueChanged(controlValue!.key, value) }} + // onPreviewChange={(value: number) => { this.onPreviewChange(controlValue!.key, value) }} + // requestIMEEdit={(uiControl: any, value: any) => this.requestImeEdit(uiControl, value)} + + // /> + // )); } diff --git a/vite/src/pipedal/PluginInfoDialog.tsx b/vite/src/pipedal/PluginInfoDialog.tsx index 6a37c86..5f3acf6 100644 --- a/vite/src/pipedal/PluginInfoDialog.tsx +++ b/vite/src/pipedal/PluginInfoDialog.tsx @@ -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 (
- - + {open && ( -
-
- +
+ + { handleClose()}} + > + + + +
+
{plugin.name}
- handleClose()} - style={{ flex: "0 0 auto" }} - size="large"> - -
diff --git a/vite/src/pipedal/PluginPresetSelector.tsx b/vite/src/pipedal/PluginPresetSelector.tsx index 8aa2f2a..9f41d78 100644 --- a/vite/src/pipedal/PluginPresetSelector.tsx +++ b/vite/src/pipedal/PluginPresetSelector.tsx @@ -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 (
- this.handlePresetMenuClick(e)} size="large"> + this.handlePresetMenuClick(e)} size="large"> - + ({ 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( { units: TimebaseUnits.Seconds, tempo: 120.0, timeSignature: { numerator: 4, denominator: 4 } }); + const [loopParameters, setLoopParameters] = React.useState({ + 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( { - 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 ( @@ -573,84 +682,55 @@ export default function ToobPlayerControl( function SliderCluster() { return ( -
- { - 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', - }), - })} - /> +
+
+ { + }} + onValueChanged={(value) => { + OnSeek(value); + }} + /> +
- {formatDuration(dragging ? sliderValue : effectivePosition)} - - {formatDuration(duration)} + > + + {loopButtonText() + } + +
-
+
); } @@ -742,10 +822,12 @@ export default function ToobPlayerControl( {showLoopDialog && ( { 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 && ( void, onUploaded: (fileName: string) => void, uploadPage: string, - fileProperty: UiFileProperty + fileProperty: UiFileProperty, + isTracksDirectory: boolean }; @@ -251,7 +277,15 @@ export default class UploadFileDialog extends ResizeResponsiveComponent