Last active
May 9, 2025 21:25
-
-
Save DerGoogler/fb27052386889b9059ab9a2ef7694dfd to your computer and use it in GitHub Desktop.
WebUI X new experimental PackageManager API.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8" /> | |
<title>App List</title> | |
<link rel="stylesheet" href="https://mui.kernelsu.org/internal/colors.css" /> | |
<style> | |
body { | |
font-family: sans-serif; | |
background: var(--background); | |
margin: 0; | |
padding: 1rem; | |
padding-top: calc(var(--window-inset-top) + 1rem); | |
} | |
#appList { | |
display: grid; | |
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); | |
gap: 16px; | |
} | |
.app { | |
display: flex; | |
align-items: center; | |
padding: 12px; | |
background: var(--tonalSurface); | |
border-radius: 12px; | |
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05); | |
transition: box-shadow 0.2s ease; | |
} | |
.app:hover { | |
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); | |
} | |
.logo-container { | |
width: 48px; | |
height: 48px; | |
position: relative; | |
margin-right: 12px; | |
} | |
.logo { | |
width: 100%; | |
height: 100%; | |
object-fit: contain; | |
border-radius: 6px; | |
display: block; | |
opacity: 0; | |
transition: opacity 0.3s ease; | |
} | |
.loader { | |
position: absolute; | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 100%; | |
background: linear-gradient(90deg, var(--surfaceContainer), var(--surfaceContainerHigh), var(--surfaceContainer)); | |
background-size: 200% 100%; | |
animation: shimmer 1.2s infinite linear; | |
border-radius: 6px; | |
} | |
@keyframes shimmer { | |
0% { | |
background-position: -200% 0; | |
} | |
100% { | |
background-position: 200% 0; | |
} | |
} | |
.app-info { | |
flex: 1; | |
display: flex; | |
flex-direction: column; | |
} | |
.app-name { | |
font-size: 14px; | |
color: var(--onSurface); | |
word-break: break-word; | |
} | |
.app-meta { | |
font-size: 12px; | |
color: var(--outline); | |
} | |
.label-placeholder { | |
color: var(--onSurfaceVariant); | |
font-style: italic; | |
} | |
.statusbar-bg, | |
.navigation-bg { | |
position: fixed; | |
width: 100%; | |
opacity: 0.5; | |
background-color: var(--background); | |
z-index: 1000; | |
} | |
.statusbar-bg { | |
top: 0; | |
height: var(--window-inset-top); | |
} | |
.navigation-bg { | |
bottom: 0; | |
height: var(--window-inset-bottom); | |
} | |
.controls { | |
display: flex; | |
gap: 8px; | |
margin-bottom: 1rem; | |
} | |
.controls input { | |
flex: 1; | |
padding: 10px 12px; | |
font-size: 14px; | |
border: none; | |
border-radius: 8px; | |
background-color: var(--surfaceContainer); | |
color: var(--onSurface); | |
outline: none; | |
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); | |
} | |
.controls input::placeholder { | |
color: var(--onSurfaceVariant); | |
} | |
#sortSelect { | |
padding: 10px 14px; | |
font-size: 14px; | |
border: none; | |
border-radius: 8px; | |
background-color: var(--surfaceContainer); | |
color: var(--onSurface); | |
appearance: none; | |
background-image: url('data:image/svg+xml,%3Csvg fill=\'%23666\' height=\'24\' viewBox=\'0 0 24 24\' width=\'24\' xmlns=\'http://www.w3.org/2000/svg\'%3E%3Cpath d=\'M7 10l5 5 5-5z\'/%3E%3C/svg%3E'); | |
background-repeat: no-repeat; | |
background-position: right 10px center; | |
background-size: 20px 20px; | |
cursor: pointer; | |
} | |
</style> | |
</head> | |
<body> | |
<div class="statusbar-bg"></div> | |
<!-- Controls --> | |
<div class="controls"> | |
<input type="text" id="searchInput" placeholder="Search apps..." /> | |
<select id="sortSelect"> | |
<option value="az">Sort A–Z</option> | |
<option value="za">Sort Z–A</option> | |
</select> | |
</div> | |
<div class="appList" id="appList"></div> | |
<div class="navigation-bg"></div> | |
<script type="module"> | |
import { wrapInputStream } from "https://mui.kernelsu.org/internal/assets/ext/wrapInputStream.mjs"; | |
const observer = new IntersectionObserver(onIntersect, { | |
rootMargin: '100px', | |
threshold: 0.1 | |
}); | |
const iconCache = new Map(); | |
let originalPkgList = []; | |
window.onload = () => { | |
const iface = window[findInterface()]; | |
iface.setLightStatusBars(!iface.isDarkMode()); | |
}; | |
const searchInput = document.getElementById('searchInput'); | |
let searchTimeout = null; | |
searchInput.addEventListener('input', () => { | |
clearTimeout(searchTimeout); | |
searchTimeout = setTimeout(() => { | |
const query = searchInput.value.trim().toLowerCase(); | |
const filtered = originalPkgList.filter(pkg => pkg.toLowerCase().includes(query)); | |
renderAppList(filtered); | |
}, 200); | |
}); | |
document.getElementById('sortSelect').addEventListener('change', (e) => { | |
const value = e.target.value; | |
let list = [...originalPkgList]; | |
if (value === 'az') { | |
list.sort((a, b) => a.localeCompare(b)); | |
} else if (value === 'za') { | |
list.sort((a, b) => b.localeCompare(a)); | |
} | |
renderAppList(list); | |
}); | |
function onIntersect(entries, obs) { | |
for (const entry of entries) { | |
if (!entry.isIntersecting) continue; | |
const container = entry.target; | |
const img = container.querySelector('img'); | |
const loader = container.querySelector('.loader'); | |
const pkg = img.dataset.pkg; | |
if (iconCache.has(pkg)) { | |
img.src = iconCache.get(pkg); | |
img.style.opacity = '1'; | |
loader.remove(); | |
obs.unobserve(container); | |
continue; | |
} | |
const appDiv = container.parentElement; | |
const nameEl = appDiv.querySelector('.app-name'); | |
const metaEl = appDiv.querySelector('.app-meta'); | |
const info = $packageManager.getApplicationInfo(pkg, 0, 0); | |
nameEl.classList.remove("label-placeholder"); | |
nameEl.textContent = info.getLabel(); | |
metaEl.textContent = `v${info.getVersionName()} (${info.getVersionCode()}) · minSdk ${info.getMinSdkVersion()}`; | |
const stream = $packageManager.getApplicationIcon(pkg, 0, 0); | |
wrapInputStream(stream) | |
.then(r => r.arrayBuffer()) | |
.then(buffer => { | |
const base64 = 'data:image/png;base64,' + arrayBufferToBase64(buffer); | |
iconCache.set(pkg, base64); | |
img.src = base64; | |
img.onload = () => { | |
img.style.opacity = '1'; | |
loader.remove(); | |
}; | |
}); | |
obs.unobserve(container); | |
} | |
} | |
function arrayBufferToBase64(buffer) { | |
const uint8Array = new Uint8Array(buffer); | |
let binary = ''; | |
uint8Array.forEach(byte => binary += String.fromCharCode(byte)); | |
return btoa(binary); | |
} | |
function loadAppList() { | |
const cached = localStorage.getItem("cachedAppList"); | |
if (cached) { | |
originalPkgList = JSON.parse(cached); | |
renderAppList(originalPkgList); | |
} | |
const freshList = JSON.parse($packageManager.getInstalledPackages(0, 0)); | |
originalPkgList = freshList; | |
localStorage.setItem("cachedAppList", JSON.stringify(freshList)); | |
renderAppList(freshList); | |
} | |
function renderAppList(pkgList) { | |
const container = document.getElementById('appList'); | |
container.innerHTML = ''; | |
for (const pkg of pkgList) { | |
const div = document.createElement('div'); | |
div.className = 'app'; | |
const logoContainer = document.createElement('div'); | |
logoContainer.className = 'logo-container'; | |
const img = document.createElement('img'); | |
img.className = 'logo'; | |
img.dataset.pkg = pkg; | |
const loader = document.createElement('div'); | |
loader.className = 'loader'; | |
logoContainer.appendChild(img); | |
logoContainer.appendChild(loader); | |
const infoContainer = document.createElement('div'); | |
infoContainer.className = 'app-info'; | |
const name = document.createElement('div'); | |
name.className = 'app-name label-placeholder'; | |
name.textContent = pkg; | |
const meta = document.createElement('div'); | |
meta.className = 'app-meta'; | |
meta.textContent = 'Loading...'; | |
infoContainer.appendChild(name); | |
infoContainer.appendChild(meta); | |
div.appendChild(logoContainer); | |
div.appendChild(infoContainer); | |
container.appendChild(div); | |
observer.observe(logoContainer); | |
} | |
} | |
function findInterface() { | |
const fileToken = Object.keys(window).find(key => key.match(/^\$(\w{2})File$/m)); | |
const fileInputToken = Object.keys(window).find(key => key.match(/^\$(\w{2})FileInputStream$/m)); | |
const token = fileToken?.slice(0, 3).toLowerCase(); | |
return Object.keys(window).find(key => { | |
if (key === fileToken || key === fileInputToken) return false; | |
return key.toLowerCase().startsWith(token); | |
}); | |
} | |
loadAppList(); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment