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()
{
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;
for (const auto &dbFile : dbFiles)
{
@@ -298,7 +304,16 @@ std::vector<DbFileInfo> AudioDirectoryInfoImpl::UpdateDbFiles()
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();
std::string name = path.filename();
+51 -44
View File
@@ -142,25 +142,27 @@ uint32_t FileMetadataFeature::getFileMetadata(
json_variant jsonData;
std::string result;
if (fs::exists(path))
try
{
try
if (fs::exists(path))
{
std::ifstream f(path);
json_reader reader(f);
reader.read(&jsonData);
result = jsonData.as_object()->at(std::string(key)).as_string();
}
catch (const std::exception &e)
else
{
Lv2Log::debug(SS("No metadata file found for " << absolute_path << " key: " << key));
return 0;
}
}
else
catch (const std::exception &e)
{
Lv2Log::debug(SS("No metadata file found for " << absolute_path << " key: " << key));
Lv2Log::debug(SS("Permission denied for " << absolute_path << " key: " << key));
return 0;
}
size_t len = result.length() + 1; // +1 for null terminator
if (len > fileMetadataSize || fileMetadata == nullptr)
{
@@ -168,7 +170,7 @@ uint32_t FileMetadataFeature::getFileMetadata(
return len;
}
memcpy(fileMetadata, result.c_str(), len);
Lv2Log::debug(SS("Get file metadata for " << absolute_path << " key: " << key << " value: " << result));
Lv2Log::debug(SS("Get file metadata for " << absolute_path << " key: " << key << " value: " << result));
return len;
}
PIPEDAL_FileMetadata_Status FileMetadataFeature::deleteFileMetadata(
@@ -181,52 +183,57 @@ PIPEDAL_FileMetadata_Status FileMetadataFeature::deleteFileMetadata(
}
Lv2Log::debug(SS("Delete file metadata for " << absolute_path << " key: " << key));
std::filesystem::path path{SS(absolute_path << ".mdata")};
try {
std::filesystem::path path{SS(absolute_path << ".mdata")};
if (!fs::exists(path))
{
return PIPEDAL_FILE_METADATA_NOT_FOUND; // Metadata file does not exist
}
if (!fs::exists(path))
{
return PIPEDAL_FILE_METADATA_NOT_FOUND; // Metadata file does not exist
}
json_variant jsonData;
try
{
std::ifstream f(path);
json_reader reader(f);
reader.read(&jsonData);
}
catch (const std::exception &e)
{
return PIPEDAL_FILE_METADATA_NOT_FOUND; // Failed to read existing metadata
}
json_variant jsonData;
try
{
std::ifstream f(path);
json_reader reader(f);
reader.read(&jsonData);
}
catch (const std::exception &e)
{
return PIPEDAL_FILE_METADATA_NOT_FOUND; // Failed to read existing metadata
}
auto obj = jsonData.as_object();
auto it = obj->find(std::string(key));
if (it == obj->end())
{
return PIPEDAL_FILE_METADATA_NOT_FOUND; // Key not found
}
auto obj = jsonData.as_object();
auto it = obj->find(std::string(key));
if (it == obj->end())
{
return PIPEDAL_FILE_METADATA_NOT_FOUND; // Key not found
}
obj->erase(it);
obj->erase(it);
if (obj->begin() == obj->end())
{
// If the object is empty, delete the metadata file
fs::remove(path);
return PIPEDAL_FILE_METADATA_SUCCESS; // Metadata deleted successfully
}
if (obj->begin() == obj->end())
{
// If the object is empty, delete the metadata file
fs::remove(path);
return PIPEDAL_FILE_METADATA_SUCCESS; // Metadata deleted successfully
}
std::ofstream f(path);
if (!f.is_open())
{
Lv2Log::error(SS("Failed to write metadata file " << path));
std::ofstream f(path);
if (!f.is_open())
{
Lv2Log::error(SS("Failed to write metadata file " << path));
return PIPEDAL_FILE_METADATA_PERMISSION_DENIED; // Failed to open existing metadata
}
json_writer writer(f);
writer.write(jsonData);
return PIPEDAL_FILE_METADATA_SUCCESS;
} 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
}
json_writer writer(f);
writer.write(jsonData);
return PIPEDAL_FILE_METADATA_SUCCESS;
}
PIPEDAL_FileMetadata_Status FileMetadataFeature::S_setFileMetadata(
+76 -50
View File
@@ -1690,7 +1690,8 @@ static void AddFilesToResult(
}
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
@@ -1723,7 +1724,7 @@ static void AddTracksToResult(
std::set<std::string> validExtensions = fileProperty.GetPermittedFileExtensions(
modDirectoryInfo ? modDirectoryInfo->modType : "");
if (validExtensions.size() == 0)
if (validExtensions.size() == 0)
{
const auto &audioExtensions = MimeTypes::instance().AudioExtensions();
validExtensions.insert(audioExtensions.begin(), audioExtensions.end());
@@ -1733,54 +1734,76 @@ static void AddTracksToResult(
try
{
// Add directories first.
for (auto const &dir_entry : std::filesystem::directory_iterator(rootPath))
try
{
if (!IsValidUtf8(dir_entry.path().string()))
for (auto const &dir_entry : std::filesystem::directory_iterator(rootPath))
{
Lv2Log::warning("Invalid UTF-8 name in directory: " + dir_entry.path().string());
continue; // skip invalid UTF-8 names.
}
const auto &path = dir_entry.path();
auto name = path.filename().string();
if (dir_entry.is_directory())
{
resultFiles.push_back(FileEntry{path, name, true, fs::is_symlink(path)});
if (!IsValidUtf8(dir_entry.path().string()))
{
Lv2Log::warning("Invalid UTF-8 name in directory: " + dir_entry.path().string());
continue; // skip invalid UTF-8 names.
}
const auto &path = dir_entry.path();
auto name = path.filename().string();
try {
if (dir_entry.is_directory())
{
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();
std::sort(
resultFiles.begin(),
resultFiles.begin(),
resultFiles.end(),
[collator](const FileEntry &l, const FileEntry &r)
{
if (l.isDirectory_ != r.isDirectory_)
{
return l.isDirectory_ > r.isDirectory_;
}
return collator->Compare(l.displayName_ ,r.displayName_) < 0;
});
if (l.isDirectory_ != r.isDirectory_)
{
return l.isDirectory_ > r.isDirectory_;
}
return collator->Compare(l.displayName_, r.displayName_) < 0;
});
// Add audio files.
auto audioFiles = AudioDirectoryInfo::Create(rootPath,
GetShadowIndexDirectory(audioRootDirectory,rootPath)
);
for (const auto &audioFile : audioFiles->GetFiles())
GetShadowIndexDirectory(audioRootDirectory, rootPath));
try
{
fs::path audioFilePath = rootPath / audioFile.fileName();
std::string extension = UiFileProperty::GetFileExtension(audioFilePath);
if (validExtensions.size() == 0 || validExtensions.contains(extension) || validExtensions.contains(".*"))
for (const auto &audioFile : audioFiles->GetFiles())
{
resultFiles.push_back(
FileEntry(
audioFilePath,
audioFile.title(),
false,
std::make_shared<AudioFileMetadata>(audioFile)));
fs::path audioFilePath = rootPath / audioFile.fileName();
std::string extension = UiFileProperty::GetFileExtension(audioFilePath);
if (validExtensions.size() == 0 || validExtensions.contains(extension) || validExtensions.contains(".*"))
{
resultFiles.push_back(
FileEntry(
audioFilePath,
audioFile.title(),
false,
std::make_shared<AudioFileMetadata>(audioFile)));
}
}
}
catch (const std::exception &error)
{
throw std::logic_error(
SS("AddTracksToResult failed to enumerate audio files. " << rootPath.string() << " - " << error.what()));
}
}
catch (const std::exception &error)
{
throw std::logic_error("GetFileList failed. Directory not found: " + rootPath.string());
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))
{
AddTracksToResult(this->GetPluginUploadDirectory(),result, rootModDirectory, fileProperty, relativePath);
AddTracksToResult(this->GetPluginUploadDirectory(), result, rootModDirectory, fileProperty, relativePath);
}
else
{
@@ -2046,7 +2069,7 @@ void Storage::DeleteSampleFile(const std::filesystem::path &fileName)
else
{
std::filesystem::remove(fileName);
if (IsInAudioTracksDirectory(fileName))
if (IsInAudioTracksDirectory(fileName))
{
// remove the metadata file as well.
std::filesystem::remove(fileName.string() + ".mdata");
@@ -2074,13 +2097,12 @@ std::filesystem::path Storage::MakeUserFilePath(const std::string &directory, co
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()))
{
// allow artwork files.
return true;
return true;
}
return false;
}
@@ -2208,9 +2230,10 @@ std::string Storage::RenameFilePropertyFile(
}
std::filesystem::rename(oldPath, newPath);
if (fs::exists(oldPath.string()+".mdata")) {
if (fs::exists(oldPath.string() + ".mdata"))
{
// 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;
}
@@ -2222,12 +2245,13 @@ fs::path MakeVersionedPath(const fs::path &path)
return path; // no need to version a non-existing file.
}
fs::path newPath = path;
auto stem = newPath.stem().string();;
auto stem = newPath.stem().string();
;
if (stem.ends_with(")"))
{
// remove the trailing (n) from the file name.
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.
if (stemVersion.find_first_not_of("0123456789") == std::string::npos)
{
@@ -2237,7 +2261,7 @@ fs::path MakeVersionedPath(const fs::path &path)
{
// remove trailing space.
stem = stem.substr(0, stem.length() - 1);
}
}
}
}
@@ -2246,12 +2270,15 @@ fs::path MakeVersionedPath(const fs::path &path)
{
// append (n) to the file name.
std::string newFileName;
if (version == 0) {
if (version == 0)
{
newFileName = SS(stem << newPath.extension().string());
} else {
}
else
{
// append (n) to the file name.
newFileName = SS(stem << " (" << std::to_string(version) << ")" << newPath.extension().string());
}
}
newPath = newPath.parent_path() / newFileName;
++version;
}
@@ -2280,7 +2307,6 @@ std::string Storage::CopyFilePropertyFile(
std::filesystem::path newPath = this->GetPluginUploadDirectory() / uiFileProperty.directory() / newRelativePath;
if (!this->IsValidSampleFileName(newPath))
{
throw std::runtime_error("Invalid file name.");
@@ -2299,14 +2325,14 @@ std::string Storage::CopyFilePropertyFile(
std::filesystem::create_hard_link(oldPath, newPath);
if (IsInAudioTracksDirectory(oldPath)
&& IsInAudioTracksDirectory(newPath))
if (IsInAudioTracksDirectory(oldPath) && IsInAudioTracksDirectory(newPath))
{
std::filesystem::path metadataPath = SS(oldPath.string() << ".mdata");
if (fs::exists(metadataPath)) {
if (fs::exists(metadataPath))
{
// copy the metadata file as well.
std::filesystem::copy_file(
metadataPath,
metadataPath,
SS(newPath.string() << ".mdata"),
fs::copy_options::overwrite_existing);
}
+81 -76
View File
@@ -52,15 +52,13 @@ static const std::string PLUGIN_PRESETS_MIME_TYPE = "application/vnd.pipedal.plu
static const std::string PRESET_MIME_TYPE = "application/vnd.pipedal.preset";
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_SHORT = "max-age=300,public"; // 5 minutes
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
using namespace pipedal;
using namespace boost::system;
namespace fs = std::filesystem;
class UserUploadResponse
{
public:
@@ -87,7 +85,7 @@ int32_t ConvertThumbnailSize(const std::string &param)
{
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)
@@ -128,7 +126,6 @@ private:
std::vector<std::string> extensions;
};
class DownloadIntercept : public RequestHandler
{
PiPedalModel *model;
@@ -392,16 +389,15 @@ public:
{
auto lastModified = std::filesystem::last_write_time(path);
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,
GetShadowIndexDirectory(
this->model->GetPluginUploadDirectory(),
path));
GetShadowIndexDirectory(
this->model->GetPluginUploadDirectory(),
path));
}
virtual void get_response(
const uri &request_uri,
HttpRequest &req,
@@ -500,89 +496,98 @@ public:
throw PiPedalException("File not found.");
}
AudioDirectoryInfo::Ptr audioDirectory = CreateDirectoryInfo(path.parent_path());
auto files = audioDirectory->GetFiles();
for (const auto &file : files)
try
{
std::string fileNameOnly = path.filename();
if (file.fileName() == fileNameOnly)
auto files = audioDirectory->GetFiles();
for (const auto &file : files)
{
std::stringstream ss;
json_writer writer(ss);
writer.write(file);
res.setBody(ss.str());
return;
std::string fileNameOnly = path.filename();
if (file.fileName() == fileNameOnly)
{
std::stringstream ss;
json_writer writer(ss);
writer.write(file);
res.setBody(ss.str());
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.
throw PiPedalException("File not found in directory.");
}
else if (segment == "Thumbnail")
{
ThumbnailTemporaryFile thumbnailTemporaryFile;
try
{
fs::path path = request_uri.query("path");
if (path.empty() ||
!fs::exists(path) ||
!this->model->IsInUploadsDirectory(path) ||
HasDotDot(path))
std::shared_ptr<TemporaryFile> thumbnail;
try
{
// path for folder thumbnails.
path = request_uri.query("ffile");
std::shared_ptr<TemporaryFile> thumbnail;
if (!path.empty() && fs::exists(path) &&
this->model->IsInUploadsDirectory(path) &&
!HasDotDot(path))
fs::path path = request_uri.query("path");
if (path.empty() ||
!fs::exists(path) ||
!this->model->IsInUploadsDirectory(path) ||
HasDotDot(path))
{
// path is a folder.
thumbnail = std::make_shared<TemporaryFile>();
thumbnail->SetNonDeletedPath(path);
}
else
{
auto t = AudioDirectoryInfo::DefaultThumbnailTemporaryFile();
thumbnail = std::make_shared<TemporaryFile>();
thumbnail->SetNonDeletedPath(t.Path());
std::shared_ptr<TemporaryFile> thumbnail;
// path for folder thumbnails.
path = request_uri.query("ffile");
if (!path.empty() && fs::exists(path) &&
this->model->IsInUploadsDirectory(path) &&
!HasDotDot(path))
{
// path is a folder.
thumbnail = std::make_shared<TemporaryFile>();
thumbnail->SetNonDeletedPath(path);
}
else
{
auto t = AudioDirectoryInfo::DefaultThumbnailTemporaryFile();
thumbnail = std::make_shared<TemporaryFile>();
thumbnail->SetNonDeletedPath(t.Path());
}
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.
setLastModifiedFromFile(res, thumbnail->Path());
res.set(HttpField::content_length, std::to_string(fs::file_size(thumbnail->Path())));
res.setBodyFile(thumbnail);
return;
}
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.
setLastModifiedFromFile(res,thumbnail->Path());
res.set(HttpField::content_length, std::to_string(fs::file_size(thumbnail->Path())));
res.setBodyFile(thumbnail);
return;
int32_t width = ConvertThumbnailSize(request_uri.query("w"));
int32_t height = ConvertThumbnailSize(request_uri.query("h"));
AudioDirectoryInfo::Ptr audioDirectory = CreateDirectoryInfo(
path.parent_path());
audioDirectory->GetFiles(); // ensure that the .index file is up to date.
thumbnailTemporaryFile = audioDirectory->GetThumbnail(path.filename(), width, height);
}
int32_t width = ConvertThumbnailSize(request_uri.query("w"));
int32_t height = ConvertThumbnailSize(request_uri.query("h"));
AudioDirectoryInfo::Ptr audioDirectory = CreateDirectoryInfo(
path.parent_path());
audioDirectory->GetFiles(); // ensure that the .index file is up to date.
ThumbnailTemporaryFile thumbnail;
try {
thumbnail = audioDirectory->GetThumbnail(path.filename(), width, height);
} catch (const std::exception &e) {
thumbnail = audioDirectory->DefaultThumbnailTemporaryFile();
catch (const std::exception &e)
{
fs::path defaultThumbnail = model->GetWebRoot() / "img/missing_thumbnail.jpg";
thumbnailTemporaryFile.SetNonDeletedPath(defaultThumbnail, "image/jpeg");
}
res.set(HttpField::content_type, thumbnail.GetMimeType());
res.set(HttpField::content_type, thumbnailTemporaryFile.GetMimeType());
res.set(HttpField::cache_control, CACHE_CONTROL_INDEFINITELY); // URL is cache-busted with time-stamp.
setLastModifiedFromFile(res, path);
res.set(HttpField::content_length, std::to_string(fs::file_size(thumbnail.Path())));
setLastModifiedFromFile(res, thumbnailTemporaryFile.Path());
res.set(HttpField::content_length, std::to_string(fs::file_size(thumbnailTemporaryFile.Path())));
std::filesystem::path t = thumbnail.Path();
res.setBodyFile(t,thumbnail.DeleteFile());
thumbnail.Detach();
std::filesystem::path t = thumbnailTemporaryFile.Path();
res.setBodyFile(t, thumbnailTemporaryFile.DeleteFile());
thumbnailTemporaryFile.Detach();
}
catch (const std::exception &e)
{
Lv2Log::error("Error getting thumbnail: %s - (%s)", request_uri.str().c_str(), e.what());
Lv2Log::error("Error getting thumbnail: %s - (%s)", request_uri.str().c_str(), e.what());
throw e;
}
}
@@ -605,11 +610,11 @@ public:
{
result = (directoryPath / result).string();
}
// json-encode the result.
std::stringstream ss;
json_writer writer(ss);
writer.write(result);
writer.write(result);
res.setBody(ss.str());
}
@@ -637,11 +642,11 @@ public:
{
result = (directoryPath / result).string();
}
// json-encode the result.
std::stringstream ss;
json_writer writer(ss);
writer.write(result);
writer.write(result);
res.setBody(ss.str());
}
-1
View File
@@ -105,7 +105,6 @@ add_custom_command(
public/var/current_pedalboard.json
public/var/config.json
public/favicon.ico
public/index.html
public/iso_codes.json
public/logo512.png
public/sample_lv2_plugins.json
+1 -1
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<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="color-scheme" content="dark light"/> <!-- uses media queries in web views. -->
<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 {
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
onDoubleClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
onLongPressStart?: (currentTarget: HTMLButtonElement, e: React.PointerEvent<HTMLButtonElement> | React.MouseEvent<HTMLButtonElement>) => boolean;
onLongPressMove?: (e: React.PointerEvent<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) {
// Ensure that the props are spread correctly
const { onClick, onLongPressStart, onLongPressMove:
const { onClick, onDoubleClick, onLongPressStart, onLongPressMove:
doLongPressMove,onLongPressEnd, longPressDelay,instantMouseLongPress, ...rest } = props;
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 [pointerDownPoint, setPointerDownPoint] = React.useState<Point>({ x: 0, y: 0 });
let [ longPressedElement, setLongPressedElement ] = React.useState<HTMLButtonElement | null>(null);
let [lastClick, setLastClick] = React.useState<number | null>(null);
function handleSuppressClick(e: MouseEvent) {
@@ -278,8 +280,26 @@ export default function DraggableButtonBase(props: DraggableButtonBaseProps) {
if (onClick) {
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) => {
if (pointerId !== null) {
e.currentTarget.releasePointerCapture(e.pointerId);
+7 -7
View File
@@ -31,7 +31,7 @@ import Typography from '@mui/material/Typography';
import Input from '@mui/material/Input';
import Select from '@mui/material/Select';
import Switch from '@mui/material/Switch';
import Utility, { nullCast } from './Utility';
import Utility from './Utility';
import MenuItem from '@mui/material/MenuItem';
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
import DialIcon from './svg/fx_dial.svg?react';
@@ -231,12 +231,12 @@ const PluginControl =
onInputFocus(event: SyntheticEvent): void {
//this.displayValueRef.current!.style.display = "none";
this.setState({ editFocused: true });
if (Utility.hasIMEKeyboard()) {
event.preventDefault();
event.stopPropagation();
this.inputRef.current?.blur();
this.props.requestIMEEdit(nullCast(this.props.uiControl), this.props.value)
}
// if (Utility.hasIMEKeyboard()) {
// event.preventDefault();
// event.stopPropagation();
// this.inputRef.current?.blur();
// this.props.requestIMEEdit(nullCast(this.props.uiControl), this.props.value)
// }
}
onInputKeyPress(e: any): void {
if (e.charCode === 13 && this.inputChanged) {
+4 -11
View File
@@ -713,24 +713,17 @@ export default function ToobPlayerControl(
bottom: 0
}}
>
{/**
*
sx={{
'& .MuiTouchRipple-root': {
},
'& .MuiTouchRipple-ripple': {
transform: 'scale(1.9) !important',
}
}}
*/}
<ButtonEx tooltip="Loop Settings" variant="dialogSecondary"
style={{
flex: "0 1 auto", maxWidth: 200,
textTransform: "none", padding: "4px 8px"
}}
disabled={audioFile === ""}
startIcon={(<RepeatIcon />)}
onClick={() => {
setShowLoopDialog(true);
if (audioFile !== "") {
setShowLoopDialog(true);
}
}}
>
<Typography noWrap variant="caption">