diff --git a/CMakeLists.txt b/CMakeLists.txt index 225a1f0a..2245bc7f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.24) -project(metalsharp VERSION 0.56.8 LANGUAGES C CXX OBJC OBJCXX) +project(metalsharp VERSION 0.57.0 LANGUAGES C CXX OBJC OBJCXX) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) diff --git a/app/package-lock.json b/app/package-lock.json index 69582756..31f680b3 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -1,12 +1,12 @@ { "name": "metalsharp", - "version": "0.56.8", + "version": "0.57.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "metalsharp", - "version": "0.56.8", + "version": "0.57.0", "dependencies": { "electron-store": "^10.0.0" }, diff --git a/app/package.json b/app/package.json index 7bc77b62..b9ac5366 100644 --- a/app/package.json +++ b/app/package.json @@ -1,6 +1,6 @@ { "name": "metalsharp", - "version": "0.56.8", + "version": "0.57.0", "description": "MetalSharp — D3D→Metal translation layer frontend", "author": "MetalSharp", "repository": { diff --git a/app/src-rust/Cargo.lock b/app/src-rust/Cargo.lock index a5bb9fa4..b0969dfc 100644 --- a/app/src-rust/Cargo.lock +++ b/app/src-rust/Cargo.lock @@ -554,7 +554,7 @@ checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" [[package]] name = "metalsharp-backend" -version = "0.56.8" +version = "0.57.0" dependencies = [ "ctrlc", "dirs", diff --git a/app/src-rust/Cargo.toml b/app/src-rust/Cargo.toml index 4a65a758..c24c94c3 100644 --- a/app/src-rust/Cargo.toml +++ b/app/src-rust/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "metalsharp-backend" -version = "0.56.8" +version = "0.57.0" edition = "2021" [dependencies] diff --git a/app/src/renderer/assets/dragonfruit-flesh.png b/app/src/renderer/assets/dragonfruit-flesh.png new file mode 100644 index 00000000..aaa8fe7c Binary files /dev/null and b/app/src/renderer/assets/dragonfruit-flesh.png differ diff --git a/app/src/renderer/assets/forest-bark.png b/app/src/renderer/assets/forest-bark.png new file mode 100644 index 00000000..71e8afdb Binary files /dev/null and b/app/src/renderer/assets/forest-bark.png differ diff --git a/app/src/renderer/assets/orange-peel.png b/app/src/renderer/assets/orange-peel.png new file mode 100644 index 00000000..087fe9f1 Binary files /dev/null and b/app/src/renderer/assets/orange-peel.png differ diff --git a/app/src/renderer/assets/skeleton-bones.png b/app/src/renderer/assets/skeleton-bones.png new file mode 100644 index 00000000..65596592 Binary files /dev/null and b/app/src/renderer/assets/skeleton-bones.png differ diff --git a/app/src/renderer/styles/base.css b/app/src/renderer/styles/base.css index f0cbb913..e0fe862a 100644 --- a/app/src/renderer/styles/base.css +++ b/app/src/renderer/styles/base.css @@ -119,9 +119,26 @@ textarea { :root[data-theme="forest"] .glass-header { background-color: rgba(13, 21, 16, 0.32); } +:root[data-theme="forest"] .bottle-button { + color: #ffffff; + border-color: rgba(95, 67, 38, 0.65); + background: rgba(95, 67, 38, 0.22); +} +:root[data-theme="forest"] .game-card .badge-ok, +:root[data-theme="forest"] .game-card .badge-warn, +:root[data-theme="forest"] .route-chip { + background: rgba(95, 67, 38, 0.3); + color: #ffffff; +} :root[data-theme="orange-peel"] .glass-header { background-color: rgba(18, 11, 8, 0.32); } +:root[data-theme="orange-peel"] .route-chip, +:root[data-theme="orange-peel"] .game-card .badge-ok, +:root[data-theme="orange-peel"] .game-card .badge-warn { + background: #ffffff; + color: #000000; +} :root[data-theme="dragonfruit"] .glass-header { background-color: rgba(26, 14, 24, 0.32); } @@ -138,23 +155,49 @@ textarea { :root[data-theme="skeleton"] .view-body-surface { background: radial-gradient(circle at 24% 16%, rgba(232, 226, 213, 0.05), transparent 36%), - radial-gradient(circle at 84% 10%, rgba(255, 255, 255, 0.04), transparent 30%), var(--bg-deep); + radial-gradient(circle at 84% 10%, rgba(255, 255, 255, 0.04), transparent 30%), + linear-gradient(rgba(19, 19, 19, 0.45), rgba(19, 19, 19, 0.45)), url("../assets/skeleton-bones.png"), var(--bg-deep); + background-size: + auto, + auto, + auto, + 512px 512px; } :root[data-theme="forest"] .view-body-surface { background: radial-gradient(circle at 24% 16%, rgba(92, 184, 112, 0.07), transparent 34%), - radial-gradient(circle at 84% 10%, rgba(168, 122, 74, 0.05), transparent 30%), var(--bg-deep); + radial-gradient(circle at 84% 10%, rgba(168, 122, 74, 0.05), transparent 30%), + linear-gradient(rgba(13, 21, 16, 0.55), rgba(13, 21, 16, 0.55)), url("../assets/forest-bark.png"), var(--bg-deep); + background-size: + auto, + auto, + auto, + 512px 512px; } :root[data-theme="orange-peel"] .view-body-surface { background: - linear-gradient(118deg, rgba(255, 122, 26, 0.03), transparent 38%, rgba(255, 160, 77, 0.024) 74%, transparent), - radial-gradient(circle at 24% 16%, rgba(255, 122, 26, 0.08), transparent 34%), var(--bg-deep); + radial-gradient(circle at 24% 16%, rgba(237, 164, 101, 0.08), transparent 34%), + radial-gradient(circle at 84% 10%, rgba(255, 184, 77, 0.05), transparent 28%), + linear-gradient(rgba(18, 11, 8, 0.45), rgba(18, 11, 8, 0.45)), url("../assets/orange-peel.png"), var(--bg-deep); + background-size: + auto, + auto, + auto, + 512px 512px; } :root[data-theme="dragonfruit"] .view-body-surface { background: - linear-gradient(118deg, rgba(255, 46, 136, 0.03), transparent 38%, rgba(168, 106, 224, 0.03) 74%, transparent), - radial-gradient(circle at 24% 16%, rgba(255, 46, 136, 0.1), transparent 34%), - radial-gradient(circle at 84% 10%, rgba(168, 106, 224, 0.08), transparent 30%), var(--bg-deep); + linear-gradient(118deg, rgba(232, 151, 184, 0.04), transparent 38%, rgba(197, 163, 222, 0.04) 74%, transparent), + radial-gradient(circle at 24% 16%, rgba(232, 151, 184, 0.1), transparent 34%), + radial-gradient(circle at 84% 10%, rgba(197, 163, 222, 0.08), transparent 30%), + linear-gradient(rgba(26, 14, 24, 0.45), rgba(26, 14, 24, 0.45)), url("../assets/dragonfruit-flesh.png"), + var(--bg-deep); + background-size: + auto, + auto, + auto, + auto, + 512px 512px; } .hidden { @@ -220,6 +263,13 @@ textarea { .btn-play:hover { background: var(--play-color-hover); } +:root[data-theme="forest"] .btn-play { + background: #5f4326; + border-color: #5f4326; +} +:root[data-theme="forest"] .btn-play:hover { + background: #4a3319; +} .btn-stop { background: var(--error); @@ -238,6 +288,13 @@ textarea { .btn-install:hover { background: var(--play-color-hover); } +:root[data-theme="forest"] .btn-install { + background: #5f4326; + border-color: #5f4326; +} +:root[data-theme="forest"] .btn-install:hover { + background: #4a3319; +} :root[data-theme="light"] .btn-install { background: var(--play-color); border-color: var(--play-color); diff --git a/app/src/renderer/styles/variables.css b/app/src/renderer/styles/variables.css index 2cedbf88..0aee72b5 100644 --- a/app/src/renderer/styles/variables.css +++ b/app/src/renderer/styles/variables.css @@ -185,14 +185,14 @@ --game-card-text: var(--text-primary); --game-card-dim: var(--text-dim); --bg-input: #101a12; - --accent: #5cb870; - --accent-hover: #7ed494; - --accent-dim: #3d8452; - --accent-glow: rgba(92, 184, 112, 0.2); - --card-glow: rgba(92, 184, 112, 0.1); - --card-installed-glow-color: #6fce88; - --success: #6fce88; - --success-bg: rgba(111, 206, 136, 0.14); + --accent: #3f7d4f; + --accent-hover: #5cb870; + --accent-dim: #2c5a3a; + --accent-glow: rgba(63, 125, 79, 0.22); + --card-glow: rgba(63, 125, 79, 0.1); + --card-installed-glow-color: #4d9e62; + --success: #4d9e62; + --success-bg: rgba(77, 158, 98, 0.14); --play-color: #4d9e62; --play-color-hover: #35704a; --error: #d66a5a; @@ -206,11 +206,11 @@ --border: rgba(140, 190, 150, 0.12); --border-strong: rgba(140, 190, 150, 0.2); --sidebar-bg: rgba(13, 21, 16, 0.58); - --sidebar-hover: rgba(92, 184, 112, 0.12); - --sidebar-active: rgba(92, 184, 112, 0.18); + --sidebar-hover: rgba(63, 125, 79, 0.12); + --sidebar-active: rgba(63, 125, 79, 0.18); --sidebar-text: #dce8dc; --sidebar-text-active: var(--sidebar-text); - --sidebar-text-hover: #7ed494; + --sidebar-text-hover: #5cb870; --sidebar-logo-color: #dce8dc; --sidebar-logo-accent: #a87a4a; --page-header-bg: linear-gradient(90deg, #121c15 0%, #1a2e1c 30%, #213a22 60%, #1a2e1c 100%); @@ -226,16 +226,16 @@ --game-card-text: var(--text-primary); --game-card-dim: var(--text-dim); --bg-input: #170e0a; - --accent: #ff7a1a; - --accent-hover: #ffa04d; - --accent-dim: #b05a14; - --accent-glow: rgba(255, 122, 26, 0.2); - --card-glow: rgba(255, 122, 26, 0.12); - --card-installed-glow-color: #ff8a2e; + --accent: #eda465; + --accent-hover: #f3bd8a; + --accent-dim: #b57f52; + --accent-glow: rgba(237, 164, 101, 0.2); + --card-glow: rgba(237, 164, 101, 0.12); + --card-installed-glow-color: #eda465; --success: #7cbf6a; --success-bg: rgba(124, 191, 106, 0.14); - --play-color: #d97a2e; - --play-color-hover: #a85a18; + --play-color: #d99e66; + --play-color-hover: #b5804e; --error: #e0604f; --error-bg: rgba(224, 96, 79, 0.13); --warn: #ffb84d; @@ -247,13 +247,13 @@ --border: rgba(255, 170, 120, 0.12); --border-strong: rgba(255, 170, 120, 0.2); --sidebar-bg: rgba(18, 11, 8, 0.58); - --sidebar-hover: rgba(255, 122, 26, 0.12); - --sidebar-active: rgba(255, 122, 26, 0.18); + --sidebar-hover: rgba(237, 164, 101, 0.12); + --sidebar-active: rgba(237, 164, 101, 0.18); --sidebar-text: #f2e4d8; --sidebar-text-active: var(--sidebar-text); - --sidebar-text-hover: #ffa04d; - --sidebar-logo-color: #ff7a1a; - --sidebar-logo-accent: #ffa04d; + --sidebar-text-hover: #f3bd8a; + --sidebar-logo-color: #eda465; + --sidebar-logo-accent: #f3bd8a; --page-header-bg: linear-gradient(90deg, #1b110c 0%, #33190c 30%, #47220f 60%, #33190c 100%); } @@ -267,16 +267,16 @@ --game-card-text: var(--text-primary); --game-card-dim: var(--text-dim); --bg-input: #200f1e; - --accent: #ff2e88; - --accent-hover: #ff66aa; - --accent-dim: #b81e62; - --accent-glow: rgba(255, 46, 136, 0.22); - --card-glow: rgba(255, 46, 136, 0.14); + --accent: #e897b8; + --accent-hover: #f2bcd0; + --accent-dim: #b06e8c; + --accent-glow: rgba(232, 151, 184, 0.2); + --card-glow: rgba(232, 151, 184, 0.13); --card-installed-glow-color: #9be34a; --success: #9be34a; --success-bg: rgba(155, 227, 74, 0.12); - --play-color: #ff2e88; - --play-color-hover: #ff66aa; + --play-color: #e897b8; + --play-color-hover: #f2bcd0; --error: #ff5c5c; --error-bg: rgba(255, 92, 92, 0.13); --warn: #ffc44d; @@ -285,18 +285,18 @@ --text-secondary: #c49ab8; --text-dim: #94708c; --text-bright: #ffffff; - --border: rgba(255, 120, 180, 0.14); - --border-strong: rgba(255, 120, 180, 0.24); + --border: rgba(232, 160, 195, 0.14); + --border-strong: rgba(232, 160, 195, 0.24); --sidebar-bg: rgba(26, 14, 24, 0.58); - --sidebar-hover: rgba(255, 46, 136, 0.1); - --sidebar-active: rgba(255, 46, 136, 0.16); + --sidebar-hover: rgba(232, 151, 184, 0.1); + --sidebar-active: rgba(232, 151, 184, 0.16); --sidebar-text: #f8e4f0; --sidebar-text-active: var(--sidebar-text); - --sidebar-text-hover: #ff66aa; - --sidebar-logo-color: #ff2e88; - --sidebar-logo-accent: #a86ae0; + --sidebar-text-hover: #f2bcd0; + --sidebar-logo-color: #e897b8; + --sidebar-logo-accent: #c5a3de; --page-header-bg: - radial-gradient(circle at 14% 14%, rgba(255, 46, 136, 0.18), transparent 34%), - radial-gradient(circle at 86% 18%, rgba(168, 106, 224, 0.16), transparent 32%), - linear-gradient(100deg, #22082a 0%, #3a1038 36%, #52123f 66%, #2a0e30 100%); + radial-gradient(circle at 14% 14%, rgba(232, 151, 184, 0.16), transparent 34%), + radial-gradient(circle at 86% 18%, rgba(197, 163, 222, 0.15), transparent 32%), + linear-gradient(100deg, #241126 0%, #371432 36%, #4a173a 66%, #2a112e 100%); } diff --git a/app/src/renderer/views/LibraryView.vue b/app/src/renderer/views/LibraryView.vue index ae34a43d..a9336899 100644 --- a/app/src/renderer/views/LibraryView.vue +++ b/app/src/renderer/views/LibraryView.vue @@ -57,6 +57,29 @@ const artworkRetryRequestedAppIds = new Set(); const filteredGames = ref([]); +const GRID_GAP = 18; +const GRID_MIN_COLUMN = 300; +const gameGridEl = ref(null); +const columnCount = ref(1); +let gridResizeObserver: ResizeObserver | null = null; + +function updateColumnCount() { + const width = gameGridEl.value?.clientWidth ?? 0; + if (width <= 0) return; + columnCount.value = Math.max(1, Math.floor((width + GRID_GAP) / (GRID_MIN_COLUMN + GRID_GAP))); +} + +// Split games into per-column stacks (round-robin keeps row-major order). +// A card that grows (e.g. bottle panel) only pushes cards below it in its +// own column, instead of stretching the whole grid row. +const gameColumns = computed(() => { + const columns: SteamGame[][] = Array.from({ length: columnCount.value }, () => []); + filteredGames.value.forEach((game, index) => { + columns[index % columnCount.value].push(game); + }); + return columns; +}); + function gameNameSort(a: SteamGame, b: SteamGame) { return a.name.localeCompare(b.name, undefined, { sensitivity: "base", numeric: true }); } @@ -288,15 +311,30 @@ async function uninstallGame(game: SteamGame) { onMounted(() => { applyFilter(); + gridResizeObserver = new ResizeObserver(updateColumnCount); + if (gameGridEl.value) gridResizeObserver.observe(gameGridEl.value); + updateColumnCount(); }); onUnmounted(() => { + if (gridResizeObserver) { + gridResizeObserver.disconnect(); + gridResizeObserver = null; + } if (artworkRetryTimer !== null) { window.clearTimeout(artworkRetryTimer); artworkRetryTimer = null; } }); +watch(gameGridEl, (el, prev) => { + if (prev) gridResizeObserver?.unobserve(prev); + if (el) { + gridResizeObserver?.observe(el); + updateColumnCount(); + } +}); + watch([library, search, filter], () => { applyFilter(); }); @@ -356,8 +394,9 @@ watch([library, search, filter], () => {

Add your Steam API key in Settings to load your library, or download a game manually.

-
-
+
+
+
{ @uninstall="uninstallGame(game)" @artwork-missing="requestArtworkRetry" /> +
@@ -572,15 +612,22 @@ watch([library, search, filter], () => { } .game-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(min(100%, 300px), 1fr)); + display: flex; + align-items: flex-start; gap: 18px; - align-items: start; min-width: 0; width: 100%; max-width: 100%; } +.game-grid-column { + flex: 1 1 0; + min-width: 0; + display: flex; + flex-direction: column; + gap: 18px; +} + .game-grid-item { min-width: 0; }