mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-09-11 11:15:12 -04:00
Improve queue design & restore failure handling
This commit is contained in:
+149
-21
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
@@ -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();
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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>(
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user