diff --git a/app.js b/app.js index 3934702..9c8e77b 100644 --- a/app.js +++ b/app.js @@ -11,6 +11,30 @@ let currentView = "discover"; let currentApp = null; + function escapeHtml(str) { + if (typeof str !== "string") return ""; + return str + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); + } + + function sanitizeUrl(url) { + if (typeof url !== "string") return "#"; + const trimmed = url.trim(); + if (!trimmed) return "#"; + try { + const parsed = new URL(trimmed); + const protocol = parsed.protocol.toLowerCase(); + if (protocol === "http:" || protocol === "https:") return parsed.href; + } catch (e) { + // invalid URL + } + return "#"; + } + const $ = (sel, ctx = document) => ctx.querySelector(sel); const $$ = (sel, ctx = document) => [...ctx.querySelectorAll(sel)]; @@ -85,6 +109,16 @@ return `${formatNumber(stars)}`; } + function platformIcon(platform) { + if (platform === "ios") { + return ``; + } + if (platform === "android") { + return ``; + } + return ``; + } + function isPaidApp(app) { return app.price && app.price !== "Free" && app.price !== "free"; } @@ -95,6 +129,8 @@ function getButtonLabel(app) { if (isPaidApp(app)) return app.price; + if (app.links && app.links.length > 0) return "Get"; + if (app.category && (app.category.includes("games") || app.category.includes("entertainment"))) return "Get"; if (app.brew || app.downloadUrl || app.installCommand) return "Get"; return "View"; } @@ -323,12 +359,27 @@
${renderIcon(app)}
-
${app.name}
-
${app.subtitle}
+
${escapeHtml(app.name)}
+
${escapeHtml(app.subtitle)}
+ ${app.links && app.links.length > 0 && !isPaidApp(app) ? ` +
+ + +
` : ` + `} ${icons.github} View on GitHub @@ -596,6 +647,44 @@ }; } + function showLinksModal(app) { + const overlay = $("#modalOverlay"); + const modal = $("#modal"); + + function sanitizeIconContainerStyle(attrStr) { + if (typeof attrStr !== "string") return ""; + const match = attrStr.match(/^\s*style=(["'])([\s\S]*)\1\s*$/i); + if (!match) return ""; + const sanitized = match[2].replace(/["'<>]/g, ""); + return ` style="${sanitized}"`; + } + + modal.innerHTML = ` + + +

${escapeHtml(app.name)}

+ + `; + + overlay.style.display = "flex"; + requestAnimationFrame(() => overlay.classList.add("visible")); + + overlay.onclick = (e) => { + if (e.target === overlay || e.target.closest("[data-action='close-modal']")) { + closeModal(); + } + }; + } + function closeModal() { const overlay = $("#modalOverlay"); overlay.classList.remove("visible"); @@ -638,6 +727,24 @@ if (!app) return; if (isPaidApp(app)) { showBuyModal(app); + } else if (app.links && app.links.length > 0) { + if (btn.classList.contains("app-detail-get-btn")) { + const dropdown = document.getElementById(`get-dropdown-${app.id}`); + if (dropdown) { + const isOpen = dropdown.classList.contains("open"); + $$(".get-dropdown.open").forEach(d => { + d.classList.remove("open"); + const prevBtn = d.previousElementSibling; + if (prevBtn) prevBtn.setAttribute("aria-expanded", "false"); + }); + if (!isOpen) { + dropdown.classList.add("open"); + btn.setAttribute("aria-expanded", "true"); + } + } + } else { + showLinksModal(app); + } } else if (app.brew || app.installCommand) { showBrewModal(app); } else if (app.homepage) { @@ -760,6 +867,11 @@ function bindKeyboard() { document.addEventListener("keydown", (e) => { if (e.key === "Escape") { + $$(".get-dropdown.open").forEach(d => { + d.classList.remove("open"); + const prevBtn = d.previousElementSibling; + if (prevBtn) prevBtn.setAttribute("aria-expanded", "false"); + }); closeModal(); if (currentApp) navigate(currentView); } @@ -768,6 +880,15 @@ $("#searchInput").focus(); } }); + document.addEventListener("click", (e) => { + if (!e.target.closest(".get-dropdown-wrapper")) { + $$(".get-dropdown.open").forEach(d => { + d.classList.remove("open"); + const prevBtn = d.previousElementSibling; + if (prevBtn) prevBtn.setAttribute("aria-expanded", "false"); + }); + } + }); } // Init diff --git a/style.css b/style.css index 0905848..53df798 100644 --- a/style.css +++ b/style.css @@ -97,9 +97,8 @@ body { font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", "SF Pro Text", "Helvetica Neue", Arial, sans-serif; background: var(--bg-primary); color: var(--text-primary); - overflow: hidden; height: 100vh; - width: 100vw; + width: 100%; user-select: none; transition: background 0.3s ease, color 0.3s ease; overflow-wrap: break-word; @@ -109,8 +108,7 @@ body { .app-store { display: flex; height: 100vh; - max-width: 100vw; - overflow: hidden; + max-width: 100%; } /* Sidebar */ @@ -1571,7 +1569,7 @@ body { @media (max-width: 600px) { .content-scroll { padding: 16px 12px 32px; - max-width: 100vw; + max-width: 100%; box-sizing: border-box; } @@ -1581,7 +1579,6 @@ body { .app-detail { min-width: 0; max-width: 100%; - overflow: hidden; } .app-row { @@ -1607,6 +1604,10 @@ body { text-align: center; } + .app-detail-actions { + justify-content: center; + } + .app-detail-icon { width: 96px; height: 96px; @@ -1644,3 +1645,125 @@ body { .content-scroll > * { animation: fadeIn 0.3s ease both; } + +/* Modal Platform Links */ +.modal-platform-links { + display: flex; + flex-direction: column; + margin-top: 8px; + border: 1px solid var(--border); + border-radius: var(--radius-md); + overflow: hidden; +} + +.modal-platform-link { + display: flex; + align-items: center; + gap: 12px; + padding: 14px 16px; + text-decoration: none; + color: var(--text-primary); + border-bottom: 1px solid var(--border); + transition: background var(--transition); +} + +.modal-platform-link:last-child { + border-bottom: none; +} + +.modal-platform-link:hover { + background: var(--row-hover); +} + +.modal-platform-icon { + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + color: var(--text-secondary); +} + +.modal-platform-label { + flex: 1; + font-size: 15px; + font-weight: 500; +} + +.modal-platform-chevron { + color: var(--text-tertiary); + font-size: 18px; + line-height: 1; +} + +/* Get Dropdown (detail page) */ +.get-dropdown-wrapper { + position: relative; + display: inline-flex; + flex-direction: column; + align-items: flex-start; +} + +.get-dropdown { + position: absolute; + top: calc(100% + 8px); + left: 0; + min-width: 200px; + background: var(--bg-secondary); + border: 1px solid var(--border-light); + border-radius: var(--radius-md); + opacity: 0; + transform: translateY(-8px) scale(0.97); + transition: opacity 0.18s ease, transform 0.18s ease; + pointer-events: none; + z-index: 50; + overflow: hidden; +} + +.get-dropdown.open { + opacity: 1; + transform: translateY(0) scale(1); + pointer-events: auto; +} + +.get-dropdown-item { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 16px; + text-decoration: none; + color: var(--text-primary); + border-bottom: 1px solid var(--border); + transition: background var(--transition); +} + +.get-dropdown-item:last-child { + border-bottom: none; +} + +.get-dropdown-item:hover { + background: var(--row-hover); +} + +.get-dropdown-icon { + width: 28px; + height: 28px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + color: var(--text-secondary); +} + +.get-dropdown-label { + flex: 1; + font-size: 14px; + font-weight: 500; +} + +.get-dropdown-arrow { + color: var(--text-tertiary); + font-size: 18px; + line-height: 1; +}