Improve queue design & restore failure handling

This commit is contained in:
Luke Street
2026-09-02 01:40:55 -06:00
parent fe821f6ce2
commit 4df3607047
11 changed files with 368 additions and 72 deletions
+149 -21
View File
@@ -411,6 +411,10 @@ icon.verifying {
decorator: text("" center center);
}
icon.download {
decorator: text("" center center);
}
icon.celebration {
decorator: text("" center center);
}
@@ -631,7 +635,7 @@ modal-content pane > * {
}
window.modal.install-queue {
max-height: 720dp;
max-height: 768dp;
}
window.modal.drop-install {
@@ -643,19 +647,62 @@ window.modal.drop-install package-row {
}
window.modal.install-queue modal-body {
font-size: var(--font-size-sm);
color: rgba(var(--color-text-rgb), 60%);
display: none;
}
window.modal.install-queue modal-content pane {
gap: 0;
padding-right: 14dp;
padding-bottom: 6dp;
}
package-row {
display: flex;
flex-direction: column;
flex-direction: row;
align-items: flex-start;
position: relative;
width: 100%;
gap: var(--space-2xs);
padding: var(--space-sm) 78dp var(--space-sm) 0;
gap: 10dp;
padding: var(--space-md) 0;
border-bottom-width: 1dp;
border-bottom-color: rgba(var(--color-border-rgb), 25%);
border-bottom-color: rgba(var(--color-border-rgb), 30%);
}
package-row-icon {
display: none;
flex: 0 0 36dp;
width: 36dp;
height: 36dp;
margin-top: var(--space-2xs);
overflow: hidden;
border-radius: var(--radius-panel);
background-color: rgba(var(--color-control-rgb), 45%);
color: rgba(var(--color-text-rgb), 45%);
font-family: var(--font-family-icons);
font-size: var(--font-size-4xl);
decorator: text("" center center);
}
package-row-icon.visible {
display: block;
}
package-row-icon.has-image {
background-color: transparent;
}
package-row.paused package-row-icon,
package-row.retrying package-row-icon,
package-row.failed package-row-icon {
filter: grayscale(1);
opacity: 0.55;
}
package-row-info {
display: flex;
flex-direction: column;
flex: 1 1 0;
min-width: 0;
}
package-row-heading {
@@ -663,25 +710,48 @@ package-row-heading {
width: 100%;
gap: var(--space-sm);
align-items: center;
padding-bottom: 6dp;
}
package-row-identity {
display: flex;
align-items: baseline;
flex: 1 1 auto;
min-width: 0;
gap: var(--space-xs);
overflow: hidden;
}
package-row-name {
display: block;
flex: 1 1 auto;
flex: 0 1 auto;
min-width: 0;
font-weight: bold;
color: var(--color-white);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
package-row-state {
package-row-version {
display: block;
flex: 0 0 auto;
font-size: var(--font-size-xs);
font-weight: normal;
color: rgba(var(--color-text-rgb), 50%);
}
package-row-state {
display: block;
flex: 0 0 auto;
font-size: var(--font-size-sm);
color: rgba(var(--color-text-rgb), 60%);
}
package-row.downloading package-row-state {
color: var(--color-accent);
}
package-row.retrying package-row-state {
color: var(--color-warning);
}
@@ -697,23 +767,43 @@ package-row.installed package-row-detail {
}
package-row progress {
height: 4dp;
margin: var(--space-2xs) 0;
width: 100%;
height: 6dp;
margin: 0 0 var(--space-sm) 0;
border-radius: 3dp;
}
package-row.failed progress fill {
background-color: var(--color-error);
}
package-row.paused progress fill {
background-color: rgba(var(--color-text-rgb), 35%);
}
package-row.retrying progress fill {
background-color: rgba(var(--color-warning-rgb), 55%);
}
package-row.installed progress fill {
background-color: var(--color-success);
}
package-row-footer {
display: flex;
align-items: flex-start;
width: 100%;
min-width: 0;
gap: var(--space-sm);
}
package-row-detail {
display: block;
flex: 1 1 0;
min-width: 0;
font-size: var(--font-size-2xs);
color: rgba(var(--color-text-rgb), 48%);
font-size: var(--font-size-xs);
line-height: 1;
color: rgba(var(--color-text-rgb), 45%);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@@ -721,16 +811,54 @@ package-row-detail {
package-row-actions {
display: flex;
position: absolute;
right: 0;
top: 22dp;
gap: var(--space-2xs);
flex: 0 0 auto;
gap: 6dp;
}
package-row-actions button {
min-width: 34dp;
padding: 4dp 7dp;
font-size: var(--font-size-3xs);
package-row-actions button.package-row-icon-action {
align-items: center;
justify-content: center;
flex: 0 0 30dp;
width: 30dp;
min-width: 30dp;
height: 26dp;
padding: 0;
border-radius: 14dp;
font-size: var(--font-size-md);
}
package-row-actions button.package-row-icon-action icon {
flex: 0 0 var(--font-size-md);
width: var(--font-size-md);
height: var(--font-size-md);
font-size: var(--font-size-md);
line-height: 1;
}
package-row.installed {
align-items: center;
}
package-row.installed package-row-icon {
margin-top: 0;
}
package-row.installed package-row-info {
padding-right: 38dp;
}
package-row.installed package-row-state {
display: none;
}
package-row.installed package-row-actions {
position: absolute;
top: 17dp;
right: 0;
}
package-row.installed package-row-actions button.package-row-icon-action {
opacity: 0.45;
}
verification-progress {
+4 -1
View File
@@ -22,6 +22,7 @@
#include "dusk/io.hpp"
#include "dusk/mods/log_buffer.hpp"
#include "dusk/mods/path.hpp"
#include "dusk/mods/queue.hpp"
#include "dusk/mods/svc/config.hpp"
#include "dusk/mods/svc/hook.hpp"
#include "dusk/mods/svc/registry.hpp"
@@ -1781,6 +1782,7 @@ void ModLoader::apply_pending_requests() {
}
const auto removedName = mod->metadata.name;
const auto removedId = mod->metadata.id;
std::error_code error;
if (!fs::remove(mod->modPath, error)) {
complete_operation(uninstall->operation, false,
@@ -1788,11 +1790,12 @@ void ModLoader::apply_pending_requests() {
continue;
}
forget_mod(*mod);
queue::remove_by_mod_id(removedId);
complete_operation(uninstall->operation);
ui::push_toast({
.title = "Mod uninstalled",
.content = removedName,
.duration = std::chrono::seconds{4},
.duration = std::chrono::seconds{2},
});
continue;
}
+39 -6
View File
@@ -477,6 +477,7 @@ Item snapshot(const QueueItem& item) {
.total = item.total,
.message = item.message,
.local = local_source(item) != nullptr,
.icon = item.request.icon,
};
if (item.task) {
result.completed = std::max(result.completed, item.task.progress().completed);
@@ -553,10 +554,13 @@ void update() {
if (item->state == State::Handoff && item->operation &&
item->operation->state != ModOperation::State::Pending)
{
item = queueItems.erase(item);
} else {
++item;
item->message = item->operation->message;
item->state = item->operation->state == ModOperation::State::Succeeded ?
State::Installed :
State::InstallFailed;
item->operation.reset();
}
++item;
}
for (auto& item : queueItems) {
@@ -678,7 +682,23 @@ void resume(std::string_view id) {
void retry(std::string_view id) {
auto* item = find_queue_item(id);
if (item == nullptr || item->state != State::Failed) {
if (item == nullptr) {
return;
}
if (item->state == State::InstallFailed) {
auto* mod = ModLoader::instance().find_mod(item->request.id);
if (mod != nullptr) {
if (mod->activation_failed()) {
item->message.clear();
item->state = State::Handoff;
item->operation = ModLoader::instance().request_reactivate(item->request.id);
} else {
item->message.clear();
item->state = State::Installed;
}
return;
}
} else if (item->state != State::Failed) {
return;
}
remove_partial(*item);
@@ -690,7 +710,7 @@ void retry(std::string_view id) {
void cancel(std::string_view id) {
auto* item = find_queue_item(id);
if (item == nullptr || item->state == State::Handoff) {
if (item == nullptr || item->state == State::Handoff || is_terminal(item->state)) {
return;
}
if (item->task) {
@@ -711,6 +731,19 @@ void cancel(std::string_view id) {
item->state = State::Canceled;
}
void clear(std::string_view id) {
const auto item = std::ranges::find(
queueItems, id, [](const QueueItem& candidate) { return std::string_view{candidate.key}; });
if (item != queueItems.end() && is_terminal(item->state)) {
queueItems.erase(item);
}
}
void remove_by_mod_id(std::string_view id) {
std::erase_if(queueItems,
[id](const QueueItem& item) { return std::string_view{item.request.id} == id; });
}
void pause_all() {
std::vector<std::string> ids;
for (const auto& item : queueItems) {
@@ -726,7 +759,7 @@ void pause_all() {
}
void clear_finished() {
std::erase_if(queueItems, [](const QueueItem& item) { return item.state == State::Canceled; });
std::erase_if(queueItems, [](const QueueItem& item) { return is_terminal(item.state); });
}
} // namespace dusk::mods::queue
+18 -1
View File
@@ -18,12 +18,19 @@ enum class State {
Retrying,
Verifying,
Handoff,
Installed,
InstallFailed,
Failed,
Canceled,
};
[[nodiscard]] constexpr bool is_terminal(State state) noexcept {
return state == State::Failed || state == State::Canceled;
return state == State::Installed || state == State::InstallFailed || state == State::Failed ||
state == State::Canceled;
}
[[nodiscard]] constexpr bool is_install_result(State state) noexcept {
return state == State::Installed || state == State::InstallFailed;
}
struct Url {
@@ -38,11 +45,18 @@ struct LocalFile {
using Source = std::variant<Url, LocalFile>;
struct Icon {
std::string url;
uint32_t width = 0;
uint32_t height = 0;
};
struct Request {
std::string id;
std::string name;
std::string version;
Source source;
std::optional<Icon> icon;
};
struct Item {
@@ -57,6 +71,7 @@ struct Item {
std::string message;
int retrySeconds = 0;
bool local = false;
std::optional<Icon> icon;
};
/** Adds an install, replacing failed or canceled work for the same package ID. */
@@ -78,6 +93,8 @@ void pause(std::string_view id);
void resume(std::string_view id);
void retry(std::string_view id);
void cancel(std::string_view id);
void clear(std::string_view id);
void remove_by_mod_id(std::string_view id);
void pause_all();
void clear_finished();
+4 -2
View File
@@ -106,12 +106,14 @@ DropInstallModal::DropInstallModal(std::vector<DropPackage> packages, PreparedTa
auto& row = pane.add_child<PackageRow>();
const auto name = package.metadata.name.empty() ?
borealis::io::fs_path_to_string(package.path.filename()) :
fmt::format("{} {}", package.metadata.name, package.metadata.version);
package.metadata.name;
const auto version =
package.metadata.name.empty() ? std::string{} : package.metadata.version;
const auto detail =
package.metadata.author.empty() ?
format_bytes(package.size) :
fmt::format("{} · {}", package.metadata.author, format_bytes(package.size));
row.set_package(name, package.status, detail, package.valid ? "queued" : "failed");
row.set_package(name, version, package.status, detail, package.valid ? "queued" : "failed");
row.set_disabled(!package.valid);
}
}
+25 -1
View File
@@ -143,6 +143,21 @@ std::string image_source(const mods::catalog::Image& image, uint32_t preferredWi
return image.sources.empty() ? std::string{} : image.sources.back().pngUrl;
}
std::optional<mods::queue::Icon> queue_icon(const std::optional<mods::catalog::Image>& image) {
if (!image) {
return std::nullopt;
}
auto source = image_source(*image, 128);
if (source.empty()) {
return std::nullopt;
}
return mods::queue::Icon{
.url = std::move(source),
.width = image->width,
.height = image->height,
};
}
void set_image(Rml::Element* element, const mods::catalog::Image& image, uint32_t preferredWidth,
std::string_view fit = "cover") {
if (element == nullptr) {
@@ -436,6 +451,7 @@ public:
.sha256 = detail.download.sha256,
.size = detail.download.size,
},
.icon = queue_icon(detail.mod.icon),
} {
mRoot->SetClass("catalog-install-action", true);
mCaption = append(parent, "catalog-install-caption");
@@ -503,6 +519,9 @@ public:
progress = 1.0f;
disabled = true;
break;
case Installed:
case InstallFailed:
break;
case Failed:
glyph = "\uE5D5";
label = queued->local ? "Retry package" : "Retry download";
@@ -571,7 +590,12 @@ public:
private:
std::optional<mods::queue::Item> matching_queue_item() const {
auto item = mods::queue::find_by_mod_id(mRequest.id);
return item && item->version == mRequest.version ? item : std::nullopt;
if (!item || item->version != mRequest.version ||
mods::queue::is_install_result(item->state))
{
return std::nullopt;
}
return item;
}
void press() {
+2 -11
View File
@@ -110,8 +110,8 @@ public:
auto* info = append(mRoot, "mod-entry-info");
auto* name = append(info, "mod-entry-name");
append_text(append(name, "mod-entry-name-text"), "Browse online mods");
append_text(append(info, "mod-entry-sub"), "Dusklight catalog");
append_text(append(info, "mod-entry-desc"), "Discover and install published mods.");
append_text(
append(info, "mod-entry-desc"), "Discover and install mods from the community.");
on_nav_command([callback = std::move(onOpen)](Rml::Event&, NavCommand cmd) {
if (cmd != NavCommand::Confirm) {
@@ -290,7 +290,6 @@ void ModsWindow::build_content(Rml::Element* content) {
mBrowserSelected = true;
mSelectedMod = nullptr;
mSelectedModId.clear();
build_browser_detail(pane);
mark_current_entry();
});
}
@@ -311,7 +310,6 @@ void ModsWindow::build_content(Rml::Element* content) {
mSelectedModId.clear();
if (borealis::http::available()) {
mBrowserSelected = true;
build_browser_detail(detailPane);
}
mark_current_entry();
return;
@@ -334,7 +332,6 @@ void ModsWindow::build_content(Rml::Element* content) {
if (mBrowserSelected && mBrowserEntry != nullptr) {
mSelectedMod = nullptr;
mSelectedModId.clear();
build_browser_detail(detailPane);
} else {
mSelectedMod = nullptr;
if (!mSelectedModId.empty()) {
@@ -353,12 +350,6 @@ void ModsWindow::build_content(Rml::Element* content) {
mark_current_entry();
}
void ModsWindow::build_browser_detail(Pane& pane) {
pane.root()->RemoveAttribute("mod-id");
append_text(append(pane.root(), "mod-title"), "Browse Mods");
pane.add_text("Find published Dusklight mods in the online catalog.");
}
void ModsWindow::build_detail(Pane& pane, mods::LoadedMod& mod) {
pane.root()->SetAttribute("mod-id", mod.metadata.id);
pane.add_child<ModDetailHeader>(
-1
View File
@@ -28,7 +28,6 @@ private:
};
void build_content(Rml::Element* content);
void build_browser_detail(Pane& pane);
void build_detail(Pane& pane, mods::LoadedMod& mod);
void confirm_uninstall(const mods::LoadedMod& mod);
void refresh_snapshot();
+39 -8
View File
@@ -25,6 +25,10 @@ const char* queue_state_class(mods::queue::State state) {
return "installing";
case Handoff:
return "installing";
case Installed:
return "installed";
case InstallFailed:
return "failed";
case Failed:
return "failed";
case Canceled:
@@ -50,8 +54,12 @@ std::string state_label(const mods::queue::Item& item) {
return "Verifying";
case Handoff:
return "Installing";
case Installed:
return "Installed";
case InstallFailed:
return "Failed";
case Failed:
return item.local ? "Package failed" : "Download failed";
return "Failed";
case Canceled:
return "Canceled";
}
@@ -59,17 +67,24 @@ std::string state_label(const mods::queue::Item& item) {
}
PackageRow::PackageRow(Rml::Element* parent) : Component{create_row(parent)} {
auto* heading = append(mRoot, "package-row-heading");
mName = append(heading, "package-row-name");
mIcon = append(mRoot, "package-row-icon");
auto* info = append(mRoot, "package-row-info");
auto* heading = append(info, "package-row-heading");
auto* identity = append(heading, "package-row-identity");
mName = append(identity, "package-row-name");
mVersion = append(identity, "package-row-version");
mState = append(heading, "package-row-state");
mProgress = append(mRoot, "progress");
mDetail = append(mRoot, "package-row-detail");
mProgress = append(info, "progress");
mFooter = append(info, "package-row-footer");
mDetail = append(mFooter, "package-row-detail");
}
void PackageRow::set_package(std::string name, std::string status, std::string detail,
std::string stateClass, std::optional<float> progress) {
void PackageRow::set_package(std::string name, std::string version, std::string status,
std::string detail, std::string stateClass, std::optional<float> progress) {
mRoot->SetClassNames(fmt::format("package-row {}", stateClass));
set_text_content(mName, name);
set_text_content(mVersion, version);
mVersion->SetProperty("display", version.empty() ? "none" : "block");
set_text_content(mState, status);
set_text_content(mDetail, detail);
if (progress) {
@@ -80,9 +95,25 @@ void PackageRow::set_package(std::string name, std::string status, std::string d
}
}
void PackageRow::set_icon(std::string source) {
mIcon->SetClass("visible", true);
if (mIconSource == source) {
return;
}
mIconSource = std::move(source);
if (mIconSource.empty()) {
mIcon->RemoveProperty("decorator");
mIcon->SetClass("has-image", false);
return;
}
mIcon->SetProperty(
"decorator", fmt::format(R"(image("{}" cover center center))", escape(mIconSource)));
mIcon->SetClass("has-image", true);
}
Rml::Element* PackageRow::actions_root() {
if (mActions == nullptr) {
mActions = append(mRoot, "package-row-actions");
mActions = append(mFooter, "package-row-actions");
}
return mActions;
}
+6 -1
View File
@@ -16,16 +16,21 @@ class PackageRow : public Component {
public:
explicit PackageRow(Rml::Element* parent);
void set_package(std::string name, std::string status, std::string detail,
void set_package(std::string name, std::string version, std::string status, std::string detail,
std::string stateClass, std::optional<float> progress = {});
void set_icon(std::string source);
Rml::Element* actions_root();
private:
Rml::Element* mIcon = nullptr;
Rml::Element* mName = nullptr;
Rml::Element* mVersion = nullptr;
Rml::Element* mState = nullptr;
Rml::Element* mProgress = nullptr;
Rml::Element* mDetail = nullptr;
Rml::Element* mFooter = nullptr;
Rml::Element* mActions = nullptr;
std::string mIconSource;
};
} // namespace dusk::ui
+82 -19
View File
@@ -1,11 +1,14 @@
#include "queue_window.hpp"
#include "button.hpp"
#include "dusk/mod_loader.hpp"
#include "dusk/mods/queue.hpp"
#include "fmt/format.h"
#include "format.hpp"
#include "mod_texture_provider.hpp"
#include "package_row.hpp"
#include "pane.hpp"
#include "remote_texture_provider.hpp"
#include <algorithm>
#include <cstdint>
@@ -17,12 +20,27 @@
namespace dusk::ui {
namespace {
void set_icon_button(Button& button, const Rml::String& glyph, const Rml::String& label,
Rml::String& currentGlyph, Rml::String& currentLabel) {
if (currentGlyph != glyph) {
clear_children(button.root());
append_text(append(button.root(), "icon"), glyph);
currentGlyph = glyph;
}
if (currentLabel != label) {
button.root()->SetAttribute("aria-label", label);
button.root()->SetAttribute("title", label);
currentLabel = label;
}
}
class QueueRow final : public PackageRow {
public:
QueueRow(Rml::Element* parent, std::string id) : PackageRow{parent}, mId{std::move(id)} {
auto* actions = actions_root();
auto pause = std::make_unique<Button>(actions, "Pause");
auto pause = std::make_unique<Button>(actions, "");
mPause = pause.get();
mPause->root()->SetClass("package-row-icon-action", true);
mPause->on_pressed([this] {
const auto item = mods::queue::find(mId);
if (!item) {
@@ -33,6 +51,7 @@ public:
mods::queue::resume(mId);
break;
case mods::queue::State::Failed:
case mods::queue::State::InstallFailed:
mods::queue::retry(mId);
break;
default:
@@ -42,18 +61,29 @@ public:
});
mChildren.push_back(std::move(pause));
auto cancel = std::make_unique<Button>(actions, "Cancel");
auto cancel = std::make_unique<Button>(actions, "");
mCancel = cancel.get();
mCancel->root()->SetClass("package-row-icon-action", true);
mCancel->on_pressed([this] {
const auto item = mods::queue::find(mId);
if (!item) {
return;
}
mods::queue::cancel(mId);
if (mods::queue::is_terminal(item->state)) {
mods::queue::clear_finished();
if (item->state == mods::queue::State::InstallFailed) {
auto& loader = mods::ModLoader::instance();
if (const auto* mod = loader.find_mod(item->modId);
mod != nullptr && loader.can_uninstall(*mod))
{
loader.request_uninstall(item->modId);
}
}
mods::queue::clear(mId);
return;
}
mods::queue::cancel(mId);
});
set_icon_button(*mCancel, "\uE5CD", "Cancel", mCancelGlyph, mCancelLabel);
mChildren.push_back(std::move(cancel));
update();
}
@@ -79,30 +109,58 @@ public:
} else {
detail = format_bytes(item->total);
}
if (!item->message.empty()) {
if (!item->message.empty() && (item->state == mods::queue::State::Failed ||
item->state == mods::queue::State::InstallFailed))
{
detail = item->message;
} else if (!item->message.empty()) {
detail = fmt::format("{} · {}", detail, item->message);
}
const auto name =
item->version.empty() ? item->name : fmt::format("{} {}", item->name, item->version);
set_package(name, state_label(*item), detail, queue_state_class(item->state), progress);
auto status = state_label(*item);
if (item->state == mods::queue::State::Installed) {
status.clear();
detail = fmt::format("Installed · {}", format_bytes(item->total));
}
const std::optional<float> progressValue =
item->state == mods::queue::State::Installed ||
item->state == mods::queue::State::Canceled ?
std::nullopt :
std::optional{progress};
set_package(item->name, item->version, std::move(status), detail,
queue_state_class(item->state), progressValue);
std::string iconSource;
if (item->icon && !item->icon->url.empty()) {
iconSource =
remote_image_source(item->icon->url, item->icon->width, item->icon->height);
} else if (const auto* mod = mods::ModLoader::instance().find_mod(item->modId);
mod != nullptr && !mod->metadata.iconPath.empty())
{
iconSource = mod_image_source(*mod, mod->metadata.iconPath);
}
set_icon(std::move(iconSource));
const bool pauseVisible = (!item->local && item->state == mods::queue::State::Queued) ||
item->state == mods::queue::State::Downloading ||
item->state == mods::queue::State::Paused ||
item->state == mods::queue::State::Retrying ||
item->state == mods::queue::State::Failed;
mPause->root()->SetProperty("display", pauseVisible ? "block" : "none");
item->state == mods::queue::State::Failed ||
item->state == mods::queue::State::InstallFailed;
mPause->root()->SetProperty("display", pauseVisible ? "flex" : "none");
if (item->state == mods::queue::State::Paused) {
mPause->set_text("Resume");
} else if (item->state == mods::queue::State::Failed) {
mPause->set_text("Retry");
set_icon_button(*mPause, "\uE037", "Resume", mPauseGlyph, mPauseLabel);
} else if (item->state == mods::queue::State::Failed ||
item->state == mods::queue::State::InstallFailed)
{
set_icon_button(*mPause, "\uE5D5", "Retry", mPauseGlyph, mPauseLabel);
} else {
mPause->set_text("Pause");
set_icon_button(*mPause, "\uE034", "Pause", mPauseGlyph, mPauseLabel);
}
mCancel->root()->SetProperty(
"display", item->state == mods::queue::State::Handoff ? "none" : "block");
mCancel->set_text(mods::queue::is_terminal(item->state) ? "Clear" : "Cancel");
"display", item->state == mods::queue::State::Handoff ? "none" : "flex");
set_icon_button(*mCancel, "\uE5CD",
mods::queue::is_terminal(item->state) ? "Clear" : "Cancel", mCancelGlyph, mCancelLabel);
Component::update();
}
@@ -110,22 +168,27 @@ private:
std::string mId;
Button* mPause = nullptr;
Button* mCancel = nullptr;
Rml::String mPauseGlyph;
Rml::String mPauseLabel;
Rml::String mCancelGlyph;
Rml::String mCancelLabel;
};
} // namespace
QueueWindow::QueueWindow(std::string focusId)
: Modal{Props{
.title = "Installs",
.bodyText = "Preparing installs...",
.title = "Download queue",
.actions =
{
ModalAction{"Close", [](Modal& modal) { modal.pop(); }, {}},
ModalAction{"Pause all", [](Modal&) { mods::queue::pause_all(); },
[] { return !mods::queue::has_active_items(); }},
ModalAction{"Clear finished", [](Modal&) { mods::queue::clear_finished(); }, {}},
ModalAction{"Clear finished", [](Modal&) { mods::queue::clear_finished(); },
[] { return mods::queue::item_count() == mods::queue::active_count(); }},
},
.variant = "install-queue",
.icon = "download",
}},
mFocusId{std::move(focusId)} {
content_pane();