diff --git a/examples/Assets/DemoUtilities.h b/examples/Assets/DemoUtilities.h index 6d27104d23..1c5534bff7 100644 --- a/examples/Assets/DemoUtilities.h +++ b/examples/Assets/DemoUtilities.h @@ -86,11 +86,26 @@ inline File getExamplesDirectory() noexcept #endif } -inline std::unique_ptr createAssetInputStream (const char* resourcePath) +enum class AssertAssetExists +{ + no, + yes +}; + +inline std::unique_ptr createAssetInputStream (const char* resourcePath, + [[maybe_unused]] AssertAssetExists assertExists = AssertAssetExists::yes) { #if JUCE_ANDROID ZipFile apkZip (File::getSpecialLocation (File::invokedExecutableFile)); - return std::unique_ptr (apkZip.createStreamForEntry (apkZip.getIndexOfFileName ("assets/" + String (resourcePath)))); + const auto fileIndex = apkZip.getIndexOfFileName ("assets/" + String (resourcePath)); + + if (fileIndex == -1) + { + jassert (assertExists == AssertAssetExists::no); + return {}; + } + + return std::unique_ptr (apkZip.createStreamForEntry (fileIndex)); #else #if JUCE_IOS auto assetsDir = File::getSpecialLocation (File::currentExecutableFile) @@ -106,7 +121,12 @@ inline std::unique_ptr createAssetInputStream (const char* resource #endif auto resourceFile = assetsDir.getChildFile (resourcePath); - jassert (resourceFile.existsAsFile()); + + if (! resourceFile.existsAsFile()) + { + jassert (assertExists == AssertAssetExists::no); + return {}; + } return resourceFile.createInputStream(); #endif diff --git a/examples/Assets/webviewplugin-gui-fallback.html b/examples/Assets/webviewplugin-gui-fallback.html new file mode 100644 index 0000000000..4870d4e85b --- /dev/null +++ b/examples/Assets/webviewplugin-gui-fallback.html @@ -0,0 +1,39 @@ + + + + + WebViewPluginDemo + + + +

WebViewPluginDemo

+

+ This document is a placeholder for the GUI component of the + WebViewPluginDemo. +

+

+ To build the fully fledged user interface you need to install + node.js +

+

+ Then navigate into the + examples/GUI/WebViewPluginDemoGUI directory inside your JUCE + directory, and issue the following commands. +

+
+        npm install
+        npm run build
+        npm run zip
+      
+

+ This will build the full GUI package and place it in the + Assets directory. +

+

After this, rebuild and restart this demo.

+ + diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index d581118ff0..9f9f99863a 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -51,6 +51,30 @@ function(_juce_add_pips) list(REMOVE_ITEM headers "${CMAKE_CURRENT_SOURCE_DIR}/ReaperEmbeddedViewPluginDemo.h") endif() + if((CMAKE_SYSTEM_NAME STREQUAL "Windows") + AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/WebViewPluginDemo.h") + + if(NOT ("${JUCE_CMAKE_UTILS_DIR}" IN_LIST CMAKE_MODULE_PATH)) + list(APPEND CMAKE_MODULE_PATH "${JUCE_CMAKE_UTILS_DIR}") + endif() + + find_package(WebView2 QUIET) + + if(NOT WebView2_FOUND) + list(REMOVE_ITEM headers "${CMAKE_CURRENT_SOURCE_DIR}/WebViewPluginDemo.h") + + message(WARNING "The WebViewPluginDemo was not enabled because WebView2 wasn't found" + " in the the local NuGet folder" + "\n" + "To install NuGet and the WebView2 package containing the statically linked library, " + "open a PowerShell and issue the following commands" + "\n" + "> Register-PackageSource -provider NuGet -name nugetRepository -location https://www.nuget.org/api/v2\n" + "> Install-Package NuGet.CommandLine -Scope CurrentUser\n" + "> Install-Package Microsoft.Web.WebView2 -Scope CurrentUser -RequiredVersion 1.0.1901.177\n") + endif() + endif() + foreach(header IN ITEMS ${headers}) juce_add_pip(${header} added_target) target_link_libraries(${added_target} PUBLIC diff --git a/examples/DemoRunner/Builds/Android/app/src/main/assets/DemoUtilities.h b/examples/DemoRunner/Builds/Android/app/src/main/assets/DemoUtilities.h index 6d27104d23..1c5534bff7 100644 --- a/examples/DemoRunner/Builds/Android/app/src/main/assets/DemoUtilities.h +++ b/examples/DemoRunner/Builds/Android/app/src/main/assets/DemoUtilities.h @@ -86,11 +86,26 @@ inline File getExamplesDirectory() noexcept #endif } -inline std::unique_ptr createAssetInputStream (const char* resourcePath) +enum class AssertAssetExists +{ + no, + yes +}; + +inline std::unique_ptr createAssetInputStream (const char* resourcePath, + [[maybe_unused]] AssertAssetExists assertExists = AssertAssetExists::yes) { #if JUCE_ANDROID ZipFile apkZip (File::getSpecialLocation (File::invokedExecutableFile)); - return std::unique_ptr (apkZip.createStreamForEntry (apkZip.getIndexOfFileName ("assets/" + String (resourcePath)))); + const auto fileIndex = apkZip.getIndexOfFileName ("assets/" + String (resourcePath)); + + if (fileIndex == -1) + { + jassert (assertExists == AssertAssetExists::no); + return {}; + } + + return std::unique_ptr (apkZip.createStreamForEntry (fileIndex)); #else #if JUCE_IOS auto assetsDir = File::getSpecialLocation (File::currentExecutableFile) @@ -106,7 +121,12 @@ inline std::unique_ptr createAssetInputStream (const char* resource #endif auto resourceFile = assetsDir.getChildFile (resourcePath); - jassert (resourceFile.existsAsFile()); + + if (! resourceFile.existsAsFile()) + { + jassert (assertExists == AssertAssetExists::no); + return {}; + } return resourceFile.createInputStream(); #endif diff --git a/examples/DemoRunner/Builds/Android/app/src/main/assets/webviewplugin-gui-fallback.html b/examples/DemoRunner/Builds/Android/app/src/main/assets/webviewplugin-gui-fallback.html new file mode 100644 index 0000000000..4870d4e85b --- /dev/null +++ b/examples/DemoRunner/Builds/Android/app/src/main/assets/webviewplugin-gui-fallback.html @@ -0,0 +1,39 @@ + + + + + WebViewPluginDemo + + + +

WebViewPluginDemo

+

+ This document is a placeholder for the GUI component of the + WebViewPluginDemo. +

+

+ To build the fully fledged user interface you need to install + node.js +

+

+ Then navigate into the + examples/GUI/WebViewPluginDemoGUI directory inside your JUCE + directory, and issue the following commands. +

+
+        npm install
+        npm run build
+        npm run zip
+      
+

+ This will build the full GUI package and place it in the + Assets directory. +

+

After this, rebuild and restart this demo.

+ + diff --git a/examples/GUI/WebViewPluginDemo.h b/examples/GUI/WebViewPluginDemo.h new file mode 100644 index 0000000000..04d58bded3 --- /dev/null +++ b/examples/GUI/WebViewPluginDemo.h @@ -0,0 +1,624 @@ +/* + ============================================================================== + + This file is part of the JUCE framework examples. + Copyright (c) Raw Material Software Limited + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + PERFORMANCE OF THIS SOFTWARE. + + ============================================================================== +*/ + +/******************************************************************************* + The block below describes the properties of this PIP. A PIP is a short snippet + of code that can be read by the Projucer and used to generate a JUCE project. + + BEGIN_JUCE_PIP_METADATA + + name: WebViewPluginDemo + version: 1.0.0 + vendor: JUCE + website: http://juce.com + description: Filtering audio plugin using an HTML/JS user interface + + dependencies: juce_audio_basics, juce_audio_devices, juce_audio_formats, + juce_audio_plugin_client, juce_audio_processors, juce_dsp, + juce_audio_utils, juce_core, juce_data_structures, + juce_events, juce_graphics, juce_gui_basics, juce_gui_extra + exporters: xcode_mac, vs2022, linux_make, androidstudio, xcode_iphone + + moduleFlags: JUCE_STRICT_REFCOUNTEDPOINTER=1, JUCE_USE_WIN_WEBVIEW2_WITH_STATIC_LINKING=1 + + type: AudioProcessor + mainClass: WebViewPluginAudioProcessorWrapper + + useLocalCopy: 1 + + END_JUCE_PIP_METADATA + +*******************************************************************************/ + +#pragma once + +#include "../Assets/DemoUtilities.h" + +namespace ID +{ +#define PARAMETER_ID(str) static const ParameterID str { #str, 1 }; + +PARAMETER_ID (cutoffFreqHz) +PARAMETER_ID (mute) +PARAMETER_ID (filterType) + +#undef PARAMETER_ID +} + +class CircularBuffer +{ +public: + CircularBuffer (int numChannels, int numSamples) + : buffer (data, (size_t) numChannels, (size_t) numSamples) + { + } + + template + void push (dsp::AudioBlock b) + { + jassert (b.getNumChannels() == buffer.getNumChannels()); + + const auto trimmed = b.getSubBlock ( b.getNumSamples() + - std::min (b.getNumSamples(), buffer.getNumSamples())); + + const auto bufferLength = (int64) buffer.getNumSamples(); + + for (auto samplesRemaining = (int64) trimmed.getNumSamples(); samplesRemaining > 0;) + { + const auto writeOffset = writeIx % bufferLength; + const auto numSamplesToWrite = std::min (samplesRemaining, bufferLength - writeOffset); + + auto destSubBlock = buffer.getSubBlock ((size_t) writeOffset, (size_t) numSamplesToWrite); + const auto sourceSubBlock = trimmed.getSubBlock (trimmed.getNumSamples() - (size_t) samplesRemaining, + (size_t) numSamplesToWrite); + + destSubBlock.copyFrom (sourceSubBlock); + + samplesRemaining -= numSamplesToWrite; + writeIx += numSamplesToWrite; + } + } + + template + void push (Span s) + { + auto* ptr = s.begin(); + dsp::AudioBlock b (&ptr, 1, s.size()); + push (b); + } + + void read (int64 readIx, dsp::AudioBlock output) const + { + const auto numChannelsToUse = std::min (buffer.getNumChannels(), output.getNumChannels()); + + jassert (output.getNumChannels() == buffer.getNumChannels()); + + const auto bufferLength = (int64) buffer.getNumSamples(); + + for (auto outputOffset = (size_t) 0; outputOffset < output.getNumSamples();) + { + const auto inputOffset = (size_t) ((readIx + (int64) outputOffset) % bufferLength); + const auto numSamplesToRead = std::min (output.getNumSamples() - outputOffset, + (size_t) bufferLength - inputOffset); + + auto destSubBlock = output.getSubBlock (outputOffset, numSamplesToRead) + .getSubsetChannelBlock (0, numChannelsToUse); + + destSubBlock.copyFrom (buffer.getSubBlock (inputOffset, numSamplesToRead) + .getSubsetChannelBlock (0, numChannelsToUse)); + + outputOffset += numSamplesToRead; + } + } + + int64 getWriteIndex() const noexcept { return writeIx; } + +private: + HeapBlock data; + dsp::AudioBlock buffer; + int64 writeIx = 0; +}; + +class SpectralBars +{ +public: + static constexpr int getNumBars() noexcept + { + return analysisWindowWidth / 2; + } + + template + void push (Span data) + { + buffer.push (data); + } + + void compute (Span output) + { + auto* ptr = output.begin(); + auto result = dsp::AudioBlock (&ptr, 1, output.size()); + result.clear(); + auto analysisData = fftTmp.getSubBlock (0, analysisWindowWidth); + + for (int i = 0; i < numAnalysisWindows; ++i) + { + buffer.read (0 + i * analysisWindowOverlap, analysisData); + fft.performFrequencyOnlyForwardTransform (fftTmp.getChannelPointer (0), true); + result.add (analysisData); + } + + result.multiplyBy (1.0f / numAnalysisWindows); + } + + static inline constexpr int64 fftOrder = 5; + static inline constexpr int64 analysisWindowWidth = 1 << fftOrder; + static inline constexpr int numAnalysisWindows = 16; + static inline constexpr int analysisWindowOverlap = analysisWindowWidth / 2; + +private: + dsp::FFT fft { fftOrder }; + + HeapBlock fftTmpData; + dsp::AudioBlock fftTmp { fftTmpData, 1, (size_t) (2 * fft.getSize()) }; + + CircularBuffer buffer { 1, (int) analysisWindowWidth + + (numAnalysisWindows - 1) * analysisWindowOverlap }; +}; + +//============================================================================== +class WebViewPluginAudioProcessor : public AudioProcessor +{ +public: + //============================================================================== + WebViewPluginAudioProcessor (AudioProcessorValueTreeState::ParameterLayout layout); + + //============================================================================== + void prepareToPlay (double sampleRate, int samplesPerBlock) override; + void releaseResources() override {} + + bool isBusesLayoutSupported (const BusesLayout& layouts) const override; + + void processBlock (AudioBuffer&, MidiBuffer&) override; + using AudioProcessor::processBlock; + + //============================================================================== + const String getName() const override { return JucePlugin_Name; } + + bool acceptsMidi() const override { return false; } + bool producesMidi() const override { return false; } + bool isMidiEffect() const override { return false; } + double getTailLengthSeconds() const override { return 0.0; } + + //============================================================================== + int getNumPrograms() override { return 1; } + int getCurrentProgram() override { return 0; } + void setCurrentProgram (int) override {} + const String getProgramName (int) override { return {}; } + void changeProgramName (int, const String&) override {} + + //============================================================================== + void getStateInformation (MemoryBlock& destData) override; + void setStateInformation (const void* data, int sizeInBytes) override; + + struct Parameters + { + public: + explicit Parameters (AudioProcessorValueTreeState::ParameterLayout& layout) + : cutoffFreqHz (addToLayout (layout, + ID::cutoffFreqHz, + "Cutoff", + NormalisableRange { 200.0f, 14000.0f, 1.0f, 0.5f }, + 11000.0f, + AudioParameterFloatAttributes{}.withLabel ("Hz"))), + mute (addToLayout (layout, ID::mute, "Mute", false)), + filterType (addToLayout (layout, + ID::filterType, + "Filter type", + StringArray { "Low-pass", "High-pass", "Band-pass" }, + 0)) + { + } + + AudioParameterFloat& cutoffFreqHz; + AudioParameterBool& mute; + AudioParameterChoice& filterType; + + private: + template + static void add (AudioProcessorParameterGroup& group, std::unique_ptr param) + { + group.addChild (std::move (param)); + } + + template + static void add (AudioProcessorValueTreeState::ParameterLayout& group, std::unique_ptr param) + { + group.add (std::move (param)); + } + + template + static Param& addToLayout (Group& layout, Ts&&... ts) + { + auto param = std::make_unique (std::forward (ts)...); + auto& ref = *param; + add (layout, std::move (param)); + return ref; + } + }; + + Parameters parameters; + AudioProcessorValueTreeState state; + + std::vector spectrumData = [] { return std::vector (16, 0.0f); }(); + SpinLock spectrumDataLock; + + SpectralBars spectralBars; + + dsp::LadderFilter filter; + +private: + //============================================================================== + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (WebViewPluginAudioProcessor) +}; + +//============================================================================== +WebViewPluginAudioProcessor::WebViewPluginAudioProcessor (AudioProcessorValueTreeState::ParameterLayout layout) + : AudioProcessor (BusesProperties() + #if ! JucePlugin_IsMidiEffect + #if ! JucePlugin_IsSynth + .withInput ("Input", juce::AudioChannelSet::stereo(), true) + #endif + .withOutput ("Output", juce::AudioChannelSet::stereo(), true) + #endif + ), + parameters (layout), + state (*this, nullptr, "STATE", std::move (layout)) +{ +} + +//============================================================================== +void WebViewPluginAudioProcessor::prepareToPlay (double sampleRate, int samplesPerBlock) +{ + const auto channels = std::max (getTotalNumInputChannels(), getTotalNumOutputChannels()); + + if (channels == 0) + return; + + filter.prepare ({ sampleRate, (uint32_t) samplesPerBlock, (uint32_t) channels }); + filter.reset(); +} + +bool WebViewPluginAudioProcessor::isBusesLayoutSupported (const BusesLayout& layouts) const +{ + if (layouts.getMainOutputChannelSet() != juce::AudioChannelSet::mono() + && layouts.getMainOutputChannelSet() != juce::AudioChannelSet::stereo()) + return false; + + if (layouts.getMainOutputChannelSet() != layouts.getMainInputChannelSet()) + return false; + + return true; +} + +void WebViewPluginAudioProcessor::processBlock (juce::AudioBuffer& buffer, + juce::MidiBuffer&) +{ + juce::ScopedNoDenormals noDenormals; + + const auto totalNumInputChannels = getTotalNumInputChannels(); + const auto totalNumOutputChannels = getTotalNumOutputChannels(); + + for (auto i = totalNumInputChannels; i < totalNumOutputChannels; ++i) + buffer.clear (i, 0, buffer.getNumSamples()); + + filter.setCutoffFrequencyHz (parameters.cutoffFreqHz.get()); + + const auto filterMode = [this] + { + switch (parameters.filterType.getIndex()) + { + case 0: + return dsp::LadderFilter::Mode::LPF12; + + case 1: + return dsp::LadderFilter::Mode::HPF12; + + default: + return dsp::LadderFilter::Mode::BPF12; + } + }(); + + filter.setMode (filterMode); + + auto outBlock = dsp::AudioBlock { buffer }.getSubsetChannelBlock (0, (size_t) getTotalNumOutputChannels()); + + if (parameters.mute.get()) + outBlock.clear(); + + filter.process (dsp::ProcessContextReplacing (outBlock)); + + spectralBars.push (Span { buffer.getReadPointer (0), (size_t) buffer.getNumSamples() }); + + { + const SpinLock::ScopedTryLockType lock (spectrumDataLock); + + if (! lock.isLocked()) + return; + + spectralBars.compute ({ spectrumData.data(), spectrumData.size() }); + } +} + +//============================================================================== +void WebViewPluginAudioProcessor::getStateInformation (juce::MemoryBlock& destData) +{ + juce::ignoreUnused (destData); +} + +void WebViewPluginAudioProcessor::setStateInformation (const void* data, int sizeInBytes) +{ + juce::ignoreUnused (data, sizeInBytes); +} + +extern const String localDevServerAddress; + +std::optional getResource (const String& url); + +//============================================================================== +struct SinglePageBrowser : WebBrowserComponent +{ + using WebBrowserComponent::WebBrowserComponent; + + // Prevent page loads from navigating away from our single page web app + bool pageAboutToLoad (const String& newURL) override; +}; + +//============================================================================== +class WebViewPluginAudioProcessorEditor : public AudioProcessorEditor, private Timer +{ +public: + explicit WebViewPluginAudioProcessorEditor (WebViewPluginAudioProcessor&); + + std::optional getResource (const String& url); + + //============================================================================== + void paint (Graphics&) override; + void resized() override; + + void timerCallback() override + { + static constexpr size_t numFramesBuffered = 5; + + SpinLock::ScopedLockType lock { processorRef.spectrumDataLock }; + + Array frame; + + for (size_t i = 1; i < processorRef.spectrumData.size(); ++i) + frame.add (processorRef.spectrumData[i]); + + spectrumDataFrames.push_back (std::move (frame)); + + while (spectrumDataFrames.size() > numFramesBuffered) + spectrumDataFrames.pop_front(); + + static int64 callbackCounter = 0; + + if ( spectrumDataFrames.size() == numFramesBuffered + && callbackCounter++ % (int64) numFramesBuffered) + { + webComponent.emitEventIfBrowserIsVisible ("spectrumData", var{}); + } + } + +private: + WebViewPluginAudioProcessor& processorRef; + + WebSliderRelay cutoffSliderRelay { webComponent, "cutoffSlider" }; + WebToggleButtonRelay muteToggleRelay { webComponent, "muteToggle" }; + WebComboBoxRelay filterTypeComboRelay { webComponent, "filterTypeCombo" }; + + SinglePageBrowser webComponent { WebBrowserComponent::Options{} + .withBackend (WebBrowserComponent::Options::Backend::webview2) + .withWinWebView2Options (WebBrowserComponent::Options::WinWebView2{} + .withUserDataFolder (File::getSpecialLocation (File::SpecialLocationType::tempDirectory))) + .withNativeIntegrationEnabled() + .withOptionsFrom (cutoffSliderRelay) + .withOptionsFrom (muteToggleRelay) + .withOptionsFrom (filterTypeComboRelay) + .withNativeFunction ("sayHello", [](auto& var, auto complete) + { + complete ("Hello " + var[0].toString()); + }) + .withResourceProvider ([this] (const auto& url) + { + return getResource (url); + }, + URL { localDevServerAddress }.getOrigin()) }; + + WebSliderParameterAttachment cutoffAttachment; + WebToggleButtonParameterAttachment muteAttachment; + WebComboBoxParameterAttachment filterTypeAttachment; + + std::deque> spectrumDataFrames; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (WebViewPluginAudioProcessorEditor) +}; + +static ZipFile* getZipFile() +{ + static auto stream = createAssetInputStream ("webviewplugin-gui_1.0.0.zip", AssertAssetExists::no); + + if (stream == nullptr) + return nullptr; + + static ZipFile f { stream.get(), false }; + return &f; +} + +static const char* getMimeForExtension (const String& extension) +{ + static const std::unordered_map mimeMap = + { + { { "htm" }, "text/html" }, + { { "html" }, "text/html" }, + { { "txt" }, "text/plain" }, + { { "jpg" }, "image/jpeg" }, + { { "jpeg" }, "image/jpeg" }, + { { "svg" }, "image/svg+xml" }, + { { "ico" }, "image/vnd.microsoft.icon" }, + { { "json" }, "application/json" }, + { { "png" }, "image/png" }, + { { "css" }, "text/css" }, + { { "map" }, "application/json" }, + { { "js" }, "text/javascript" }, + { { "woff2" }, "font/woff2" } + }; + + if (const auto it = mimeMap.find (extension.toLowerCase()); it != mimeMap.end()) + return it->second; + + jassertfalse; + return ""; +} + +static String getExtension (String filename) +{ + return filename.fromLastOccurrenceOf (".", false, false); +} + +static auto streamToVector (InputStream& stream) +{ + std::vector result ((size_t) stream.getTotalLength()); + stream.setPosition (0); + [[maybe_unused]] const auto bytesRead = stream.read (result.data(), result.size()); + jassert (bytesRead == (ssize_t) result.size()); + return result; +} + +std::optional WebViewPluginAudioProcessorEditor::getResource (const String& url) +{ + const auto urlToRetrive = url == "/" ? String { "index.html" } + : url.fromFirstOccurrenceOf ("/", false, false); + + if (auto* archive = getZipFile()) + { + if (auto* entry = archive->getEntry (urlToRetrive)) + { + auto stream = rawToUniquePtr (archive->createStreamForEntry (*entry)); + auto v = streamToVector (*stream); + auto mime = getMimeForExtension (getExtension (entry->filename).toLowerCase()); + return WebBrowserComponent::Resource { std::move (v), + std::move (mime) }; + } + } + + if (urlToRetrive == "index.html") + { + auto fallbackIndexHtml = createAssetInputStream ("webviewplugin-gui-fallback.html"); + return WebBrowserComponent::Resource { streamToVector (*fallbackIndexHtml), + String { "text/html" } }; + } + + if (urlToRetrive == "data.txt") + { + WebBrowserComponent::Resource resource; + static constexpr char testData[] = "testdata"; + MemoryInputStream stream { testData, numElementsInArray (testData) - 1, false }; + return WebBrowserComponent::Resource { streamToVector (stream), String { "text/html" } }; + } + + if (urlToRetrive == "spectrumData.json") + { + Array frames; + + for (const auto& frame : spectrumDataFrames) + frames.add (frame); + + DynamicObject::Ptr d (new DynamicObject()); + d->setProperty ("timeResolutionMs", getTimerInterval()); + d->setProperty ("frames", std::move (frames)); + + const auto s = JSON::toString (d.get()); + MemoryInputStream stream { s.getCharPointer(), s.getNumBytesAsUTF8(), false }; + return WebBrowserComponent::Resource { streamToVector (stream), String { "application/json" } }; + } + + return std::nullopt; +} + +#if JUCE_ANDROID +// The localhost is available on this address to the emulator +const String localDevServerAddress = "http://10.0.2.2:3000/"; +#else +const String localDevServerAddress = "http://localhost:3000/"; +#endif + +bool SinglePageBrowser::pageAboutToLoad (const String& newURL) +{ + return newURL == localDevServerAddress || newURL == getResourceProviderRoot(); +} + +//============================================================================== +WebViewPluginAudioProcessorEditor::WebViewPluginAudioProcessorEditor (WebViewPluginAudioProcessor& p) + : AudioProcessorEditor (&p), processorRef (p), + cutoffAttachment (*processorRef.state.getParameter (ID::cutoffFreqHz.getParamID()), + cutoffSliderRelay, + processorRef.state.undoManager), + muteAttachment (*processorRef.state.getParameter (ID::mute.getParamID()), + muteToggleRelay, + processorRef.state.undoManager), + filterTypeAttachment (*processorRef.state.getParameter (ID::filterType.getParamID()), + filterTypeComboRelay, + processorRef.state.undoManager) +{ + addAndMakeVisible (webComponent); + + // webComponent.goToURL (localDevServerAddress); + webComponent.goToURL (WebBrowserComponent::getResourceProviderRoot()); + + setSize (500, 500); + + startTimerHz (20); +} + +//============================================================================== +void WebViewPluginAudioProcessorEditor::paint (Graphics& g) +{ + // (Our component is opaque, so we must completely fill the background with a solid colour) + g.fillAll (getLookAndFeel().findColour (ResizableWindow::backgroundColourId)); +} + +void WebViewPluginAudioProcessorEditor::resized() +{ + webComponent.setBounds (getLocalBounds()); +} + +class WebViewPluginAudioProcessorWrapper : public WebViewPluginAudioProcessor +{ +public: + WebViewPluginAudioProcessorWrapper() : WebViewPluginAudioProcessor ({}) + {} + + bool hasEditor() const override { return true; } + AudioProcessorEditor* createEditor() override { return new WebViewPluginAudioProcessorEditor (*this); } +}; diff --git a/examples/GUI/WebViewPluginDemoGUI/package-lock.json b/examples/GUI/WebViewPluginDemoGUI/package-lock.json index 042c7f0196..f15486cba5 100644 --- a/examples/GUI/WebViewPluginDemoGUI/package-lock.json +++ b/examples/GUI/WebViewPluginDemoGUI/package-lock.json @@ -1,12 +1,12 @@ { - "name": "ladder-filter-gui", - "version": "0.1.0", + "name": "webviewplugin-gui", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "ladder-filter-gui", - "version": "0.1.0", + "name": "webviewplugin-gui", + "version": "1.0.0", "dependencies": { "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", @@ -16,6 +16,7 @@ "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", + "juce-framework-frontend": "file:../../../modules/juce_gui_extra/native/javascript", "react": "^18.2.0", "react-dom": "^18.2.0", "react-scripts": "5.0.1", @@ -23,9 +24,19 @@ }, "devDependencies": { "eslint": "^8.43.0", - "eslint-plugin-react": "^7.32.2" + "eslint-plugin-react": "^7.32.2", + "npm-build-zip": "^1.0.4" } }, + "../../../../modules/juce_gui_extra/native/javascript": { + "name": "juce-framework-frontend", + "version": "7.0.7", + "extraneous": true + }, + "../../../modules/juce_gui_extra/native/javascript": { + "name": "juce-framework-frontend", + "version": "7.0.7" + }, "node_modules/@adobe/css-tools": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.2.0.tgz", @@ -5491,6 +5502,145 @@ "node": ">= 8" } }, + "node_modules/archiver": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-1.3.0.tgz", + "integrity": "sha512-4q/CtGPNVyC5aT9eYHhFP7SAEjKYzQIDIJWXfexUIPNxitNs1y6hORdX+sYxERSZ6qPeNNBJ5UolFsJdWTU02g==", + "dev": true, + "dependencies": { + "archiver-utils": "^1.3.0", + "async": "^2.0.0", + "buffer-crc32": "^0.2.1", + "glob": "^7.0.0", + "lodash": "^4.8.0", + "readable-stream": "^2.0.0", + "tar-stream": "^1.5.0", + "walkdir": "^0.0.11", + "zip-stream": "^1.1.0" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/archiver-promise": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/archiver-promise/-/archiver-promise-1.0.0.tgz", + "integrity": "sha512-6/vW4PWUKyc1KsuacbRh7a7W2s8BBRwL6IM2zNCRUESWks22tMAMr1h45pGbMzzeyNi5XIlniuubcxuc5sBkxQ==", + "dev": true, + "dependencies": { + "archiver": "^1.2.0" + } + }, + "node_modules/archiver-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-1.3.0.tgz", + "integrity": "sha512-h+hTREBXcW5e1L9RihGXdH4PHHdGipG/jE2sMZrqIH6BmZAxeGU5IWjVsKhokdCSWX7km6Kkh406zZNEElHFPQ==", + "dev": true, + "dependencies": { + "glob": "^7.0.0", + "graceful-fs": "^4.1.0", + "lazystream": "^1.0.0", + "lodash": "^4.8.0", + "normalize-path": "^2.0.0", + "readable-stream": "^2.0.0" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/archiver-utils/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/archiver-utils/node_modules/normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", + "dev": true, + "dependencies": { + "remove-trailing-separator": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/archiver-utils/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/archiver-utils/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/archiver-utils/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/archiver/node_modules/async": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "dev": true, + "dependencies": { + "lodash": "^4.17.14" + } + }, + "node_modules/archiver/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/archiver/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/archiver/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/archiver/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/arg": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", @@ -5991,6 +6141,26 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/batch": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", @@ -6026,6 +6196,52 @@ "node": ">=8" } }, + "node_modules/bl": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.3.tgz", + "integrity": "sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==", + "dev": true, + "dependencies": { + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" + } + }, + "node_modules/bl/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/bl/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/bl/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/bl/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/bluebird": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", @@ -6166,6 +6382,61 @@ "node-int64": "^0.4.0" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-alloc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", + "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", + "dev": true, + "dependencies": { + "buffer-alloc-unsafe": "^1.1.0", + "buffer-fill": "^1.0.0" + } + }, + "node_modules/buffer-alloc-unsafe": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", + "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==", + "dev": true + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/buffer-fill": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", + "integrity": "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==", + "dev": true + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -6490,6 +6761,69 @@ "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==" }, + "node_modules/compress-commons": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-1.2.2.tgz", + "integrity": "sha512-SLTU8iWWmcORfUN+4351Z2aZXKJe1tr0jSilPMCZlLPzpdTXnkBW1LevW/MfuANBKJek8Xu9ggqrtVmQrChLtg==", + "dev": true, + "dependencies": { + "buffer-crc32": "^0.2.1", + "crc32-stream": "^2.0.0", + "normalize-path": "^2.0.0", + "readable-stream": "^2.0.0" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/compress-commons/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/compress-commons/node_modules/normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", + "dev": true, + "dependencies": { + "remove-trailing-separator": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/compress-commons/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/compress-commons/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/compress-commons/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/compressible": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", @@ -6643,6 +6977,64 @@ "node": ">=10" } }, + "node_modules/crc": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", + "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", + "dev": true, + "dependencies": { + "buffer": "^5.1.0" + } + }, + "node_modules/crc32-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-2.0.0.tgz", + "integrity": "sha512-UjZSqFCbn+jZUHJIh6Y3vMF7EJLcJWNm4tKDf2peJRwlZKHvkkvOMTvAei6zjU9gO1xONVr3rRFw0gixm2eUng==", + "dev": true, + "dependencies": { + "crc": "^3.4.4", + "readable-stream": "^2.0.0" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/crc32-stream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/crc32-stream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/crc32-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/crc32-stream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -7093,6 +7485,15 @@ } } }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/decimal.js": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", @@ -7484,6 +7885,15 @@ "node": ">= 0.8" } }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.15.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", @@ -8971,6 +9381,12 @@ "node": ">= 0.6" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true + }, "node_modules/fs-extra": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", @@ -9636,6 +10052,26 @@ "node": ">=4" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/ignore": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", @@ -9644,6 +10080,15 @@ "node": ">= 4" } }, + "node_modules/ignore-walk": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.4.tgz", + "integrity": "sha512-PY6Ii8o1jMRA1z4F2hRkH/xN59ox43DavKvD3oDpfurRlOJyAHpifIwpbdv1n4jt4ov0jSpw3kQ4GhJnpBL6WQ==", + "dev": true, + "dependencies": { + "minimatch": "^3.0.4" + } + }, "node_modules/immer": { "version": "9.0.21", "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", @@ -12344,6 +12789,10 @@ "node": ">=4.0" } }, + "node_modules/juce-framework-frontend": { + "resolved": "../../../modules/juce_gui_extra/native/javascript", + "link": true + }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -12390,6 +12839,54 @@ "shell-quote": "^1.7.3" } }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "dev": true, + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -12892,6 +13389,225 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/npm-build-zip": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/npm-build-zip/-/npm-build-zip-1.0.4.tgz", + "integrity": "sha512-mn4IPDkB+LlJiIaXMJ+vFa1yFrWhT4MGo0AeNN3Blyc+pIdqJzL1mQBvk+qnVT6tP+FbohnYvWFyNaTYEHtpZg==", + "dev": true, + "dependencies": { + "archiver-promise": "1.0.0", + "npm-packlist": "1.4.4", + "sanitize-filename": "1.6.3", + "yargs": "13.3.0" + }, + "bin": { + "npm-build-zip": "bin/npm-build-zip.js" + } + }, + "node_modules/npm-build-zip/node_modules/ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/npm-build-zip/node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/npm-build-zip/node_modules/cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "dev": true, + "dependencies": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + } + }, + "node_modules/npm-build-zip/node_modules/emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "node_modules/npm-build-zip/node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/npm-build-zip/node_modules/is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-build-zip/node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/npm-build-zip/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-build-zip/node_modules/p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/npm-build-zip/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-build-zip/node_modules/string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "dependencies": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/npm-build-zip/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/npm-build-zip/node_modules/wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/npm-build-zip/node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "dev": true + }, + "node_modules/npm-build-zip/node_modules/yargs": { + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.0.tgz", + "integrity": "sha512-2eehun/8ALW8TLoIl7MVaRUrg+yCnenu8B4kBlRxj3GJGDKU1Og7sMXPNm1BYyM1DOJmTZ4YeN/Nwxv+8XJsUA==", + "dev": true, + "dependencies": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.1" + } + }, + "node_modules/npm-build-zip/node_modules/yargs-parser": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", + "dev": true, + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + }, + "node_modules/npm-bundled": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.2.tgz", + "integrity": "sha512-x5DHup0SuyQcmL3s7Rx/YQ8sbw/Hzg0rj48eN0dV7hf5cmQq5PXIeioroH3raV1QC1yh3uTYuMThvEQF3iKgGQ==", + "dev": true, + "dependencies": { + "npm-normalize-package-bin": "^1.0.1" + } + }, + "node_modules/npm-normalize-package-bin": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", + "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==", + "dev": true + }, + "node_modules/npm-packlist": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.4.tgz", + "integrity": "sha512-zTLo8UcVYtDU3gdeaFu2Xu0n0EvelfHDGuqtNIn5RO7yQj4H1TqNdBc/yZjxnWA0PVB8D3Woyp0i5B43JwQ6Vw==", + "dev": true, + "dependencies": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1" + } + }, "node_modules/npm-run-path": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", @@ -15265,6 +15981,12 @@ "node": ">= 0.10" } }, + "node_modules/remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==", + "dev": true + }, "node_modules/renderkid": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz", @@ -15293,6 +16015,12 @@ "node": ">=0.10.0" } }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, "node_modules/requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", @@ -15573,6 +16301,15 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "node_modules/sanitize-filename": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz", + "integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==", + "dev": true, + "dependencies": { + "truncate-utf8-bytes": "^1.0.0" + } + }, "node_modules/sanitize.css": { "version": "13.0.0", "resolved": "https://registry.npmjs.org/sanitize.css/-/sanitize.css-13.0.0.tgz", @@ -15835,6 +16572,12 @@ "node": ">= 0.8.0" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "dev": true + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -16498,6 +17241,60 @@ "node": ">=6" } }, + "node_modules/tar-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz", + "integrity": "sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==", + "dev": true, + "dependencies": { + "bl": "^1.0.0", + "buffer-alloc": "^1.2.0", + "end-of-stream": "^1.0.0", + "fs-constants": "^1.0.0", + "readable-stream": "^2.3.0", + "to-buffer": "^1.1.1", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/tar-stream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/tar-stream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/tar-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/tar-stream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/temp-dir": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz", @@ -16656,6 +17453,12 @@ "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==" }, + "node_modules/to-buffer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz", + "integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==", + "dev": true + }, "node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", @@ -16716,6 +17519,15 @@ "node": ">=8" } }, + "node_modules/truncate-utf8-bytes": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", + "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", + "dev": true, + "dependencies": { + "utf8-byte-length": "^1.0.1" + } + }, "node_modules/tryer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz", @@ -16993,6 +17805,12 @@ "requires-port": "^1.0.0" } }, + "node_modules/utf8-byte-length": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz", + "integrity": "sha512-4+wkEYLBbWxqTahEsWrhxepcoVOJ+1z5PGIjPZxRkytcdSUaNjIjBM7Xn8E+pdSuV7SzvWovBFA54FO0JSoqhA==", + "dev": true + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -17074,6 +17892,15 @@ "node": ">=10" } }, + "node_modules/walkdir": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/walkdir/-/walkdir-0.0.11.tgz", + "integrity": "sha512-lMFYXGpf7eg+RInVL021ZbJJT4hqsvsBvq5sZBp874jfhs3IWlA7OPoG0ojQrYcXHuUSi+Nqp6qGN+pPGaMgPQ==", + "dev": true, + "engines": { + "node": ">=0.6.0" + } + }, "node_modules/walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", @@ -17528,6 +18355,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "dev": true + }, "node_modules/which-typed-array": { "version": "1.1.9", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz", @@ -17942,6 +18775,15 @@ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==" }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "engines": { + "node": ">=0.4" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -17998,6 +18840,57 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zip-stream": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-1.2.0.tgz", + "integrity": "sha512-2olrDUuPM4NvRIgGPhvrp84f7/HmWR6RiQrgwFF2VctmnssFiogtYL3DcA8Vl2bsSmju79sVXe38TsII7JleUg==", + "dev": true, + "dependencies": { + "archiver-utils": "^1.3.0", + "compress-commons": "^1.2.0", + "lodash": "^4.8.0", + "readable-stream": "^2.0.0" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/zip-stream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/zip-stream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/zip-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/zip-stream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } } } } diff --git a/examples/GUI/WebViewPluginDemoGUI/package.json b/examples/GUI/WebViewPluginDemoGUI/package.json index ebe571b603..9c186b0983 100644 --- a/examples/GUI/WebViewPluginDemoGUI/package.json +++ b/examples/GUI/WebViewPluginDemoGUI/package.json @@ -1,6 +1,6 @@ { - "name": "ladder-filter-gui", - "version": "0.1.0", + "name": "webviewplugin-gui", + "version": "1.0.0", "private": true, "dependencies": { "@emotion/react": "^11.11.1", @@ -11,6 +11,7 @@ "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", + "juce-framework-frontend": "file:../../../modules/juce_gui_extra/native/javascript", "react": "^18.2.0", "react-dom": "^18.2.0", "react-scripts": "5.0.1", @@ -20,7 +21,8 @@ "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test", - "eject": "react-scripts eject" + "eject": "react-scripts eject", + "zip": "npm-build-zip --destination=../../Assets" }, "eslintConfig": { "extends": [ @@ -42,6 +44,7 @@ }, "devDependencies": { "eslint": "^8.43.0", - "eslint-plugin-react": "^7.32.2" + "eslint-plugin-react": "^7.32.2", + "npm-build-zip": "^1.0.4" } } diff --git a/examples/GUI/WebViewPluginDemoGUI/src/App.js b/examples/GUI/WebViewPluginDemoGUI/src/App.js index 4aa8b465c6..9b46036f5a 100644 --- a/examples/GUI/WebViewPluginDemoGUI/src/App.js +++ b/examples/GUI/WebViewPluginDemoGUI/src/App.js @@ -1,23 +1,416 @@ +import "@fontsource/roboto/300.css"; +import "@fontsource/roboto/400.css"; +import "@fontsource/roboto/500.css"; +import "@fontsource/roboto/700.css"; + +import Box from "@mui/material/Container"; +import Checkbox from "@mui/material/Checkbox"; +import Typography from "@mui/material/Typography"; +import Container from "@mui/material/Container"; +import Slider from "@mui/material/Slider"; +import Button from "@mui/material/Button"; +import CardActions from "@mui/material/CardActions"; +import Snackbar from "@mui/material/Snackbar"; +import IconButton from "@mui/material/IconButton"; +import CloseIcon from "@mui/icons-material/Close"; +import InputLabel from "@mui/material/InputLabel"; +import MenuItem from "@mui/material/MenuItem"; +import FormControl from "@mui/material/FormControl"; +import Select from "@mui/material/Select"; +import FormGroup from "@mui/material/FormGroup"; +import FormControlLabel from "@mui/material/FormControlLabel"; + +import { React, useState, useEffect, useRef } from "react"; +import PropTypes from "prop-types"; + +import * as Juce from "juce-framework-frontend"; + import "./App.css"; -import { React } from "react"; +function JuceSlider({ identifier, title }) { + JuceSlider.propTypes = { + identifier: PropTypes.string, + title: PropTypes.string, + }; + + const sliderState = Juce.getSliderState(identifier); + + const [value, setValue] = useState(sliderState.getNormalisedValue()); + const [properties, setProperties] = useState(sliderState.properties); + + const handleChange = (event, newValue) => { + sliderState.setNormalisedValue(newValue); + setValue(newValue); + }; + + const mouseDown = () => { + sliderState.sliderDragStarted(); + }; + + const changeCommitted = (event, newValue) => { + sliderState.setNormalisedValue(newValue); + sliderState.sliderDragEnded(); + }; + + useEffect(() => { + const valueListenerId = sliderState.valueChangedEvent.addListener(() => { + setValue(sliderState.getNormalisedValue()); + }); + const propertiesListenerId = sliderState.propertiesChangedEvent.addListener( + () => setProperties(sliderState.properties) + ); + + return function cleanup() { + sliderState.valueChangedEvent.removeListener(valueListenerId); + sliderState.propertiesChangedEvent.removeListener(propertiesListenerId); + }; + }); + + function calculateValue() { + return sliderState.getScaledValue(); + } + + return ( + + + {properties.name}: {sliderState.getScaledValue()} {properties.label} + + + + ); +} + +function JuceCheckbox({ identifier }) { + JuceCheckbox.propTypes = { + identifier: PropTypes.string, + }; + + const checkboxState = Juce.getToggleState(identifier); + + const [value, setValue] = useState(checkboxState.getValue()); + const [properties, setProperties] = useState(checkboxState.properties); + + const handleChange = (event) => { + checkboxState.setValue(event.target.checked); + setValue(event.target.checked); + }; + + useEffect(() => { + const valueListenerId = checkboxState.valueChangedEvent.addListener(() => { + setValue(checkboxState.getValue()); + }); + const propertiesListenerId = + checkboxState.propertiesChangedEvent.addListener(() => + setProperties(checkboxState.properties) + ); + + return function cleanup() { + checkboxState.valueChangedEvent.removeListener(valueListenerId); + checkboxState.propertiesChangedEvent.removeListener(propertiesListenerId); + }; + }); + + const cb = ; + + return ( + + + + + + ); +} + +function JuceComboBox({ identifier }) { + JuceComboBox.propTypes = { + identifier: PropTypes.string, + }; + + const comboBoxState = Juce.getComboBoxState(identifier); + + const [value, setValue] = useState(comboBoxState.getChoiceIndex()); + const [properties, setProperties] = useState(comboBoxState.properties); + + const handleChange = (event) => { + comboBoxState.setChoiceIndex(event.target.value); + setValue(event.target.value); + }; + + useEffect(() => { + const valueListenerId = comboBoxState.valueChangedEvent.addListener(() => { + setValue(comboBoxState.getChoiceIndex()); + }); + const propertiesListenerId = + comboBoxState.propertiesChangedEvent.addListener(() => { + setProperties(comboBoxState.properties); + }); + + return function cleanup() { + comboBoxState.valueChangedEvent.removeListener(valueListenerId); + comboBoxState.propertiesChangedEvent.removeListener(propertiesListenerId); + }; + }); + + return ( + + + {properties.name} + + + + ); +} + +const sayHello = Juce.getNativeFunction("sayHello"); + +const SpectrumDataReceiver_eventId = "spectrumData"; + +function interpolate(a, b, s) { + let result = new Array(a.length).fill(0); + + for (const [i, val] of a.entries()) result[i] += (1 - s) * val; + + for (const [i, val] of b.entries()) result[i] += s * val; + + return result; +} + +function mod(dividend, divisor) { + const quotient = Math.floor(dividend / divisor); + return dividend - divisor * quotient; +} + +class SpectrumDataReceiver { + constructor(bufferLength) { + this.bufferLength = bufferLength; + this.buffer = new Array(this.bufferLength); + this.readIndex = 0; + this.writeIndex = 0; + this.lastTimeStampMs = 0; + this.timeResolutionMs = 0; + + let self = this; + this.spectrumDataRegistrationId = window.__JUCE__.backend.addEventListener( + SpectrumDataReceiver_eventId, + () => { + fetch(Juce.getBackendResourceAddress("spectrumData.json")) + .then((response) => response.text()) + .then((text) => { + const data = JSON.parse(text); + + if (self.timeResolutionMs == 0) { + self.timeResolutionMs = data.timeResolutionMs; + + // We want to stay behind the write index by a full batch plus one + // so that we can keep reading buffered frames until we receive the + // new batch + self.readIndex = -data.frames.length - 1; + + self.buffer.fill(new Array(data.frames[0].length).fill(0)); + } + + for (const f of data.frames) + self.buffer[mod(self.writeIndex++, self.bufferLength)] = f; + }); + } + ); + } + + getBufferItem(index) { + return this.buffer[mod(index, this.buffer.length)]; + } + + getLevels(timeStampMs) { + if (this.timeResolutionMs == 0) return null; + + const previousTimeStampMs = this.lastTimeStampMs; + this.lastTimeStampMs = timeStampMs; + + if (previousTimeStampMs == 0) return this.buffer[0]; + + const timeAdvance = + (timeStampMs - previousTimeStampMs) / this.timeResolutionMs; + this.readIndex += timeAdvance; + + const integralPart = Math.floor(this.readIndex); + const fractionalPart = this.readIndex - integralPart; + + return interpolate( + this.getBufferItem(integralPart), + this.getBufferItem(integralPart + 1), + fractionalPart + ); + } + + unregister() { + window.__JUCE__.backend.removeEventListener( + this.spectrumDataRegistrationId + ); + } +} + +function FreqBandInfo() { + const canvasRef = useRef(null); + let dataReceiver = null; + let isActive = true; + + // eslint-disable-next-line no-unused-vars + const render = (timeStampMs) => { + const canvas = canvasRef.current; + const ctx = canvas.getContext("2d"); + ctx.clearRect(0, 0, canvas.width, canvas.height); + + var grd = ctx.createLinearGradient(0, 0, 0, canvas.height); + grd.addColorStop(0, "#1976d2"); + grd.addColorStop(1, "#dae9f8"); + ctx.fillStyle = grd; + + if (dataReceiver != null) { + const levels = dataReceiver.getLevels(timeStampMs); + + if (levels != null) { + const numBars = levels.length; + const barWidth = canvas.width / numBars; + const barHeight = canvas.height; + + for (const [i, l] of levels.entries()) { + ctx.fillRect( + i * barWidth, + barHeight - l * barHeight, + barWidth, + l * barHeight + ); + } + } + } + + if (isActive) window.requestAnimationFrame(render); + }; + + useEffect(() => { + dataReceiver = new SpectrumDataReceiver(10); + isActive = true; + window.requestAnimationFrame(render); + + return function cleanup() { + isActive = false; + dataReceiver.unregister(); + }; + }); + + const canvasStyle = { + marginLeft: "0", + marginRight: "0", + marginTop: "1em", + display: "block", + width: "94%", + bottom: "0", + position: "absolute", + }; + + return ( + + + + ); +} function App() { + const [open, setOpen] = useState(false); + const [snackbarMessage, setMessage] = useState("No message received yet"); + + const openSnackbar = () => { + setOpen(true); + }; + + const handleClose = (event, reason) => { + if (reason === "clickaway") { + return; + } + + setOpen(false); + }; + + const action = ( + <> + + + + + ); + return ( -
-
-

- Edit src/App.js and save to reload. -

- + + + + +
+ Call backend function + + + + + + +

+ + +
); } diff --git a/examples/GUI/WebViewPluginDemoGUI/src/index.css b/examples/GUI/WebViewPluginDemoGUI/src/index.css index ec2585e8c0..41faa692db 100644 --- a/examples/GUI/WebViewPluginDemoGUI/src/index.css +++ b/examples/GUI/WebViewPluginDemoGUI/src/index.css @@ -5,6 +5,7 @@ body { sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; + background-color: white; } code { diff --git a/extras/AudioPluginHost/Builds/Android/app/src/main/assets/DemoUtilities.h b/extras/AudioPluginHost/Builds/Android/app/src/main/assets/DemoUtilities.h index 6d27104d23..1c5534bff7 100644 --- a/extras/AudioPluginHost/Builds/Android/app/src/main/assets/DemoUtilities.h +++ b/extras/AudioPluginHost/Builds/Android/app/src/main/assets/DemoUtilities.h @@ -86,11 +86,26 @@ inline File getExamplesDirectory() noexcept #endif } -inline std::unique_ptr createAssetInputStream (const char* resourcePath) +enum class AssertAssetExists +{ + no, + yes +}; + +inline std::unique_ptr createAssetInputStream (const char* resourcePath, + [[maybe_unused]] AssertAssetExists assertExists = AssertAssetExists::yes) { #if JUCE_ANDROID ZipFile apkZip (File::getSpecialLocation (File::invokedExecutableFile)); - return std::unique_ptr (apkZip.createStreamForEntry (apkZip.getIndexOfFileName ("assets/" + String (resourcePath)))); + const auto fileIndex = apkZip.getIndexOfFileName ("assets/" + String (resourcePath)); + + if (fileIndex == -1) + { + jassert (assertExists == AssertAssetExists::no); + return {}; + } + + return std::unique_ptr (apkZip.createStreamForEntry (fileIndex)); #else #if JUCE_IOS auto assetsDir = File::getSpecialLocation (File::currentExecutableFile) @@ -106,7 +121,12 @@ inline std::unique_ptr createAssetInputStream (const char* resource #endif auto resourceFile = assetsDir.getChildFile (resourcePath); - jassert (resourceFile.existsAsFile()); + + if (! resourceFile.existsAsFile()) + { + jassert (assertExists == AssertAssetExists::no); + return {}; + } return resourceFile.createInputStream(); #endif diff --git a/extras/AudioPluginHost/Builds/Android/app/src/main/assets/webviewplugin-gui-fallback.html b/extras/AudioPluginHost/Builds/Android/app/src/main/assets/webviewplugin-gui-fallback.html new file mode 100644 index 0000000000..4870d4e85b --- /dev/null +++ b/extras/AudioPluginHost/Builds/Android/app/src/main/assets/webviewplugin-gui-fallback.html @@ -0,0 +1,39 @@ + + + + + WebViewPluginDemo + + + +

WebViewPluginDemo

+

+ This document is a placeholder for the GUI component of the + WebViewPluginDemo. +

+

+ To build the fully fledged user interface you need to install + node.js +

+

+ Then navigate into the + examples/GUI/WebViewPluginDemoGUI directory inside your JUCE + directory, and issue the following commands. +

+
+        npm install
+        npm run build
+        npm run zip
+      
+

+ This will build the full GUI package and place it in the + Assets directory. +

+

After this, rebuild and restart this demo.

+ +