Downloads

This commit is contained in:
Robin E. R. Davies
2025-02-27 03:34:27 -05:00
parent 759132dd4c
commit 229e85d608
11 changed files with 263 additions and 59 deletions
+4
View File
@@ -167,6 +167,10 @@ MimeTypes::MimeTypes()
AddMimeType("MPG", "video/mp2p"); AddMimeType("MPG", "video/mp2p");
AddMimeType("MPEG", "video/mp2p"); AddMimeType("MPEG", "video/mp2p");
// custom.
AddMimeType("NAM", "application/x-nam+json");
} }
const MimeTypes&MimeTypes::instance() const MimeTypes&MimeTypes::instance()
+5
View File
@@ -2879,3 +2879,8 @@ void PiPedalModel::OnAlsaDriverTerminatedAbnormally() {
} }
}); });
} }
bool PiPedalModel::IsInUploadsDirectory(const std::string &path)
{
return !storage.IsInUploadsDirectory(path);
}
+2
View File
@@ -456,6 +456,8 @@ namespace pipedal
std::string RenameFilePropertyFile(const std::string &oldRelativePath, const std::string &newRelativePath, const UiFileProperty &uiFileProperty); std::string RenameFilePropertyFile(const std::string &oldRelativePath, const std::string &newRelativePath, const UiFileProperty &uiFileProperty);
FilePropertyDirectoryTree::ptr GetFilePropertydirectoryTree(const UiFileProperty &uiFileProperty,const std::string&selectedPath); FilePropertyDirectoryTree::ptr GetFilePropertydirectoryTree(const UiFileProperty &uiFileProperty,const std::string&selectedPath);
bool IsInUploadsDirectory(const std::string &path);
std::string UploadUserFile(const std::string &directory, int64_t instanceId, const std::string &patchProperty, const std::string &filename, std::istream &inputStream, size_t streamLength); std::string UploadUserFile(const std::string &directory, int64_t instanceId, const std::string &patchProperty, const std::string &filename, std::istream &inputStream, size_t streamLength);
uint64_t CreateNewPreset(); uint64_t CreateNewPreset();
+88 -14
View File
@@ -153,7 +153,7 @@ namespace pipedal
} }
} }
template <class Clock, class Duration> template <class Clock, class Duration>
RingBufferStatus readWait_until(size_t size,const std::chrono::time_point<Clock, Duration> &time_point) RingBufferStatus readWait_until(size_t size, const std::chrono::time_point<Clock, Duration> &time_point)
{ {
while (true) while (true)
{ {
@@ -161,7 +161,7 @@ namespace pipedal
{ {
std::unique_lock lock(mutex); std::unique_lock lock(mutex);
size_t available = readSpace_(); size_t available = readSpace_();
if (available >= size) if (available >= size)
{ {
return RingBufferStatus::Ready; return RingBufferStatus::Ready;
} }
@@ -215,12 +215,69 @@ namespace pipedal
return (size_t)size; return (size_t)size;
} }
size_t readSpace() size_t readSpace()
{ {
std::unique_lock lock(mutex); std::unique_lock lock(mutex);
return readSpace_(); return readSpace_();
} }
bool write_packet(size_t bytes, void *data)
{
if (MULTI_WRITER)
{
std::lock_guard writeLock{writeMutex};
if (writeSpace() < bytes + sizeof(bytes))
{
return false;
}
size_t index = this->writePosition;
for (size_t i = 0; i < sizeof(bytes); ++i)
{
buffer[(index++() & ringBufferMask] = ((uint8_t*)(&bytes))[i];
}
for (size_t i = 0; i < bytes; ++i)
{
buffer[(index++) & ringBufferMask] = data[i];
}
{
std::lock_guard lock(mutex);
this->writePosition = (index) & ringBufferMask;
}
if (SEMAPHORE_READER)
{
cvRead.notify_all();
}
return true;
}
else
{
if (writeSpace() < bytes + sizeof(bytes))
{
return false;
}
size_t index = this->writePosition;
for (size_t i = 0; i < sizeof(bytes); ++i)
{
buffer[(index++() & ringBufferMask] = ((uint8_t*)(&bytes))[i];
}
for (size_t i = 0; i < bytes; ++i)
{
buffer[(index++) & ringBufferMask] = data[i];
}
{
std::lock_guard lock(mutex);
this->writePosition = (index) & ringBufferMask;
}
if (SEMAPHORE_READER)
{
cvRead.notify_all();
}
return true;
}
}
bool write(size_t bytes, uint8_t *data) bool write(size_t bytes, uint8_t *data)
{ {
if (MULTI_WRITER) if (MULTI_WRITER)
@@ -273,7 +330,7 @@ namespace pipedal
if (MULTI_WRITER) if (MULTI_WRITER)
{ {
std::lock_guard guard(writeMutex); std::lock_guard guard(writeMutex);
if (writeSpace() <= sizeof(bytes) + bytes +bytes2) if (writeSpace() <= sizeof(bytes) + bytes + bytes2)
{ {
return false; return false;
} }
@@ -340,6 +397,23 @@ namespace pipedal
} }
} }
size_t read_packet(size_t maxSize, void*data) {
size_t packet_size;
if (!read(sizeof(packet_size), (uint8_t*)&packet_size))
{
throw std::runtime_error("RingBuffer::read_packet: failed to read packet size.");
}
if (packet_size > maxSize)
{
throw std::runtime_error("RingBuffer::read_packet: packet size too large.");
}
if (!read(packet_size, (uint8_t*)data))
{
throw std::runtime_error("RingBuffer::read_packet: failed to read packet data.");
}
return packet_size;
}
bool read(size_t bytes, uint8_t *data) bool read(size_t bytes, uint8_t *data)
{ {
if (readSpace() < bytes) if (readSpace() < bytes)
@@ -366,18 +440,20 @@ namespace pipedal
delete[] buffer; delete[] buffer;
} }
bool isReadReady() { bool isReadReady()
{
std::lock_guard lock(mutex); std::lock_guard lock(mutex);
if (isReadReady_()) return true; if (isReadReady_())
return true;
return !this->is_open; return !this->is_open;
} }
bool isReadReady(size_t size) { bool isReadReady(size_t size)
{
size_t available = readSpace(); size_t available = readSpace();
return available >= size; return available >= size;
} }
private: private:
size_t readSpace_() size_t readSpace_()
{ {
int64_t size = writePosition - readPosition; int64_t size = writePosition - readPosition;
@@ -389,7 +465,7 @@ namespace pipedal
uint32_t peekSize() uint32_t peekSize()
{ {
volatile uint32_t result; volatile uint32_t result;
uint8_t *p = (uint8_t*)&result; uint8_t *p = (uint8_t *)&result;
size_t ix = this->readPosition; size_t ix = this->readPosition;
for (size_t i = 0; i < sizeof(result); ++i) for (size_t i = 0; i < sizeof(result); ++i)
{ {
@@ -400,14 +476,12 @@ namespace pipedal
bool isReadReady_() bool isReadReady_()
{ {
size_t available = readSpace_(); size_t available = readSpace_();
if (available < sizeof(uint32_t)) return false; if (available < sizeof(uint32_t))
return false;
// peak to get the size! // peak to get the size!
uint32_t packetSize = peekSize(); uint32_t packetSize = peekSize();
return packetSize+sizeof(uint32_t) <= available; return packetSize + sizeof(uint32_t) <= available;
} }
}; };
}; };
+1
View File
@@ -327,6 +327,7 @@ namespace pipedal
return; return;
} }
} }
template <typename T> template <typename T>
void write(RingBufferCommand command, const T &value, size_t dataLength, uint8_t *variableData) void write(RingBufferCommand command, const T &value, size_t dataLength, uint8_t *variableData)
{ {
+46 -1
View File
@@ -34,6 +34,7 @@
#include "PresetBundle.hpp" #include "PresetBundle.hpp"
#include "json.hpp" #include "json.hpp"
#include "HotspotManager.hpp" #include "HotspotManager.hpp"
#include "MimeTypes.hpp"
#define OLD_PRESET_EXTENSION ".piPreset" #define OLD_PRESET_EXTENSION ".piPreset"
#define PRESET_EXTENSION ".piPreset" #define PRESET_EXTENSION ".piPreset"
@@ -51,6 +52,22 @@ using namespace pipedal;
using namespace boost::system; using namespace boost::system;
namespace fs = std::filesystem; namespace fs = std::filesystem;
static bool HasDotDot(const std::filesystem::path &path) {
for (auto &part : path)
{
if (part == "..") {
return true;
}
if (part == ".")
{
return true;
}
}
return false;
}
class UserUploadResponse class UserUploadResponse
{ {
public: public:
@@ -63,6 +80,13 @@ JSON_MAP_REFERENCE(UserUploadResponse, errorMessage)
JSON_MAP_REFERENCE(UserUploadResponse, path) JSON_MAP_REFERENCE(UserUploadResponse, path)
JSON_MAP_END() JSON_MAP_END()
static std::string GetMimeType(const std::filesystem::path&path) {
std::string extension = path.extension();
const MimeTypes&mimeTypes = MimeTypes::instance();
auto result = mimeTypes.MimeTypeFromExtension(extension);
return result;
}
static bool IsZipFile(const std::filesystem::path &path) static bool IsZipFile(const std::filesystem::path &path)
{ {
std::ifstream f(path); std::ifstream f(path);
@@ -119,6 +143,10 @@ public:
return false; return false;
} }
std::string segment = request_uri.segment(1); std::string segment = request_uri.segment(1);
if (segment == "downloadMediaFile")
{
return true;
}
if (segment == "uploadPluginPresets") if (segment == "uploadPluginPresets")
{ {
return true; return true;
@@ -154,7 +182,7 @@ public:
return false; return false;
} }
std::string GetContentDispositionHeader(const std::string &name, const std::string &extension) static std::string GetContentDispositionHeader(const std::string &name, const std::string &extension)
{ {
std::string fileName = name.substr(0, 64) + extension; std::string fileName = name.substr(0, 64) + extension;
std::stringstream s; std::stringstream s;
@@ -423,6 +451,23 @@ public:
{ {
std::string segment = request_uri.segment(1); std::string segment = request_uri.segment(1);
if (segment == "downloadMediaFile") {
fs::path path = request_uri.query("path");
if (!fs::exists(path) || !this->model->IsInUploadsDirectory(path) || HasDotDot(path))
{
throw PiPedalException("File not found.");
}
auto mimeType = GetMimeType(path);
if (mimeType.empty()) {
throw PiPedalException("Can't download files of this type.");
}
res.set(HttpField::content_type, mimeType);
res.set(HttpField::cache_control, "no-cache");
std::string disposition = GetContentDispositionHeader(path.stem().string(), path.extension().string());
res.set(HttpField::content_disposition, disposition);
res.setBodyFile(path,false);
}
if (segment == "uploadPluginPresets") if (segment == "uploadPluginPresets")
{ {
PluginPresets presets; PluginPresets presets;
+1 -1
View File
@@ -29,7 +29,7 @@
"@vitejs/plugin-react": "^4.3.4", "@vitejs/plugin-react": "^4.3.4",
"eslint": "^9.19.0", "eslint": "^9.19.0",
"eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.18", "eslint-plugin-react-refresh": "^0.4.19",
"globals": "^15.14.0", "globals": "^15.14.0",
"typescript": "~5.7.2", "typescript": "~5.7.2",
"typescript-eslint": "^8.22.0", "typescript-eslint": "^8.22.0",
+1 -1
View File
@@ -31,7 +31,7 @@
"@vitejs/plugin-react": "^4.3.4", "@vitejs/plugin-react": "^4.3.4",
"eslint": "^9.19.0", "eslint": "^9.19.0",
"eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.18", "eslint-plugin-react-refresh": "^0.4.19",
"globals": "^15.14.0", "globals": "^15.14.0",
"typescript": "~5.7.2", "typescript": "~5.7.2",
"typescript-eslint": "^8.22.0", "typescript-eslint": "^8.22.0",
+104 -41
View File
@@ -19,7 +19,7 @@
import React from 'react'; import React from 'react';
import {createStyles} from './WithStyles'; import { createStyles } from './WithStyles';
import { Theme } from '@mui/material/styles'; import { Theme } from '@mui/material/styles';
@@ -33,6 +33,7 @@ import { PiPedalModel, PiPedalModelFactory, FileEntry, BreadcrumbEntry, FileRequ
import { isDarkMode } from './DarkMode'; import { isDarkMode } from './DarkMode';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
import FileUploadIcon from '@mui/icons-material/FileUpload'; import FileUploadIcon from '@mui/icons-material/FileUpload';
import FileDownloadIcon from '@mui/icons-material/FileDownload';
import AudioFileIcon from '@mui/icons-material/AudioFile'; import AudioFileIcon from '@mui/icons-material/AudioFile';
import ArrowBackIcon from '@mui/icons-material/ArrowBack'; import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import FolderIcon from '@mui/icons-material/Folder'; import FolderIcon from '@mui/icons-material/Folder';
@@ -108,9 +109,11 @@ export interface FilePropertyDialogState {
selectedFileIsDirectory: boolean; selectedFileIsDirectory: boolean;
selectedFileProtected: boolean; selectedFileProtected: boolean;
hasSelection: boolean; hasSelection: boolean;
hasFileSelection: boolean;
canDelete: boolean; canDelete: boolean;
fileResult: FileRequestResult; fileResult: FileRequestResult;
navDirectory: string; navDirectory: string;
windowWidth: number;
uploadDirectory: string; uploadDirectory: string;
isProtectedDirectory: boolean; isProtectedDirectory: boolean;
columns: number; columns: number;
@@ -199,10 +202,12 @@ export default withStyles(
fullScreen: this.getFullScreen(), fullScreen: this.getFullScreen(),
selectedFile: selectedFile, selectedFile: selectedFile,
selectedFileProtected: true, selectedFileProtected: true,
windowWidth: this.windowSize.width,
selectedFileIsDirectory: false, selectedFileIsDirectory: false,
navDirectory: this.getNavDirectoryFromFile(selectedFile, props.fileProperty), navDirectory: this.getNavDirectoryFromFile(selectedFile, props.fileProperty),
uploadDirectory: "", uploadDirectory: "",
hasSelection: false, hasSelection: false,
hasFileSelection: false,
canDelete: false, canDelete: false,
columns: 0, columns: 0,
columnWidth: 1, columnWidth: 1,
@@ -259,12 +264,13 @@ export default withStyles(
// } // }
filesResult.files.splice(0, 0, { pathname: "", displayName: "<none>", isDirectory: false, isProtected: true }); filesResult.files.splice(0, 0, { pathname: "", displayName: "<none>", isDirectory: false, isProtected: true });
let fileEntry = this.getFileEntry(filesResult.files, this.state.selectedFile); let fileEntry = this.getFileEntry(filesResult.files, this.state.selectedFile);
this.setState({ this.setState({
isProtectedDirectory: filesResult.isProtected, isProtectedDirectory: filesResult.isProtected,
fileResult: filesResult, fileResult: filesResult,
hasSelection: !!fileEntry, hasSelection: !!fileEntry,
selectedFileProtected: fileEntry ? fileEntry.isProtected: true, hasFileSelection: !!fileEntry && (!fileEntry.isDirectory) && (!fileEntry.isProtected),
selectedFileProtected: fileEntry ? fileEntry.isProtected : true,
navDirectory: navPath, navDirectory: navPath,
uploadDirectory: filesResult.currentDirectory uploadDirectory: filesResult.currentDirectory
}); });
@@ -297,7 +303,8 @@ export default withStyles(
onWindowSizeChanged(width: number, height: number): void { onWindowSizeChanged(width: number, height: number): void {
this.setState({ this.setState({
fullScreen: this.getFullScreen() fullScreen: this.getFullScreen(),
windowWidth: width
}) })
if (this.lastDivRef !== null) { if (this.lastDivRef !== null) {
this.onMeasureRef(this.lastDivRef); this.onMeasureRef(this.lastDivRef);
@@ -377,7 +384,8 @@ export default withStyles(
selectedFile: fileEntry.pathname, selectedFile: fileEntry.pathname,
selectedFileIsDirectory: fileEntry.isDirectory, selectedFileIsDirectory: fileEntry.isDirectory,
selectedFileProtected: fileEntry.isProtected, selectedFileProtected: fileEntry.isProtected,
hasSelection: this.isFileInList(this.state.fileResult.files, fileEntry.pathname) hasSelection: this.isFileInList(this.state.fileResult.files, fileEntry.pathname),
hasFileSelection: !fileEntry.isDirectory && !fileEntry.isDirectory && !fileEntry.isProtected
}) })
} }
onDoubleClickValue(selectedFile: string) { onDoubleClickValue(selectedFile: string) {
@@ -392,14 +400,21 @@ export default withStyles(
this.setState({ menuAnchorEl: null }); this.setState({ menuAnchorEl: null });
} }
handleDelete() { handleDelete() {
if (this.state.selectedFileProtected) { if (this.state.selectedFileProtected) {
return; return;
} }
this.setState({ openConfirmDeleteDialog: true }); this.setState({ openConfirmDeleteDialog: true });
} }
handleDownloadFile() {
if (this.state.selectedFileProtected || !this.state.hasFileSelection) {
return;
}
let file = this.state.selectedFile;
this.model.downloadAudioFile(file);
}
handleConfirmDelete() { handleConfirmDelete() {
if (this.state.selectedFileProtected) { if (this.state.selectedFileProtected) {
return; return;
} }
this.setState({ openConfirmDeleteDialog: false }); this.setState({ openConfirmDeleteDialog: false });
if (this.state.hasSelection) { if (this.state.hasSelection) {
@@ -424,20 +439,21 @@ export default withStyles(
this.model.deleteUserFile(this.state.selectedFile) this.model.deleteUserFile(this.state.selectedFile)
.then( .then(
() => { () => {
if (newSelection) if (newSelection) {
{ this.setState({
this.setState({
selectedFile: newSelection.pathname, selectedFile: newSelection.pathname,
selectedFileIsDirectory: newSelection.isDirectory, selectedFileIsDirectory: newSelection.isDirectory,
selectedFileProtected: newSelection.isProtected, selectedFileProtected: newSelection.isProtected,
hasSelection: true hasSelection: true,
hasFileSelection: !newSelection.isDirectory && !newSelection.isProtected
}); });
} else { } else {
this.setState({ this.setState({
selectedFile: "", selectedFile: "",
selectedFileIsDirectory: false, selectedFileIsDirectory: false,
selectedFileProtected: true, selectedFileProtected: true,
hasSelection: false hasSelection: false,
hasFileSelection: false
}); });
} }
@@ -580,8 +596,7 @@ export default withStyles(
} }
let protectedDirectory = this.state.fileResult.isProtected; let protectedDirectory = this.state.fileResult.isProtected;
let protectedItem = true; let protectedItem = true;
if (this.state.hasSelection) if (this.state.hasSelection) {
{
protectedItem = this.state.selectedFileProtected; protectedItem = this.state.selectedFileProtected;
} }
let canMoveOrRename = this.hasSelectedFileOrFolder() && !protectedItem; let canMoveOrRename = this.hasSelectedFileOrFolder() && !protectedItem;
@@ -719,31 +734,79 @@ export default withStyles(
</DialogContent> </DialogContent>
<Divider /> <Divider />
<DialogActions style={{ justifyContent: "stretch" }}> <DialogActions style={{ justifyContent: "stretch" }}>
<div style={{ display: "flex", width: "100%", alignItems: "center", flexFlow: "row nowrap" }}> {this.state.windowWidth > 500 ? (
<IconButton style={{ visibility: (this.state.hasSelection ? "visible" : "hidden") }} aria-label="delete" component="label" color="primary" <div style={{ display: "flex", width: "100%", alignItems: "center", flexFlow: "row nowrap" }}>
disabled={!this.state.hasSelection || this.state.selectedFile === "" || protectedItem} <IconButton style={{ visibility: (this.state.hasSelection ? "visible" : "hidden") }} aria-label="delete" component="label" color="primary"
onClick={() => this.handleDelete()} > disabled={!this.state.hasSelection || this.state.selectedFile === "" || protectedItem}
<OldDeleteIcon fontSize='small' /> onClick={() => this.handleDelete()} >
</IconButton> <OldDeleteIcon fontSize='small' />
</IconButton>
<Button style={{ flex: "0 0 auto" }} aria-label="upload" variant="text" startIcon={<FileUploadIcon />} <Button style={{ flex: "0 0 auto" }} aria-label="upload" variant="text" startIcon={<FileUploadIcon />}
onClick={() => { this.setState({ openUploadFileDialog: true }) }} disabled={protectedDirectory} onClick={() => { this.setState({ openUploadFileDialog: true }) }} disabled={protectedDirectory}
> >
<div>Upload</div> <div>Upload</div>
</Button> </Button>
<div style={{ flex: "1 1 auto" }}>&nbsp;</div>
<Button variant="dialogSecondary" onClick={() => { this.props.onCancel(); }} aria-label="cancel"> <Button style={{ flex: "0 0 auto" }} aria-label="upload" variant="text" startIcon={<FileDownloadIcon />}
Cancel onClick={() => { this.handleDownloadFile(); }} disabled={protectedDirectory || !this.state.hasFileSelection}
</Button> >
<Button variant="dialogPrimary" style={{ flex: "0 0 auto" }} <div>Download</div>
onClick={() => { this.openSelectedFile(); }} </Button>
disabled={(!this.state.hasSelection) && this.state.selectedFile !== ""} aria-label="select" <div style={{ flex: "1 1 auto" }}>&nbsp;</div>
>
{okButtonText} <Button variant="dialogSecondary" onClick={() => { this.props.onCancel(); }} aria-label="cancel">
</Button> Cancel
</div> </Button>
<Button variant="dialogPrimary" style={{ flex: "0 0 auto" }}
onClick={() => { this.openSelectedFile(); }}
disabled={(!this.state.hasSelection) && this.state.selectedFile !== ""} aria-label="select"
>
{okButtonText}
</Button>
</div>
) : (
<div style={{width: "100%"}}>
<div style={{ display: "flex", width: "100%", alignItems: "center", flexFlow: "row nowrap" }}>
<IconButton style={{ visibility: (this.state.hasSelection ? "visible" : "hidden") }} aria-label="delete" component="label" color="primary"
disabled={!this.state.hasSelection || this.state.selectedFile === "" || protectedItem}
onClick={() => this.handleDelete()} >
<OldDeleteIcon fontSize='small' />
</IconButton>
<Button style={{ flex: "0 0 auto" }} aria-label="upload" variant="text" startIcon={<FileUploadIcon />}
onClick={() => { this.setState({ openUploadFileDialog: true }) }} disabled={protectedDirectory}
>
<div>Upload</div>
</Button>
<Button style={{ flex: "0 0 auto" }} aria-label="upload" variant="text" startIcon={<FileDownloadIcon />}
onClick={() => { this.handleDownloadFile(); }} disabled={protectedDirectory || !this.state.hasFileSelection}
>
<div>Download</div>
</Button>
<div style={{ flex: "1 1 auto" }}>&nbsp;</div>
</div>
<div style={{ display: "flex", width: "100%", alignItems: "center", flexFlow: "row nowrap" }}>
<div style={{ flex: "1 1 auto" }}>&nbsp;</div>
<Button variant="dialogSecondary" onClick={() => { this.props.onCancel(); }} aria-label="cancel">
Cancel
</Button>
<Button variant="dialogPrimary" style={{ flex: "0 0 auto" }}
onClick={() => { this.openSelectedFile(); }}
disabled={(!this.state.hasSelection) && this.state.selectedFile !== ""} aria-label="select"
>
{okButtonText}
</Button>
</div>
</div>
)}
</DialogActions> </DialogActions>
<UploadFileDialog <UploadFileDialog
open={this.state.openUploadFileDialog} open={this.state.openUploadFileDialog}
@@ -897,10 +960,10 @@ export default withStyles(
private onExecuteNewFolder(newName: string) { private onExecuteNewFolder(newName: string) {
this.model.createNewSampleDirectory(pathConcat(this.state.navDirectory, newName), this.props.fileProperty) this.model.createNewSampleDirectory(pathConcat(this.state.navDirectory, newName), this.props.fileProperty)
.then((newPath) => { .then((newPath) => {
this.setState({ this.setState({
selectedFile: newPath, selectedFile: newPath,
selectedFileIsDirectory: selectedFileIsDirectory:
true,selectedFileProtected: false true, selectedFileProtected: false
}); });
this.requestFiles(this.state.navDirectory); this.requestFiles(this.state.navDirectory);
this.requestScroll = true this.requestScroll = true
+10
View File
@@ -2388,6 +2388,16 @@ export class PiPedalModel //implements PiPedalModel
this.webSocket?.send("cancelListenForMidiEvent", listenHandle._handle); this.webSocket?.send("cancelListenForMidiEvent", listenHandle._handle);
} }
downloadAudioFile(filePath: string) {
let downloadUrl = this.varServerUrl + "downloadMediaFile?path=" + encodeURIComponent(filePath);
// download with no flashing temporary tab.
let link = window.document.createElement("A") as HTMLLinkElement;
link.href = downloadUrl;
link.setAttribute("download", "");
link.click();
}
download(targetType: string, instanceId: number | string): void { download(targetType: string, instanceId: number | string): void {
if (instanceId === -1) return; if (instanceId === -1) return;
let url = this.varServerUrl + targetType + "?id=" + instanceId; let url = this.varServerUrl + targetType + "?id=" + instanceId;
+1 -1
View File
@@ -790,7 +790,7 @@ const PluginControlView =
}) })
.catch((error) => { .catch((error) => {
this.model.showAlert("Unable to complete the operation. Audio is not running." + error); this.model.showAlert("Unable to complete the operation. " + error);
}); });
this.setState({ showFileDialog: false }); this.setState({ showFileDialog: false });
} }