Handle permission denied errors.

This commit is contained in:
Robin E. R. Davies
2025-06-24 13:59:57 -04:00
parent 30938d967b
commit 98ee16bb82
10 changed files with 258 additions and 329 deletions
+17 -2
View File
@@ -207,7 +207,13 @@ std::vector<DbFileInfo> AudioDirectoryInfoImpl::QueryTracks()
std::vector<AudioFileMetadata> AudioDirectoryInfoImpl::GetFiles() std::vector<AudioFileMetadata> AudioDirectoryInfoImpl::GetFiles()
{ {
OpenAudioDb(); OpenAudioDb();
std::vector<DbFileInfo> dbFiles = UpdateDbFiles(); std::vector<DbFileInfo> dbFiles;
try {
dbFiles = UpdateDbFiles();
} catch (const std::exception &e) {
Lv2Log::error("Error updating audio file info: %s - %s",
path.string().c_str(), e.what());
}
std::vector<AudioFileMetadata> metadataResults; std::vector<AudioFileMetadata> metadataResults;
for (const auto &dbFile : dbFiles) for (const auto &dbFile : dbFiles)
{ {
@@ -298,7 +304,16 @@ std::vector<DbFileInfo> AudioDirectoryInfoImpl::UpdateDbFiles()
for (auto dirEntry : fs::directory_iterator(path)) for (auto dirEntry : fs::directory_iterator(path))
{ {
if (!dirEntry.is_directory()) bool isDirectory;
try {
isDirectory = dirEntry.is_directory();
} catch (
const std::filesystem::filesystem_error &e) {
Lv2Log::error("Error accessing directory entry: %s - %s", dirEntry.path().string().c_str(), e.what());
continue; // skip this entry if we cannot access it.
}
if (!isDirectory)
{ {
auto path = dirEntry.path(); auto path = dirEntry.path();
std::string name = path.filename(); std::string name = path.filename();
+14 -7
View File
@@ -142,25 +142,27 @@ uint32_t FileMetadataFeature::getFileMetadata(
json_variant jsonData; json_variant jsonData;
std::string result; std::string result;
if (fs::exists(path))
{
try try
{
if (fs::exists(path))
{ {
std::ifstream f(path); std::ifstream f(path);
json_reader reader(f); json_reader reader(f);
reader.read(&jsonData); reader.read(&jsonData);
result = jsonData.as_object()->at(std::string(key)).as_string(); result = jsonData.as_object()->at(std::string(key)).as_string();
} }
catch (const std::exception &e)
{
return 0;
}
}
else else
{ {
Lv2Log::debug(SS("No metadata file found for " << absolute_path << " key: " << key)); Lv2Log::debug(SS("No metadata file found for " << absolute_path << " key: " << key));
return 0; return 0;
} }
}
catch (const std::exception &e)
{
Lv2Log::debug(SS("Permission denied for " << absolute_path << " key: " << key));
return 0;
}
size_t len = result.length() + 1; // +1 for null terminator size_t len = result.length() + 1; // +1 for null terminator
if (len > fileMetadataSize || fileMetadata == nullptr) if (len > fileMetadataSize || fileMetadata == nullptr)
{ {
@@ -181,6 +183,7 @@ PIPEDAL_FileMetadata_Status FileMetadataFeature::deleteFileMetadata(
} }
Lv2Log::debug(SS("Delete file metadata for " << absolute_path << " key: " << key)); Lv2Log::debug(SS("Delete file metadata for " << absolute_path << " key: " << key));
try {
std::filesystem::path path{SS(absolute_path << ".mdata")}; std::filesystem::path path{SS(absolute_path << ".mdata")};
if (!fs::exists(path)) if (!fs::exists(path))
@@ -227,6 +230,10 @@ PIPEDAL_FileMetadata_Status FileMetadataFeature::deleteFileMetadata(
writer.write(jsonData); writer.write(jsonData);
return PIPEDAL_FILE_METADATA_SUCCESS; return PIPEDAL_FILE_METADATA_SUCCESS;
} catch (const std::exception &e) {
Lv2Log::error(SS("Exception while deleting metadata: " << e.what()));
return PIPEDAL_FILE_METADATA_PERMISSION_DENIED; // Failed to open existing metadata
}
} }
PIPEDAL_FileMetadata_Status FileMetadataFeature::S_setFileMetadata( PIPEDAL_FileMetadata_Status FileMetadataFeature::S_setFileMetadata(
+45 -19
View File
@@ -1690,7 +1690,8 @@ static void AddFilesToResult(
} }
catch (const std::exception &error) catch (const std::exception &error)
{ {
throw std::logic_error("GetFileList failed. Directory not found: " + rootPath.string()); throw std::logic_error(
SS("GetFileList failed. " << rootPath.string() << " - " << error.what()));
} }
// sort lexicographically // sort lexicographically
@@ -1733,6 +1734,8 @@ static void AddTracksToResult(
try try
{ {
// Add directories first. // Add directories first.
try
{
for (auto const &dir_entry : std::filesystem::directory_iterator(rootPath)) for (auto const &dir_entry : std::filesystem::directory_iterator(rootPath))
{ {
if (!IsValidUtf8(dir_entry.path().string())) if (!IsValidUtf8(dir_entry.path().string()))
@@ -1742,10 +1745,21 @@ static void AddTracksToResult(
} }
const auto &path = dir_entry.path(); const auto &path = dir_entry.path();
auto name = path.filename().string(); auto name = path.filename().string();
try {
if (dir_entry.is_directory()) if (dir_entry.is_directory())
{ {
resultFiles.push_back(FileEntry{path, name, true, fs::is_symlink(path)}); resultFiles.push_back(FileEntry{path, name, true, dir_entry.is_symlink()});
} }
} catch (const std::exception &e)
{
Lv2Log::warning(SS("Failed to add directory entry: " << path.string() << " - " << e.what()));
}
}
}
catch (const std::exception &error)
{
throw std::logic_error(
SS("AddTracksToResult failed to enumerate directories. " << rootPath.string() << " - " << error.what()));
} }
auto collator = Locale::GetInstance()->GetCollator(); auto collator = Locale::GetInstance()->GetCollator();
std::sort( std::sort(
@@ -1757,12 +1771,14 @@ static void AddTracksToResult(
{ {
return l.isDirectory_ > r.isDirectory_; return l.isDirectory_ > r.isDirectory_;
} }
return collator->Compare(l.displayName_ ,r.displayName_) < 0; return collator->Compare(l.displayName_, r.displayName_) < 0;
}); });
// Add audio files. // Add audio files.
auto audioFiles = AudioDirectoryInfo::Create(rootPath, auto audioFiles = AudioDirectoryInfo::Create(rootPath,
GetShadowIndexDirectory(audioRootDirectory,rootPath) GetShadowIndexDirectory(audioRootDirectory, rootPath));
);
try
{
for (const auto &audioFile : audioFiles->GetFiles()) for (const auto &audioFile : audioFiles->GetFiles())
{ {
fs::path audioFilePath = rootPath / audioFile.fileName(); fs::path audioFilePath = rootPath / audioFile.fileName();
@@ -1780,7 +1796,14 @@ static void AddTracksToResult(
} }
catch (const std::exception &error) catch (const std::exception &error)
{ {
throw std::logic_error("GetFileList failed. Directory not found: " + rootPath.string()); throw std::logic_error(
SS("AddTracksToResult failed to enumerate audio files. " << rootPath.string() << " - " << error.what()));
}
}
catch (const std::exception &error)
{
throw std::logic_error(SS("GetFileList failed. " << error.what() << "(" << rootPath.string() << ")"));
} }
} }
@@ -1869,7 +1892,7 @@ FileRequestResult Storage::GetModFileList2(const std::string &relativePath, cons
if (IsInAudioTracksDirectory(relativePath)) if (IsInAudioTracksDirectory(relativePath))
{ {
AddTracksToResult(this->GetPluginUploadDirectory(),result, rootModDirectory, fileProperty, relativePath); AddTracksToResult(this->GetPluginUploadDirectory(), result, rootModDirectory, fileProperty, relativePath);
} }
else else
{ {
@@ -2074,8 +2097,7 @@ std::filesystem::path Storage::MakeUserFilePath(const std::string &directory, co
return result; return result;
} }
bool Storage::IsValidArtworkFile(const std::filesystem::path &fullPath)
bool Storage::IsValidArtworkFile(const std::filesystem::path& fullPath)
{ {
if (IsInAudioTracksDirectory(fullPath) && isArtworkFileName(fullPath.filename().string())) if (IsInAudioTracksDirectory(fullPath) && isArtworkFileName(fullPath.filename().string()))
{ {
@@ -2208,9 +2230,10 @@ std::string Storage::RenameFilePropertyFile(
} }
std::filesystem::rename(oldPath, newPath); std::filesystem::rename(oldPath, newPath);
if (fs::exists(oldPath.string()+".mdata")) { if (fs::exists(oldPath.string() + ".mdata"))
{
// rename the metadata file as well. // rename the metadata file as well.
std::filesystem::rename(oldPath.string()+".mdata", newPath.string()+".mdata"); std::filesystem::rename(oldPath.string() + ".mdata", newPath.string() + ".mdata");
} }
return newPath; return newPath;
} }
@@ -2222,12 +2245,13 @@ fs::path MakeVersionedPath(const fs::path &path)
return path; // no need to version a non-existing file. return path; // no need to version a non-existing file.
} }
fs::path newPath = path; fs::path newPath = path;
auto stem = newPath.stem().string();; auto stem = newPath.stem().string();
;
if (stem.ends_with(")")) if (stem.ends_with(")"))
{ {
// remove the trailing (n) from the file name. // remove the trailing (n) from the file name.
size_t pos = stem.find_last_of('('); size_t pos = stem.find_last_of('(');
std::string stemVersion = stem.substr(pos+1, stem.length()-1-(pos+1)); std::string stemVersion = stem.substr(pos + 1, stem.length() - 1 - (pos + 1));
// check if it is a number. // check if it is a number.
if (stemVersion.find_first_not_of("0123456789") == std::string::npos) if (stemVersion.find_first_not_of("0123456789") == std::string::npos)
{ {
@@ -2246,9 +2270,12 @@ fs::path MakeVersionedPath(const fs::path &path)
{ {
// append (n) to the file name. // append (n) to the file name.
std::string newFileName; std::string newFileName;
if (version == 0) { if (version == 0)
{
newFileName = SS(stem << newPath.extension().string()); newFileName = SS(stem << newPath.extension().string());
} else { }
else
{
// append (n) to the file name. // append (n) to the file name.
newFileName = SS(stem << " (" << std::to_string(version) << ")" << newPath.extension().string()); newFileName = SS(stem << " (" << std::to_string(version) << ")" << newPath.extension().string());
} }
@@ -2280,7 +2307,6 @@ std::string Storage::CopyFilePropertyFile(
std::filesystem::path newPath = this->GetPluginUploadDirectory() / uiFileProperty.directory() / newRelativePath; std::filesystem::path newPath = this->GetPluginUploadDirectory() / uiFileProperty.directory() / newRelativePath;
if (!this->IsValidSampleFileName(newPath)) if (!this->IsValidSampleFileName(newPath))
{ {
throw std::runtime_error("Invalid file name."); throw std::runtime_error("Invalid file name.");
@@ -2299,11 +2325,11 @@ std::string Storage::CopyFilePropertyFile(
std::filesystem::create_hard_link(oldPath, newPath); std::filesystem::create_hard_link(oldPath, newPath);
if (IsInAudioTracksDirectory(oldPath) if (IsInAudioTracksDirectory(oldPath) && IsInAudioTracksDirectory(newPath))
&& IsInAudioTracksDirectory(newPath))
{ {
std::filesystem::path metadataPath = SS(oldPath.string() << ".mdata"); std::filesystem::path metadataPath = SS(oldPath.string() << ".mdata");
if (fs::exists(metadataPath)) { if (fs::exists(metadataPath))
{
// copy the metadata file as well. // copy the metadata file as well.
std::filesystem::copy_file( std::filesystem::copy_file(
metadataPath, metadataPath,
+29 -24
View File
@@ -55,12 +55,10 @@ static const std::string BANK_MIME_TYPE = "application/vnd.pipedal.bank";
static const std::string CACHE_CONTROL_INDEFINITELY = "max-age=31536000,public,immutable"; // 1 year static const std::string CACHE_CONTROL_INDEFINITELY = "max-age=31536000,public,immutable"; // 1 year
static const std::string CACHE_CONTROL_SHORT = "max-age=300,public"; // 5 minutes static const std::string CACHE_CONTROL_SHORT = "max-age=300,public"; // 5 minutes
using namespace pipedal; using namespace pipedal;
using namespace boost::system; using namespace boost::system;
namespace fs = std::filesystem; namespace fs = std::filesystem;
class UserUploadResponse class UserUploadResponse
{ {
public: public:
@@ -87,7 +85,7 @@ int32_t ConvertThumbnailSize(const std::string &param)
{ {
return 0; return 0;
} }
return static_cast<int32_t > (std::stoi(param)); return static_cast<int32_t>(std::stoi(param));
} }
static bool IsZipFile(const std::filesystem::path &path) static bool IsZipFile(const std::filesystem::path &path)
@@ -128,7 +126,6 @@ private:
std::vector<std::string> extensions; std::vector<std::string> extensions;
}; };
class DownloadIntercept : public RequestHandler class DownloadIntercept : public RequestHandler
{ {
PiPedalModel *model; PiPedalModel *model;
@@ -393,15 +390,14 @@ public:
auto lastModified = std::filesystem::last_write_time(path); auto lastModified = std::filesystem::last_write_time(path);
res.set(HttpField::LastModified, HtmlHelper::timeToHttpDate(lastModified)); res.set(HttpField::LastModified, HtmlHelper::timeToHttpDate(lastModified));
} }
AudioDirectoryInfo::Ptr CreateDirectoryInfo(const fs::path &path) { AudioDirectoryInfo::Ptr CreateDirectoryInfo(const fs::path &path)
{
return AudioDirectoryInfo::Create(path, return AudioDirectoryInfo::Create(path,
GetShadowIndexDirectory( GetShadowIndexDirectory(
this->model->GetPluginUploadDirectory(), this->model->GetPluginUploadDirectory(),
path)); path));
} }
virtual void get_response( virtual void get_response(
const uri &request_uri, const uri &request_uri,
HttpRequest &req, HttpRequest &req,
@@ -500,6 +496,8 @@ public:
throw PiPedalException("File not found."); throw PiPedalException("File not found.");
} }
AudioDirectoryInfo::Ptr audioDirectory = CreateDirectoryInfo(path.parent_path()); AudioDirectoryInfo::Ptr audioDirectory = CreateDirectoryInfo(path.parent_path());
try
{
auto files = audioDirectory->GetFiles(); auto files = audioDirectory->GetFiles();
for (const auto &file : files) for (const auto &file : files)
{ {
@@ -513,11 +511,21 @@ public:
return; return;
} }
} }
}
catch (const std::exception &e)
{
Lv2Log::error("Error getting audio directory info: %s - (%s)", path.c_str(), e.what());
throw e;
}
// If we get here, the file was not found in the directory. // If we get here, the file was not found in the directory.
throw PiPedalException("File not found in directory."); throw PiPedalException("File not found in directory.");
} }
else if (segment == "Thumbnail") else if (segment == "Thumbnail")
{ {
ThumbnailTemporaryFile thumbnailTemporaryFile;
try
{
std::shared_ptr<TemporaryFile> thumbnail;
try try
{ {
fs::path path = request_uri.query("path"); fs::path path = request_uri.query("path");
@@ -526,11 +534,10 @@ public:
!this->model->IsInUploadsDirectory(path) || !this->model->IsInUploadsDirectory(path) ||
HasDotDot(path)) HasDotDot(path))
{ {
std::shared_ptr<TemporaryFile> thumbnail;
// path for folder thumbnails. // path for folder thumbnails.
path = request_uri.query("ffile"); path = request_uri.query("ffile");
std::shared_ptr<TemporaryFile> thumbnail;
if (!path.empty() && fs::exists(path) && if (!path.empty() && fs::exists(path) &&
this->model->IsInUploadsDirectory(path) && this->model->IsInUploadsDirectory(path) &&
!HasDotDot(path)) !HasDotDot(path))
@@ -544,19 +551,16 @@ public:
auto t = AudioDirectoryInfo::DefaultThumbnailTemporaryFile(); auto t = AudioDirectoryInfo::DefaultThumbnailTemporaryFile();
thumbnail = std::make_shared<TemporaryFile>(); thumbnail = std::make_shared<TemporaryFile>();
thumbnail->SetNonDeletedPath(t.Path()); thumbnail->SetNonDeletedPath(t.Path());
} }
res.set(HttpField::content_type, MimeTypes::instance().MimeTypeFromExtension(path.extension())); res.set(HttpField::content_type, MimeTypes::instance().MimeTypeFromExtension(path.extension()));
res.set(HttpField::cache_control, CACHE_CONTROL_INDEFINITELY); // URL is cache-busted, and will change if the file ismodified. res.set(HttpField::cache_control, CACHE_CONTROL_INDEFINITELY); // URL is cache-busted, and will change if the file ismodified.
setLastModifiedFromFile(res,thumbnail->Path()); setLastModifiedFromFile(res, thumbnail->Path());
res.set(HttpField::content_length, std::to_string(fs::file_size(thumbnail->Path()))); res.set(HttpField::content_length, std::to_string(fs::file_size(thumbnail->Path())));
res.setBodyFile(thumbnail); res.setBodyFile(thumbnail);
return; return;
} }
int32_t width = ConvertThumbnailSize(request_uri.query("w")); int32_t width = ConvertThumbnailSize(request_uri.query("w"));
int32_t height = ConvertThumbnailSize(request_uri.query("h")); int32_t height = ConvertThumbnailSize(request_uri.query("h"));
@@ -564,21 +568,22 @@ public:
path.parent_path()); path.parent_path());
audioDirectory->GetFiles(); // ensure that the .index file is up to date. audioDirectory->GetFiles(); // ensure that the .index file is up to date.
ThumbnailTemporaryFile thumbnail; thumbnailTemporaryFile = audioDirectory->GetThumbnail(path.filename(), width, height);
try {
thumbnail = audioDirectory->GetThumbnail(path.filename(), width, height);
} catch (const std::exception &e) {
thumbnail = audioDirectory->DefaultThumbnailTemporaryFile();
} }
res.set(HttpField::content_type, thumbnail.GetMimeType()); catch (const std::exception &e)
{
fs::path defaultThumbnail = model->GetWebRoot() / "img/missing_thumbnail.jpg";
thumbnailTemporaryFile.SetNonDeletedPath(defaultThumbnail, "image/jpeg");
}
res.set(HttpField::content_type, thumbnailTemporaryFile.GetMimeType());
res.set(HttpField::cache_control, CACHE_CONTROL_INDEFINITELY); // URL is cache-busted with time-stamp. res.set(HttpField::cache_control, CACHE_CONTROL_INDEFINITELY); // URL is cache-busted with time-stamp.
setLastModifiedFromFile(res, path); setLastModifiedFromFile(res, thumbnailTemporaryFile.Path());
res.set(HttpField::content_length, std::to_string(fs::file_size(thumbnail.Path()))); res.set(HttpField::content_length, std::to_string(fs::file_size(thumbnailTemporaryFile.Path())));
std::filesystem::path t = thumbnail.Path(); std::filesystem::path t = thumbnailTemporaryFile.Path();
res.setBodyFile(t,thumbnail.DeleteFile()); res.setBodyFile(t, thumbnailTemporaryFile.DeleteFile());
thumbnail.Detach(); thumbnailTemporaryFile.Detach();
} }
catch (const std::exception &e) catch (const std::exception &e)
{ {
-1
View File
@@ -105,7 +105,6 @@ add_custom_command(
public/var/current_pedalboard.json public/var/current_pedalboard.json
public/var/config.json public/var/config.json
public/favicon.ico public/favicon.ico
public/index.html
public/iso_codes.json public/iso_codes.json
public/logo512.png public/logo512.png
public/sample_lv2_plugins.json public/sample_lv2_plugins.json
+1 -1
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, interactive-widget=resizes-visual" />
<meta name="theme-color" content="#000000" /> <meta name="theme-color" content="#000000" />
<meta name="color-scheme" content="dark light"/> <!-- uses media queries in web views. --> <meta name="color-scheme" content="dark light"/> <!-- uses media queries in web views. -->
<meta name="description" content="PiPedal Guitar Stomp Box for Raspberry Pi" /> <meta name="description" content="PiPedal Guitar Stomp Box for Raspberry Pi" />
-136
View File
@@ -1,136 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="color-scheme" content="dark light"/> <!-- uses media queries in web views. -->
<meta name="description" content="PiPedal Guitar Pedals" />
<!--
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" />
-->
<link rel="stylesheet" href="/css/roboto.css" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>PiPedal</title>
<style>
BODY {
background: #D0D0D0;
}
</style>
<style id="bgStyle">
BODY {
background: #333; overscroll-behavior: "none"
}
</style>
<script>
const androidHosted = !!(window.AndroidHost);
var colorScheme = localStorage.getItem("colorScheme");
if (androidHosted)
{
var hostColorScheme = window.AndroidHost.getThemePreference();
switch (hostColorScheme)
{
case 0:
colorScheme = "Light";
break;
case 1:
colorScheme = "Dark";
break;
case 2:
{
// use the host's interpretation of the current system night mode.
colorScheme = window.AndroidHost.isDarkTheme() ? "Dark": "Light";
}
break;
}
}
if (!colorScheme)
{
colorScheme = "Dark";
}
var darkMode = false;
var useSystem = false;
var prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
switch (colorScheme) {
case null:
default:
darkMode = false;
break;
case "Light":
darkMode = false;
break;
case "Dark":
darkMode = true;
break;
case "System":
useSystem = true;
break;
}
if (useSystem) {
darkMode = prefersDark;
}
if (!darkMode) {
let bgStyle = document.getElementById("bgStyle");
if (bgStyle) {
// disable the style block.
bgStyle.setAttribute('media', "max-width: 1px");
}
}
function removeHashOnLoad() {
if (window.location.hash) {
// Store the hash value (without the '#' symbol)
var hash = window.location.hash.substring(1);
// Replace the current URL without the hash
var newUrl = window.location.href.replace(window.location.hash, '');
// Use HTML5 history API to change the URL without reloading the page
history.replaceState(null, document.title, newUrl);
}
}
// Run the function when the window loads
window.addEventListener('load', removeHashOnLoad);
</script>
</head>
<body style="overscroll-behavior: none; overflow: hidden">
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root" style="height: 100%;position: absolute;left: px;right: 0px;top:0px;bottom: 0px"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
+21 -1
View File
@@ -26,6 +26,7 @@ import ButtonBase, { ButtonBaseProps } from "@mui/material/ButtonBase";
export interface DraggableButtonBaseProps extends ButtonBaseProps { export interface DraggableButtonBaseProps extends ButtonBaseProps {
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void; onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
onDoubleClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
onLongPressStart?: (currentTarget: HTMLButtonElement, e: React.PointerEvent<HTMLButtonElement> | React.MouseEvent<HTMLButtonElement>) => boolean; onLongPressStart?: (currentTarget: HTMLButtonElement, e: React.PointerEvent<HTMLButtonElement> | React.MouseEvent<HTMLButtonElement>) => boolean;
onLongPressMove?: (e: React.PointerEvent<HTMLButtonElement>) => void; onLongPressMove?: (e: React.PointerEvent<HTMLButtonElement>) => void;
onLongPressEnd?: (e: React.PointerEvent<HTMLButtonElement>| React.MouseEvent<HTMLButtonElement>) => void; onLongPressEnd?: (e: React.PointerEvent<HTMLButtonElement>| React.MouseEvent<HTMLButtonElement>) => void;
@@ -59,7 +60,7 @@ function screenToClient(e: React.PointerEvent): Point {
export default function DraggableButtonBase(props: DraggableButtonBaseProps) { export default function DraggableButtonBase(props: DraggableButtonBaseProps) {
// Ensure that the props are spread correctly // Ensure that the props are spread correctly
const { onClick, onLongPressStart, onLongPressMove: const { onClick, onDoubleClick, onLongPressStart, onLongPressMove:
doLongPressMove,onLongPressEnd, longPressDelay,instantMouseLongPress, ...rest } = props; doLongPressMove,onLongPressEnd, longPressDelay,instantMouseLongPress, ...rest } = props;
let [hTimeout, setHTimeout] = React.useState<number | null>(null); let [hTimeout, setHTimeout] = React.useState<number | null>(null);
@@ -68,6 +69,7 @@ export default function DraggableButtonBase(props: DraggableButtonBaseProps) {
let [clickSuppressed, setClickSuppressed] = React.useState<boolean>(false); let [clickSuppressed, setClickSuppressed] = React.useState<boolean>(false);
let [pointerDownPoint, setPointerDownPoint] = React.useState<Point>({ x: 0, y: 0 }); let [pointerDownPoint, setPointerDownPoint] = React.useState<Point>({ x: 0, y: 0 });
let [ longPressedElement, setLongPressedElement ] = React.useState<HTMLButtonElement | null>(null); let [ longPressedElement, setLongPressedElement ] = React.useState<HTMLButtonElement | null>(null);
let [lastClick, setLastClick] = React.useState<number | null>(null);
function handleSuppressClick(e: MouseEvent) { function handleSuppressClick(e: MouseEvent) {
@@ -278,7 +280,25 @@ export default function DraggableButtonBase(props: DraggableButtonBaseProps) {
if (onClick) { if (onClick) {
onClick(e); onClick(e);
} }
// check to see whether this is a double-click
let time = Date.now();
if (lastClick && time-lastClick < 500) {
if (onDoubleClick) {
onDoubleClick(e);
} }
setLastClick(null);
} else {
setLastClick(time);
}
}
}}
onDoubleClick={(e) => {
// chrome doesnt' do double-click for touch events
// so we need to re-implement it on the onclick handler.
e.stopPropagation();
e.preventDefault();
return false;
}} }}
onPointerCancelCapture={(e) => { onPointerCancelCapture={(e) => {
if (pointerId !== null) { if (pointerId !== null) {
+7 -7
View File
@@ -31,7 +31,7 @@ import Typography from '@mui/material/Typography';
import Input from '@mui/material/Input'; import Input from '@mui/material/Input';
import Select from '@mui/material/Select'; import Select from '@mui/material/Select';
import Switch from '@mui/material/Switch'; import Switch from '@mui/material/Switch';
import Utility, { nullCast } from './Utility'; import Utility from './Utility';
import MenuItem from '@mui/material/MenuItem'; import MenuItem from '@mui/material/MenuItem';
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel'; import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
import DialIcon from './svg/fx_dial.svg?react'; import DialIcon from './svg/fx_dial.svg?react';
@@ -231,12 +231,12 @@ const PluginControl =
onInputFocus(event: SyntheticEvent): void { onInputFocus(event: SyntheticEvent): void {
//this.displayValueRef.current!.style.display = "none"; //this.displayValueRef.current!.style.display = "none";
this.setState({ editFocused: true }); this.setState({ editFocused: true });
if (Utility.hasIMEKeyboard()) { // if (Utility.hasIMEKeyboard()) {
event.preventDefault(); // event.preventDefault();
event.stopPropagation(); // event.stopPropagation();
this.inputRef.current?.blur(); // this.inputRef.current?.blur();
this.props.requestIMEEdit(nullCast(this.props.uiControl), this.props.value) // this.props.requestIMEEdit(nullCast(this.props.uiControl), this.props.value)
} // }
} }
onInputKeyPress(e: any): void { onInputKeyPress(e: any): void {
if (e.charCode === 13 && this.inputChanged) { if (e.charCode === 13 && this.inputChanged) {
+3 -10
View File
@@ -713,24 +713,17 @@ export default function ToobPlayerControl(
bottom: 0 bottom: 0
}} }}
> >
{/**
*
sx={{
'& .MuiTouchRipple-root': {
},
'& .MuiTouchRipple-ripple': {
transform: 'scale(1.9) !important',
}
}}
*/}
<ButtonEx tooltip="Loop Settings" variant="dialogSecondary" <ButtonEx tooltip="Loop Settings" variant="dialogSecondary"
style={{ style={{
flex: "0 1 auto", maxWidth: 200, flex: "0 1 auto", maxWidth: 200,
textTransform: "none", padding: "4px 8px" textTransform: "none", padding: "4px 8px"
}} }}
disabled={audioFile === ""}
startIcon={(<RepeatIcon />)} startIcon={(<RepeatIcon />)}
onClick={() => { onClick={() => {
if (audioFile !== "") {
setShowLoopDialog(true); setShowLoopDialog(true);
}
}} }}
> >
<Typography noWrap variant="caption"> <Typography noWrap variant="caption">