LibWebView+UI: Add a context menu item to download images

This introduces a simple FileDownloader to download files in the UI
process from RequestServer. We use this to download the context menu
image - this download is likely to hit the disk cache.
This commit is contained in:
Timothy Flynn 2026-02-03 08:00:15 -05:00 committed by Tim Flynn
parent 70482687a2
commit f322e8a29c
9 changed files with 130 additions and 3 deletions

View file

@ -24,6 +24,7 @@
#include <LibWeb/CSS/PreferredMotion.h>
#include <LibWeb/Clipboard/SystemClipboard.h>
#include <LibWeb/HTML/ActivateTab.h>
#include <LibWebView/FileDownloader.h>
#include <LibWebView/Forward.h>
#include <LibWebView/Options.h>
#include <LibWebView/Process.h>
@ -126,6 +127,8 @@ public:
Menu& debug_menu() { return *m_debug_menu; }
FileDownloader& file_downloader() { return m_file_downloader; }
void apply_view_options(Badge<ViewImplementation>, ViewImplementation&);
ErrorOr<void> toggle_devtools_enabled();
@ -256,6 +259,8 @@ private:
Optional<Web::Clipboard::SystemClipboardRepresentation> m_clipboard;
FileDownloader m_file_downloader;
#if defined(AK_OS_MACOS)
OwnPtr<MachPortServer> m_mach_port_server;
#endif

View file

