/* ============================================================================== PluginEditor.cpp Created: 4 Nov 2025 6:20:46pm Author: mickl ============================================================================== */ #include "PluginEditor.h" #include "DemoUtilities.h" 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 == "midNoteData.json") { juce::Array notes; int voice_num = 0; for (auto& voice : processorRef.shifter.voices) { if (voice.onoff_) { auto obj = new DynamicObject(); obj->setProperty("voice", voice_num); obj->setProperty("midi", voice.GetMidiNote()); notes.add(var(obj)); } voice_num++; } DynamicObject::Ptr d(new DynamicObject()); d->setProperty("notes", notes); 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) { auto options = WebBrowserComponent::Options{} .withBackend(WebBrowserComponent::Options::Backend::webview2) .withWinWebView2Options(WebBrowserComponent::Options::WinWebView2{} .withUserDataFolder(File::getSpecialLocation(File::SpecialLocationType::tempDirectory))) .withNativeIntegrationEnabled() .withOptionsFrom(controlParameterIndexReceiver) .withResourceProvider([this](const auto& url) { return getResource(url); }, URL{ localDevServerAddress }.getOrigin()); for (auto& sliderId : p.parameters.sliderIds) { slider_relays.push_back(new WebSliderRelay{ sliderId }); slider_attatchments.push_back(new WebSliderParameterAttachment( *processorRef.state.getParameter(sliderId), *slider_relays.back(), processorRef.state.undoManager)); options = options.withOptionsFrom(*slider_relays.back()); } for (auto& toggleId : p.parameters.toggleIds) { toggle_relays.push_back(new WebToggleButtonRelay{ toggleId }); toggle_attatchments.push_back(new WebToggleButtonParameterAttachment( *processorRef.state.getParameter(toggleId), *toggle_relays.back(), processorRef.state.undoManager)); options = options.withOptionsFrom(*toggle_relays.back()); } webComponent = new SinglePageBrowser(options); addAndMakeVisible(*webComponent); webComponent->goToURL(localDevServerAddress); //webComponent.goToURL (WebBrowserComponent::getResourceProviderRoot()); setSize(800, 390); startTimerHz(60); } 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() { if (webComponent == nullptr) return; webComponent->setBounds(getLocalBounds()); }