Skip to content

Instantly share code, notes, and snippets.

@DerGoogler
Last active May 9, 2025 21:25
Show Gist options
  • Save DerGoogler/fb27052386889b9059ab9a2ef7694dfd to your computer and use it in GitHub Desktop.
Save DerGoogler/fb27052386889b9059ab9a2ef7694dfd to your computer and use it in GitHub Desktop.
WebUI X new experimental PackageManager API.
<!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