fix: merge branch feat/plugin-feature

This commit is contained in:
Ian Chua
2026-07-17 14:02:10 +08:00
151 changed files with 5144 additions and 1418 deletions

View File

@@ -116,8 +116,8 @@ struct PluginDialogItem
PluginAvailableActions available_actions;
};
constexpr bool kFetchCloudCatalog = true;
constexpr bool kUseCurrentCloudCatalog = false;
constexpr bool kFetchCloudMeta = true;
constexpr bool kUseCurrentCloudMeta = false;
// The type of a package's first loaded capability. A package that is not loaded has no type.
PluginCapabilityType primary_capability_type_of(PluginManager& manager, const std::string& plugin_key)
@@ -126,7 +126,7 @@ PluginCapabilityType primary_capability_type_of(PluginManager& manager, const st
return capabilities.empty() ? PluginCapabilityType::Unknown : capabilities.front()->type();
}
std::vector<PluginDescriptor> current_cloud_catalog_snapshot()
std::vector<PluginDescriptor> current_cloud_metadata_snapshot()
{
std::vector<PluginDescriptor> cloud_entries;
for (const PluginDescriptor& entry : PluginManager::instance().get_plugin_descriptors(/*include_invalid=*/true))
@@ -147,20 +147,18 @@ PluginDescriptor as_cloud_only_descriptor(PluginDescriptor descriptor)
return descriptor;
}
// rescan_plugins() clears the whole catalog and rediscovers only local packages, so when no fresh cloud
// data is needed, restore the current cloud rows afterwards or cloud-only rows would disappear.
void refresh_plugin_catalog_blocking(bool fetch_cloud)
void refresh_plugin_metadata_blocking(bool fetch_cloud)
{
PluginManager& manager = PluginManager::instance();
std::vector<std::string> not_found, unauthorized;
const std::vector<PluginDescriptor> current_cloud_catalog = fetch_cloud ? std::vector<PluginDescriptor>{} :
current_cloud_catalog_snapshot();
const std::vector<PluginDescriptor> current_cloud_metadata = fetch_cloud ? std::vector<PluginDescriptor>{} :
current_cloud_metadata_snapshot();
manager.rescan_plugins();
if (!fetch_cloud) {
manager.update_cloud_catalog(current_cloud_catalog);
manager.update_cloud_metadata(current_cloud_metadata);
return;
}
@@ -450,7 +448,7 @@ void PluginsDialog::set_open_terminal_dlg_fn()
void PluginsDialog::update_plugin_dialog_ui()
{
// Called after the shared catalog is already updated, for example from the
// Called after the any metadata is already updated, for example from the
// cloud-plugin state callback. Do not fetch here or the callback can re-enter.
send_plugins();
resolve_pending_activation();
@@ -482,10 +480,21 @@ void PluginsDialog::on_script_message(const nlohmann::json& payload)
if (handle_common_script_command(payload))
return;
// Defer command handling out of the webview script-message callback: GTK and macOS
// deliver it synchronously inside the native webview callback (see ui_create_window
// in PluginHostUi.cpp), and window work on that stack is the crash class fixed in
// b779a7bfed/f2ccbfc8b5. Deferring at this single entry point keeps every command
// handler, current and future, off that stack by construction.
wxGetApp().CallAfter([this, alive = m_alive, payload]() {
if (alive->load(std::memory_order_acquire))
handle_web_command(payload);
});
}
void PluginsDialog::handle_web_command(const nlohmann::json& payload)
{
const std::string command = payload.value("command", "");
if (command == "request_plugins") {
// Discovery already runs at startup and on login, so the shared catalog is up to date by the
// time the dialog opens: render it without a blocking fetch. Refresh is what rediscovers.
send_plugins();
} else if (command == "refresh_plugins") {
refresh_plugins();
@@ -498,8 +507,8 @@ void PluginsDialog::on_script_message(const nlohmann::json& payload)
install_plugin_from_file();
} else if (command == "plugin_menu_action") {
handle_plugin_menu_action(payload.value("plugin_key", ""), payload.value("action", ""));
} else if (command == "run_script_plugin") {
run_script_plugin(payload.value("plugin_key", ""), payload.value("capability_name", ""));
} else if (command == "run_script_plugin_capability") {
run_script_plugin_capability(payload.value("plugin_key", ""), payload.value("capability_name", ""));
} else if (command == "open_terminal") {
m_open_terminal_dlg_fn();
} else if (command == "update_plugin") {
@@ -575,16 +584,16 @@ bool PluginsDialog::get_descriptor(const std::string& plugin_key, PluginDescript
return manager.try_get_plugin_descriptor(plugin_key, descriptor) && descriptor.is_invalid_package();
}
void PluginsDialog::refresh_plugin_catalog_async(const wxString& title, const wxString& message, bool fetch_cloud)
void PluginsDialog::refresh_plugin_metadata_async(const wxString& title, const wxString& message, bool fetch_cloud)
{
run_with_dialog([fetch_cloud]() { refresh_plugin_catalog_blocking(fetch_cloud); }, [this]() { send_plugins(); }, title, message);
run_with_dialog([fetch_cloud]() { refresh_plugin_metadata_blocking(fetch_cloud); }, [this]() { send_plugins(); }, title, message);
}
void PluginsDialog::refresh_plugins()
{
BOOST_LOG_TRIVIAL(info) << "Refreshing plugins from Plugins dialog";
refresh_plugin_catalog_async(_L("Refreshing"), _L("Refreshing plugins data"), kFetchCloudCatalog);
refresh_plugin_metadata_async(_L("Refreshing"), _L("Refreshing plugins data"), kFetchCloudMeta);
}
void PluginsDialog::toggle_plugin(const std::string& plugin_key, bool enabled)
@@ -596,7 +605,6 @@ void PluginsDialog::toggle_plugin(const std::string& plugin_key, bool enabled)
PluginDescriptor row_data;
if (!get_descriptor(plugin_key, row_data)) {
// The catalog changed under the UI. Toggling is local, so just re-render from it.
send_plugins();
return;
}
@@ -688,7 +696,6 @@ void PluginsDialog::toggle_plugin_capability(const std::string& plugin_key,
PluginDescriptor row_data;
if (!get_descriptor(plugin_key, row_data)) {
// The catalog changed under the UI. Toggling is local, so just re-render from it.
send_plugins();
return;
}
@@ -741,12 +748,24 @@ void PluginsDialog::handle_plugin_menu_action(const std::string& plugin_key, con
}
}
void PluginsDialog::restore_z_order()
{
// Deferred so it runs on a clean stack after the modal has fully torn down; the
// alive guard covers the dialog being destroyed while the CallAfter is queued.
wxGetApp().CallAfter([this, alive = m_alive]() {
if (alive->load(std::memory_order_acquire) && IsShown())
Raise();
});
}
void PluginsDialog::install_plugin_from_file()
{
wxFileDialog dialog(this, _L("Select plugin package"), wxEmptyString, wxEmptyString, _L("Plugin files (*.py;*.whl)|*.py;*.whl"),
wxFD_OPEN | wxFD_FILE_MUST_EXIST);
if (dialog.ShowModal() != wxID_OK)
const int rc = dialog.ShowModal();
restore_z_order();
if (rc != wxID_OK)
return;
if (!install_plugin_package(dialog.GetPath().ToUTF8().data())) {
@@ -799,7 +818,9 @@ bool PluginsDialog::install_plugin_package(const std::string& package_path)
plugin_name),
kOverwritePluginTitle, wxOK | wxCANCEL | wxCANCEL_DEFAULT | wxICON_WARNING);
dialog.SetOKCancelLabels(_L("Overwrite"), _L("Cancel"));
if (dialog.ShowModal() != wxID_OK) {
const int overwrite_rc = dialog.ShowModal();
restore_z_order();
if (overwrite_rc != wxID_OK) {
BOOST_LOG_TRIVIAL(info) << "Plugin package installation cancelled before overwrite. package=" << package_path
<< " plugin=" << plugin_descriptor.name;
return false;
@@ -827,7 +848,7 @@ bool PluginsDialog::install_plugin_package(const std::string& package_path)
BOOST_LOG_TRIVIAL(info) << "Plugin package installed successfully from " << package_path;
const wxString installed_name = from_u8(plugin_descriptor.name.empty() ? package_file.filename().string() : plugin_descriptor.name);
show_status(wxString::Format(_L("Installed \"%s\"."), installed_name), "success");
refresh_plugin_catalog_async(_L("Refreshing"), _L("Refreshing plugins data"), kUseCurrentCloudCatalog);
refresh_plugin_metadata_async(_L("Refreshing"), _L("Refreshing plugins data"), kUseCurrentCloudMeta);
return true;
}
@@ -910,7 +931,7 @@ void PluginsDialog::restore_capability_config(const PluginCapabilityId& id)
show_status(_L("Default configuration restored."), "success");
}
void PluginsDialog::run_script_plugin(const std::string& plugin_key, const std::string& capability_name)
void PluginsDialog::run_script_plugin_capability(const std::string& plugin_key, const std::string& capability_name)
{
PluginManager& manager = PluginManager::instance();
@@ -1046,16 +1067,17 @@ void PluginsDialog::delete_local_plugin(const PluginDescriptor& plugin)
const wxString plugin_name = from_u8(plugin.name);
const int rc = wxMessageBox(wxString::Format(_L("Delete plugin \"%s\"?\n\nThis permanently removes the plugin folder."), plugin_name),
kDeletePluginTitle, wxYES_NO | wxNO_DEFAULT | wxICON_WARNING, this);
restore_z_order();
if (rc != wxYES)
return;
auto state = std::make_shared<PluginOperationState>();
run_with_dialog(
[plugin_key = plugin.plugin_key, refresh_catalog = plugin.is_cloud_plugin(), state]() {
[plugin_key = plugin.plugin_key, should_refresh = plugin.is_cloud_plugin(), state]() {
std::string error;
const bool succeeded = PluginManager::instance().delete_plugin(plugin_key, error);
if (succeeded && refresh_catalog)
refresh_plugin_catalog_blocking(kFetchCloudCatalog);
if (succeeded && should_refresh)
refresh_plugin_metadata_blocking(kFetchCloudMeta);
store_plugin_operation_result(state, succeeded, std::move(error));
},
[this, state, plugin_name]() {
@@ -1078,6 +1100,7 @@ void PluginsDialog::unsubscribe_cloud_plugin(const PluginDescriptor& plugin)
wxString::Format(_L("Unsubscribe plugin \"%s\"?\n\nThis will stop tracking the plugin and delete any local plugin files."),
plugin_name),
kUnsubscribeTitle, wxYES_NO | wxNO_DEFAULT | wxICON_WARNING, this);
restore_z_order();
if (rc != wxYES)
return;
@@ -1159,7 +1182,7 @@ void PluginsDialog::reinstall_cloud_plugin(const PluginDescriptor& plugin)
return;
}
manager.update_cloud_catalog(std::vector<PluginDescriptor>{as_cloud_only_descriptor(plugin)});
manager.update_cloud_metadata(std::vector<PluginDescriptor>{as_cloud_only_descriptor(plugin)});
}
if (!install_cloud_plugin(plugin_key, plugin.version, from_u8(plugin.name))) {
@@ -1207,6 +1230,7 @@ void PluginsDialog::delete_mine_local_and_cloud_plugin(const std::string& plugin
"deletes the plugin from the cloud. This action cannot be undone."),
plugin_name),
kDeletePluginTitle, wxYES_NO | wxNO_DEFAULT | wxICON_WARNING, this);
restore_z_order();
if (rc != wxYES)
return;
@@ -1216,8 +1240,6 @@ void PluginsDialog::delete_mine_local_and_cloud_plugin(const std::string& plugin
return;
}
// delete_mine_local_and_cloud_plugin already updated the in-memory catalog (see
// finalize_cloud_plugin_removal), so a UI refresh is sufficient here.
send_plugins();
show_status(wxString::Format(_L("Deleted \"%s\"."), plugin_name), "success");
}