From 8c4d156ecf487b37081dee0dabddc243e44b4e97 Mon Sep 17 00:00:00 2001 From: Roman Tarasov Date: Wed, 21 Oct 2026 03:44:54 +0300 Subject: [PATCH] Fix web UI compatibility with older WebKit --- src/sm_appdb.c | 26 +++++++++++++++++--- web/index.html | 67 ++++++++++++++++++++++++++++---------------------- 2 files changed, 60 insertions(+), 33 deletions(-) diff --git a/src/sm_appdb.c b/src/sm_appdb.c index 27c6c6f..9ca1c04 100644 --- a/src/sm_appdb.c +++ b/src/sm_appdb.c @@ -565,10 +565,30 @@ static void copy_sqlite_text(sqlite3_stmt *stmt, int column, char *out, static bool copy_json_string(struct json_object *object, const char *key, char *out, size_t out_size) { struct json_object *value = NULL; - if (!json_object_object_get_ex(object, key, &value) || - !json_object_is_type(value, json_type_string)) { - return false; + if (!json_object_object_get_ex(object, key, &value)) { + struct json_object *fields = NULL; + if (!json_object_object_get_ex(object, "field_list", &fields) || + !json_object_is_type(fields, json_type_array)) + return false; + + // Older app.db stores AppInfoJson values as {key, data} records. + size_t key_length = strlen(key); + size_t count = json_object_array_length(fields); + for (size_t i = 0; i < count; ++i) { + struct json_object *field = json_object_array_get_idx(fields, i); + struct json_object *name = NULL; + if (!json_object_is_type(field, json_type_object) || + !json_object_object_get_ex(field, "key", &name) || + !json_object_is_type(name, json_type_string) || + (size_t)json_object_get_string_len(name) != key_length || + memcmp(json_object_get_string(name), key, key_length) != 0) + continue; + (void)json_object_object_get_ex(field, "data", &value); + break; + } } + if (!json_object_is_type(value, json_type_string)) + return false; const char *text = json_object_get_string(value); size_t length = (size_t)json_object_get_string_len(value); if (length == 0 || length >= out_size || memchr(text, '\0', length)) diff --git a/web/index.html b/web/index.html index 7c305c5..b8d6472 100644 --- a/web/index.html +++ b/web/index.html @@ -55,21 +55,19 @@ margin-bottom: 18px; } - .brand { display: flex; align-items: center; gap: 13px; } + .brand { display: flex; align-items: center; } .brand-mark { - display: grid; - place-items: center; + display: block; + flex-shrink: 0; width: 42px; height: 42px; - border: 1px solid rgba(85, 167, 255, .45); - border-radius: 12px; - background: linear-gradient(145deg, #147ee8, #74baff); - box-shadow: 0 8px 24px rgba(20, 126, 232, .25); - font-size: 20px; - font-weight: 800; + margin-right: 13px; + object-fit: contain; } + [dir="rtl"] .brand-mark { margin-right: 0; margin-left: 13px; } + h1, h2, h3, p { margin: 0; } h1 { font-size: 20px; letter-spacing: -.02em; } h2 { font-size: 17px; } @@ -212,14 +210,14 @@ .cover { position: relative; width: 88px; - aspect-ratio: 1; + height: 88px; overflow: hidden; border: 1px solid var(--line); border-radius: 12px; background: linear-gradient(145deg, #1d2c43, #0c111a); } - .cover img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; } + .cover img { position: absolute; top: 0; right: 0; bottom: 0; left: 0; width: 100%; height: 100%; object-fit: cover; } .cover-fallback { display: grid; place-items: center; width: 100%; height: 100%; color: var(--blue); font-weight: 800; } .game-main { min-width: 0; } .game-heading { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: start; gap: 10px; } @@ -313,7 +311,10 @@ .modal { position: fixed; z-index: 20; - inset: 0; + top: 0; + right: 0; + bottom: 0; + left: 0; display: grid; place-items: center; padding: 20px; @@ -327,7 +328,7 @@ .modal-head { display: flex; align-items: start; justify-content: space-between; gap: 20px; padding: 18px; border-bottom: 1px solid var(--line); } .close { width: 36px; min-height: 36px; padding: 0; font-size: 22px; } .details { display: grid; grid-template-columns: 168px minmax(0, 1fr); align-items: start; gap: 18px; padding: 18px; } - .details .cover { width: 168px; } + .details .cover { width: 168px; height: 168px; } .detail-grid { display: grid; grid-template-columns: 130px minmax(0, 1fr); gap: 7px 12px; margin-top: 13px; font-size: 13px; } .detail-key { color: var(--muted); } .detail-value { min-width: 0; overflow-wrap: anywhere; } @@ -372,7 +373,7 @@ .games { grid-template-columns: 1fr; } .sidebar { grid-template-columns: 1fr; } .details { grid-template-columns: 1fr; } - .details .cover { width: 120px; } + .details .cover { width: 120px; height: 120px; } .detail-grid { grid-template-columns: 105px minmax(0, 1fr); } } @@ -381,7 +382,7 @@
- +

Shadow Mount+ Library

Connecting to API…

@@ -1098,6 +1099,12 @@ return item; } + function replaceContent(parent, child) { + // Older WebKit can leave children intact on replaceChildren() with no arguments. + parent.textContent = ""; + if (child) parent.appendChild(child); + } + function formatBytes(value) { if (!Number.isFinite(value) || value < 0) return "—"; const units = byteUnits[currentLanguage] || byteUnits[defaultLanguage]; @@ -1181,7 +1188,7 @@ state.activity.unshift({ message, kind, time: new Date().toLocaleTimeString(currentLanguage, { hour: "2-digit", minute: "2-digit" }) }); state.activity = state.activity.slice(0, 8); const list = $("activity"); - list.replaceChildren(); + replaceContent(list); state.activity.forEach((entry) => { list.append(node("div", `activity-item ${entry.kind}`, `${entry.time} · ${entry.message}`)); }); @@ -1278,7 +1285,7 @@ function renderGames() { const games = filteredGames(); const list = $("games"); - list.replaceChildren(); + replaceContent(list); if (!games.length) { list.append(node("div", "empty", t(state.games.length ? "nothingFound" : "libraryEmpty"))); } else { @@ -1427,7 +1434,7 @@ return true; }); if (!$("destinationModal").hidden) renderDestinationPicker(); - list.replaceChildren(); + replaceContent(list); if (!mounts.length) { list.append(node("span", "subtle", t("storageNotFound"))); return; @@ -1467,7 +1474,7 @@ } catch (error) { state.storageDestinations = []; if (!$("destinationModal").hidden) renderDestinationPicker(); - list.replaceChildren(node("span", "subtle", errorMessage(error))); + replaceContent(list, node("span", "subtle", errorMessage(error))); } } @@ -1488,7 +1495,7 @@ renderGames(); } catch (error) { setConnection("offline", "disconnected"); - $("games").replaceChildren(node("div", "empty", t("apiError", { error: errorMessage(error) }))); + replaceContent($("games"), node("div", "empty", t("apiError", { error: errorMessage(error) }))); toast(errorMessage(error), true); } finally { state.busy = false; @@ -1504,7 +1511,7 @@ const list = $("manualList"); try { const data = await api("/manual/list"); - list.replaceChildren(); + replaceContent(list); if (!data.paths.length) { list.append(node("span", "subtle", t("listEmpty"))); return; @@ -1520,13 +1527,13 @@ list.append(row); }); } catch (error) { - list.replaceChildren(node("span", "subtle", errorMessage(error))); + replaceContent(list, node("span", "subtle", errorMessage(error))); } } function renderScanPaths() { const list = $("scanPathList"); - list.replaceChildren(); + replaceContent(list); if (!state.scanPaths.length) { list.append(node("span", "subtle", t("listEmpty"))); return; @@ -1563,7 +1570,7 @@ state.scanPaths = Array.isArray(settings.scan_paths) ? settings.scan_paths.slice() : []; renderScanPaths(); } catch (error) { - $("scanPathList").replaceChildren(node("span", "subtle", errorMessage(error))); + replaceContent($("scanPathList"), node("span", "subtle", errorMessage(error))); } } @@ -1771,7 +1778,7 @@ $("modalTitle").textContent = game.title_name || game.title_id; $("modalSubtitle").textContent = `${game.title_id} · ${(game.platform || t("unknown")).toUpperCase()}`; const details = $("details"); - details.replaceChildren(); + replaceContent(details); details.append(coverFor(game, true)); const content = node("div"); const badges = node("div", "badges"); @@ -1819,7 +1826,7 @@ details.append(content); const actions = $("modalActions"); - actions.replaceChildren(); + replaceContent(actions); const runtimeAction = appendRuntimeActions(actions, game); if (game.can_uninstall !== false) { actions.append(actionButton(t("uninstall"), () => uninstallGame(game), "danger")); @@ -1855,14 +1862,14 @@ $("modal").hidden = false; $("modalTitle").textContent = t("loading"); $("modalSubtitle").textContent = titleId; - $("details").replaceChildren(node("div", "empty", t("loadingSource"))); - $("modalActions").replaceChildren(); + replaceContent($("details"), node("div", "empty", t("loadingSource"))); + replaceContent($("modalActions")); try { const game = await api("/games/info", { title_id: titleId }); if (state.detailRequest === request) renderDetails(game, true); } catch (error) { if (state.detailRequest === request) { - $("details").replaceChildren(node("div", "empty", errorMessage(error))); + replaceContent($("details"), node("div", "empty", errorMessage(error))); } } } @@ -1885,7 +1892,7 @@ ? "chooseCopyDestination" : "chooseMoveDestination"); const choices = $("destinationChoices"); - choices.replaceChildren(); + replaceContent(choices); if (!state.storageDestinations.length) { choices.append(node("span", "subtle", t("storageNotFound"))); }