LibWebView: Add a settings section to manage browsing data caches

This adds a section to allow users to clear the HTTP disk cache and
cookies / local storage / session storage. There are a few options to
limit this action to a specific time range (e.g. "last hour"). The
user is informed how much disk space is being used currently, and how
much will be removed given the selected time range.

The idea is that in the future, we can add more settings here to auto-
delete data on exit, disable caching altogether, etc.
This commit is contained in:
Timothy Flynn 2025-11-02 18:44:17 -05:00 committed by Tim Flynn
parent 48aa16d74b
commit c34119cb29
Notes: github-actions[bot] 2025-11-12 14:08:01 +00:00
7 changed files with 307 additions and 0 deletions

View file

@ -110,6 +110,22 @@
margin-top: 20px;
}
.input-field-container {
display: flex;
align-items: center;
justify-content: flex-start;
gap: 10px;
}
.input-field-container + .input-field-container {
margin-top: 16px;
}
.input-field-container label,
.input-field-container p {
margin: 0;
}
.forcibly-enabled {
font-size: 14px;
opacity: 0.6;
@ -372,6 +388,13 @@
<div class="card">
<div class="card-header">Privacy</div>
<div class="card-body">
<div class="card-group inline-container">
<span>Browsing Data</span>
<button id="clear-browsing-data" class="secondary-button">Clear...</button>
</div>
<hr />
<div class="card-group">
<div class="inline-container">
<label for="global-privacy-control-toggle">Enable Global Privacy Control</label>
@ -495,6 +518,45 @@
</div>
</dialog>
<dialog id="clear-browsing-data-dialog">
<div class="dialog-header">
<h3 id="clear-browsing-data-title" class="dialog-title">Clear Browsing Data</h3>
<button id="clear-browsing-data-close" class="close-button dialog-button">&times;</button>
</div>
<div class="dialog-body">
<p id="clear-browsing-data-total-size" class="description"></p>
<hr />
<div class="input-field-container">
<p>From:</p>
<select id="clear-browsing-data-time-range">
<option value="lastHour">Last hour</option>
<option value="last4Hours">Last 4 hours</option>
<option value="today">Today</option>
<option value="all" selected>All time</option>
</select>
</div>
<div class="input-field-container">
<input id="clear-browsing-data-cached-files" type="checkbox" value="" checked />
<label for="clear-browsing-data-cached-files">
Cached files<span id="clear-browsing-data-cached-files-size"></span>
<p class="description">Remove items that help pages load faster</p>
</label>
</div>
<div class="input-field-container">
<input id="clear-browsing-data-site-data" type="checkbox" value="" checked />
<label for="clear-browsing-data-site-data">
Cookies and site data<span id="clear-browsing-data-site-data-size"></span>
<p class="description">Remove items that may sign you out of most sites</p>
</label>
</div>
</div>
<div class="dialog-footer">
<button id="clear-browsing-data-remove-data" class="secondary-button">Remove data</button>
</div>
</dialog>
<script>
// FIXME: When we support per-glyph font fallbacks, replace these SVGs with analogous code points.
// https://github.com/LadybirdBrowser/ladybird/issues/864

View file

@ -1,9 +1,117 @@
const clearBrowsingData = document.querySelector("#clear-browsing-data");
const clearBrowsingDataCachedFiles = document.querySelector("#clear-browsing-data-cached-files");
const clearBrowsingDataCachedFilesSize = document.querySelector("#clear-browsing-data-cached-files-size");
const clearBrowsingDataClose = document.querySelector("#clear-browsing-data-close");
const clearBrowsingDataDialog = document.querySelector("#clear-browsing-data-dialog");
const clearBrowsingDataRemoveData = document.querySelector("#clear-browsing-data-remove-data");
const clearBrowsingDataSiteData = document.querySelector("#clear-browsing-data-site-data");
const clearBrowsingDataSiteDataSize = document.querySelector("#clear-browsing-data-site-data-size");
const clearBrowsingDataTimeRange = document.querySelector("#clear-browsing-data-time-range");
const clearBrowsingDataTotalSize = document.querySelector("#clear-browsing-data-total-size");
const globalPrivacyControlToggle = document.querySelector("#global-privacy-control-toggle");
const BYTE_UNITS = ["byte", "kilobyte", "megabyte", "gigabyte", "terabyte"];
const BYTE_FORMATTERS = {
byte: undefined,
kilobyte: undefined,
megabyte: undefined,
gigabyte: undefined,
terabyte: undefined,
};
function formatBytes(bytes) {
let index = 0;
while (bytes >= 1024 && index < BYTE_UNITS.length - 1) {
bytes /= 1024;
++index;
}
const unit = BYTE_UNITS[index];
if (!BYTE_FORMATTERS[unit]) {
BYTE_FORMATTERS[unit] = new Intl.NumberFormat([], {
style: "unit",
unit: unit,
unitDisplay: unit === "byte" ? "long" : "short",
maximumFractionDigits: 1,
});
}
return BYTE_FORMATTERS[unit].format(bytes);
}
function loadSettings(settings) {
globalPrivacyControlToggle.checked = settings.globalPrivacyControl;
}
function computeTimeRange() {
const now = Temporal.Now.zonedDateTimeISO();
switch (clearBrowsingDataTimeRange.value) {
case "lastHour":
return now.subtract({ hours: 1 });
case "last4Hours":
return now.subtract({ hours: 4 });
case "today":
return now.startOfDay();
case "all":
return null;
default:
console.error(`Unrecognized time range: ${clearBrowsingDataTimeRange.value}`);
return now;
}
}
function estimateBrowsingDataSizes() {
const since = computeTimeRange();
ladybird.sendMessage("estimateBrowsingDataSizes", {
since: since?.epochMilliseconds,
});
}
function updateBrowsingDataSizes(sizes) {
const totalSize = sizes.totalCacheSize + sizes.totalSiteDataSize;
clearBrowsingDataTotalSize.innerText = `Your browsing data is currently using ${formatBytes(totalSize)} of disk space`;
clearBrowsingDataCachedFilesSize.innerText = ` (remove ${formatBytes(sizes.cacheSizeSinceRequestedTime)})`;
clearBrowsingDataSiteDataSize.innerText = ` (remove ${formatBytes(sizes.siteDataSizeSinceRequestedTime)})`;
}
clearBrowsingData.addEventListener("click", () => {
estimateBrowsingDataSizes();
clearBrowsingDataDialog.showModal();
});
clearBrowsingDataTimeRange.addEventListener("change", () => {
estimateBrowsingDataSizes();
});
clearBrowsingDataClose.addEventListener("click", () => {
clearBrowsingDataDialog.close();
});
function setRemoveDataEnabledState() {
clearBrowsingDataRemoveData.disabled = !clearBrowsingDataCachedFiles.checked && !clearBrowsingDataSiteData.checked;
}
clearBrowsingDataCachedFiles.addEventListener("change", setRemoveDataEnabledState);
clearBrowsingDataSiteData.addEventListener("change", setRemoveDataEnabledState);
clearBrowsingDataRemoveData.addEventListener("click", () => {
const since = computeTimeRange();
ladybird.sendMessage("clearBrowsingData", {
since: since?.epochMilliseconds,
cachedFiles: clearBrowsingDataCachedFiles.checked,
siteData: clearBrowsingDataSiteData.checked,
});
clearBrowsingDataDialog.close();
});
globalPrivacyControlToggle.addEventListener("change", () => {
ladybird.sendMessage("setGlobalPrivacyControl", globalPrivacyControlToggle.checked);
});
@ -11,5 +119,7 @@ globalPrivacyControlToggle.addEventListener("change", () => {
document.addEventListener("WebUIMessage", event => {
if (event.detail.name === "loadSettings") {
loadSettings(event.detail.data);
} else if (event.detail.name === "estimatedBrowsingDataSizes") {
updateBrowsingDataSizes(event.detail.data);
}
});

View file

@ -150,6 +150,15 @@ select:focus {
outline: none;
}
input[type="checkbox"] {
width: 14px;
height: 14px;
accent-color: var(--violet-100);
outline: none;
}
input[type="checkbox"][switch] {
width: 50px;
height: 24px;

View file

@ -661,6 +661,52 @@ void Application::insert_clipboard_entry(Web::Clipboard::SystemClipboardRepresen
m_clipboard = move(entry);
}
NonnullRefPtr<Core::Promise<Application::BrowsingDataSizes>> Application::estimate_browsing_data_size_accessed_since(UnixDateTime since)
{
auto promise = Core::Promise<BrowsingDataSizes>::construct();
m_request_server_client->estimate_cache_size_accessed_since(since)
->when_resolved([this, promise, since](Requests::CacheSizes cache_sizes) {
auto cookie_sizes = m_cookie_jar->estimate_storage_size_accessed_since(since);
auto storage_sizes = m_storage_jar->estimate_storage_size_accessed_since(since);
BrowsingDataSizes sizes;
sizes.cache_size_since_requested_time = cache_sizes.since_requested_time;
sizes.total_cache_size = cache_sizes.total;
sizes.site_data_size_since_requested_time = cookie_sizes.since_requested_time + storage_sizes.since_requested_time;
sizes.total_site_data_size = cookie_sizes.total + storage_sizes.total;
promise->resolve(sizes);
})
.when_rejected([promise](Error& error) {
promise->reject(move(error));
});
return promise;
}
void Application::clear_browsing_data(ClearBrowsingDataOptions const& options)
{
if (options.delete_cached_files == ClearBrowsingDataOptions::Delete::Yes) {
m_request_server_client->async_remove_cache_entries_accessed_since(options.since);
// FIXME: Maybe we should forward the "since" parameter to the WebContent process, but the in-memory cache is
// transient anyways, so just assuming they were all accessed in the last hour is fine for now.
ViewImplementation::for_each_view([](ViewImplementation& view) {
// FIXME: This should be promoted from a debug request to a proper endpoint.
view.debug_request("clear-cache"sv);
return IterationDecision::Continue;
});
}
if (options.delete_site_data == ClearBrowsingDataOptions::Delete::Yes) {
m_cookie_jar->expire_cookies_accessed_since(options.since);
m_storage_jar->remove_items_accessed_since(options.since);
}
}
void Application::initialize_actions()
{
auto debug_request = [this](auto request) {

View file

@ -83,6 +83,27 @@ public:
virtual Vector<Web::Clipboard::SystemClipboardRepresentation> clipboard_entries() const;
virtual void insert_clipboard_entry(Web::Clipboard::SystemClipboardRepresentation);
struct BrowsingDataSizes {
u64 cache_size_since_requested_time { 0 };
u64 total_cache_size { 0 };
u64 site_data_size_since_requested_time { 0 };
u64 total_site_data_size { 0 };
};
NonnullRefPtr<Core::Promise<BrowsingDataSizes>> estimate_browsing_data_size_accessed_since(UnixDateTime since);
struct ClearBrowsingDataOptions {
enum class Delete {
No,
Yes,
};
UnixDateTime since { UnixDateTime::earliest() };
Delete delete_cached_files { Delete::No };
Delete delete_site_data { Delete::No };
};
void clear_browsing_data(ClearBrowsingDataOptions const&);
Action& reload_action() { return *m_reload_action; }
Action& copy_selection_action() { return *m_copy_selection_action; }
Action& paste_action() { return *m_paste_action; }

View file

@ -63,6 +63,12 @@ void SettingsUI::register_interfaces()
remove_all_site_setting_filters(data);
});
register_interface("estimateBrowsingDataSizes"sv, [this](auto const& data) {
estimate_browsing_data_sizes(data);
});
register_interface("clearBrowsingData"sv, [this](auto const& data) {
clear_browsing_data(data);
});
register_interface("setGlobalPrivacyControl"sv, [this](auto const& data) {
set_global_privacy_control(data);
});
@ -275,6 +281,57 @@ void SettingsUI::remove_all_site_setting_filters(JsonValue const& site_setting)
load_current_settings();
}
void SettingsUI::estimate_browsing_data_sizes(JsonValue const& options)
{
if (!options.is_object())
return;
auto& application = Application::the();
auto since = [&]() {
if (auto since = options.as_object().get_integer<i64>("since"sv); since.has_value())
return UnixDateTime::from_milliseconds_since_epoch(*since);
return UnixDateTime::earliest();
}();
application.estimate_browsing_data_size_accessed_since(since)
->when_resolved([this](Application::BrowsingDataSizes sizes) {
JsonObject result;
result.set("cacheSizeSinceRequestedTime"sv, sizes.cache_size_since_requested_time);
result.set("totalCacheSize"sv, sizes.total_cache_size);
result.set("siteDataSizeSinceRequestedTime"sv, sizes.site_data_size_since_requested_time);
result.set("totalSiteDataSize"sv, sizes.total_site_data_size);
async_send_message("estimatedBrowsingDataSizes"sv, move(result));
})
.when_rejected([](Error const& error) {
dbgln("Failed to estimate browsing data sizes: {}", error);
});
}
void SettingsUI::clear_browsing_data(JsonValue const& options)
{
if (!options.is_object())
return;
Application::ClearBrowsingDataOptions clear_browsing_data_options;
if (auto since = options.as_object().get_integer<i64>("since"sv); since.has_value())
clear_browsing_data_options.since = UnixDateTime::from_milliseconds_since_epoch(*since);
clear_browsing_data_options.delete_cached_files = options.as_object().get_bool("cachedFiles"sv).value_or(false)
? Application::ClearBrowsingDataOptions::Delete::Yes
: Application::ClearBrowsingDataOptions::Delete::No;
clear_browsing_data_options.delete_site_data = options.as_object().get_bool("siteData"sv).value_or(false)
? Application::ClearBrowsingDataOptions::Delete::Yes
: Application::ClearBrowsingDataOptions::Delete::No;
Application::the().clear_browsing_data(clear_browsing_data_options);
}
void SettingsUI::set_global_privacy_control(JsonValue const& global_privacy_control)
{
if (!global_privacy_control.is_bool())

View file

@ -36,6 +36,8 @@ private:
void remove_site_setting_filter(JsonValue const&);
void remove_all_site_setting_filters(JsonValue const&);
void estimate_browsing_data_sizes(JsonValue const&);
void clear_browsing_data(JsonValue const&);
void set_global_privacy_control(JsonValue const&);
void set_dns_settings(JsonValue const&);