From 548b98072d65f83ec7a41b8828866a6cb09946c1 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Mon, 31 Aug 2026 20:40:30 -0600 Subject: [PATCH] Initial mod browser + RmlUi CSS variables --- CMakeLists.txt | 2 +- files.cmake | 8 + res/rml/logs.rcss | 74 +- res/rml/mod_browser.rcss | 801 +++++++++++++++++++++ res/rml/mods.rcss | 140 ++-- res/rml/overlay.rcss | 84 ++- res/rml/popover.rcss | 66 +- res/rml/popup.rcss | 17 +- res/rml/prelaunch.rcss | 175 +++-- res/rml/tabbing.rcss | 31 +- res/rml/theme.rcss | 72 ++ res/rml/touch_controls.rcss | 86 +-- res/rml/touch_controls_editor.rcss | 42 +- res/rml/tuner.rcss | 65 +- res/rml/window.rcss | 294 ++++---- src/dusk/mods/catalog.cpp | 456 ++++++++++++ src/dusk/mods/catalog.hpp | 131 ++++ src/dusk/mods/svc/ui.cpp | 23 +- src/dusk/ui/achievements.cpp | 73 +- src/dusk/ui/button.cpp | 2 +- src/dusk/ui/color_input.cpp | 4 +- src/dusk/ui/graphics_tuner.cpp | 43 +- src/dusk/ui/logs_window.cpp | 38 +- src/dusk/ui/menu_bar.cpp | 19 +- src/dusk/ui/mod_browser.cpp | 906 ++++++++++++++++++++++++ src/dusk/ui/mod_browser.hpp | 49 ++ src/dusk/ui/mod_texture_provider.cpp | 75 +- src/dusk/ui/modal.cpp | 37 +- src/dusk/ui/modal.hpp | 4 + src/dusk/ui/mods_window.cpp | 142 ++-- src/dusk/ui/mods_window.hpp | 3 + src/dusk/ui/overlay.cpp | 56 +- src/dusk/ui/pane.cpp | 3 +- src/dusk/ui/popover.cpp | 1 + src/dusk/ui/prelaunch.cpp | 100 ++- src/dusk/ui/preset.cpp | 48 +- src/dusk/ui/remote_texture_provider.cpp | 243 +++++++ src/dusk/ui/remote_texture_provider.hpp | 14 + src/dusk/ui/reporting.cpp | 54 +- src/dusk/ui/runtime_image.cpp | 73 ++ src/dusk/ui/runtime_image.hpp | 19 + src/dusk/ui/select_button.cpp | 19 +- src/dusk/ui/settings.cpp | 80 ++- src/dusk/ui/touch_controls.cpp | 3 +- src/dusk/ui/touch_controls_editor.cpp | 4 +- src/dusk/ui/ui.cpp | 29 +- src/dusk/ui/ui.hpp | 4 + src/dusk/ui/window.cpp | 7 +- src/dusk/ui/window.hpp | 2 +- 49 files changed, 3765 insertions(+), 956 deletions(-) create mode 100644 res/rml/mod_browser.rcss create mode 100644 res/rml/theme.rcss create mode 100644 src/dusk/mods/catalog.cpp create mode 100644 src/dusk/mods/catalog.hpp create mode 100644 src/dusk/ui/mod_browser.cpp create mode 100644 src/dusk/ui/mod_browser.hpp create mode 100644 src/dusk/ui/remote_texture_provider.cpp create mode 100644 src/dusk/ui/remote_texture_provider.hpp create mode 100644 src/dusk/ui/runtime_image.cpp create mode 100644 src/dusk/ui/runtime_image.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index dcd53086fe..e81a7ce434 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -304,7 +304,7 @@ include(cmake/GameABIConfig.cmake) find_package(Threads REQUIRED) set(GAME_COMPILE_DEFS DUSK_BUILDING_GAME=1) set(GAME_LIBS aurora::core aurora::gx aurora::gd aurora::si aurora::vi aurora::pad aurora::mtx aurora::os aurora::dvd - aurora::card borealis::cli borealis::crash borealis::data borealis::disc borealis::discord borealis::file_select borealis::io borealis::log borealis::presentation borealis::sentry borealis::update freeverb cxxopts::cxxopts absl::flat_hash_map nlohmann_json::nlohmann_json TracyClient fmt::fmt + aurora::card borealis::cli borealis::crash borealis::data borealis::disc borealis::discord borealis::file_select borealis::http borealis::io borealis::log borealis::presentation borealis::sentry borealis::update freeverb cxxopts::cxxopts absl::flat_hash_map nlohmann_json::nlohmann_json TracyClient fmt::fmt Threads::Threads zstd::libzstd dusklight_game_headers) if (DUSK_HAS_FUNCHOOK) list(APPEND GAME_LIBS funchook-static) diff --git a/files.cmake b/files.cmake index 7cde5bfb2f..c8f1470e5f 100644 --- a/files.cmake +++ b/files.cmake @@ -1482,6 +1482,8 @@ set(DUSK_FILES src/dusk/mods/loader/native_module.hpp src/dusk/mods/loader/prepatch.cpp src/dusk/mods/loader/prepatch.hpp + src/dusk/mods/catalog.cpp + src/dusk/mods/catalog.hpp src/dusk/mods/item.hpp src/dusk/mods/item_checks.cpp src/dusk/mods/item_gives.cpp @@ -1563,8 +1565,14 @@ set(DUSK_FILES src/dusk/ui/list.hpp src/dusk/ui/menu_bar.cpp src/dusk/ui/menu_bar.hpp + src/dusk/ui/mod_browser.cpp + src/dusk/ui/mod_browser.hpp src/dusk/ui/mod_texture_provider.cpp src/dusk/ui/mod_texture_provider.hpp + src/dusk/ui/remote_texture_provider.cpp + src/dusk/ui/remote_texture_provider.hpp + src/dusk/ui/runtime_image.cpp + src/dusk/ui/runtime_image.hpp src/dusk/ui/mod_window.cpp src/dusk/ui/mod_window.hpp src/dusk/ui/modal.cpp diff --git a/res/rml/logs.rcss b/res/rml/logs.rcss index 126684f9d2..fd1933ed3a 100644 --- a/res/rml/logs.rcss +++ b/res/rml/logs.rcss @@ -2,19 +2,20 @@ window.logs content { flex-flow: column; } -window.logs .log-toolbar { +window.logs log-toolbar { display: flex; flex-flow: row; - flex: 0 0 64dp; - height: 64dp; + flex: 0 0 var(--toolbar-height); + height: var(--toolbar-height); align-items: center; - gap: 8dp; + gap: var(--space-sm); padding-right: 72dp; - background-color: rgba(217, 217, 217, 10%); - border-bottom: 2dp #92875B; - font-family: "Fira Sans Condensed"; + background-color: rgba(var(--color-neutral-rgb), 10%); + border-bottom-width: 2dp; + border-bottom-color: var(--color-border); + font-family: var(--font-family-heading); font-weight: bold; - font-size: 18dp; + font-size: var(--font-size-xl); } window.logs > close { @@ -22,76 +23,77 @@ window.logs > close { right: 8dp; } -window.logs .log-title { +window.logs log-title { align-self: stretch; flex: 0 0 auto; - padding: 0 24dp; - line-height: 64dp; + padding: 0 var(--space-xl); + line-height: var(--toolbar-height); text-transform: uppercase; - border-bottom: 4dp #C2A42D; + border-bottom-width: 4dp; + border-bottom-color: var(--color-accent); font-effect: glow(0dp 4dp 0dp 4dp black); } -window.logs .log-title-mod { +window.logs log-title-mod { flex: 0 1 auto; min-width: 0; white-space: nowrap; overflow: hidden; - font-family: "Fira Sans"; + font-family: var(--font-family-body); font-weight: normal; - font-size: 15dp; - color: rgba(224, 219, 200, 55%); + font-size: var(--font-size-base); + color: rgba(var(--color-text-rgb), 55%); } -.log-toolbar-spacer { +log-toolbar-spacer { flex: 1 1 0; } -.log-toolbar button { +log-toolbar button { flex: 0 0 auto; - font-family: "Fira Sans"; + font-family: var(--font-family-body); font-weight: normal; - font-size: 15dp; - padding: 5dp 12dp; + font-size: var(--font-size-base); + padding: 5dp var(--space-md); } window.logs content pane.log-view { flex: 1 1 0; - padding: 12dp 16dp; + padding: var(--space-md) var(--space-lg); padding-bottom: 0dp; gap: 0dp; } -.log-lines { +log-lines { display: block; } -.log-line { +log-line { display: block; - font-family: "Noto Mono"; - font-size: 13dp; + font-family: var(--font-family-monospace); + font-size: var(--font-size-xs); line-height: 1.5; word-break: break-word; white-space: pre-wrap; } -.log-line .log-time { - color: rgba(224, 219, 200, 45%); +log-line log-time { + color: rgba(var(--color-text-rgb), 45%); } -.log-line .log-mod { - color: rgba(194, 164, 45, 80%); +log-line log-mod { + color: rgba(var(--color-accent-rgb), 80%); } -.log-line.lvl-trace, -.log-line.lvl-debug { +log-line.lvl-trace, +log-line.lvl-debug { opacity: 0.55; } -.log-line.lvl-warn .log-msg { - color: #ffa826; +log-line.lvl-warn log-msg { + color: var(--color-warning); } -.log-line.lvl-error .log-msg { - color: #cc4444; +log-line.lvl-error log-msg { + color: var(--color-error); } diff --git a/res/rml/mod_browser.rcss b/res/rml/mod_browser.rcss new file mode 100644 index 0000000000..22186313e3 --- /dev/null +++ b/res/rml/mod_browser.rcss @@ -0,0 +1,801 @@ +window.mod-browser, +window.mod-browser-detail, +window.screenshot-viewer { + --color-official-badge: #46edd5; + --color-catalog-warning-background-rgb: 180, 107, 42; + --color-catalog-warning-text: #e8cba7; + --color-catalog-missing: #e8b177; + background-color: rgba(var(--color-surface-rgb), 96%); +} + +window.mod-browser > content { + flex-flow: row; +} + +catalog-filters { + display: flex; + flex-flow: column; + flex: 0 0 264dp; + min-width: 0; + padding: var(--space-xl) 18dp; + gap: var(--space-sm); + border-right-width: 1dp; + border-right-color: var(--color-border); + background-color: rgba(var(--color-control-rgb), 45%); +} + +catalog-filters h1, +catalog-results header h1 { + margin: 0; + font-family: var(--font-family-heading); + font-size: var(--font-size-5xl); + font-weight: bold; +} + +catalog-filters h1 { + padding: 0 var(--space-sm) var(--space-sm) var(--space-sm); +} + +catalog-filters h2 { + margin: var(--space-md) var(--space-sm) 0 var(--space-sm); + font-family: var(--font-family-heading); + font-size: var(--font-size-xs); + font-weight: bold; + text-transform: uppercase; + opacity: 0.42; +} + +catalog-filters select-button { + padding: var(--space-sm) 10dp; + border-radius: var(--radius-panel); +} + +catalog-filters select-button key { + font-size: var(--font-size-xs); +} + +catalog-filters select-button value, +catalog-filters select-button input { + font-size: var(--font-size-sm); +} + +catalog-filters button.catalog-library-link { + padding: var(--space-sm) 10dp; + border-radius: var(--radius-panel); + text-align: left; + font-size: var(--font-size-sm); +} + +catalog-results { + display: flex; + flex-flow: column; + flex: 1 1 auto; + min-width: 0; + min-height: 0; + padding: var(--space-xl); + gap: var(--space-lg); +} + +catalog-results header { + display: block; + flex: 0 0 auto; + padding-right: var(--space-2xl); +} + +catalog-results header catalog-results-summary { + display: block; + margin-top: var(--space-2xs); + font-size: var(--font-size-xs); + opacity: 0.5; +} + +catalog-viewport { + display: block; + flex: 1 1 auto; + min-width: 0; + min-height: 0; + overflow: hidden auto; +} + +catalog-grid { + display: flex; + flex-flow: row wrap; + min-width: 0; + gap: 14dp; + padding: var(--space-2xs); +} + +button.catalog-card { + display: flex; + flex-flow: column; + flex: 0 0 48%; + min-width: 260dp; + height: 310dp; + padding: 0; + overflow: hidden; + text-align: left; + border-radius: 12dp; +} + +catalog-card-art { + display: block; + position: relative; + flex: 0 0 118dp; + min-height: 118dp; + background-color: rgba(var(--color-border-rgb), 12%); +} + +catalog-card-art-shadow { + display: block; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + pointer-events: none; + decorator: linear-gradient(180deg, rgba(var(--color-surface-rgb), 0%) 45%, rgba(var(--color-surface-rgb), 85%)); +} + +catalog-card-body { + display: flex; + flex-flow: column; + flex: 1 1 auto; + min-height: 0; + padding: var(--space-xl) var(--space-lg) var(--space-md) 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); + background-color: rgba(var(--color-accent-rgb), 28%); + color: var(--color-official-badge); + font-style: normal; + font-size: 9dp; +} + +catalog-card-kicker > catalog-card-category { + font-family: var(--font-family-heading); + font-size: var(--font-size-sm); + font-weight: bold; + text-transform: uppercase; + color: var(--color-accent); +} + +catalog-card-updated { + margin-left: auto; + font-weight: normal; + text-transform: none; + opacity: 0.45; +} + +catalog-card-identity { + display: block; +} + +catalog-card-icon { + display: block; + position: absolute; + left: 14dp; + bottom: -18dp; + z-index: 1; + width: 56dp; + height: 56dp; + border-radius: var(--radius-panel); + overflow: hidden; + background-color: rgba(var(--color-border-rgb), 18%); + box-shadow: rgba(0, 0, 0, 60%) 0 6dp 16dp; +} + +catalog-card-icon-image { + display: block; + width: 100%; + height: 100%; +} + +catalog-card-title { + display: block; + font-family: var(--font-family-body); + font-size: var(--font-size-3xl); + font-weight: bold; + color: var(--color-neutral); + line-height: 1.5; +} + +catalog-card-author { + display: block; + font-size: var(--font-size-3xs); + opacity: 0.55; +} + +catalog-card-summary { + flex: 1 1 auto; + min-height: 0; + overflow: hidden; + font-size: var(--font-size-base); + line-height: 1.35; + color: rgba(var(--color-text-rgb), 68%); +} + +catalog-card-meta { + display: flex; + align-items: center; + gap: 10dp; + font-size: var(--font-size-3xs); + opacity: 0.52; +} + +catalog-card-stat { + display: flex; + align-items: center; + gap: 3dp; +} + +catalog-card-meta icon { + flex: 0 0 13dp; + font-size: var(--font-size-sm); + line-height: 1; +} + +catalog-card-size { + margin-left: auto; + font-size: var(--font-size-sm); +} + +catalog-card-size.installed { + color: var(--color-success); +} + +catalog-pagination { + display: flex; + flex-flow: row; + align-items: center; + flex: 0 0 auto; + gap: var(--space-sm); +} + +catalog-pagination-label { + flex: 1 1 auto; + text-align: center; + font-size: var(--font-size-xs); + opacity: 0.5; +} + +catalog-pagination button { + font-size: var(--font-size-sm); + padding: 6dp var(--space-md); +} + +catalog-results-status, +catalog-detail-status { + display: flex; + flex-flow: column; + align-items: center; + justify-content: center; + height: 100%; + gap: var(--space-sm); + text-align: center; +} + +catalog-status-title { + display: block; + font-family: var(--font-family-heading); + font-size: 25dp; + font-weight: bold; +} + +catalog-status-message { + display: block; + opacity: 0.58; +} + +catalog-results-status button, +catalog-detail-status button { + margin-top: var(--space-sm); + font-size: var(--font-size-base); +} + +window.mod-browser-detail > content { + display: block; +} + +detail-scroll { + display: flex; + flex-flow: column; + width: 100%; + height: 100%; + min-width: 0; + overflow: hidden auto; + padding-bottom: var(--space-2xl); +} + +catalog-detail-hero { + display: flex; + position: relative; + flex-flow: column; + justify-content: space-between; + flex: 0 0 220dp; + min-height: 220dp; + padding: 18dp var(--space-xl) 20dp var(--space-xl); + background-color: rgba(var(--color-control-rgb), 75%); +} + +catalog-detail-hero-shadow { + display: block; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + pointer-events: none; + decorator: linear-gradient(180deg, rgba(var(--color-surface-rgb), 25%) 40%, rgba(var(--color-surface-rgb), 92%)); +} + +catalog-detail-actions { + display: flex; + position: relative; + z-index: 1; + flex-flow: row; + gap: var(--space-sm); +} + +catalog-detail-actions button, +catalog-source-actions button { + font-size: var(--font-size-sm); + padding: 7dp var(--space-md); + --button-background: rgba(var(--color-control-rgb), 75%); + --button-background-hover: rgba(var(--color-control-rgb), 90%); + --button-background-selected: rgba(var(--color-control-rgb), 90%); + --button-background-active: rgba(var(--color-surface-rgb), 90%); +} + +.catalog-icon-action { + display: flex; + align-items: center; + gap: var(--space-sm); +} + +.catalog-icon-action icon { + flex: 0 0 18dp; + font-size: var(--font-size-xl); + line-height: 1; +} + +catalog-detail-identity { + display: flex; + position: relative; + z-index: 1; + align-items: center; + gap: 14dp; +} + +catalog-detail-icon { + display: block; + flex: 0 0 70dp; + width: 70dp; + height: 70dp; + border-radius: var(--radius-panel); + overflow: hidden; + background-color: rgba(var(--color-border-rgb), 25%); + box-shadow: rgba(0, 0, 0, 65%) 0 8dp 20dp; +} + +catalog-detail-icon-image { + display: block; + width: 100%; + height: 100%; +} + +catalog-detail-heading { + display: block; + min-width: 0; +} + +catalog-detail-category { + display: block; + font-family: var(--font-family-heading); + font-size: var(--font-size-sm); + font-weight: bold; + text-transform: uppercase; + color: var(--color-accent); +} + +catalog-detail-identity h1 { + display: block; + margin: 1dp 0; + font-family: var(--font-family-body); + font-size: var(--font-size-5xl); + font-weight: bold; + color: var(--color-neutral); +} + +catalog-detail-identity h1 small { + margin-left: 9dp; + font-family: var(--font-family-body); + font-size: var(--font-size-xs); + font-weight: normal; + opacity: 0.55; +} + +catalog-detail-identity p { + display: block; + margin: 0; + font-size: var(--font-size-xs); + opacity: 0.72; +} + +catalog-detail-stats { + display: flex; + flex-flow: row; + align-items: center; + gap: 28dp; + padding: 13dp 28dp; + border-top-width: 1dp; + border-top-color: rgba(var(--color-border-rgb), 30%); + border-bottom-width: 1dp; + border-bottom-color: rgba(var(--color-border-rgb), 30%); + font-size: var(--font-size-2xs); + color: rgba(var(--color-text-rgb), 58%); +} + +catalog-detail-stat { + display: flex; + align-items: center; + gap: 5dp; +} + +catalog-detail-stats icon { + flex: 0 0 17dp; + font-size: var(--font-size-lg); + line-height: 1; +} + +catalog-stat-value { + color: var(--color-text); + font-weight: bold; +} + +/*catalog-native-warning { + display: flex; + align-items: center; + gap: 10dp; + margin: 18dp var(--space-xl) 0 var(--space-xl); + padding: 11dp 14dp; + border-radius: var(--radius-panel); + background-color: rgba(var(--color-catalog-warning-background-rgb), 18%); + box-shadow: rgba(206, 137, 67, 45%) 0 0 0 1dp; + color: var(--color-catalog-warning-text); +} + +catalog-native-warning icon { + font-size: var(--font-size-4xl); +} + +catalog-warning-copy { + display: flex; + flex-flow: column; + font-size: var(--font-size-2xs); +} + +catalog-warning-title { + display: block; + font-size: var(--font-size-xs); + font-weight: bold; +} + +catalog-warning-message { + display: block; + font-size: var(--font-size-3xs); +}*/ + +catalog-detail-body { + display: flex; + flex-flow: row; + align-items: flex-start; + gap: 28dp; + padding: var(--space-xl); +} + +catalog-detail-body main { + display: flex; + flex-flow: column; + flex: 1 1 auto; + min-width: 0; + gap: 28dp; +} + +catalog-detail-body section { + display: block; +} + +catalog-detail-body section.catalog-scroll-anchor { + focus: auto; +} + +catalog-detail-body section.catalog-scroll-anchor:focus-visible { + border-radius: var(--radius-control); + box-shadow: rgba(194, 164, 45, 45%) 0 0 0 1dp; +} + +catalog-detail-body h2, +catalog-detail-body h3 { + display: block; + margin: 0 0 10dp 0; + font-family: var(--font-family-heading); + font-size: 21dp; + font-weight: bold; +} + +catalog-detail-body h2 small { + margin-left: 7dp; + font-family: var(--font-family-body); + font-size: var(--font-size-3xs); + font-weight: normal; + opacity: 0.5; +} + +catalog-fragment { + display: block; + font-size: var(--font-size-md); + line-height: 1.55; + color: var(--color-neutral); +} + +catalog-fragment p, +catalog-fragment ul, +catalog-fragment ol { + display: block; + margin: 0 0 10dp 0; +} + +catalog-fragment ul, +catalog-fragment ol { + padding-left: var(--space-xl); +} + +catalog-fragment li { + display: block; + position: relative; + margin: 3dp 0; +} + +catalog-list-marker { + display: block; + position: absolute; + right: 100%; + width: var(--space-xl); + padding-right: var(--space-sm); + text-align: right; +} + +catalog-fragment h1, +catalog-fragment h2, +catalog-fragment h3, +catalog-fragment h4, +catalog-fragment h5, +catalog-fragment h6 { + display: block; + margin: 15dp 0 5dp 0; + font-family: var(--font-family-heading); + font-weight: bold; + font-size: var(--font-size-xl); + color: var(--color-neutral); +} + +catalog-fragment h1 { + font-size: var(--font-size-5xl); +} + +catalog-fragment h2 { + font-size: var(--font-size-4xl); +} + +catalog-fragment h3 { + font-size: var(--font-size-2xl); +} + +window.screenshot-viewer > close { + display: none; +} + +catalog-gallery { + display: flex; + flex-flow: row; + height: 210dp; + gap: var(--space-sm); +} + +button.catalog-screenshot { + flex: 1 1 0; + height: 100%; + min-width: 0; + padding: 0; + border-radius: var(--radius-panel); + --button-background: rgba(var(--color-border-rgb), 14%); + font-size: var(--font-size-5xl); +} + +button.catalog-screenshot.primary { + flex: 2 1 0; +} + +catalog-dependencies { + display: flex; + flex-flow: column; + gap: 7dp; + font-size: var(--font-size-xs); +} + +catalog-dependency { + display: flex; + flex-flow: column; + padding: 9dp 11dp; + border-radius: var(--radius-control); + background-color: rgba(var(--color-border-rgb), 9%); +} + +catalog-dependency-status { + display: block; + font-size: var(--font-size-3xs); + opacity: 0.56; +} + +catalog-dependency.missing { + color: var(--color-catalog-missing); +} + +catalog-detail-body aside { + display: flex; + flex-flow: column; + flex: 0 0 264dp; + min-width: 0; + padding: var(--space-lg); + gap: var(--space-md); + border-radius: 9dp; + background-color: rgba(var(--color-control-rgb), 42%); + box-shadow: rgba(146, 135, 91, 24%) 0 0 0 1dp; +} + +catalog-detail-body dl { + display: flex; + flex-flow: row wrap; + margin: 0; + font-size: var(--font-size-2xs); +} + +catalog-detail-body dt { + flex: 0 0 42%; + padding: 5dp 0; + opacity: 0.5; +} + +catalog-detail-body dd { + flex: 1 1 52%; + margin: 0; + padding: 5dp 0; + text-align: right; +} + +catalog-source-actions { + display: flex; + flex-flow: column; +} + +window.screenshot-viewer > content { + flex-flow: column; + padding: 18dp; + gap: var(--space-md); +} + +catalog-screenshot-full { + display: block; + flex: 1 1 auto; + min-width: 0; + min-height: 0; + background-color: rgba(var(--color-black-rgb), 45%); +} + +catalog-screenshot-actions { + display: flex; + flex-flow: row; + justify-content: center; + flex: 0 0 auto; + gap: var(--space-sm); +} + +catalog-screenshot-actions button { + font-size: var(--font-size-sm); + padding: 7dp 13dp; +} + +@media (max-height: 640dp) { + catalog-filters { + flex-basis: 232dp; + padding: 15dp var(--space-md); + gap: 5dp; + } + + catalog-filters h1, + catalog-results header h1 { + font-size: 23dp; + } + + catalog-filters h2 { + margin-top: 6dp; + } + + catalog-results { + padding: 15dp; + gap: 10dp; + } + + button.catalog-card { + min-width: 220dp; + height: 254dp; + } + + catalog-card-art { + flex-basis: 78dp; + min-height: 78dp; + } + + catalog-card-body { + padding: 18dp 10dp var(--space-sm) 10dp; + } + + catalog-card-icon { + left: 10dp; + bottom: -14dp; + width: 40dp; + height: 40dp; + } + + catalog-card-kicker, + catalog-card-kicker > catalog-card-category { + font-size: var(--font-size-3xs); + } + + catalog-card-title { + font-size: var(--font-size-md); + } + + catalog-card-summary { + font-size: var(--font-size-3xs); + } + + catalog-detail-hero { + flex-basis: 160dp; + min-height: 160dp; + padding: var(--space-md) 18dp; + } + + catalog-detail-icon { + flex-basis: 56dp; + width: 56dp; + height: 56dp; + } + + catalog-detail-category { + font-size: var(--font-size-2xs); + } + + catalog-detail-identity h1 { + font-size: var(--font-size-3xl); + } + + catalog-detail-body { + padding: 18dp; + gap: 18dp; + } + + catalog-gallery { + height: 160dp; + } +} diff --git a/res/rml/mods.rcss b/res/rml/mods.rcss index 34e86f485b..81ef5800de 100644 --- a/res/rml/mods.rcss +++ b/res/rml/mods.rcss @@ -1,8 +1,8 @@ window.mods content pane.mod-list { flex: 0 0 360dp; - padding: 16dp; + padding: var(--space-lg); padding-bottom: 0dp; - gap: 4dp; + gap: var(--space-xs); } @media (max-height: 640dp) { @@ -12,29 +12,36 @@ window.mods content pane.mod-list { } window.mods content pane.mod-detail { - gap: 12dp; + gap: var(--space-md); } -.mod-info-row { +window.mods content pane.mod-list button.mod-browser-entry { + margin-bottom: var(--space-sm); + text-align: left; + font-family: var(--font-family-heading); + font-weight: bold; +} + +mod-info-row { display: flex; align-items: center; - gap: 12dp; - padding: 4dp 0; + gap: var(--space-md); + padding: var(--space-xs) 0; } -.mod-info-label { - font-family: "Fira Sans Condensed"; +mod-info-label { + font-family: var(--font-family-heading); font-weight: bold; opacity: 0.55; flex: 0 0 auto; } -.mod-info-value { +mod-info-value { flex: 1 1 0; } .mod-path { - font-size: 14dp; + font-size: var(--font-size-sm); word-break: break-all; opacity: 0.7; } @@ -42,10 +49,10 @@ window.mods content pane.mod-detail { mod-entry { display: flex; flex-flow: row; - gap: 12dp; + gap: var(--space-md); padding: 10dp; border-radius: 10dp; - decorator: vertical-gradient(#c2a42d00 #c2a42d00); + decorator: vertical-gradient(rgba(var(--color-accent-rgb), 0%) rgba(var(--color-accent-rgb), 0%)); transition: decorator 0.1s linear-in-out; cursor: pointer; focus: auto; @@ -57,43 +64,43 @@ mod-entry.current { mod-entry:hover, mod-entry:focus-visible { - decorator: vertical-gradient(#c2a42d00 #c2a42d26); + decorator: vertical-gradient(rgba(var(--color-accent-rgb), 0) rgba(var(--color-accent-rgb), 38)); } mod-entry:selected { - decorator: vertical-gradient(#c2a42d10 #c2a42d40); + decorator: vertical-gradient(rgba(var(--color-accent-rgb), 16) rgba(var(--color-accent-rgb), 64)); } mod-entry .mod-icon { flex: 0 0 auto; width: 56dp; height: 56dp; - border-radius: 8dp; + border-radius: var(--radius-panel); } mod-entry icon.mod-icon { font-size: 36dp; - background-color: rgba(17, 16, 10, 20%); - color: rgba(224, 219, 200, 45%); + background-color: rgba(var(--color-control-rgb), 20%); + color: rgba(var(--color-text-rgb), 45%); decorator: text("" center center); } -mod-entry .mod-entry-info { +mod-entry mod-entry-info { display: flex; flex-flow: column; flex: 1 1 0; min-width: 0; - gap: 2dp; + gap: var(--space-2xs); } -mod-entry .mod-entry-name { +mod-entry mod-entry-name { display: flex; flex-flow: row; align-items: baseline; gap: 6dp; } -mod-entry .mod-entry-name-text { +mod-entry mod-entry-name-text { flex: 0 1 auto; min-width: 0; font-weight: bold; @@ -101,47 +108,47 @@ mod-entry .mod-entry-name-text { overflow: hidden; } -mod-entry .mod-entry-version { +mod-entry mod-entry-version { flex: 0 0 auto; - font-size: 13dp; - color: rgba(224, 219, 200, 50%); + font-size: var(--font-size-xs); + color: rgba(var(--color-text-rgb), 50%); } -mod-entry .mod-entry-status.active { - color: #44cc55; +mod-entry mod-entry-status.active { + color: var(--color-success); } -mod-entry .mod-entry-status.failed { - color: #cc4444; +mod-entry mod-entry-status.failed { + color: var(--color-error); } -mod-entry .mod-entry-network { +mod-entry mod-entry-network { margin-left: 6dp; padding: 1dp 5dp; border-radius: 5dp; - background-color: rgba(67, 151, 219, 20%); - color: #6fb7ef; + background-color: rgba(var(--color-info-rgb), 20%); + color: var(--color-info); } -mod-entry .mod-entry-desc { - font-size: 14dp; +mod-entry mod-entry-desc { + font-size: var(--font-size-sm); line-height: 1.3; - color: rgba(224, 219, 200, 65%); + color: rgba(var(--color-text-rgb), 65%); max-height: 2.6em; overflow: hidden; white-space: pre-wrap; } -mod-entry .mod-entry-sub { - font-size: 13dp; - color: rgba(224, 219, 200, 50%); +mod-entry mod-entry-sub { + font-size: var(--font-size-xs); + color: rgba(var(--color-text-rgb), 50%); } mod-entry.inactive .mod-icon { filter: grayscale(1); } -mod-entry.inactive .mod-entry-info { +mod-entry.inactive mod-entry-info { opacity: 0.5; } @@ -155,19 +162,19 @@ mod-header.has-banner { margin: -24dp -24dp 0dp -24dp; } -mod-header .mod-actions { +mod-header mod-actions { position: absolute; top: 24dp; left: 24dp; display: flex; flex-flow: row; - gap: 8dp; + gap: var(--space-sm); } -mod-header .mod-actions button { - font-size: 16dp; +mod-header mod-actions button { + font-size: var(--font-size-md); padding: 6dp 14dp; - background-color: rgba(21, 22, 16, 80%); + background-color: rgba(var(--color-surface-rgb), 80%); box-shadow: rgba(146, 135, 91, 60%) 0 0 0 1dp; } @@ -177,56 +184,55 @@ mod-header.no-banner { align-items: center; } -mod-header.no-banner .mod-actions { +mod-header.no-banner mod-actions { position: static; } -window.mods .mod-title { +window.mods mod-title { display: block; - font-size: 28dp; + font-size: var(--font-size-5xl); font-weight: bold; } -window.mods .mod-title .mod-title-version { +window.mods mod-title mod-title-version { font-weight: normal; - font-size: 16dp; - color: rgba(224, 219, 200, 55%); + font-size: var(--font-size-md); + color: rgba(var(--color-text-rgb), 55%); } -window.mods .mod-author { +window.mods mod-author { display: block; - font-size: 15dp; - color: rgba(224, 219, 200, 55%); + font-size: var(--font-size-base); + color: rgba(var(--color-text-rgb), 55%); } -window.mods .mod-restart-note { - font-size: 15dp; - color: #ffa826; +window.mods mod-restart-note { + font-size: var(--font-size-base); + color: var(--color-warning); opacity: 0.85; } -window.mods .mod-description { +window.mods mod-description { line-height: 1.5; } -.status-badge { - font-size: 14dp; +status-badge { + font-size: var(--font-size-sm); opacity: 0.7; } -.status-badge.active, -.mod-info-label.active { - color: #44cc55; +status-badge.active, +mod-info-label.active { + color: var(--color-success); opacity: 1; } -.status-badge.failed, -.mod-info-label.failed { - color: #cc4444; +status-badge.failed, +mod-info-label.failed { + color: var(--color-error); opacity: 1; } - -.status-badge.network { - color: #6fb7ef; +status-badge.network { + color: var(--color-info); opacity: 1; } diff --git a/res/rml/overlay.rcss b/res/rml/overlay.rcss index 98f8bfa27a..fed6597c70 100644 --- a/res/rml/overlay.rcss +++ b/res/rml/overlay.rcss @@ -8,10 +8,10 @@ body { height: 100%; margin: 0; padding: 0; - font-family: "Fira Sans"; + font-family: var(--font-family-body); font-weight: normal; - font-size: 20dp; - color: #E0DBC8; + font-size: var(--font-size-2xl); + color: var(--color-text); display: flex; flex-direction: column; justify-content: flex-end; @@ -24,16 +24,22 @@ fps, pipeline-progress, toast { position: absolute; - border: 1dp #92875B; - background-color: rgba(21, 22, 16, 80%); + border-width: 1dp; + background-color: rgba(var(--color-surface-rgb), 80%); +} + +fps, +pipeline-progress { + border-color: var(--color-border); } toast { + border-color: var(--toast-border-color); top: 40dp; right: 40dp; display: flex; flex-flow: column; - border-radius: 14dp; + border-radius: var(--radius-window); overflow: hidden; backdrop-filter: blur(5dp); box-shadow: 0 0 15dp 3dp; @@ -41,8 +47,8 @@ toast { transform: scale(0.9); transform-origin: center; transition: filter transform 0.2s cubic-in-out; - padding: 18dp 24dp; - gap: 8dp; + padding: 18dp var(--space-xl); + gap: var(--space-sm); } toast[open] { @@ -67,14 +73,14 @@ toast heading { display: flex; gap: 18dp; align-items: center; - font-family: "Fira Sans Condensed"; - font-size: 18dp; + font-family: var(--font-family-heading); + font-size: var(--font-size-xl); font-weight: bold; text-transform: uppercase; - color: #92875B; + color: var(--toast-heading-color); } -toast heading > span { +toast heading > toast-title { flex: 1 0 auto; } @@ -82,13 +88,13 @@ toast heading > row { flex: 1 0 auto; display: flex; align-items: center; - gap: 4dp; + gap: var(--space-xs); } toast message { display: flex; flex-flow: column; - gap: 8dp; + gap: var(--space-sm); } toast message row { @@ -108,7 +114,7 @@ progress { } progress fill { - background-color: rgba(194, 164, 45, 80%); + background-color: rgba(var(--color-accent-rgb), 80%); } pipeline-progress { @@ -119,8 +125,8 @@ pipeline-progress { z-index: 100; min-width: 260dp; max-width: 90%; - padding: 10dp 16dp 12dp; - border-radius: 7dp; + padding: 10dp var(--space-lg) var(--space-md); + border-radius: var(--radius-control); overflow: hidden; filter: opacity(0); transition: filter 0.2s linear-in-out; @@ -134,8 +140,8 @@ pipeline-progress[open] { pipeline-status { display: flex; align-items: center; - gap: 8dp; - font-size: 18dp; + gap: var(--space-sm); + font-size: var(--font-size-xl); font-weight: normal; white-space: nowrap; } @@ -145,26 +151,16 @@ icon.pipeline-spinner { height: 1.2em; line-height: 1.2em; font-size: 1.2em; - color: #C2A42D; + color: var(--color-accent); text-align: center; transform-origin: center; animation: 1s linear infinite pipeline-spinner-spin; } -toast.achievement { - border: 1dp #C2A42D; -} - -toast.achievement heading { - color: #C2A42D; -} - +toast.achievement, toast.warning { - border: 1dp #C2A42D; -} - -toast.warning heading { - color: #C2A42D; + --toast-border-color: var(--color-accent); + --toast-heading-color: var(--color-accent); } toast.controller-warning { @@ -181,8 +177,8 @@ toast.controller-warning[open] { transform: translateX(-50%) scale(1); } -toast.controller-warning heading { - color: #C2A42D; +toast.controller-warning { + --toast-heading-color: var(--color-accent); } toast.menu-notification { @@ -209,7 +205,7 @@ toast.menu-notification message row { } icon { - font-family: "Material Symbols Rounded"; + font-family: var(--font-family-icons); font-weight: normal; display: inline-block; vertical-align: middle; @@ -246,10 +242,10 @@ icon.warning { fps { display: none; z-index: 99; - font-size: 18dp; + font-size: var(--font-size-xl); font-weight: bold; - padding: 9dp 12dp; - border-radius: 7dp; + padding: 9dp var(--space-md); + border-radius: var(--radius-control); pointer-events: none; white-space: nowrap; } @@ -260,12 +256,12 @@ speedrun-timer { bottom: 0; right: 0; z-index: 99; - background-color: rgba(0, 0, 0, 65%); - padding: 2dp 4dp; + background-color: rgba(var(--color-black-rgb), 65%); + padding: var(--space-2xs) var(--space-xs); pointer-events: none; - font-family: "Noto Mono"; - font-size: 16dp; - color: #ffffff; + font-family: var(--font-family-monospace); + font-size: var(--font-size-md); + color: var(--color-white); white-space: nowrap; } diff --git a/res/rml/popover.rcss b/res/rml/popover.rcss index 1db4f64a05..d7074b7ac3 100644 --- a/res/rml/popover.rcss +++ b/res/rml/popover.rcss @@ -3,6 +3,8 @@ } body { + --button-background: rgba(var(--color-control-rgb), 35%); + width: 100%; height: 100%; z-index: 10; @@ -12,12 +14,13 @@ popover { position: absolute; display: flex; flex-flow: column; - font-family: "Fira Sans"; - font-size: 14dp; - color: #E0DBC8; - border-radius: 14dp; - border: 2dp #92875B; - background-color: rgba(21, 22, 16, 96%); + font-family: var(--font-family-body); + font-size: var(--font-size-sm); + color: var(--color-text); + border-radius: var(--radius-window); + border-width: 2dp; + border-color: var(--color-border); + background-color: rgba(var(--color-surface-rgb), 96%); backdrop-filter: blur(5dp); box-shadow: 0 6dp 24dp 2dp rgba(0, 0, 0, 55%); filter: opacity(0); @@ -43,7 +46,7 @@ color-sv { position: relative; width: 240dp; height: 150dp; - border-radius: 8dp; + border-radius: var(--radius-panel); box-shadow: rgba(146, 135, 91, 50%) 0 0 0 1dp; drag: drag; focus: auto; @@ -55,7 +58,7 @@ color-alpha { position: relative; width: 240dp; height: 14dp; - border-radius: 7dp; + border-radius: var(--radius-control); box-shadow: rgba(146, 135, 91, 50%) 0 0 0 1dp; drag: drag; focus: auto; @@ -82,20 +85,21 @@ color-cursor { position: absolute; width: 14dp; height: 14dp; - border-radius: 7dp; - border: 2dp #ffffff; + border-radius: var(--radius-control); + border-width: 2dp; + border-color: var(--color-white); box-shadow: 0 0 4dp 1dp rgba(0, 0, 0, 70%); pointer-events: none; } color-heading { display: block; - margin-top: 2dp; - font-family: "Fira Sans Condensed"; + margin-top: var(--space-2xs); + font-family: var(--font-family-heading); font-weight: bold; - font-size: 13dp; + font-size: var(--font-size-xs); text-transform: uppercase; - color: rgba(224, 219, 200, 55%); + color: rgba(var(--color-text-rgb), 55%); } color-presets { @@ -114,7 +118,7 @@ button.color-swatch-button { width: 25dp; height: 25dp; padding: 0; - border-radius: 7dp; + border-radius: var(--radius-control); } color-chip { @@ -129,56 +133,56 @@ color-chip { button.color-swatch-button color-chip { width: 25dp; height: 25dp; - border-radius: 7dp; + border-radius: var(--radius-control); } color-chip.empty, color-swatch.empty { - background-color: rgba(224, 219, 200, 12%); - decorator: linear-gradient(135deg, rgba(224, 219, 200, 0) 45%, - rgba(194, 164, 45, 70%) 48%, rgba(194, 164, 45, 70%) 52%, - rgba(224, 219, 200, 0) 55%); + background-color: rgba(var(--color-text-rgb), 12%); + decorator: linear-gradient(135deg, rgba(var(--color-text-rgb), 0) 45%, + rgba(var(--color-accent-rgb), 70%) 48%, rgba(var(--color-accent-rgb), 70%) 52%, + rgba(var(--color-text-rgb), 0) 55%); } color-footer { display: flex; align-items: center; gap: 6dp; - padding-top: 2dp; + padding-top: var(--space-2xs); } button { - background-color: rgba(17, 16, 10, 35%); - padding: 4dp 8dp; - border-radius: 8dp; + background-color: var(--button-background); + padding: var(--space-xs) var(--space-sm); + border-radius: var(--radius-panel); box-shadow: rgba(146, 135, 91, 30%) 0 0 0 1dp; - color: #E0DBC8; + color: var(--button-color); cursor: pointer; focus: auto; } button:hover, button:focus-visible { - background-color: rgba(204, 184, 119, 20%); + background-color: var(--button-background-hover); box-shadow: #C2A42D 0 0 0 2dp; } button:active { - background-color: rgba(204, 184, 119, 40%); + background-color: var(--button-background-active); } color-value { display: block; flex: 1 1 auto; text-align: right; - font-family: "Noto Mono"; - font-size: 12dp; - color: #FFFFFF; + font-family: var(--font-family-monospace); + font-size: var(--font-size-2xs); + color: var(--color-white); cursor: pointer; focus: auto; } color-value:hover, color-value:focus-visible { - color: #C2A42D; + color: var(--color-accent); } diff --git a/res/rml/popup.rcss b/res/rml/popup.rcss index effc80344d..1ed1652ecd 100644 --- a/res/rml/popup.rcss +++ b/res/rml/popup.rcss @@ -8,10 +8,10 @@ body { height: 100%; margin: 0; padding: 0; - font-family: "Fira Sans Condensed"; + font-family: var(--font-family-heading); font-weight: bold; - font-size: 18dp; - color: #E0DBC8; + font-size: var(--font-size-xl); + color: var(--color-text); } button { @@ -23,11 +23,12 @@ popup { width: 100%; display: flex; align-items: stretch; - height: 64dp; - background-color: rgba(21, 22, 16, 80%); - border-bottom: 2dp #92875B; + height: var(--toolbar-height); + background-color: rgba(var(--color-surface-rgb), 80%); + border-bottom-width: 2dp; + border-bottom-color: var(--color-border); backdrop-filter: blur(5dp); - transform: translateY(-64dp); + transform: translateY(var(--toolbar-hidden-offset)); transition: transform 0.2s cubic-in-out; } @@ -41,5 +42,5 @@ popup tab-bar { popup tab-bar tab { opacity: 0.35; - color: #E0DBC8; + color: var(--color-text); } diff --git a/res/rml/prelaunch.rcss b/res/rml/prelaunch.rcss index 6cc5da65ae..bebba20a50 100644 --- a/res/rml/prelaunch.rcss +++ b/res/rml/prelaunch.rcss @@ -1,32 +1,38 @@ -*, *:before, *:after { - box-sizing: border-box; -} - body { + --color-prelaunch-accent: #FEE685; + --color-prelaunch-muted: #A6A09B; + --color-ready: #D8F999; + --menu-button-decorator: horizontal-gradient(rgba(var(--color-black-rgb), 0%) rgba(var(--color-black-rgb), 0%)); + --menu-button-decorator-hover: horizontal-gradient(#FEE685FF #FEE68500); + width: 100%; height: 100%; - font-family: "Fira Sans"; + font-family: var(--font-family-body); font-weight: normal; - font-size: 20dp; - color: #FFFFFF; + font-size: var(--font-size-2xl); + color: var(--color-white); filter: opacity(0); transition: filter 1s 0.2s linear-in-out; z-index: -1; } -.gradient { +body.mirrored { + --menu-button-decorator-hover: horizontal-gradient(#FEE68500 #FEE685FF); +} + +prelaunch-gradient { position: absolute; width: 100%; height: 100%; /* The color gradient from the Figma bands really badly. A fully black gradient does as well, but not as badly. */ - decorator: horizontal-gradient(#000000FF #00000000); + decorator: horizontal-gradient(rgba(var(--color-black-rgb), 100%) rgba(var(--color-black-rgb), 0%)); } -body.mirrored .gradient { - decorator: horizontal-gradient(#00000000 #000000FF); +body.mirrored prelaunch-gradient { + decorator: horizontal-gradient(rgba(var(--color-black-rgb), 0%) rgba(var(--color-black-rgb), 100%)); } -.background { +prelaunch-background { position: absolute; width: 100%; height: 100%; @@ -39,11 +45,11 @@ body[open] { filter: opacity(1); } -body[open] .background { +body[open] prelaunch-background { opacity: 1; } -body.disc-ready .background { +body.disc-ready prelaunch-background { opacity: 0; } @@ -84,7 +90,7 @@ hero { display: flex; flex-direction: column; align-items: flex-start; - gap: 4dp; + gap: var(--space-xs); } body.mirrored hero { @@ -96,8 +102,8 @@ hero img { } eyebrow { - font-family: "Alegreya SC"; - font-size: 32dp; + font-family: var(--font-family-display); + font-size: var(--font-size-6xl); } @media (min-width: 1216dp) { @@ -107,36 +113,36 @@ eyebrow { } } -eyebrow span { +eyebrow studio-name { font-weight: bold; } -#menu-list { +menu-list { display: flex; flex-direction: column; - gap: 12dp; + gap: var(--space-md); align-items: flex-start; } #menu-list button { width: 428dp; height: 54dp; - padding: 8dp 16dp; - border-radius: 8dp; + padding: var(--space-sm) var(--space-lg); + border-radius: var(--radius-panel); text-align: left; text-transform: uppercase; - font-family: "Fira Sans Condensed"; - font-size: 32dp; + font-family: var(--font-family-heading); + font-size: var(--font-size-6xl); font-weight: normal; cursor: pointer; /* Define a fully transparent gradient as the default state, otherwise a white flash occurs */ - decorator: horizontal-gradient(#00000000 #00000000); + decorator: var(--menu-button-decorator); } #menu-list button:disabled { opacity: 0.75; cursor: default; - decorator: horizontal-gradient(#00000000 #00000000); + decorator: var(--menu-button-decorator); } #menu-list button.anim-done { @@ -157,7 +163,7 @@ eyebrow span { width: 100%; height: 100%; overflow: hidden; - border-radius: 8dp; + border-radius: var(--radius-panel); pointer-events: none; z-index: 0; } @@ -169,7 +175,7 @@ eyebrow span { left: 0; width: 100%; height: 100%; - padding: 8dp 16dp; + padding: var(--space-sm) var(--space-lg); opacity: 0; text-overflow: ellipsis; white-space: nowrap; @@ -189,8 +195,8 @@ eyebrow span { height: 54dp; align-items: center; justify-content: center; - color: #FFFFFF; - font-family: "Material Symbols Rounded"; + color: var(--color-white); + font-family: var(--font-family-icons); font-weight: normal; font-size: 30dp; z-index: 1; @@ -213,8 +219,8 @@ eyebrow span { #menu-list button:hover, #menu-list button:focus-visible { - color: black; - decorator: horizontal-gradient(#FEE685FF #FEE68500); + color: var(--color-black); + decorator: var(--menu-button-decorator-hover); } body.mirrored #menu-list { @@ -225,11 +231,6 @@ body.mirrored #menu-list button { text-align: right; } -body.mirrored #menu-list button:hover, -body.mirrored #menu-list button:focus-visible { - decorator: horizontal-gradient(#FEE68500 #FEE685FF); -} - disc-info { position: absolute; left: 96dp; @@ -237,8 +238,8 @@ disc-info { bottom: 72dp; display: flex; flex-direction: column; - gap: 12dp; - font-size: 24dp; + gap: var(--space-md); + font-size: var(--font-size-4xl); font-effect: glow(0dp 4dp 0dp 4dp black); text-align: left; } @@ -256,9 +257,9 @@ version-info { bottom: 72dp; display: flex; flex-direction: column; - gap: 12dp; + gap: var(--space-md); text-align: right; - font-size: 24dp; + font-size: var(--font-size-4xl); font-effect: glow(0dp 4dp 0dp 4dp black); text-align: right; } @@ -272,11 +273,11 @@ body.mirrored version-info { #disc-status { display: flex; align-items: center; - gap: 8dp; + gap: var(--space-sm); } #disc-status[status=good] { - color: #D8F999; + color: var(--color-ready); } #disc-status[status=bad] { @@ -284,7 +285,7 @@ body.mirrored version-info { } #disc-status[status=verifying] { - color: #FFFFFF; + color: var(--color-white); } #disc-status[status=mismatch] { @@ -292,20 +293,20 @@ body.mirrored version-info { } #disc-status[status=unknown] { - color: rgba(224, 219, 200, 65%); + color: rgba(var(--color-text-rgb), 65%); } #disc-status[status=pending] { - color: #FEE685; + color: var(--color-prelaunch-accent); } #disc-status icon { display: none; width: 24dp; height: 24dp; - font-family: "Material Symbols Rounded"; + font-family: var(--font-family-icons); font-weight: normal; - font-size: 24dp; + font-size: var(--font-size-4xl); } #disc-status[status] icon { @@ -337,24 +338,24 @@ body.mirrored version-info { } #disc-version { - font-size: 20dp; + font-size: var(--font-size-2xl); } -.update { +update-status { display: none; - color: #A6A09B; + color: var(--color-prelaunch-muted); align-items: center; justify-content: flex-end; - gap: 8dp; - font-size: 20dp; + gap: var(--space-sm); + font-size: var(--font-size-2xl); } -.update[state=checking], -.update[state=failed] { +update-status[state=checking], +update-status[state=failed] { display: block; } -.update[state=available] { +update-status[state=available] { display: flex; } @@ -364,33 +365,33 @@ body.mirrored version-info { padding: 0dp; border-width: 0dp; background-color: transparent; - color: #D8F999; + color: var(--color-ready); cursor: pointer; text-transform: uppercase; font-weight: bold; - decorator: horizontal-gradient(#00000000 #00000000); + decorator: var(--menu-button-decorator); } -.update[state=available] #update-download { +update-status[state=available] #update-download { display: flex; align-items: center; - gap: 2dp; + gap: var(--space-2xs); } #update-download icon { display: block; width: 18dp; height: 18dp; - font-family: "Material Symbols Rounded"; + font-family: var(--font-family-icons); font-weight: normal; decorator: text("" center center); } -.detail { - color: #A6A09B; +disc-version { + color: var(--color-prelaunch-muted); } -body.mirrored .update { +body.mirrored update-status { justify-content: flex-start; } @@ -436,12 +437,20 @@ body.animate-in .intro-item { /* Mobile layout */ @media (max-height: 640dp) { - .gradient { - decorator: horizontal-gradient(#00000000 #000000FF); + body { + --menu-button-decorator-hover: horizontal-gradient(#FEE68500 #FEE685FF); } - body.mirrored .gradient { - decorator: horizontal-gradient(#000000FF #00000000); + body.mirrored { + --menu-button-decorator-hover: horizontal-gradient(#FEE685FF #FEE68500); + } + + prelaunch-gradient { + decorator: horizontal-gradient(rgba(var(--color-black-rgb), 0%) rgba(var(--color-black-rgb), 100%)); + } + + body.mirrored prelaunch-gradient { + decorator: horizontal-gradient(rgba(var(--color-black-rgb), 100%) rgba(var(--color-black-rgb), 0%)); } menu { @@ -453,7 +462,7 @@ body.animate-in .intro-item { flex-direction: row; align-items: center; justify-content: space-between; - gap: 16dp; + gap: var(--space-lg); } body.mirrored menu { @@ -466,7 +475,7 @@ body.animate-in .intro-item { flex: 1 1 0; min-width: 0; max-width: 48%; - margin-left: 32dp; + margin-left: var(--space-2xl); } body.mirrored hero { @@ -490,11 +499,6 @@ body.animate-in .intro-item { text-align: right; } - #menu-list button:hover, - #menu-list button:focus-visible { - decorator: horizontal-gradient(#FEE68500 #FEE685FF); - } - body.mirrored #menu-list { align-items: flex-start; } @@ -503,11 +507,6 @@ body.animate-in .intro-item { text-align: left; } - body.mirrored #menu-list button:hover, - body.mirrored #menu-list button:focus-visible { - decorator: horizontal-gradient(#FEE685FF #FEE68500); - } - eyebrow { display: none; } @@ -518,8 +517,8 @@ body.animate-in .intro-item { bottom: 32dp; top: auto; text-align: right; - font-size: 16dp; - gap: 8dp; + font-size: var(--font-size-md); + gap: var(--space-sm); } #disc-status { @@ -527,11 +526,11 @@ body.animate-in .intro-item { } #disc-status icon { - font-size: 20dp; + font-size: var(--font-size-2xl); } #disc-version { - font-size: 16dp; + font-size: var(--font-size-md); } version-info { @@ -540,12 +539,12 @@ body.animate-in .intro-item { bottom: auto; top: 32dp; text-align: right; - font-size: 16dp; - gap: 8dp; + font-size: var(--font-size-md); + gap: var(--space-sm); } - .update { - font-size: 16dp; + update-status { + font-size: var(--font-size-md); } body.mirrored disc-info { diff --git a/res/rml/tabbing.rcss b/res/rml/tabbing.rcss index 8f42dd84d5..c508a354d5 100644 --- a/res/rml/tabbing.rcss +++ b/res/rml/tabbing.rcss @@ -18,17 +18,18 @@ tab-bar scrollbarhorizontal sliderbar { tab-bar tab { flex: 0 0 auto; - padding: 0 24dp; - line-height: 64dp; + padding: 0 var(--space-xl); + line-height: var(--toolbar-height); white-space: nowrap; - decorator: vertical-gradient(#c2a42d00 #c2a42d00); + decorator: vertical-gradient(rgba(var(--color-accent-rgb), 0%) rgba(var(--color-accent-rgb), 0%)); transition: decorator 0.1s linear-in-out, opacity 0.1s linear-in-out; cursor: pointer; } tab-bar tab:selected { opacity: 1; - border-bottom: 4dp #C2A42D; + border-bottom-width: 4dp; + border-bottom-color: var(--color-accent); font-effect: glow(0dp 4dp 0dp 4dp black); } @@ -36,17 +37,17 @@ tab-bar tab:focus-visible, tab-bar tab:hover { opacity: 1; font-effect: glow(0dp 4dp 0dp 4dp black); - decorator: vertical-gradient(#c2a42d00 #c2a42d26); + decorator: vertical-gradient(rgba(var(--color-accent-rgb), 0) rgba(var(--color-accent-rgb), 38)); } tab-bar tab:active { - decorator: vertical-gradient(#c2a42d10 #c2a42d40); + decorator: vertical-gradient(rgba(var(--color-accent-rgb), 16) rgba(var(--color-accent-rgb), 64)); } tab-bar[closable] tab-end-spacer { display: block; - flex: 0 0 64dp; - width: 64dp; + flex: 0 0 var(--toolbar-height); + width: var(--toolbar-height); pointer-events: none; } @@ -59,10 +60,10 @@ window > close { z-index: 1; width: 48dp; height: 48dp; - font-family: "Material Symbols Rounded"; + font-family: var(--font-family-icons); font-weight: normal; - font-size: 24dp; - color: rgba(224, 219, 200, 70%); + font-size: var(--font-size-4xl); + color: rgba(var(--color-text-rgb), 70%); backdrop-filter: blur(2dp); border-radius: 6dp; decorator: text("" center center); @@ -74,8 +75,8 @@ tab-bar[closable] close:hover, tab-bar[closable] close:focus-visible, window > close:hover, window > close:focus-visible { - color: #fff; - background-color: rgba(194, 164, 45, 24%); + color: var(--color-white); + background-color: rgba(var(--color-accent-rgb), 24%); } window > close { @@ -85,6 +86,6 @@ window > close { tab-bar[closable] close:active, window > close:active { - color: #fff; - background-color: rgba(194, 164, 45, 40%); + color: var(--color-white); + background-color: rgba(var(--color-accent-rgb), 40%); } diff --git a/res/rml/theme.rcss b/res/rml/theme.rcss new file mode 100644 index 0000000000..29ae6a5e4d --- /dev/null +++ b/res/rml/theme.rcss @@ -0,0 +1,72 @@ +*, *:before, *:after { + box-sizing: border-box; +} + +body { + --font-family-body: Fira Sans; + --font-family-heading: Fira Sans Condensed; + --font-family-monospace: Noto Mono; + --font-family-icons: Material Symbols Rounded; + --font-family-display: Alegreya SC; + + --font-size-3xs: 11dp; + --font-size-2xs: 12dp; + --font-size-xs: 13dp; + --font-size-sm: 14dp; + --font-size-base: 15dp; + --font-size-md: 16dp; + --font-size-lg: 17dp; + --font-size-xl: 18dp; + --font-size-2xl: 20dp; + --font-size-3xl: 22dp; + --font-size-4xl: 24dp; + --font-size-5xl: 28dp; + --font-size-6xl: 32dp; + + --space-2xs: 2dp; + --space-xs: 4dp; + --space-sm: 8dp; + --space-md: 12dp; + --space-lg: 16dp; + --space-xl: 24dp; + --space-2xl: 32dp; + + --radius-small: 4dp; + --radius-control: 7dp; + --radius-panel: 8dp; + --radius-window: 14dp; + + --toolbar-height: 64dp; + --toolbar-hidden-offset: -64dp; + + --color-text-rgb: 224, 219, 200; + --color-text: rgb(var(--color-text-rgb)); + --color-accent-rgb: 194, 164, 45; + --color-accent: rgb(var(--color-accent-rgb)); + --color-border-rgb: 146, 135, 91; + --color-border: rgb(var(--color-border-rgb)); + --color-surface-rgb: 21, 22, 16; + --color-interactive-rgb: 204, 184, 119; + --color-control-rgb: 17, 16, 10; + --color-neutral-rgb: 217, 217, 217; + --color-neutral: rgb(var(--color-neutral-rgb)); + --color-white-rgb: 255, 255, 255; + --color-white: rgb(var(--color-white-rgb)); + --color-black-rgb: 0, 0, 0; + --color-black: rgb(var(--color-black-rgb)); + + --color-success: #44cc55; + --color-info-rgb: 111, 183, 239; + --color-info: rgb(var(--color-info-rgb)); + --color-warning: #ffa826; + --color-error: #cc4444; + + --button-color: var(--color-text); + --button-background: rgba(var(--color-control-rgb), 20%); + --button-background-hover: rgba(var(--color-interactive-rgb), 20%); + --button-background-selected: rgba(var(--color-interactive-rgb), 40%); + --button-background-active: rgba(var(--color-interactive-rgb), 40%); + + --toast-border-color: var(--color-border); + --toast-heading-color: var(--color-border); +} diff --git a/res/rml/touch_controls.rcss b/res/rml/touch_controls.rcss index 4c8057d6c0..33ad894fa1 100644 --- a/res/rml/touch_controls.rcss +++ b/res/rml/touch_controls.rcss @@ -1,16 +1,18 @@ -*, *:before, *:after { - box-sizing: border-box; -} - body { + --button-color: rgba(248, 244, 232, 90%); + --button-background: rgba(22, 24, 28, 48%); + --button-background-active: rgba(63, 78, 90, 68%); + --button-border-color: rgba(var(--color-white-rgb), 22%); + --button-border-color-active: rgba(var(--color-white-rgb), 48%); + width: 100%; height: 100%; margin: 0; padding: 0; overflow: hidden; - font-family: "Fira Sans Condensed"; + font-family: var(--font-family-heading); font-weight: bold; - color: rgba(248, 244, 232, 90%); + color: var(--button-color); z-index: 1; filter: opacity(0); transition: filter 0.2s linear-in-out; @@ -30,9 +32,10 @@ button { justify-content: center; decorator: none; padding: 0; - border: 1dp rgba(255, 255, 255, 22%); - background-color: rgba(22, 24, 28, 48%); - color: rgba(248, 244, 232, 90%); + border-width: 1dp; + border-color: var(--button-border-color); + background-color: var(--button-background); + color: var(--button-color); text-align: center; /* backdrop-filter: blur(7dp); */ /* box-shadow: 0 6dp 18dp rgba(0, 0, 0, 28%); */ @@ -43,8 +46,8 @@ button { button.pressed, button.active { - background-color: rgba(63, 78, 90, 68%); - border-color: rgba(255, 255, 255, 48%); + background-color: var(--button-background-active); + border-color: var(--button-border-color-active); filter: brightness(1.18); } @@ -68,9 +71,9 @@ button icon { button icon glyph { display: block; - font-family: "Material Symbols Rounded"; + font-family: var(--font-family-icons); font-weight: normal; - font-size: 24dp; + font-size: var(--font-size-4xl); line-height: 1; } @@ -92,9 +95,9 @@ button icon glyph { position: absolute; } -.trigger-l.active { - background-color: rgba(57, 116, 133, 74%); - border-color: rgba(128, 222, 234, 72%); +.trigger-l { + --button-background-active: rgba(57, 116, 133, 74%); + --button-border-color-active: rgba(128, 222, 234, 72%); } .trigger, @@ -103,12 +106,14 @@ button icon glyph { } .trigger { - font-size: 22dp; + font-size: var(--font-size-3xl); } .button-z { - background-color: rgba(118, 79, 158, 58%); - border-color: rgba(203, 170, 255, 36%); + --button-background: rgba(118, 79, 158, 58%); + --button-background-active: rgba(139, 91, 187, 82%); + --button-border-color: rgba(203, 170, 255, 36%); + --button-border-color-active: rgba(220, 194, 255, 70%); } .midna-icon { @@ -121,7 +126,7 @@ button icon glyph { .button-z.has-icon span, .face.has-item span { position: absolute; - font-size: 13dp; + font-size: var(--font-size-xs); line-height: 1; } @@ -130,18 +135,14 @@ button icon glyph { bottom: 7dp; } -.button-z.pressed { - background-color: rgba(139, 91, 187, 82%); - border-color: rgba(220, 194, 255, 70%); -} - action-bar { position: absolute; display: flex; align-items: center; - border: 1dp rgba(255, 255, 255, 22%); + border-width: 1dp; + border-color: var(--button-border-color); border-radius: 23dp; - background-color: rgba(22, 24, 28, 48%); + background-color: var(--button-background); /* backdrop-filter: blur(7dp); */ /* box-shadow: 0 -6dp 18dp rgba(0, 0, 0, 28%); */ overflow: hidden; @@ -175,7 +176,7 @@ action-bar:hidden separator { } .utility.pressed { - background-color: rgba(63, 78, 90, 68%); + background-color: var(--button-background-active); } .utility.pressed, @@ -185,7 +186,7 @@ action-bar:hidden separator { .skip { z-index: 1; - border-color: rgba(255, 255, 255, 36%); + border-color: rgba(var(--color-white-rgb), 36%); } separator { @@ -193,7 +194,7 @@ separator { flex: 0 0 1dp; width: 1dp; height: 24dp; - background-color: rgba(255, 255, 255, 18%); + background-color: rgba(var(--color-white-rgb), 18%); opacity: 1; transition: opacity 0.2s linear-in-out; } @@ -201,7 +202,7 @@ separator { .face { position: absolute; border-radius: 29dp; - font-size: 24dp; + font-size: var(--font-size-4xl); overflow: visible; } @@ -219,10 +220,10 @@ separator { min-width: 17dp; height: 15dp; padding: 1dp 3dp; - border-radius: 7dp; - background-color: rgba(0, 0, 0, 52%); - color: rgba(255, 255, 255, 92%); - font-size: 12dp; + border-radius: var(--radius-control); + background-color: rgba(var(--color-black-rgb), 52%); + color: rgba(var(--color-white-rgb), 92%); + font-size: var(--font-size-2xs); line-height: 13dp; text-align: center; } @@ -233,9 +234,9 @@ separator { bottom: -5dp; width: 34dp; height: 8dp; - padding: 2dp; + padding: var(--space-2xs); border: 1dp rgba(42, 32, 18, 82%); - border-radius: 4dp; + border-radius: var(--radius-small); background-color: rgba(18, 14, 10, 70%); /* box-shadow: 0 2dp 6dp rgba(0, 0, 0, 35%); */ } @@ -251,7 +252,7 @@ oil-fill { .face.has-item span { right: 6dp; bottom: 6dp; - color: rgba(255, 255, 255, 88%); + color: rgba(var(--color-white-rgb), 88%); } .face.a { @@ -307,7 +308,8 @@ touch-stick { height: 124dp; border-radius: 62dp; background-color: rgba(18, 20, 24, 35%); - border: 1dp rgba(255, 255, 255, 20%); + border-width: 1dp; + border-color: rgba(var(--color-white-rgb), 20%); /* backdrop-filter: blur(7dp); */ /* box-shadow: 0 8dp 24dp rgba(0, 0, 0, 24%); */ opacity: 0; @@ -326,7 +328,8 @@ stick-ring { width: 88dp; height: 88dp; border-radius: 44dp; - border: 1dp rgba(255, 255, 255, 18%); + border-width: 1dp; + border-color: rgba(var(--color-white-rgb), 18%); } stick-knob { @@ -335,5 +338,6 @@ stick-knob { height: 48dp; border-radius: 24dp; background-color: rgba(238, 236, 226, 55%); - border: 1dp rgba(255, 255, 255, 45%); + border-width: 1dp; + border-color: rgba(var(--color-white-rgb), 45%); } diff --git a/res/rml/touch_controls_editor.rcss b/res/rml/touch_controls_editor.rcss index 2ca99d935a..6b27118cb6 100644 --- a/res/rml/touch_controls_editor.rcss +++ b/res/rml/touch_controls_editor.rcss @@ -1,4 +1,7 @@ body.touch-editor { + --color-editor-accent-rgb: 255, 232, 128; + --color-editor-highlight-rgb: 255, 244, 190; + background-color: rgba(4, 6, 8, 34%); z-index: 8; } @@ -14,7 +17,7 @@ body.touch-editor .control:hover, body.touch-editor action-bar:hover, body.touch-editor .control.editor-selected, body.touch-editor action-bar.editor-selected { - border-color: rgba(255, 232, 128, 80%); + border-color: rgba(var(--color-editor-accent-rgb), 80%); filter: brightness(1.15); } @@ -27,8 +30,9 @@ selection-frame { display: none; position: absolute; z-index: 20; - border: 2dp rgba(255, 232, 128, 88%); - background-color: rgba(255, 232, 128, 7%); + border-width: 2dp; + border-color: rgba(var(--color-editor-accent-rgb), 88%); + background-color: rgba(var(--color-editor-accent-rgb), 7%); pointer-events: none; } @@ -41,7 +45,8 @@ resize-handle { position: absolute; width: 22dp; height: 22dp; - border: 2dp rgba(255, 244, 190, 96%); + border-width: 2dp; + border-color: rgba(var(--color-editor-highlight-rgb), 96%); border-radius: 11dp; background-color: rgba(34, 37, 42, 86%); pointer-events: auto; @@ -90,6 +95,12 @@ resize-handle.corner.bottom { } editor-toolbar { + --button-background: rgba(17, 19, 24, 88%); + --button-background-hover: rgba(78, 85, 96, 92%); + --button-border-color: rgba(var(--color-white-rgb), 26%); + --button-border-color-hover: rgba(var(--color-editor-highlight-rgb), 92%); + --button-color: rgba(255, 250, 232, 94%); + display: flex; position: absolute; left: 24dp; @@ -98,7 +109,7 @@ editor-toolbar { z-index: 30; height: 48dp; margin-top: -24dp; - gap: 8dp; + gap: var(--space-sm); justify-content: center; pointer-events: auto; } @@ -108,12 +119,13 @@ editor-toolbar button.editor-command { min-width: 96dp; height: 48dp; padding: 0 14dp; - border-radius: 8dp; - border: 1dp rgba(255, 255, 255, 26%); - background-color: rgba(17, 19, 24, 88%); - color: rgba(255, 250, 232, 94%); - font-family: "Fira Sans"; - font-size: 18dp; + border-radius: var(--radius-panel); + border-width: 1dp; + border-color: var(--button-border-color); + background-color: var(--button-background); + color: var(--button-color); + font-family: var(--font-family-body); + font-size: var(--font-size-xl); line-height: 48dp; opacity: 1; cursor: pointer; @@ -127,12 +139,12 @@ editor-toolbar button.editor-command span { } editor-toolbar button.editor-command.primary { - border-color: rgba(255, 232, 128, 70%); - background-color: rgba(96, 82, 38, 90%); + --button-border-color: rgba(var(--color-editor-accent-rgb), 70%); + --button-background: rgba(96, 82, 38, 90%); } editor-toolbar button.editor-command:hover, editor-toolbar button.editor-command:focus-visible { - border-color: rgba(255, 244, 190, 92%); - background-color: rgba(78, 85, 96, 92%); + border-color: var(--button-border-color-hover); + background-color: var(--button-background-hover); } diff --git a/res/rml/tuner.rcss b/res/rml/tuner.rcss index 86dd2043f6..5e0d96e855 100644 --- a/res/rml/tuner.rcss +++ b/res/rml/tuner.rcss @@ -1,90 +1,87 @@ -*, *:before, *:after { - box-sizing: border-box; -} - body { overflow: visible; width: 100%; height: 100%; margin: 0; padding: 0; - font-family: "Fira Sans Condensed"; - font-size: 24dp; - color: #FFFFFF; + font-family: var(--font-family-heading); + font-size: var(--font-size-4xl); + color: var(--color-white); display: flex; flex-direction: column; justify-content: flex-end; align-items: stretch; } -.tuner-root { +tuner-root { width: 100%; min-height: 45%; display: flex; flex-direction: column; justify-content: flex-end; align-items: stretch; - decorator: vertical-gradient(#00000000 #151610F2); + decorator: vertical-gradient(rgba(var(--color-black-rgb), 0%) rgba(var(--color-surface-rgb), 242)); filter: opacity(0); transition: filter 0.2s linear-in-out; } -.tuner-root[open] { +tuner-root[open] { filter: opacity(1); } -.tuner { +graphics-tuner { width: 100%; max-width: 1216dp; margin-left: auto; margin-right: auto; display: flex; flex-direction: column; - gap: 24dp; + gap: var(--space-xl); padding: 48dp 64dp; } @media (max-height: 800dp) { - .tuner-root { + tuner-root { min-height: 38%; } - .tuner { - gap: 16dp; - padding: 32dp 48dp; + graphics-tuner { + gap: var(--space-lg); + padding: var(--space-2xl) 48dp; } } -.header { +tuner-header { display: flex; justify-content: space-between; align-items: center; - gap: 24dp; + gap: var(--space-xl); } -.carousel-container { +carousel-container { flex: 1 1 auto; display: flex; justify-content: flex-end; min-width: 0; } -.description { - font-size: 18dp; +tuner-description { + font-size: var(--font-size-xl); line-height: 22dp; - color: rgba(255, 255, 255, 50%); + color: rgba(var(--color-white-rgb), 50%); } -.divider { +tuner-divider { margin: 1dp 0; - border-top: 1dp rgba(217, 217, 217, 50%); + border-top-width: 1dp; + border-top-color: rgba(var(--color-neutral-rgb), 50%); } -.footer { +tuner-footer { display: flex; justify-content: space-between; align-items: center; - gap: 24dp; + gap: var(--space-xl); } footer-button { @@ -94,12 +91,12 @@ footer-button { border: 0; padding: 0; background-color: transparent; - font-family: "Fira Sans Condensed"; + font-family: var(--font-family-heading); font-weight: bold; - font-size: 20dp; + font-size: var(--font-size-2xl); line-height: 24dp; text-transform: uppercase; - color: #FFFFFF; + color: var(--color-white); opacity: 1; cursor: pointer; } @@ -112,20 +109,20 @@ footer-button.reset { text-align: right; } -.stepped-carousel { +stepped-carousel { display: flex; align-items: center; justify-content: center; - gap: 16dp; + gap: var(--space-lg); width: auto; min-width: 246dp; padding: 0; background-color: transparent; - font-family: "Fira Sans Condensed"; + font-family: var(--font-family-heading); font-weight: bold; } -.stepped-carousel-value { +stepped-carousel-value { line-height: 29dp; min-width: 166dp; text-align: center; @@ -142,6 +139,6 @@ footer-button.reset { background-color: transparent; opacity: 1; cursor: pointer; - font-family: "Material Symbols Rounded"; + font-family: var(--font-family-icons); font-weight: normal; } diff --git a/res/rml/window.rcss b/res/rml/window.rcss index 98b63dfcd9..c652396726 100644 --- a/res/rml/window.rcss +++ b/res/rml/window.rcss @@ -1,17 +1,13 @@ -*, *:before, *:after { - box-sizing: border-box; -} - body { display: flex; width: 100%; height: 100%; padding: 64dp; - font-family: "Fira Sans"; + font-family: var(--font-family-body); font-weight: normal; font-style: normal; - font-size: 15dp; - color: #E0DBC8; + font-size: var(--font-size-base); + color: var(--color-text); } b { @@ -27,12 +23,13 @@ window { max-width: 1088dp; max-height: 768dp; margin: auto; - border-radius: 14dp; + border-radius: var(--radius-window); overflow: hidden; - border: 2dp #92875B; + border-width: 2dp; + border-color: var(--color-border); backdrop-filter: blur(5dp); box-shadow: 0 0 25dp 5dp; - background-color: rgba(21, 22, 16, 90%); + background-color: rgba(var(--color-surface-rgb), 90%); filter: opacity(0); transform: scale(0.9); transform-origin: center; @@ -61,7 +58,7 @@ window[open] { @media (max-height: 640dp) { body { - padding: 16dp; + padding: var(--space-lg); } window { box-shadow: none; @@ -70,7 +67,7 @@ window[open] { @media (max-width: 768dp) { body { - padding: 16dp; + padding: var(--space-lg); } window.modal { width: 100%; @@ -79,13 +76,14 @@ window[open] { } window tab-bar { - flex: 0 0 64dp; - height: 64dp; - background-color: rgba(217, 217, 217, 10%); - font-family: "Fira Sans Condensed"; + flex: 0 0 var(--toolbar-height); + height: var(--toolbar-height); + background-color: rgba(var(--color-neutral-rgb), 10%); + font-family: var(--font-family-heading); font-weight: bold; - font-size: 18dp; - border-bottom: 2dp #92875B; + font-size: var(--font-size-xl); + border-bottom-width: 2dp; + border-bottom-color: var(--color-border); } window tab-bar tab { @@ -106,14 +104,15 @@ window content pane { flex: 1 1 0; min-width: 0; min-height: 0; - padding: 24dp; - gap: 8dp; + padding: var(--space-xl); + gap: var(--space-sm); overflow: hidden auto; - font-size: 20dp; + font-size: var(--font-size-2xl); } window content pane:not(:last-of-type) { - border-right: 1dp #92875B; + border-right-width: 1dp; + border-right-color: var(--color-border); } window content pane > * { @@ -129,7 +128,7 @@ ui-list { } window content pane > ui-list, -.modal-content pane > ui-list { +modal-content pane > ui-list { flex: 1 1 0; min-width: 0; min-height: 0; @@ -146,7 +145,7 @@ ui-list-viewport { ui-list-content { display: flex; flex-flow: column; - gap: 8dp; + gap: var(--space-sm); min-width: 0; } @@ -156,7 +155,7 @@ ui-list-content > button.ui-list-row { ui-list-empty { display: block; - padding: 16dp; + padding: var(--space-lg); text-align: center; opacity: 0.45; } @@ -176,33 +175,33 @@ window content pane > ui-list:last-child { window content pane > ui-list ui-list-content, window content pane > ui-list ui-list-empty { - padding-left: 24dp; - padding-right: 24dp; + padding-left: var(--space-xl); + padding-right: var(--space-xl); } window content pane > ui-list:first-child ui-list-content, window content pane > ui-list:first-child ui-list-empty { - padding-top: 24dp; + padding-top: var(--space-xl); } window content pane > ui-list:last-child ui-list-content, window content pane > ui-list:last-child ui-list-empty { - padding-bottom: 24dp; + padding-bottom: var(--space-xl); } window content pane:last-of-type > div { line-height: 1.625; } -.data-folder-current { +data-folder-current { display: block; - font-size: 16dp; - color: rgba(224, 219, 200, 65%); + font-size: var(--font-size-md); + color: rgba(var(--color-text-rgb), 65%); } scrollbarvertical { width: 8dp; - margin: 4dp 4dp 4dp 0; + margin: var(--space-xs) var(--space-xs) var(--space-xs) 0; } scrollbarvertical sliderarrowdec, @@ -218,14 +217,14 @@ scrollbarvertical slidertrack { scrollbarvertical sliderbar { width: 8dp; min-height: 24dp; - background-color: rgba(224, 219, 200, 45%); + background-color: rgba(var(--color-text-rgb), 45%); border-radius: 2dp; transition: background-color 0.2s cubic-in-out; } scrollbarvertical sliderbar:hover, scrollbarvertical sliderbar:active { - background-color: rgba(194, 164, 45, 80%); + background-color: rgba(var(--color-accent-rgb), 80%); } scrollbarhorizontal { @@ -244,26 +243,27 @@ scrollbarhorizontal sliderbar { height: 0; } -.section-heading { - font-family: "Fira Sans Condensed"; +section-heading { + font-family: var(--font-family-heading); font-weight: bold; text-transform: uppercase; - font-size: 22dp; + font-size: var(--font-size-3xl); opacity: 0.25; } -.section-heading:not(:first-of-type) { - padding-top: 12dp; +section-heading:not(:first-of-type) { + padding-top: var(--space-md); } button { text-align: center; - background-color: rgba(17, 16, 10, 20%); + color: var(--button-color); + background-color: var(--button-background); opacity: 0.9; - padding: 8dp 16dp; - border-radius: 14dp; + padding: var(--space-sm) var(--space-lg); + border-radius: var(--radius-window); box-shadow: rgba(146, 135, 91, 25%) 0 0 0 1dp; - font-size: 20dp; + font-size: var(--font-size-2xl); transition: background-color 0.1s linear-in-out, opacity 0.1s linear-in-out; cursor: pointer; focus: auto; @@ -271,18 +271,18 @@ button { button:not(:disabled):hover, button:not(:disabled):focus-visible { - background-color: rgba(204, 184, 119, 20%); + background-color: var(--button-background-hover); box-shadow: #C2A42D 0 0 0 2dp; } button:not(:disabled):selected { opacity: 1; - background-color: rgba(204, 184, 119, 40%); + background-color: var(--button-background-selected); } button:not(:disabled):active { opacity: 1; - background-color: rgba(204, 184, 119, 40%); + background-color: var(--button-background-active); box-shadow: #C2A42D 0 0 0 2dp; } @@ -299,11 +299,12 @@ button.modal-btn { select-button { display: flex; align-items: center; - gap: 8dp; - background-color: rgba(17, 16, 10, 20%); + gap: var(--space-sm); + color: var(--button-color); + background-color: var(--button-background); opacity: 0.9; - padding: 8dp 16dp; - border-radius: 14dp; + padding: var(--space-sm) var(--space-lg); + border-radius: var(--radius-window); box-shadow: rgba(146, 135, 91, 25%) 0 0 0 1dp; transition: background-color 0.1s linear-in-out, opacity 0.1s linear-in-out; cursor: pointer; @@ -312,18 +313,18 @@ select-button { select-button:not(:disabled):hover, select-button:not(:disabled):focus-visible { - background-color: rgba(204, 184, 119, 20%); + background-color: var(--button-background-hover); box-shadow: #C2A42D 0 0 0 2dp; } select-button:not(:disabled):selected { opacity: 1; - background-color: rgba(204, 184, 119, 40%); + background-color: var(--button-background-selected); } select-button:not(:disabled):active { opacity: 1; - background-color: rgba(204, 184, 119, 40%); + background-color: var(--button-background-active); box-shadow: #C2A42D 0 0 0 2dp; } @@ -333,9 +334,9 @@ select-button:disabled { } select-button key { - font-family: "Fira Sans Condensed"; + font-family: var(--font-family-heading); font-weight: bold; - font-size: 18dp; + font-size: var(--font-size-xl); text-transform: uppercase; flex: 0 1 auto; } @@ -343,7 +344,7 @@ select-button key { select-button value { flex: 1 1 auto; text-align: right; - font-size: 20dp; + font-size: var(--font-size-2xl); } select-button value.modified { @@ -352,7 +353,7 @@ select-button value.modified { select-button input { text-align: right; - font-size: 20dp; + font-size: var(--font-size-2xl); } select-button.group-button icon { @@ -360,7 +361,7 @@ select-button.group-button icon { margin-left: auto; width: 24dp; height: 24dp; - font-size: 24dp; + font-size: var(--font-size-4xl); color: inherit; decorator: text("" center center); } @@ -371,8 +372,8 @@ select-button.group-button value { select-button.color-input value { min-width: 0; - font-family: "Noto Mono"; - font-size: 15dp; + font-family: var(--font-family-monospace); + font-size: var(--font-size-base); } select-button.color-input color-swatch { @@ -380,19 +381,19 @@ select-button.color-input color-swatch { flex: 0 0 48dp; width: 48dp; height: 24dp; - border-radius: 7dp; + border-radius: var(--radius-control); box-shadow: rgba(255, 255, 255, 45%) 0 0 0 1dp; } select-button.color-input color-swatch.empty { - background-color: rgba(224, 219, 200, 12%); - decorator: linear-gradient(135deg, rgba(224, 219, 200, 0) 45%, rgba(194, 164, 45, 70%) 48%, rgba(194, 164, 45, 70%) 52%, rgba(224, 219, 200, 0) 55%); + background-color: rgba(var(--color-text-rgb), 12%); + decorator: linear-gradient(135deg, rgba(var(--color-text-rgb), 0) 45%, rgba(var(--color-accent-rgb), 70%) 48%, rgba(var(--color-accent-rgb), 70%) 52%, rgba(var(--color-text-rgb), 0) 55%); } icon { width: 1em; height: 1em; - font-family: "Material Symbols Rounded"; + font-family: var(--font-family-icons); font-weight: normal; display: inline-block; vertical-align: middle; @@ -418,73 +419,74 @@ icon.question-mark { decorator: text("" center center); } -.achievement-total { +achievement-total { position: absolute; top: 0; - right: 64dp; - height: 64dp; - line-height: 64dp; - font-family: "Fira Sans Condensed"; + right: var(--toolbar-height); + height: var(--toolbar-height); + line-height: var(--toolbar-height); + font-family: var(--font-family-heading); font-weight: bold; - font-size: 16dp; - color: rgba(224, 219, 200, 55%); + font-size: var(--font-size-md); + color: rgba(var(--color-text-rgb), 55%); pointer-events: none; } -.achievement-row { +achievement-row { display: flex; align-items: flex-start; gap: 10dp; - padding: 12dp 0; - border-bottom: 1dp rgba(146, 135, 91, 30%); + padding: var(--space-md) 0; + border-bottom-width: 1dp; + border-bottom-color: rgba(var(--color-border-rgb), 30%); } -.achievement-info { +achievement-info { display: block; flex: 1 1 0; min-width: 0; } -.achievement-header { +achievement-header { display: flex; align-items: center; } -.achievement-name { +achievement-name { flex: 1; font-weight: bold; } -.achievement-name.unlocked { - color: #ffa826; +achievement-name.unlocked { + color: var(--color-warning); } -.achievement-badge { - font-size: 14dp; +achievement-badge { + font-size: var(--font-size-sm); opacity: 0.7; } -.achievement-badge.unlocked { - color: #44cc55; +achievement-badge.unlocked { + color: var(--color-success); opacity: 1; } -.achievement-badge.locked { - color: #cc4444; +achievement-badge.locked { + color: var(--color-error); opacity: 1; } .achievement-desc { display: block; - color: rgba(224, 219, 200, 55%); - font-size: 16dp; - margin: 4dp 0 0 0; + color: rgba(var(--color-text-rgb), 55%); + font-size: var(--font-size-md); + margin: var(--space-xs) 0 0 0; } -.achievement-progress { +achievement-progress { display: block; - font-size: 13dp; - color: rgba(224, 219, 200, 45%); + font-size: var(--font-size-xs); + color: rgba(var(--color-text-rgb), 45%); } progress { @@ -492,12 +494,12 @@ progress { width: 100%; height: 6dp; border-radius: 3dp; - background-color: rgba(255, 255, 255, 10%); - margin: 6dp 0 2dp 0; + background-color: rgba(var(--color-white-rgb), 10%); + margin: 6dp 0 var(--space-2xs) 0; } progress fill { - background-color: rgba(194, 164, 45, 80%); + background-color: rgba(var(--color-accent-rgb), 80%); border-radius: 3dp; } @@ -512,12 +514,12 @@ progress.progress-ongoing fill { button.achievement-clear { flex: 0 0 auto; align-self: center; - font-size: 14dp; - padding: 2dp 8dp; + font-size: var(--font-size-sm); + padding: var(--space-2xs) var(--space-sm); opacity: 0.45; } -.preset-grid { +preset-grid { display: flex; flex-direction: row; gap: 20dp; @@ -526,24 +528,24 @@ button.achievement-clear { width: 100%; } -.preset-col { +preset-option { display: flex; flex-flow: column; - gap: 12dp; + gap: var(--space-md); flex: 1 1 0; } -.preset-desc { +preset-description { display: block; - font-size: 16dp; + font-size: var(--font-size-md); text-align: center; } -.modal-dialog { +modal-dialog { display: flex; flex-direction: column; align-items: flex-start; - padding: 24dp; + padding: var(--space-xl); gap: 20dp; flex: 0 1 auto; min-height: 0; @@ -556,52 +558,52 @@ window.modal.danger { border: 2dp #852221; } -.modal-header { +modal-header { display: flex; flex-direction: row; align-items: center; justify-content: space-between; width: 100%; flex: 0 0 auto; - gap: 16dp; + gap: var(--space-lg); } -.modal-header icon { - font-size: 24dp; - color: #92875B; +modal-header icon { + font-size: var(--font-size-4xl); + color: var(--color-border); } -.modal-title { +modal-title { display: block; - font-family: "Fira Sans Condensed"; + font-family: var(--font-family-heading); font-weight: bold; text-transform: uppercase; - font-size: 18dp; - color: #92875B; + font-size: var(--font-size-xl); + color: var(--color-border); flex: 1 1 auto; } -window.modal.danger .modal-title, -window.modal.danger .modal-header icon { +window.modal.danger modal-title, +window.modal.danger modal-header icon { color: #B3261E; } -.modal-body { +modal-body { display: block; width: 100%; flex: 0 0 auto; min-width: 0; - font-size: 20dp; - color: #FFFFFF; + font-size: var(--font-size-2xl); + color: var(--color-white); font-weight: normal; } -.modal-body span.tip { - font-size: 14dp; - color: #92875B; +modal-body modal-tip { + font-size: var(--font-size-sm); + color: var(--color-border); } -.modal-content { +modal-content { display: none; width: 100%; flex: 1 1 auto; @@ -609,87 +611,87 @@ window.modal.danger .modal-header icon { overflow: hidden; } -.modal-content.active { +modal-content.active { display: flex; flex-direction: column; } -.modal-content pane { +modal-content pane { display: flex; flex: 1 1 auto; flex-direction: column; min-height: 0; width: 100%; - gap: 8dp; + gap: var(--space-sm); overflow: hidden auto; } -.modal-content pane > * { +modal-content pane > * { flex: 0 0 auto; } -.verification-progress { +verification-progress { display: flex; flex-direction: column; gap: 10dp; width: 100%; } -.verification-file { +verification-file { display: block; - font-size: 17dp; - color: #FFFFFF; + font-size: var(--font-size-lg); + color: var(--color-white); } progress.verification-progress-bar { height: 8dp; - margin: 2dp 0 0 0; + margin: var(--space-2xs) 0 0 0; } -.verification-detail { +verification-detail { display: block; - font-size: 14dp; - color: rgba(224, 219, 200, 65%); + font-size: var(--font-size-sm); + color: rgba(var(--color-text-rgb), 65%); } -.modal-actions { +modal-actions { display: flex; flex-direction: row; flex-wrap: nowrap; align-items: stretch; - gap: 12dp; + gap: var(--space-md); width: 100%; flex: 0 0 auto; - padding-top: 4dp; + padding-top: var(--space-xs); } -.modal-actions-vertical { +modal-actions.vertical { flex-direction: column; align-items: stretch; } -.modal-actions-vertical button.modal-btn { +modal-actions.vertical button.modal-btn { flex: 0 0 auto; width: 100%; } @media (max-height: 640dp) { - .modal-dialog { - padding: 16dp; - gap: 12dp; + modal-dialog { + padding: var(--space-lg); + gap: var(--space-md); } - .modal-body { - font-size: 17dp; + modal-body { + font-size: var(--font-size-lg); } } @media (max-width: 640dp) { - .modal-actions { + modal-actions { flex-direction: column; } - .modal-actions button.modal-btn { + modal-actions button.modal-btn { flex: 0 0 auto; width: 100%; } diff --git a/src/dusk/mods/catalog.cpp b/src/dusk/mods/catalog.cpp new file mode 100644 index 0000000000..bda411ca37 --- /dev/null +++ b/src/dusk/mods/catalog.cpp @@ -0,0 +1,456 @@ +#include "catalog.hpp" + +#include "dusk/app_info.hpp" +#include "fmt/format.h" +#include "nlohmann/json.hpp" + +#include +#include +#include +#include +#include + +#if defined(__APPLE__) +#include +#endif + +namespace dusk::mods::catalog { +namespace { + +using json = nlohmann::json; +using namespace std::chrono_literals; + +constexpr std::string_view catalogUrl = + "https://staging.twilitrealm.workers.dev/api/v1/games/dusklight/mods"; + +std::string_view sort_value(Sort sort) noexcept { + switch (sort) { + case Sort::Endorsements: + return "endorsements"; + case Sort::Updated: + return "updated"; + case Sort::Newest: + return "newest"; + case Sort::Name: + return "name"; + case Sort::Downloads: + default: + return "downloads"; + } +} + +std::string_view catalog_platform() noexcept { +#if defined(_WIN32) && defined(_M_ARM64) + return "windows-arm64"; +#elif defined(_WIN32) && defined(_M_X64) + return "windows-amd64"; +#elif defined(__ANDROID__) && defined(__aarch64__) + return "android-aarch64"; +#elif defined(__APPLE__) && TARGET_OS_IOS + return "ios-arm64"; +#elif defined(__APPLE__) && !TARGET_OS_TV && defined(__aarch64__) + return "macos-arm64"; +#elif defined(__APPLE__) && !TARGET_OS_TV && defined(__x86_64__) + return "macos-x86_64"; +#elif defined(__linux__) && defined(__aarch64__) + return "linux-aarch64"; +#elif defined(__linux__) && defined(__x86_64__) + return "linux-x86_64"; +#else + // The catalog rejects platforms outside its published package matrix. + return {}; +#endif +} + +std::string url_encode(std::string_view value) { + constexpr char hex[] = "0123456789ABCDEF"; + std::string encoded; + encoded.reserve(value.size()); + for (const unsigned char c : value) { + const bool unreserved = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9') || c == '-' || c == '.' || c == '_' || + c == '~'; + if (unreserved) { + encoded.push_back(static_cast(c)); + } else { + encoded.push_back('%'); + encoded.push_back(hex[c >> 4]); + encoded.push_back(hex[c & 0x0f]); + } + } + return encoded; +} + +void append_query(std::string& url, std::string_view name, std::string_view value) { + url.push_back(url.find('?') == std::string::npos ? '?' : '&'); + url.append(name); + url.push_back('='); + url.append(url_encode(value)); +} + +std::string make_url(const Query& query) { + std::string url{catalogUrl}; + if (!query.search.empty()) { + append_query(url, "q", query.search); + } + if (!query.category.empty()) { + append_query(url, "category", query.category); + } + append_query(url, "sort", sort_value(query.sort)); + append_query(url, "page", fmt::format("{}", std::max(query.page, 1))); + if (query.thisDevice) { + const auto platform = catalog_platform(); + if (!platform.empty()) { + append_query(url, "platform", platform); + } + } + return url; +} + +std::string make_detail_url(std::string_view id) { + return fmt::format("{}/{}", catalogUrl, url_encode(id)); +} + +const json& required_field(const json& object, const char* name) { + if (!object.is_object()) { + throw std::runtime_error{"expected an object"}; + } + const auto iter = object.find(name); + if (iter == object.end()) { + throw std::runtime_error{fmt::format("missing field '{}'", name)}; + } + return *iter; +} + +std::string required_string(const json& object, const char* name) { + const auto& value = required_field(object, name); + if (!value.is_string()) { + throw std::runtime_error{fmt::format("field '{}' is not a string", name)}; + } + return value.get(); +} + +bool required_bool(const json& object, const char* name) { + const auto& value = required_field(object, name); + if (!value.is_boolean()) { + throw std::runtime_error{fmt::format("field '{}' is not a boolean", name)}; + } + return value.get(); +} + +std::uint64_t required_count(const json& object, const char* name) { + const auto& value = required_field(object, name); + if (value.is_number_unsigned()) { + return value.get(); + } + if (value.is_number_integer()) { + const auto count = value.get(); + if (count >= 0) { + return static_cast(count); + } + } + throw std::runtime_error{fmt::format("field '{}' is not a non-negative integer", name)}; +} + +int required_int(const json& object, const char* name) { + const std::uint64_t value = required_count(object, name); + if (value > static_cast(std::numeric_limits::max())) { + throw std::runtime_error{fmt::format("field '{}' is too large", name)}; + } + return static_cast(value); +} + +std::optional optional_string(const json& object, const char* name) { + const auto& value = required_field(object, name); + if (value.is_null()) { + return std::nullopt; + } + if (!value.is_string()) { + throw std::runtime_error{fmt::format("field '{}' is not a string or null", name)}; + } + return value.get(); +} + +std::uint16_t required_u16(const json& object, const char* name) { + const auto value = required_count(object, name); + if (value > std::numeric_limits::max()) { + throw std::runtime_error{fmt::format("field '{}' is too large", name)}; + } + return static_cast(value); +} + +Image parse_image(const json& value) { + const auto width = required_count(value, "width"); + const auto height = required_count(value, "height"); + if (width > std::numeric_limits::max() || + height > std::numeric_limits::max()) + { + throw std::runtime_error{"image dimensions are too large"}; + } + Image image{ + .width = static_cast(width), + .height = static_cast(height), + }; + const auto& sources = required_field(value, "sources"); + if (!sources.is_array()) { + throw std::runtime_error{"field 'sources' is not an array"}; + } + image.sources.reserve(sources.size()); + for (const auto& source : sources) { + const auto sourceWidth = required_count(source, "width"); + if (sourceWidth > std::numeric_limits::max()) { + throw std::runtime_error{"image source width is too large"}; + } + image.sources.push_back({ + .width = static_cast(sourceWidth), + .pngUrl = required_string(source, "png_url"), + }); + } + if (image.sources.empty()) { + throw std::runtime_error{"image has no sources"}; + } + return image; +} + +Category parse_category(const json& value) { + return { + .slug = required_string(value, "slug"), + .name = required_string(value, "name"), + .modCount = required_count(value, "mod_count"), + }; +} + +Category parse_mod_category(const json& value) { + return { + .slug = required_string(value, "slug"), + .name = required_string(value, "name"), + }; +} + +Tag parse_tag(const json& value) { + return { + .slug = required_string(value, "slug"), + .name = required_string(value, "name"), + }; +} + +Author parse_author(const json& value) { + return { + .name = required_string(value, "name"), + .handle = required_string(value, "handle"), + .official = required_bool(value, "official"), + }; +} + +Mod parse_mod(const json& value) { + Mod mod{ + .id = required_string(value, "id"), + .name = required_string(value, "name"), + .version = required_string(value, "version"), + .author = parse_author(required_field(value, "author")), + .summary = required_string(value, "summary"), + .downloads = required_count(value, "downloads"), + .endorsements = required_count(value, "endorsements"), + .publishedAt = required_string(value, "published_at"), + .updatedAt = required_string(value, "updated_at"), + .packageSize = required_count(value, "package_size"), + .containsNativeCode = required_bool(value, "contains_native_code"), + }; + + const auto& category = required_field(value, "category"); + if (!category.is_null()) { + mod.category = parse_mod_category(category); + } + + const auto& tags = required_field(value, "tags"); + if (!tags.is_array()) { + throw std::runtime_error{"field 'tags' is not an array"}; + } + mod.tags.reserve(tags.size()); + for (const auto& tag : tags) { + mod.tags.push_back(parse_tag(tag)); + } + + const auto& platforms = required_field(value, "supported_platforms"); + if (!platforms.is_array()) { + throw std::runtime_error{"field 'supported_platforms' is not an array"}; + } + mod.supportedPlatforms.reserve(platforms.size()); + for (const auto& platform : platforms) { + if (!platform.is_string()) { + throw std::runtime_error{"supported platform is not a string"}; + } + mod.supportedPlatforms.push_back(platform.get()); + } + + const auto& icon = required_field(value, "icon"); + if (!icon.is_null()) { + mod.icon = parse_image(icon); + } + const auto& banner = required_field(value, "banner"); + if (!banner.is_null()) { + mod.banner = parse_image(banner); + } + return mod; +} + +Detail parse_detail(std::string_view body) { + const json root = json::parse(body); + Detail detail{ + .mod = parse_mod(root), + .slug = required_string(root, "slug"), + .siteUrl = required_string(root, "site_url"), + .sourceUrl = optional_string(root, "source_url"), + .license = optional_string(root, "license"), + .descriptionHtml = required_string(root, "description_html"), + .changelogHtml = required_string(root, "changelog_html"), + .packageSha256 = required_string(root, "package_sha256"), + }; + + const auto& modAbi = required_field(root, "mod_abi"); + if (!modAbi.is_null()) { + const auto value = required_count(root, "mod_abi"); + if (value > std::numeric_limits::max()) { + throw std::runtime_error{"field 'mod_abi' is too large"}; + } + detail.modAbi = static_cast(value); + } + + const auto& banner = required_field(root, "banner"); + if (!banner.is_null()) { + detail.banner = parse_image(banner); + } + + const auto& screenshots = required_field(root, "screenshots"); + if (!screenshots.is_array()) { + throw std::runtime_error{"field 'screenshots' is not an array"}; + } + detail.screenshots.reserve(screenshots.size()); + for (const auto& screenshot : screenshots) { + detail.screenshots.push_back({ + .altText = required_string(screenshot, "alt_text"), + .image = parse_image(required_field(screenshot, "image")), + }); + } + + const auto& imports = required_field(root, "service_imports"); + if (!imports.is_array()) { + throw std::runtime_error{"field 'service_imports' is not an array"}; + } + detail.serviceImports.reserve(imports.size()); + for (const auto& import : imports) { + detail.serviceImports.push_back({ + .id = required_string(import, "id"), + .major = required_u16(import, "major"), + .minMinor = required_u16(import, "min_minor"), + .optional = required_bool(import, "optional"), + }); + } + return detail; +} + +Page parse_page(std::string_view body) { + const json root = json::parse(body); + const auto& game = required_field(root, "game"); + if (required_string(game, "id") != "dusklight") { + throw std::runtime_error{"catalog response is for a different game"}; + } + + Page page; + const auto& categories = required_field(root, "categories"); + if (!categories.is_array()) { + throw std::runtime_error{"field 'categories' is not an array"}; + } + page.categories.reserve(categories.size()); + for (const auto& category : categories) { + page.categories.push_back(parse_category(category)); + } + + const auto& mods = required_field(root, "mods"); + if (!mods.is_array()) { + throw std::runtime_error{"field 'mods' is not an array"}; + } + page.mods.reserve(mods.size()); + for (const auto& mod : mods) { + page.mods.push_back(parse_mod(mod)); + } + + const auto& pagination = required_field(root, "pagination"); + page.pagination = { + .page = required_int(pagination, "page"), + .pageSize = required_int(pagination, "page_size"), + .pageCount = required_int(pagination, "page_count"), + .total = required_count(pagination, "total"), + }; + return page; +} + +std::string api_error(const borealis::http::Response& response) { + try { + const auto body = json::parse(response.body); + const auto& error = required_field(body, "error"); + return required_string(error, "message"); + } catch (...) { + return fmt::format("The catalog returned HTTP {}.", response.statusCode); + } +} + +FetchResult finish_request(borealis::http::Result result) { + if (result.error != borealis::http::Error::None) { + return {.error = result.message.empty() ? "The catalog request failed." : + std::move(result.message)}; + } + if (result.response.statusCode != 200) { + return {.error = api_error(result.response)}; + } + try { + return {.page = parse_page(result.response.body)}; + } catch (const std::exception& exception) { + return {.error = fmt::format("The catalog response was invalid: {}", exception.what())}; + } catch (...) { + return {.error = "The catalog response was invalid."}; + } +} + +DetailFetchResult finish_detail_request(borealis::http::Result result) { + if (result.error != borealis::http::Error::None) { + return {.error = + result.message.empty() ? "The mod request failed." : std::move(result.message)}; + } + if (result.response.statusCode != 200) { + return {.error = api_error(result.response)}; + } + try { + return {.detail = parse_detail(result.response.body)}; + } catch (const std::exception& exception) { + return {.error = fmt::format("The mod response was invalid: {}", exception.what())}; + } catch (...) { + return {.error = "The mod response was invalid."}; + } +} + +borealis::http::Request make_request(std::string url) { + return { + .url = std::move(url), + .headers = + { + {.name = "User-Agent", .value = borealis::user_agent(dusk::AppInfo)}, + {.name = "Accept", .value = "application/json"}, + }, + .connectTimeout = 10s, + .idleTimeout = 10s, + .totalTimeout = 20s, + }; +} + +} // namespace + +borealis::Task fetch_page(Query query) { + return borealis::http::start(make_request(make_url(query))).map(finish_request); +} + +borealis::Task fetch_detail(std::string id) { + return borealis::http::start(make_request(make_detail_url(id))).map(finish_detail_request); +} + +} // namespace dusk::mods::catalog diff --git a/src/dusk/mods/catalog.hpp b/src/dusk/mods/catalog.hpp new file mode 100644 index 0000000000..b819525d2b --- /dev/null +++ b/src/dusk/mods/catalog.hpp @@ -0,0 +1,131 @@ +#pragma once + +#include + +#include +#include +#include +#include + +namespace dusk::mods::catalog { + +enum class Sort { + Downloads, + Endorsements, + Updated, + Newest, + Name, +}; + +struct Query { + std::string search; + std::string category; + Sort sort = Sort::Downloads; + int page = 1; + bool thisDevice = true; +}; + +struct Category { + std::string slug; + std::string name; + std::uint64_t modCount = 0; +}; + +struct Tag { + std::string slug; + std::string name; +}; + +struct Author { + std::string name; + std::string handle; + bool official = false; +}; + +struct ImageSource { + std::uint32_t width = 0; + std::string pngUrl; +}; + +struct Image { + std::uint32_t width = 0; + std::uint32_t height = 0; + std::vector sources; +}; + +struct Mod { + std::string id; + std::string name; + std::string version; + Author author; + std::string summary; + std::optional category; + std::vector tags; + std::uint64_t downloads = 0; + std::uint64_t endorsements = 0; + std::string publishedAt; + std::string updatedAt; + std::uint64_t packageSize = 0; + bool containsNativeCode = false; + std::vector supportedPlatforms; + std::optional icon; + std::optional banner; +}; + +struct Screenshot { + std::string altText; + Image image; +}; + +struct ServiceImport { + std::string id; + std::uint16_t major = 0; + std::uint16_t minMinor = 0; + bool optional = false; +}; + +struct Detail { + Mod mod; + std::string slug; + std::string siteUrl; + std::optional sourceUrl; + std::optional license; + std::string descriptionHtml; + std::string changelogHtml; + std::string packageSha256; + std::optional modAbi; + std::optional banner; + std::vector screenshots; + std::vector serviceImports; +}; + +struct Pagination { + int page = 1; + int pageSize = 0; + int pageCount = 0; + std::uint64_t total = 0; +}; + +struct Page { + std::vector categories; + std::vector mods; + Pagination pagination; +}; + +struct FetchResult { + std::optional page; + std::string error; +}; + +struct DetailFetchResult { + std::optional detail; + std::string error; +}; + +/** Fetches one filtered page from the configured Dusklight catalog. */ +borealis::Task fetch_page(Query query); + +/** Fetches the full catalog record for one mod. */ +borealis::Task fetch_detail(std::string id); + +} // namespace dusk::mods::catalog diff --git a/src/dusk/mods/svc/ui.cpp b/src/dusk/mods/svc/ui.cpp index 75e1d514cf..1a429e90f2 100644 --- a/src/dusk/mods/svc/ui.cpp +++ b/src/dusk/mods/svc/ui.cpp @@ -114,9 +114,10 @@ struct UiSlot { std::string styleId; // Cached rendered values for element setters. These make the natural "set every update" // style cheap when the displayed value has not changed. - std::string elementRml; + std::string elementValue; float elementFloat = 0.0f; bool hasElementValue = false; + bool elementValueIsRml = false; }; SlotMap s_slots; @@ -550,7 +551,7 @@ ModResult ui_pane_add_text(LoadedMod& mod, uint64_t pane, const char* text, uint auto* elem = slot->pane->add_text(text); if (outElem != nullptr) { auto& elemSlot = alloc_slot(mod, UiSlotKind::Text, *outElem); - elemSlot.elementRml = ui::escape(text); + elemSlot.elementValue = text; elemSlot.hasElementValue = true; track_element(*outElem, elemSlot, *elem); } @@ -565,8 +566,9 @@ ModResult ui_pane_add_rml(LoadedMod& mod, uint64_t pane, const char* rml, uint64 auto* elem = slot->pane->add_rml(rml); if (outElem != nullptr) { auto& elemSlot = alloc_slot(mod, UiSlotKind::Text, *outElem); - elemSlot.elementRml = rml; + elemSlot.elementValue = rml; elemSlot.hasElementValue = true; + elemSlot.elementValueIsRml = true; track_element(*outElem, elemSlot, *elem); } return MOD_OK; @@ -797,13 +799,13 @@ ModResult ui_elem_set_text(LoadedMod& mod, uint64_t elem, const char* text) { if (slot == nullptr) { return MOD_INVALID_ARGUMENT; } - const std::string rml = ui::escape(text); - if (slot->hasElementValue && slot->elementRml == rml) { + if (slot->hasElementValue && !slot->elementValueIsRml && slot->elementValue == text) { return MOD_OK; } - slot->elementRml = rml; + slot->elementValue = text; slot->hasElementValue = true; - slot->element->SetInnerRML(slot->elementRml); + slot->elementValueIsRml = false; + ui::set_text_content(slot->element, slot->elementValue); return MOD_OK; } @@ -812,11 +814,12 @@ ModResult ui_elem_set_rml(LoadedMod& mod, uint64_t elem, const char* rml) { if (slot == nullptr) { return MOD_INVALID_ARGUMENT; } - if (slot->hasElementValue && slot->elementRml == rml) { + if (slot->hasElementValue && slot->elementValueIsRml && slot->elementValue == rml) { return MOD_OK; } - slot->elementRml = rml; + slot->elementValue = rml; slot->hasElementValue = true; + slot->elementValueIsRml = true; slot->element->SetInnerRML(rml); return MOD_OK; } @@ -940,7 +943,7 @@ ModResult ui_dialog_push(LoadedMod& mod, const UiDialogDesc& desc, uint64_t& out default: break; } - props.title = ui::escape(desc.title); + props.title = desc.title; props.bodyRml = desc.body_rml; props.icon = desc.icon != nullptr ? desc.icon : defaultIcon; props.onDismiss = [modPtr = &mod, handle, fn = desc.on_dismiss, userData = desc.user_data]( diff --git a/src/dusk/ui/achievements.cpp b/src/dusk/ui/achievements.cpp index 905fa57f59..ec27248431 100644 --- a/src/dusk/ui/achievements.cpp +++ b/src/dusk/ui/achievements.cpp @@ -16,47 +16,39 @@ struct CategoryInfo { }; constexpr CategoryInfo kCategories[] = { - {AchievementCategory::Challenge, "Challenge"}, + {AchievementCategory::Challenge, "Challenge"}, {AchievementCategory::Collection, "Collection"}, - {AchievementCategory::Minigame, "Minigame"}, - {AchievementCategory::Misc, "Misc"}, - {AchievementCategory::Glitched, "Glitched"}, + {AchievementCategory::Minigame, "Minigame"}, + {AchievementCategory::Misc, "Misc"}, + {AchievementCategory::Glitched, "Glitched"}, }; -Rml::String build_achievement_info_rml(const Achievement& a) { - Rml::String s = fmt::format( - R"(
)" - R"({})" - R"({})" - R"(
)" - R"(

{}

)", - a.unlocked ? " unlocked" : "", - a.name, - a.unlocked ? " unlocked" : " locked", - a.unlocked ? "Unlocked" : "Locked", - a.description - ); +void append_achievement_info(Rml::Element* parent, const Achievement& a) { + auto* header = append(parent, "achievement-header"); + auto* name = append(header, "achievement-name"); + name->SetClass("unlocked", a.unlocked); + append_text(name, a.name); + auto* badge = append(header, "achievement-badge"); + badge->SetClass(a.unlocked ? "unlocked" : "locked", true); + append_text(badge, a.unlocked ? "Unlocked" : "Locked"); + auto* description = append(parent, "p"); + description->SetClass("achievement-desc", true); + append_text(description, a.description); if (a.isCounter) { - float fraction = a.goal > 0 ? float(a.progress) / float(a.goal) : 1.0f; - s += fmt::format( - R"()" - R"({} / {})", - fraction, - a.unlocked ? "progress-done" : "progress-ongoing", - a.progress, - a.goal - ); + const float fraction = a.goal > 0 ? float(a.progress) / float(a.goal) : 1.0f; + auto* progress = append(parent, "progress"); + progress->SetAttribute("value", fraction); + progress->SetClass(a.unlocked ? "progress-done" : "progress-ongoing", true); + append_text( + append(parent, "achievement-progress"), fmt::format("{} / {}", a.progress, a.goal)); } - - return s; } class AchievementRow : public FluentComponent { public: AchievementRow(Rml::Element* parent, const Achievement& a) - : FluentComponent(createRowRoot(parent)) - { + : FluentComponent(createRowRoot(parent)) { auto& btn = add_child - + @@ -329,7 +330,7 @@ void apply_disc_verification_result(const DiscVerificationResult& result) { state.pendingDiscPath = result.path; state.pendingDiscInfo = result.info; state.pendingDiscValidation = result.validation; - state.errorString = escape(get_error_msg(result.validation)); + state.errorString = get_error_msg(result.validation); return; } @@ -345,41 +346,34 @@ void apply_disc_verification_result(const DiscVerificationResult& result) { state.pendingDiscPath.clear(); state.pendingDiscInfo = {}; state.pendingDiscValidation = iso::ValidationError::Unknown; - state.errorString = escape(get_error_msg(result.validation)); + state.errorString = get_error_msg(result.validation); } class DiscVerificationModal : public WindowSmall { public: - DiscVerificationModal() : WindowSmall("modal", "modal-dialog") { - auto* header = append(mDialog, "div"); - header->SetClass("modal-header", true); + DiscVerificationModal() : WindowSmall("modal") { + auto* header = append(mDialog, "modal-header"); - auto* title = append(header, "div"); - title->SetClass("modal-title", true); - title->SetInnerRML("Verifying disc image"); + auto* title = append(header, "modal-title"); + append_text(title, "Verifying disc image"); auto* icon = append(header, "icon"); icon->SetClass("verifying", true); - auto* body = append(mDialog, "div"); - body->SetClass("modal-body", true); + auto* body = append(mDialog, "modal-body"); - auto* content = append(body, "div"); - content->SetClass("verification-progress", true); + auto* content = append(body, "verification-progress"); - mFileName = append(content, "div"); - mFileName->SetClass("verification-file", true); + mFileName = append(content, "verification-file"); mProgress = append(content, "progress"); mProgress->SetClass("progress-ongoing", true); mProgress->SetClass("verification-progress-bar", true); mProgress->SetAttribute("value", 0.f); - mDetail = append(content, "div"); - mDetail->SetClass("verification-detail", true); + mDetail = append(content, "verification-detail"); - auto* actions = append(mDialog, "div"); - actions->SetClass("modal-actions", true); + auto* actions = append(mDialog, "modal-actions"); mCancelButton = std::make_unique