Toast improvement & UI cleanup

This commit is contained in:
Luke Street
2026-09-02 11:23:30 -06:00
parent 4df3607047
commit 0094f54c82
12 changed files with 363 additions and 284 deletions
+73 -70
View File
@@ -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);
}
+42 -36
View File
@@ -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;
}
+51
View File
@@ -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;
+42 -29
View File
@@ -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;
}
+1 -1
View File
@@ -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<LoadedMod*> collect_lifecycle_set(LoadedMod& target);
std::vector<LoadedMod*> collect_lifecycle_set(LoadedMod& target) const;
void resume_lifecycle_set(const std::vector<LoadedMod*>& mods);
bool reload_bundle(LoadedMod& mod);
bool ensure_native_loaded(LoadedMod& mod);
+12 -2
View File
@@ -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<LoadedMod*> ModLoader::collect_lifecycle_set(LoadedMod& target) {
std::vector<LoadedMod*> 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<InstallRequest>(&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"(<img src="{}" />)",
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"(<row><mod-icon>{}</mod-icon><mod-info><mod-name><b>{}</b><small class="version">v{}</small></mod-name><small>{}</small></mod-info></row>)",
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) {
+64
View File
@@ -2,6 +2,8 @@
#include <fmt/format.h>
#include <charconv>
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <string>
@@ -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<double>(value) / 1'000'000.0);
}
if (value >= 1'000) {
return fmt::format("{:.1f}k", static_cast<double>(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::days>(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;
+28 -94
View File
@@ -18,7 +18,6 @@
#include <algorithm>
#include <array>
#include <charconv>
#include <chrono>
#include <cstddef>
#include <exception>
@@ -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<double>(value) / 1'000'000.0);
}
if (value >= 1'000) {
return fmt::format("{:.1f}k", static_cast<double>(value) / 1'000.0);
}
return fmt::format("{}", value);
}
std::string display_date(std::string_view timestamp) {
return std::string{timestamp.substr(0, std::min<size_t>(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::days>(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<NavGroup>(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;
+29 -38
View File
@@ -59,32 +59,28 @@ class ModListEntry : public FluentComponent<ModListEntry> {
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<BrowseModsEntry> {
public:
BrowseModsEntry(Rml::Element* parent, std::function<void()> 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<InstallQueueEntry> {
public:
InstallQueueEntry(Rml::Element* parent, std::function<void()> 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<std::string_view> 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<std::string_view> activeDependents;
+3
View File
@@ -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);
}
}
{
+12 -12
View File
@@ -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<float> 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;
}
+6 -2
View File
@@ -40,7 +40,7 @@ public:
auto* actions = actions_root();
auto pause = std::make_unique<Button>(actions, "");
mPause = pause.get();
mPause->root()->SetClass("package-row-icon-action", true);
mPause->root()->SetClass("icon-action", true);
mPause->on_pressed([this] {
const auto item = mods::queue::find(mId);
if (!item) {
@@ -63,7 +63,7 @@ public:
auto cancel = std::make_unique<Button>(actions, "");
mCancel = cancel.get();
mCancel->root()->SetClass("package-row-icon-action", true);
mCancel->root()->SetClass("icon-action", true);
mCancel->on_pressed([this] {
const auto item = mods::queue::find(mId);
if (!item) {
@@ -196,6 +196,10 @@ QueueWindow::QueueWindow(std::string focusId)
}
void QueueWindow::update() {
if (!mItemIds.empty() && mods::queue::item_count() == 0) {
pop();
return;
}
refresh_queue();
Modal::update();
}