diff --git a/res/rml/mod_browser.rcss b/res/rml/mod_browser.rcss index 8964d7bfc1..5eea680dbf 100644 --- a/res/rml/mod_browser.rcss +++ b/res/rml/mod_browser.rcss @@ -55,7 +55,7 @@ catalog-filters select-button input { font-size: var(--font-size-sm); } -catalog-filters button.catalog-library-link { +.catalog-library-link { padding: var(--space-sm) 10dp; border-radius: var(--radius-panel); text-align: left; @@ -78,7 +78,7 @@ catalog-results header { padding-right: var(--space-2xl); } -catalog-results header catalog-results-summary { +catalog-results > header small { display: block; margin-top: var(--space-2xs); font-size: var(--font-size-xs); @@ -101,7 +101,7 @@ catalog-grid { padding: var(--space-2xs); } -button.catalog-card { +.catalog-card { display: flex; flex-flow: column; flex: 0 0 48%; @@ -134,20 +134,14 @@ catalog-card-art-shadow { catalog-card-body { display: flex; + position: relative; flex-flow: column; flex: 1 1 auto; min-height: 0; - padding: var(--space-xl) var(--space-lg) var(--space-md) var(--space-lg); + padding: var(--space-xl) var(--space-lg) var(--space-lg); gap: 7dp; } -catalog-card-kicker { - display: flex; - align-items: center; - gap: 6dp; - font-size: var(--font-size-sm); -} - catalog-official-badge { padding: var(--space-2xs) 5dp; border-radius: var(--radius-small); @@ -157,7 +151,8 @@ catalog-official-badge { font-size: 9dp; } -catalog-card-kicker > catalog-card-category { +catalog-card-body > section > b { + display: block; font-family: var(--font-family-heading); font-size: var(--font-size-sm); font-weight: bold; @@ -165,18 +160,19 @@ catalog-card-kicker > catalog-card-category { color: var(--color-accent); } -catalog-card-updated { - margin-left: auto; +catalog-card-body > small { + position: absolute; + top: var(--space-md); + right: var(--space-lg); + margin: 0; + padding: 0; + font-size: var(--font-size-sm); font-weight: normal; text-transform: none; opacity: 0.45; } -catalog-card-identity { - display: block; -} - -catalog-card-icon { +.catalog-card mod-icon { display: block; position: absolute; left: 14dp; @@ -190,14 +186,22 @@ catalog-card-icon { box-shadow: rgba(var(--color-black-rgb), 60%) 0 6dp 16dp; } -catalog-card-icon-image { +mod-icon-image { display: block; width: 100%; height: 100%; + border-radius: var(--radius-panel); } -catalog-card-title { +catalog-card-body > section { display: block; + margin: 0; + padding: 0; +} + +catalog-card-body > section h2 { + display: block; + margin: 0; font-family: var(--font-family-body); font-size: var(--font-size-3xl); font-weight: bold; @@ -205,22 +209,23 @@ catalog-card-title { line-height: 1.5; } -catalog-card-author { +catalog-card-body > section small { display: block; font-size: var(--font-size-3xs); opacity: 0.55; } -catalog-card-summary { +catalog-card-body > p { flex: 1 1 auto; min-height: 0; + margin: 0; overflow: hidden; font-size: var(--font-size-base); line-height: 1.35; color: rgba(var(--color-text-rgb), 68%); } -catalog-card-meta { +catalog-card-body > footer { display: flex; align-items: center; gap: 10dp; @@ -228,24 +233,24 @@ catalog-card-meta { opacity: 0.52; } -catalog-card-stat { +catalog-card-body > footer stat { display: flex; align-items: center; gap: 3dp; } -catalog-card-meta icon { +catalog-card-body > footer icon { flex: 0 0 13dp; font-size: var(--font-size-sm); line-height: 1; } -catalog-card-size { +catalog-card-body > footer .size { margin-left: auto; font-size: var(--font-size-sm); } -catalog-card-size.installed { +catalog-card-body > footer .size.installed { color: var(--color-success); } @@ -280,15 +285,19 @@ catalog-detail-status { text-align: center; } -catalog-status-title { +catalog-results-status h2, +catalog-detail-status h2 { display: block; + margin: 0; font-family: var(--font-family-heading); font-size: 25dp; font-weight: bold; } -catalog-status-message { +catalog-results-status p, +catalog-detail-status p { display: block; + margin: 0; opacity: 0.58; } @@ -364,7 +373,7 @@ catalog-source-actions button { line-height: 1; } -button.catalog-install-action { +.catalog-install-action { display: flex; position: relative; align-items: center; @@ -383,33 +392,33 @@ button.catalog-install-action { box-shadow: var(--color-accent) 0 0 0 2dp; } -button.catalog-install-action icon { +.catalog-install-action icon { flex: 0 0 22dp; font-size: 22dp; line-height: 1; } -button.catalog-install-action catalog-action-label { +.catalog-install-action > span { white-space: nowrap; } -button.catalog-install-action.idle { +.catalog-install-action.idle { --button-background: rgba(var(--color-interactive-rgb), 40%); --button-background-hover: rgba(var(--color-interactive-rgb), 55%); --button-background-selected: rgba(var(--color-interactive-rgb), 55%); --button-background-active: rgba(var(--color-interactive-rgb), 55%); } -button.catalog-install-action.paused { +.catalog-install-action.paused { box-shadow: rgba(var(--color-border-rgb), 25%) 0 0 0 1dp; } -button.catalog-install-action.paused:not(:disabled):hover, -button.catalog-install-action.paused:not(:disabled):focus-visible { +.catalog-install-action.paused:not(:disabled):hover, +.catalog-install-action.paused:not(:disabled):focus-visible { box-shadow: var(--color-accent) 0 0 0 2dp; } -button.catalog-install-action progress { +.catalog-install-action progress { position: absolute; left: 0; right: 0; @@ -422,25 +431,25 @@ button.catalog-install-action progress { background-color: rgba(var(--color-white-rgb), 10%); } -button.catalog-install-action progress fill { +.catalog-install-action progress fill { border-radius: 0; background-color: rgba(var(--color-accent-rgb), 80%); } -button.catalog-install-action.paused progress fill { +.catalog-install-action.paused progress fill { background-color: rgba(var(--color-text-rgb), 35%); } -button.catalog-install-action.retrying { +.catalog-install-action.retrying { color: var(--color-warning); box-shadow: rgba(var(--color-warning-rgb), 60%) 0 0 0 2dp; } -button.catalog-install-action.retrying progress fill { +.catalog-install-action.retrying progress fill { background-color: rgba(var(--color-warning-rgb), 60%); } -button.catalog-install-action.failed { +.catalog-install-action.failed { color: var(--color-white); --button-background: rgba(var(--color-error-rgb), 20%); --button-background-hover: rgba(var(--color-error-rgb), 35%); @@ -449,20 +458,20 @@ button.catalog-install-action.failed { box-shadow: var(--color-error) 0 0 0 2dp; } -button.catalog-install-action.failed progress fill { +.catalog-install-action.failed progress fill { background-color: rgba(var(--color-error-rgb), 70%); } -button.catalog-install-action.installed { +.catalog-install-action.installed { color: var(--color-success); box-shadow: rgba(var(--color-success-rgb), 50%) 0 0 0 2dp; } -button.catalog-install-action.installed:disabled { +.catalog-install-action.installed:disabled { opacity: 1; } -button.catalog-install-action.installing progress fill { +.catalog-install-action.installing progress fill { background-color: rgba(var(--color-info-rgb), 80%); } @@ -497,7 +506,7 @@ catalog-detail-identity { gap: var(--space-lg); } -catalog-detail-icon { +catalog-detail-identity mod-icon { display: block; flex: 0 0 70dp; width: 70dp; @@ -508,19 +517,13 @@ catalog-detail-icon { box-shadow: rgba(var(--color-black-rgb), 65%) 0 8dp 20dp; } -catalog-detail-icon-image { - display: block; - width: 100%; - height: 100%; -} - -catalog-detail-heading { +catalog-detail-identity > header { display: block; flex: 1 1 auto; min-width: 0; } -catalog-detail-category { +catalog-detail-identity > header > b { display: block; font-family: var(--font-family-heading); font-size: var(--font-size-sm); @@ -567,7 +570,7 @@ catalog-detail-stats { color: rgba(var(--color-text-rgb), 58%); } -catalog-detail-stat { +catalog-detail-stats > stat { display: flex; align-items: center; gap: 5dp; @@ -579,7 +582,7 @@ catalog-detail-stats icon { line-height: 1; } -catalog-stat-value { +catalog-detail-stats > stat > b { color: var(--color-text); font-weight: bold; } @@ -701,7 +704,7 @@ catalog-gallery { gap: var(--space-sm); } -button.catalog-screenshot { +.catalog-screenshot { flex: 1 1 0; height: 100%; min-width: 0; @@ -711,7 +714,7 @@ button.catalog-screenshot { font-size: var(--font-size-5xl); } -button.catalog-screenshot.primary { +.catalog-screenshot.primary { flex: 2 1 0; } @@ -825,7 +828,7 @@ catalog-screenshot-actions button { gap: 10dp; } - button.catalog-card { + .catalog-card { min-width: 220dp; height: 254dp; } @@ -839,23 +842,23 @@ catalog-screenshot-actions button { padding: 18dp 10dp var(--space-sm) 10dp; } - catalog-card-icon { + .catalog-card mod-icon { left: 10dp; bottom: -14dp; width: 40dp; height: 40dp; } - catalog-card-kicker, - catalog-card-kicker > catalog-card-category { + catalog-card-body > section > b, + catalog-card-body > small { font-size: var(--font-size-3xs); } - catalog-card-title { + catalog-card-body > section h2 { font-size: var(--font-size-md); } - catalog-card-summary { + catalog-card-body > p { font-size: var(--font-size-3xs); } @@ -865,7 +868,7 @@ catalog-screenshot-actions button { padding: var(--space-md) 18dp; } - catalog-detail-icon { + catalog-detail-identity mod-icon { flex-basis: 56dp; width: 56dp; height: 56dp; @@ -875,11 +878,11 @@ catalog-screenshot-actions button { gap: var(--space-md); } - catalog-detail-category { + catalog-detail-identity > header > b { font-size: var(--font-size-2xs); } - button.catalog-install-action { + .catalog-install-action { gap: var(--space-sm); min-width: 112dp; padding: 7dp 18dp; @@ -887,7 +890,7 @@ catalog-screenshot-actions button { font-size: var(--font-size-md); } - button.catalog-install-action icon { + .catalog-install-action icon { flex-basis: 18dp; font-size: var(--font-size-xl); } diff --git a/res/rml/mods.rcss b/res/rml/mods.rcss index c815dc95c7..58231d982d 100644 --- a/res/rml/mods.rcss +++ b/res/rml/mods.rcss @@ -15,33 +15,33 @@ window.mods content pane.mod-detail { gap: var(--space-md); } -mod-entry.mod-browser-entry, -mod-entry.mod-installs-entry { +mod-entry.browser, +mod-entry.installs { min-height: 76dp; } -mod-entry icon.mod-icon.mod-browser-icon { +mod-entry.browser mod-icon { color: var(--color-accent); decorator: text("" center center); } -mod-entry icon.mod-icon.mod-installs-icon { +mod-entry.installs mod-icon { color: var(--color-accent); decorator: text("" center center); } -mod-entry.mod-installs-entry { +mod-entry.installs { background-color: rgba(var(--color-control-rgb), 20%); box-shadow: rgba(var(--color-border-rgb), 25%) 0 0 0 1dp; } -mod-entry.mod-installs-entry:hover, -mod-entry.mod-installs-entry:focus-visible { +mod-entry.installs:hover, +mod-entry.installs:focus-visible { background-color: rgba(var(--color-interactive-rgb), 12%); box-shadow: var(--color-accent) 0 0 0 2dp; } -mod-entry.mod-installs-entry progress { +mod-entry.installs progress { height: 6dp; margin: var(--space-xs) 0 0 0; } @@ -60,14 +60,13 @@ mod-info-row { padding: var(--space-xs) 0; } -mod-info-label { +mod-info-row > b { font-family: var(--font-family-heading); - font-weight: bold; opacity: 0.55; flex: 0 0 auto; } -mod-info-value { +mod-info-row > span { flex: 1 1 0; } @@ -102,21 +101,28 @@ mod-entry:selected { decorator: vertical-gradient(rgba(var(--color-accent-rgb), 16) rgba(var(--color-accent-rgb), 64)); } -mod-entry .mod-icon { +mod-icon { + display: block; flex: 0 0 auto; width: 56dp; height: 56dp; border-radius: var(--radius-panel); -} - -mod-entry icon.mod-icon { + font-family: var(--font-family-icons); font-size: 36dp; background-color: rgba(var(--color-control-rgb), 20%); color: rgba(var(--color-text-rgb), 45%); decorator: text("" center center); + overflow: hidden; } -mod-entry mod-entry-info { +mod-icon img { + display: block; + width: 100%; + height: 100%; + border-radius: var(--radius-panel); +} + +mod-info { display: flex; flex-flow: column; flex: 1 1 0; @@ -124,36 +130,35 @@ mod-entry mod-entry-info { gap: var(--space-2xs); } -mod-entry mod-entry-name { +mod-info header { display: flex; flex-flow: row; align-items: baseline; gap: 6dp; } -mod-entry mod-entry-name-text { +mod-info header b { flex: 0 1 auto; min-width: 0; - font-weight: bold; white-space: nowrap; overflow: hidden; } -mod-entry mod-entry-version { +mod-info header small { flex: 0 0 auto; font-size: var(--font-size-xs); color: rgba(var(--color-text-rgb), 50%); } -mod-entry mod-entry-status.active { +mod-status.active { color: var(--color-success); } -mod-entry mod-entry-status.failed { +mod-status.failed { color: var(--color-error); } -mod-entry mod-entry-network { +mod-network { margin-left: 6dp; padding: 1dp 5dp; border-radius: 5dp; @@ -161,7 +166,8 @@ mod-entry mod-entry-network { color: var(--color-info); } -mod-entry mod-entry-desc { +mod-info > p { + margin: 0; font-size: var(--font-size-sm); line-height: 1.3; color: rgba(var(--color-text-rgb), 65%); @@ -170,16 +176,16 @@ mod-entry mod-entry-desc { white-space: pre-wrap; } -mod-entry mod-entry-sub { +mod-info > small { font-size: var(--font-size-xs); color: rgba(var(--color-text-rgb), 50%); } -mod-entry.inactive .mod-icon { +mod-entry.inactive mod-icon { filter: grayscale(1); } -mod-entry.inactive mod-entry-info { +mod-entry.inactive mod-info { opacity: 0.5; } @@ -193,7 +199,7 @@ mod-header.has-banner { margin: -24dp -24dp 0dp -24dp; } -mod-header mod-actions { +mod-actions { position: absolute; top: 24dp; left: 24dp; @@ -202,7 +208,7 @@ mod-header mod-actions { gap: var(--space-sm); } -mod-header mod-actions button { +mod-actions button { font-size: var(--font-size-md); padding: 6dp 14dp; background-color: rgba(var(--color-surface-rgb), 80%); @@ -219,31 +225,31 @@ mod-header.no-banner mod-actions { position: static; } -window.mods mod-title { +mod-title { display: block; font-size: var(--font-size-5xl); font-weight: bold; } -window.mods mod-title mod-title-version { +mod-title small { font-weight: normal; font-size: var(--font-size-md); color: rgba(var(--color-text-rgb), 55%); } -window.mods mod-author { +mod-author { display: block; font-size: var(--font-size-base); color: rgba(var(--color-text-rgb), 55%); } -window.mods mod-restart-note { +mod-restart-note { font-size: var(--font-size-base); color: var(--color-warning); opacity: 0.85; } -window.mods mod-description { +mod-description { line-height: 1.5; } @@ -253,13 +259,13 @@ status-badge { } status-badge.active, -mod-info-label.active { +mod-info-row > b.active { color: var(--color-success); opacity: 1; } status-badge.failed, -mod-info-label.failed { +mod-info-row > b.failed { color: var(--color-error); opacity: 1; } diff --git a/res/rml/overlay.rcss b/res/rml/overlay.rcss index fed6597c70..b89365c0dc 100644 --- a/res/rml/overlay.rcss +++ b/res/rml/overlay.rcss @@ -105,6 +105,50 @@ toast message row.muted { opacity: 0.5; } +toast.mod-installed row { + align-items: center; + gap: var(--space-md); +} + +mod-icon { + flex: 0 0 42dp; + width: 42dp; + height: 42dp; + 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); +} + +mod-icon img { + width: 100%; + height: 100%; + border-radius: var(--radius-panel); +} + +mod-info { + display: flex; + flex-flow: column; + min-width: 0; + gap: var(--space-xs); +} + +mod-name { + color: var(--color-white); +} + +toast.mod-installed small { + font-size: var(--font-size-sm); + color: rgba(var(--color-text-rgb), 55%); +} + +toast.mod-installed small.version { + margin-left: var(--space-sm); +} + progress { height: 4dp; position: absolute; @@ -239,6 +283,13 @@ icon.warning { decorator: text("" center center); } +icon.download-done { + width: 1.2em; + height: 1.2em; + font-size: 1.2em; + decorator: text("" center center); +} + fps { display: none; z-index: 99; diff --git a/res/rml/window.rcss b/res/rml/window.rcss index 9c5ae28d9d..47ce3f1010 100644 --- a/res/rml/window.rcss +++ b/res/rml/window.rcss @@ -668,7 +668,11 @@ package-row { border-bottom-color: rgba(var(--color-border-rgb), 30%); } -package-row-icon { +package-row:last-child { + border-bottom-width: 0; +} + +package-row > mod-icon { display: none; flex: 0 0 36dp; width: 36dp; @@ -683,46 +687,51 @@ package-row-icon { decorator: text("" center center); } -package-row-icon.visible { +package-row > mod-icon.visible { display: block; } -package-row-icon.has-image { +package-row > mod-icon.has-image { background-color: transparent; } -package-row.paused package-row-icon, -package-row.retrying package-row-icon, -package-row.failed package-row-icon { +package-row.paused > mod-icon, +package-row.retrying > mod-icon, +package-row.failed > mod-icon { filter: grayscale(1); opacity: 0.55; } -package-row-info { +package-row > section { display: flex; flex-direction: column; flex: 1 1 0; min-width: 0; + margin: 0; + padding: 0; } -package-row-heading { +package-row > section > header { display: flex; width: 100%; gap: var(--space-sm); align-items: center; - padding-bottom: 6dp; + margin: 0; + padding: 0 0 6dp 0; } -package-row-identity { +package-row h3 { display: flex; align-items: baseline; flex: 1 1 auto; min-width: 0; gap: var(--space-xs); + margin: 0; + padding: 0; overflow: hidden; } -package-row-name { +package-row h3 > span { display: block; flex: 0 1 auto; min-width: 0; @@ -733,7 +742,7 @@ package-row-name { white-space: nowrap; } -package-row-version { +package-row h3 > small { display: block; flex: 0 0 auto; font-size: var(--font-size-xs); @@ -741,28 +750,28 @@ package-row-version { color: rgba(var(--color-text-rgb), 50%); } -package-row-state { +package-row header > small { 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 { +package-row.downloading header > small { color: var(--color-accent); } -package-row.retrying package-row-state { +package-row.retrying header > small { color: var(--color-warning); } -package-row.failed package-row-state, -package-row.failed package-row-detail { +package-row.failed header > small, +package-row.failed footer > small { color: var(--color-error); } -package-row.installed package-row-state, -package-row.installed package-row-detail { +package-row.installed header > small, +package-row.installed footer > small { color: var(--color-success); } @@ -789,15 +798,17 @@ package-row.installed progress fill { background-color: var(--color-success); } -package-row-footer { +package-row footer { display: flex; align-items: flex-start; width: 100%; min-width: 0; gap: var(--space-sm); + margin: 0; + padding: 0; } -package-row-detail { +package-row footer > small { display: block; flex: 1 1 0; min-width: 0; @@ -809,13 +820,15 @@ package-row-detail { white-space: nowrap; } -package-row-actions { +package-row nav { display: flex; flex: 0 0 auto; gap: 6dp; + margin: 0; + padding: 0; } -package-row-actions button.package-row-icon-action { +package-row nav > button.icon-action { align-items: center; justify-content: center; flex: 0 0 30dp; @@ -827,7 +840,7 @@ package-row-actions button.package-row-icon-action { font-size: var(--font-size-md); } -package-row-actions button.package-row-icon-action icon { +package-row nav > button.icon-action icon { flex: 0 0 var(--font-size-md); width: var(--font-size-md); height: var(--font-size-md); @@ -839,25 +852,25 @@ package-row.installed { align-items: center; } -package-row.installed package-row-icon { +package-row.installed > mod-icon { margin-top: 0; } -package-row.installed package-row-info { +package-row.installed > section { padding-right: 38dp; } -package-row.installed package-row-state { +package-row.installed header > small { display: none; } -package-row.installed package-row-actions { +package-row.installed nav { position: absolute; top: 17dp; right: 0; } -package-row.installed package-row-actions button.package-row-icon-action { +package-row.installed nav > button.icon-action { opacity: 0.45; } diff --git a/src/dusk/mod_loader.hpp b/src/dusk/mod_loader.hpp index f8c97521d6..352e189279 100644 --- a/src/dusk/mod_loader.hpp +++ b/src/dusk/mod_loader.hpp @@ -354,7 +354,7 @@ private: // bundle from disk, then reactivates whatever the current cvar/provider state allows. void apply_lifecycle_change(LoadedMod& target, bool reload); // `target` plus transitive active/suspended dependents, in m_mods (init) order. - std::vector collect_lifecycle_set(LoadedMod& target); + std::vector collect_lifecycle_set(LoadedMod& target) const; void resume_lifecycle_set(const std::vector& mods); bool reload_bundle(LoadedMod& mod); bool ensure_native_loaded(LoadedMod& mod); diff --git a/src/dusk/mods/loader/loader.cpp b/src/dusk/mods/loader/loader.cpp index 5af8d4b874..8bd1a07ad4 100644 --- a/src/dusk/mods/loader/loader.cpp +++ b/src/dusk/mods/loader/loader.cpp @@ -26,6 +26,7 @@ #include "dusk/mods/svc/config.hpp" #include "dusk/mods/svc/hook.hpp" #include "dusk/mods/svc/registry.hpp" +#include "dusk/ui/mod_texture_provider.hpp" #include "dusk/ui/mods_window.hpp" #include "dusk/ui/ui.hpp" #include "miniz.h" @@ -1271,7 +1272,7 @@ void ModLoader::flush_toasts() { ui::push_toast(std::move(toast)); } -std::vector ModLoader::collect_lifecycle_set(LoadedMod& target) { +std::vector ModLoader::collect_lifecycle_set(LoadedMod& target) const { std::vector included{&target}; std::vector pending{&target}; while (!pending.empty()) { @@ -1730,10 +1731,19 @@ void ModLoader::apply_pending_requests() { if (const auto* install = std::get_if(&request)) { auto result = install_staged(install->stagedPath); if (result.success && result.mod != nullptr) { + const auto& metadata = result.mod->metadata; + const std::string iconRml = + metadata.iconPath.empty() ? + std::string{} : + fmt::format(R"()", + ui::escape(ui::mod_image_source(*result.mod, metadata.iconPath))); ui::push_toast({ + .type = "mod-installed", .title = "Mod installed", .content = fmt::format( - "{} {}", result.mod->metadata.name, result.mod->metadata.version), + R"({}{}v{}{})", + iconRml, ui::escape(metadata.name), ui::escape(metadata.version), + ui::escape(metadata.author)), .duration = std::chrono::seconds{4}, }); } else if (!result.success && result.mod == nullptr) { diff --git a/src/dusk/ui/format.hpp b/src/dusk/ui/format.hpp index 28aa5efe3e..03a8dadf90 100644 --- a/src/dusk/ui/format.hpp +++ b/src/dusk/ui/format.hpp @@ -2,6 +2,8 @@ #include +#include +#include #include #include #include @@ -14,6 +16,68 @@ struct ByteFormat { int mibFractionDigits = 1; }; +inline std::string format_count(uint64_t value) { + if (value >= 1'000'000) { + return fmt::format("{:.1f}m", static_cast(value) / 1'000'000.0); + } + if (value >= 1'000) { + return fmt::format("{:.1f}k", static_cast(value) / 1'000.0); + } + return fmt::format("{}", value); +} + +inline std::string display_date(std::string_view timestamp) { + return std::string{timestamp.substr(0, 10)}; +} + +inline std::string relative_date(std::string_view timestamp) { + if (timestamp.size() < 10 || timestamp[4] != '-' || timestamp[7] != '-') { + return display_date(timestamp); + } + int yearValue = 0; + unsigned monthValue = 0; + unsigned dayValue = 0; + const auto parse = [](std::string_view text, auto& value) { + const auto [end, error] = std::from_chars(text.data(), text.data() + text.size(), value); + return error == std::errc{} && end == text.data() + text.size(); + }; + if (!parse(timestamp.substr(0, 4), yearValue) || !parse(timestamp.substr(5, 2), monthValue) || + !parse(timestamp.substr(8, 2), dayValue)) + { + return display_date(timestamp); + } + + const std::chrono::year_month_day date{ + std::chrono::year{yearValue}, std::chrono::month{monthValue}, std::chrono::day{dayValue}}; + if (!date.ok()) { + return display_date(timestamp); + } + const auto today = std::chrono::floor(std::chrono::system_clock::now()); + const auto age = (today - std::chrono::sys_days{date}).count(); + if (age < 0) { + return display_date(timestamp); + } + if (age == 0) { + return "today"; + } + if (age == 1) { + return "yesterday"; + } + if (age < 7) { + return fmt::format("{} days ago", age); + } + if (age < 30) { + const auto weekCount = age / 7; + return fmt::format("{} week{} ago", weekCount, weekCount == 1 ? "" : "s"); + } + if (age < 365) { + const auto monthCount = age / 30; + return fmt::format("{} month{} ago", monthCount, monthCount == 1 ? "" : "s"); + } + const auto yearCount = age / 365; + return fmt::format("{} year{} ago", yearCount, yearCount == 1 ? "" : "s"); +} + inline std::string format_bytes(uint64_t bytes, ByteFormat options = {}) { constexpr double kiB = 1024.0; constexpr double miB = kiB * 1024.0; diff --git a/src/dusk/ui/mod_browser.cpp b/src/dusk/ui/mod_browser.cpp index db06a5b3a2..8143885d8b 100644 --- a/src/dusk/ui/mod_browser.cpp +++ b/src/dusk/ui/mod_browser.cpp @@ -18,7 +18,6 @@ #include #include -#include #include #include #include @@ -47,65 +46,6 @@ std::string_view sort_label(mods::catalog::Sort sort) noexcept { return iter != sortOptions.end() ? iter->label : sortOptions.front().label; } -std::string format_count(uint64_t value) { - if (value >= 1'000'000) { - return fmt::format("{:.1f}m", static_cast(value) / 1'000'000.0); - } - if (value >= 1'000) { - return fmt::format("{:.1f}k", static_cast(value) / 1'000.0); - } - return fmt::format("{}", value); -} - -std::string display_date(std::string_view timestamp) { - return std::string{timestamp.substr(0, std::min(timestamp.size(), 10))}; -} - -std::string relative_date(std::string_view timestamp) { - if (timestamp.size() < 10) { - return display_date(timestamp); - } - int yearValue = 0; - unsigned monthValue = 0; - unsigned dayValue = 0; - const auto parse = [](std::string_view text, auto& value) { - const auto [end, error] = std::from_chars(text.data(), text.data() + text.size(), value); - return error == std::errc{} && end == text.data() + text.size(); - }; - if (!parse(timestamp.substr(0, 4), yearValue) || !parse(timestamp.substr(5, 2), monthValue) || - !parse(timestamp.substr(8, 2), dayValue)) - { - return display_date(timestamp); - } - - const std::chrono::year_month_day date{ - std::chrono::year{yearValue}, std::chrono::month{monthValue}, std::chrono::day{dayValue}}; - if (!date.ok()) { - return display_date(timestamp); - } - const auto today = std::chrono::floor(std::chrono::system_clock::now()); - const auto age = (today - std::chrono::sys_days{date}).count(); - if (age < 0) { - return display_date(timestamp); - } - if (age == 0) { - return "today"; - } - if (age == 1) { - return "yesterday"; - } - if (age < 7) { - return fmt::format("{} days ago", age); - } - if (age < 30) { - return fmt::format("{} weeks ago", age / 7); - } - if (age < 365) { - return fmt::format("{} months ago", age / 30); - } - return fmt::format("{} years ago", age / 365); -} - void add_list_markers(Rml::Element* fragment) { Rml::ElementList lists; fragment->QuerySelectorAll(lists, "ul, ol"); @@ -179,12 +119,8 @@ std::string_view activation_failure(const mods::LoadedMod& mod) { return mod.suspendedByProvider ? "A required provider is unavailable" : "Activation failed"; } -bool safe_web_url(std::string_view url) { - return url.starts_with("https://") || url.starts_with("http://"); -} - void open_web_url(const std::string& url) { - if (safe_web_url(url)) { + if (url.starts_with("https://")) { SDL_OpenURL(url.c_str()); } } @@ -192,21 +128,21 @@ void open_web_url(const std::string& url) { void set_icon_button_content(Button& button, const Rml::String& glyph, const Rml::String& label) { clear_children(button.root()); append_text(append(button.root(), "icon"), glyph); - append_text_element(button.root(), "catalog-action-label", label); + append_text_element(button.root(), "span", label); } void append_status(Rml::Element* parent, const Rml::String& title, const Rml::String& message) { - append_text_element(parent, "catalog-status-title", title); - append_text_element(parent, "catalog-status-message", message); + append_text_element(parent, "h2", title); + append_text_element(parent, "p", message); } void append_stat(Rml::Element* parent, const Rml::String& glyph, const Rml::String& value, const Rml::String& suffix) { - auto* stat = append(parent, "catalog-detail-stat"); + auto* stat = append(parent, "stat"); if (!glyph.empty()) { append_text(append(stat, "icon"), glyph); } - append_text_element(stat, "catalog-stat-value", value); + append_text_element(stat, "b", value); append_text(stat, suffix); } @@ -228,27 +164,25 @@ public: auto* art = append(mRoot, "catalog-card-art"); append(art, "catalog-card-art-shadow"); - auto* icon = append(art, "catalog-card-icon"); - auto* iconImage = append(icon, "catalog-card-icon-image"); + auto* icon = append(art, "mod-icon"); + auto* iconImage = append(icon, "mod-icon-image"); auto* body = append(mRoot, "catalog-card-body"); - auto* kicker = append(body, "catalog-card-kicker"); - append_text_element(kicker, "catalog-card-category", category); - append_text_element(kicker, "catalog-card-updated", relative_date(mod.updatedAt)); - - auto* identity = append(body, "catalog-card-identity"); - append_text_element(identity, "catalog-card-title", mod.name); - append_text_element(identity, "catalog-card-author", fmt::format("by {}", mod.author.name)); - - append_text_element(body, "catalog-card-summary", snippet(mod.summary, 126)); - auto* meta = append(body, "catalog-card-meta"); - auto* downloads = append(meta, "catalog-card-stat"); + append_text_element(body, "small", relative_date(mod.updatedAt)); + auto* identity = append(body, "section"); + append_text_element(identity, "b", category); + append_text_element(identity, "h2", mod.name); + append_text_element(identity, "small", fmt::format("by {}", mod.author.name)); + append_text_element(body, "p", snippet(mod.summary, 126)); + auto* meta = append(body, "footer"); + auto* downloads = append(meta, "stat"); append_text(append(downloads, "icon"), "\uF090"); append_text(downloads, format_count(mod.downloads)); - auto* endorsements = append(meta, "catalog-card-stat"); + auto* endorsements = append(meta, "stat"); append_text(append(endorsements, "icon"), "\uE87D"); append_text(endorsements, format_count(mod.endorsements)); - auto* size = append_text_element(meta, "catalog-card-size", installedLabel); + auto* size = append_text_element(meta, "small", installedLabel); + size->SetClass("size", true); if (isInstalled) { size->SetClass("installed", true); } @@ -566,7 +500,7 @@ public: if (mLabel != label || mGlyph != glyph) { ui::clear_children(mRoot); append_text(append(mRoot, "icon"), glyph); - append_text_element(mRoot, "catalog-action-label", label); + append_text_element(mRoot, "span", label); mProgress = append(mRoot, "progress"); mLabel = std::move(label); mGlyph = std::move(glyph); @@ -664,11 +598,11 @@ DetailContent::DetailContent( open.on_pressed([url = detail.siteUrl] { open_web_url(url); }); auto* identity = append(hero, "catalog-detail-identity"); - auto* detailIcon = append(identity, "catalog-detail-icon"); - auto* detailIconImage = append(detailIcon, "catalog-detail-icon-image"); - auto* detailHeading = append(identity, "catalog-detail-heading"); - append_text_element(detailHeading, "catalog-detail-category", - detail.mod.category ? detail.mod.category->name : "Uncategorized"); + auto* detailIcon = append(identity, "mod-icon"); + auto* detailIconImage = append(detailIcon, "mod-icon-image"); + auto* detailHeading = append(identity, "header"); + append_text_element( + detailHeading, "b", detail.mod.category ? detail.mod.category->name : "Uncategorized"); auto* title = append(detailHeading, "h1"); append_text(title, detail.mod.name); append_text_element(title, "small", fmt::format("v{}", detail.mod.version)); @@ -803,7 +737,7 @@ DetailContent::DetailContent( append_detail_field(detailList, "License", detail.license.value_or("Not specified")); append_detail_field( detailList, "Mod ABI", detail.modAbi ? fmt::format("{}", *detail.modAbi) : "Assets only"); - if (detail.sourceUrl && safe_web_url(*detail.sourceUrl)) { + if (detail.sourceUrl && detail.sourceUrl->starts_with("https://")) { auto* sourceActions = append(sidebar, "catalog-source-actions"); auto& sourceGroup = add_existing_item(sourceActions, Props{ @@ -908,8 +842,8 @@ void ModBrowser::build_content(Rml::Element* content) { }(); const uint64_t total = mPage ? mPage->pagination.total : 0; append_text_element(heading, "h1", categoryName); - append_text_element(heading, "catalog-results-summary", - fmt::format("{} mods · sorted by {}", total, sort_label(mQuery.sort))); + append_text_element( + heading, "small", fmt::format("{} mods · sorted by {}", total, sort_label(mQuery.sort))); Component* resultFocus = nullptr; Component* retryFocus = nullptr; diff --git a/src/dusk/ui/mods_window.cpp b/src/dusk/ui/mods_window.cpp index 948cde76ed..812f5d5f9f 100644 --- a/src/dusk/ui/mods_window.cpp +++ b/src/dusk/ui/mods_window.cpp @@ -59,32 +59,28 @@ class ModListEntry : public FluentComponent { public: ModListEntry(Rml::Element* parent, const mods::LoadedMod& mod) : FluentComponent{append(parent, "mod-entry")} { - Rml::Element* icon = nullptr; + auto* icon = append(mRoot, "mod-icon"); if (!mod.metadata.iconPath.empty()) { - icon = append(mRoot, "img"); - icon->SetAttribute("src", mod_image_source(mod, mod.metadata.iconPath)); - } else { - icon = append(mRoot, "icon"); - icon->SetClass("placeholder", true); + auto* image = append(icon, "img"); + image->SetAttribute("src", mod_image_source(mod, mod.metadata.iconPath)); } - icon->SetClass("mod-icon", true); const auto status = mod_status(mod); - auto* info = append(mRoot, "mod-entry-info"); - auto* name = append(info, "mod-entry-name"); - append_text(append(name, "mod-entry-name-text"), mod.metadata.name); - append_text(append(name, "mod-entry-version"), fmt::format("v{}", mod.metadata.version)); - auto* sub = append(info, "mod-entry-sub"); + auto* info = append(mRoot, "mod-info"); + auto* heading = append(info, "header"); + append_text(append(heading, "b"), mod.metadata.name); + append_text(append(heading, "small"), fmt::format("v{}", mod.metadata.version)); + auto* sub = append(info, "small"); append_text(sub, fmt::format("{} - ", mod.metadata.author)); - auto* statusElement = append(sub, "mod-entry-status"); + auto* statusElement = append(sub, "mod-status"); if (status.badgeClass[0] != '\0') { statusElement->SetClass(status.badgeClass, true); } append_text(statusElement, status.text); if (mod_uses_network(mod)) { - append_text(append(sub, "mod-entry-network"), "Network"); + append_text(append(sub, "mod-network"), "Network"); } - append_text(append(info, "mod-entry-desc"), snippet(mod.metadata.description, 90)); + append_text(append(info, "p"), snippet(mod.metadata.description, 90)); mRoot->SetClass("inactive", !mod.active); mRoot->SetClass("failed", mod.loadFailed); @@ -102,16 +98,13 @@ class BrowseModsEntry final : public FluentComponent { public: BrowseModsEntry(Rml::Element* parent, std::function onOpen) : FluentComponent{append(parent, "mod-entry")} { - mRoot->SetClass("mod-browser-entry", true); - auto* icon = append(mRoot, "icon"); - icon->SetClass("mod-icon", true); - icon->SetClass("mod-browser-icon", true); + mRoot->SetClass("browser", true); + append(mRoot, "mod-icon"); - 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-desc"), "Discover and install mods from the community."); + auto* info = append(mRoot, "mod-info"); + auto* heading = append(info, "header"); + append_text(append(heading, "b"), "Browse online mods"); + append_text(append(info, "p"), "Discover and install mods from the community."); on_nav_command([callback = std::move(onOpen)](Rml::Event&, NavCommand cmd) { if (cmd != NavCommand::Confirm) { @@ -127,15 +120,13 @@ class InstallQueueEntry final : public FluentComponent { public: InstallQueueEntry(Rml::Element* parent, std::function onOpen) : FluentComponent{append(parent, "mod-entry")} { - mRoot->SetClass("mod-installs-entry", true); - auto* icon = append(mRoot, "icon"); - icon->SetClass("mod-icon", true); - icon->SetClass("mod-installs-icon", true); + mRoot->SetClass("installs", true); + append(mRoot, "mod-icon"); - auto* info = append(mRoot, "mod-entry-info"); - auto* name = append(info, "mod-entry-name"); - append_text(append(name, "mod-entry-name-text"), "Installs"); - mSummary = append(info, "mod-entry-sub"); + auto* info = append(mRoot, "mod-info"); + auto* heading = append(info, "header"); + append_text(append(heading, "b"), "Installs"); + mSummary = append(info, "small"); mProgress = append(info, "progress"); on_nav_command([callback = std::move(onOpen)](Rml::Event&, NavCommand cmd) { @@ -183,7 +174,7 @@ public: const bool hasBanner = !mod.metadata.bannerPath.empty(); mRoot->SetClass(hasBanner ? "has-banner" : "no-banner", true); if (hasBanner) { - mRoot->SetProperty("decorator", fmt::format(R"(image("{}" cover center top))", + mRoot->SetProperty("decorator", fmt::format(R"(image("{}" cover center center))", mod_image_source(mod, mod.metadata.bannerPath))); } @@ -358,7 +349,7 @@ void ModsWindow::build_detail(Pane& pane, mods::LoadedMod& mod) { auto* title = append(pane.root(), "mod-title"); append_text(title, fmt::format("{} ", mod.metadata.name)); - append_text(append(title, "mod-title-version"), fmt::format("v{}", mod.metadata.version)); + append_text(append(title, "small"), fmt::format("v{}", mod.metadata.version)); if (mod.loadFailed || mod.suspendedByProvider) { const auto status = mod_status(mod); append_text(title, "\u00a0"); @@ -376,10 +367,10 @@ void ModsWindow::build_detail(Pane& pane, mods::LoadedMod& mod) { if (mod.loadFailed && !mod.failureReason.empty()) { auto* row = append(pane.root(), "mod-info-row"); - auto* label = append(row, "mod-info-label"); + auto* label = append(row, "b"); label->SetClass("failed", true); append_text(label, "Reason"); - append_text(append(row, "mod-info-value"), mod.failureReason); + append_text(append(row, "span"), mod.failureReason); } else if (mod.suspendedByProvider) { std::vector providers; for (const auto& edge : mod.dependencies) { @@ -388,8 +379,8 @@ void ModsWindow::build_detail(Pane& pane, mods::LoadedMod& mod) { } } auto* row = append(pane.root(), "mod-info-row"); - append_text(append(row, "mod-info-label"), "Waiting on"); - append_text(append(row, "mod-info-value"), fmt::format("{}", fmt::join(providers, ", "))); + append_text(append(row, "b"), "Waiting on"); + append_text(append(row, "span"), fmt::format("{}", fmt::join(providers, ", "))); } std::vector activeDependents; diff --git a/src/dusk/ui/overlay.cpp b/src/dusk/ui/overlay.cpp index 7c85121542..b11fe8b523 100644 --- a/src/dusk/ui/overlay.cpp +++ b/src/dusk/ui/overlay.cpp @@ -95,6 +95,9 @@ Rml::Element* create_toast(Rml::Element* parent, const Toast& toast) { } else if (toast.type == "warning") { auto* icon = append(heading, "icon"); icon->SetClass("warning", true); + } else if (toast.type == "mod-installed") { + auto* icon = append(heading, "icon"); + icon->SetClass("download-done", true); } } { diff --git a/src/dusk/ui/package_row.cpp b/src/dusk/ui/package_row.cpp index e4f9539cf7..e3849c9e5d 100644 --- a/src/dusk/ui/package_row.cpp +++ b/src/dusk/ui/package_row.cpp @@ -67,23 +67,23 @@ std::string state_label(const mods::queue::Item& item) { } PackageRow::PackageRow(Rml::Element* parent) : Component{create_row(parent)} { - 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"); + mIcon = append(mRoot, "mod-icon"); + auto* info = append(mRoot, "section"); + auto* heading = append(info, "header"); + auto* identity = append(heading, "h3"); + mName = append(identity, "span"); + mVersion = append(identity, "small"); + mState = append(heading, "small"); mProgress = append(info, "progress"); - mFooter = append(info, "package-row-footer"); - mDetail = append(mFooter, "package-row-detail"); + mFooter = append(info, "footer"); + mDetail = append(mFooter, "small"); } void PackageRow::set_package(std::string name, std::string version, std::string status, std::string detail, std::string stateClass, std::optional progress) { - mRoot->SetClassNames(fmt::format("package-row {}", stateClass)); + mRoot->SetClassNames(stateClass); set_text_content(mName, name); - set_text_content(mVersion, version); + set_text_content(mVersion, fmt::format("v{}", version)); mVersion->SetProperty("display", version.empty() ? "none" : "block"); set_text_content(mState, status); set_text_content(mDetail, detail); @@ -113,7 +113,7 @@ void PackageRow::set_icon(std::string source) { Rml::Element* PackageRow::actions_root() { if (mActions == nullptr) { - mActions = append(mFooter, "package-row-actions"); + mActions = append(mFooter, "nav"); } return mActions; } diff --git a/src/dusk/ui/queue_window.cpp b/src/dusk/ui/queue_window.cpp index 2cf95a3480..28ac34de61 100644 --- a/src/dusk/ui/queue_window.cpp +++ b/src/dusk/ui/queue_window.cpp @@ -40,7 +40,7 @@ public: auto* actions = actions_root(); auto pause = std::make_unique