@ -8,6 +8,7 @@ set(SOURCES
ConsoleOutput.cpp
CookieJar.cpp
DOMNodeProperties.cpp
FileDownloader.cpp
HeadlessWebView.cpp
HelperProcess.cpp
Menu.cpp

View file

@ -0,0 +1,75 @@
/*
* Copyright (c) 2026, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibCore/File.h>
#include <LibHTTP/HeaderList.h>
#include <LibRequests/Request.h>
#include <LibRequests/RequestClient.h>
#include <LibTextCodec/Encoder.h>
#include <LibWeb/Loader/UserAgent.h>
#include <LibWebView/Application.h>
#include <LibWebView/CookieJar.h>
#include <LibWebView/FileDownloader.h>
namespace WebView {
FileDownloader::FileDownloader() = default;
FileDownloader::~FileDownloader() = default;
static ErrorOr<void> save_file(LexicalPath const& destination, ReadonlyBytes data)
{
auto file = TRY(Core::File::open(destination.string(), Core::File::OpenMode::Write));
TRY(file->write_until_depleted(data));
return {};
}
void FileDownloader::download_file(URL::URL const& url, LexicalPath destination)
{
static u64 next_request_id = 0;
// FIXME: What other request headers should be set? Perhaps we want to use exactly the same request headers used to
// originally fetch the image in WebContent.
auto request_headers = HTTP::HeaderList::create();
request_headers->set({ "Cookie"sv, TextCodec::isomorphic_encode(Application::cookie_jar().get_cookie(url, Web::Cookie::Source::Http)) });
request_headers->set({ "User-Agent"sv, Web::default_user_agent });
auto request = Application::request_server_client().start_request("GET"sv, url, *request_headers);
if (!request) {
Application::the().display_error_dialog("Unable to start request to download file"sv);
return;
}
auto request_id = next_request_id++;
request->set_buffered_request_finished_callback(
[this, request_id, destination = move(destination)](u64, Requests::RequestTimingInfo const&, Optional<Requests::NetworkError> const& network_error, HTTP::HeaderList const&, Optional<u32> response_code, Optional<String> const& reason_phrase, ReadonlyBytes payload) {
Core::deferred_invoke([this, request_id]() { m_requests.remove(request_id); });
if (network_error.has_value()) {
auto error = MUST(String::formatted("Unable to download file: {}", Requests::network_error_to_string(*network_error)));
Application::the().display_error_dialog(error);
return;
}
if (response_code.has_value() && *response_code >= 400) {
auto error = reason_phrase.has_value()
? MUST(String::formatted("Received error response code {} while downloading file: {}", *response_code, reason_phrase))
: MUST(String::formatted("Received error response code {} while downloading file", *response_code));
Application::the().display_error_dialog(error);
return;
}
if (auto result = save_file(destination, payload); result.is_error()) {
auto error = MUST(String::formatted("Unable to save downloaded file file: {}", result.error()));
Application::the().display_error_dialog(error);
}
// FIXME: Add a UI element (i.e. a download manager) to indicate download completion.
});
m_requests.set(request_id, request.release_nonnull());
}
}

View file

@ -0,0 +1,29 @@
/*
* Copyright (c) 2026, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/HashMap.h>
#include <AK/LexicalPath.h>
#include <AK/NonnullRefPtr.h>
#include <LibRequests/Forward.h>
#include <LibURL/Forward.h>
#include <LibWebView/Forward.h>
namespace WebView {
class WEBVIEW_API FileDownloader {
public:
FileDownloader();
~FileDownloader();
void download_file(URL::URL const&, LexicalPath);
private:
HashMap<u64, NonnullRefPtr<Requests::Request>> m_requests;
};
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
* Copyright (c) 2025-2026, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@ -46,6 +46,7 @@ enum class ActionID {
CopyURL,
OpenImage,
SaveImage,
CopyImage,
OpenAudio,

View file

@ -891,6 +891,13 @@ void ViewImplementation::initialize_context_menus()
m_open_image_action = Action::create("Open Image"sv, ActionID::OpenImage, [this]() {
load(m_context_menu_url);
});
m_save_image_action = Action::create("Save Image As..."sv, ActionID::SaveImage, [this]() {
auto download_path = Application::the().path_for_downloaded_file(m_context_menu_url.basename());
if (download_path.is_error())
return;
Application::the().file_downloader().download_file(m_context_menu_url, download_path.release_value());
});
m_copy_image_action = Action::create("Copy Image"sv, ActionID::CopyImage, [this]() {
if (!m_image_context_menu_bitmap.has_value())
return;
@ -958,6 +965,8 @@ void ViewImplementation::initialize_context_menus()
m_image_context_menu->add_action(*m_open_image_action);
m_image_context_menu->add_action(*m_open_in_new_tab_action);
m_image_context_menu->add_separator();
m_image_context_menu->add_action(*m_save_image_action);
m_image_context_menu->add_separator();
m_image_context_menu->add_action(*m_copy_image_action);
m_image_context_menu->add_action(*m_copy_url_action);

View file

@ -334,6 +334,7 @@ protected:
URL::URL m_context_menu_url;
RefPtr<Action> m_open_image_action;
RefPtr<Action> m_save_image_action;
RefPtr<Action> m_copy_image_action;
Optional<Gfx::ShareableBitmap> m_image_context_menu_bitmap;

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
* Copyright (c) 2025-2026, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@ -179,6 +179,9 @@ static void initialize_native_control(WebView::Action& action, id control)
case WebView::ActionID::OpenImage:
set_control_image(control, @"photo");
break;
case WebView::ActionID::SaveImage:
set_control_image(control, @"square.and.arrow.down");
break;
case WebView::ActionID::CopyImage:
set_control_image(control, @"document.on.document");
break;

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
* Copyright (c) 2025-2026, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@ -142,6 +142,9 @@ static void initialize_native_control(WebView::Action& action, QAction& qaction,
case WebView::ActionID::OpenImage:
qaction.setIcon(load_icon_from_uri("resource://icons/16x16/filetype-image.png"sv));
break;
case WebView::ActionID::SaveImage:
qaction.setIcon(load_icon_from_uri("resource://icons/16x16/download.png"sv));
break;
case WebView::ActionID::CopyImage:
qaction.setIcon(load_icon_from_uri("resource://icons/16x16/edit-copy.png"sv));
break;