From 89943a99e91742a64681473268972c94b2df634f Mon Sep 17 00:00:00 2001 From: KOWX712 Date: Sat, 10 May 2025 23:48:59 +0800 Subject: [PATCH] feat: add icon display for webuix --- module/webui/scripts/applist.js | 132 ++++++++++++++++++++++++++++---- module/webui/styles/applist.css | 45 ++++++++++- 2 files changed, 158 insertions(+), 19 deletions(-) diff --git a/module/webui/scripts/applist.js b/module/webui/scripts/applist.js index b5af484..ba93e5c 100644 --- a/module/webui/scripts/applist.js +++ b/module/webui/scripts/applist.js @@ -7,6 +7,17 @@ export const appListContainer = document.getElementById('apps-list'); export const updateCard = document.getElementById('update-card'); let targetList = []; +let wrapInputStream; + +if (typeof $packageManager !== 'undefined') { + import("https://mui.kernelsu.org/internal/assets/ext/wrapInputStream.mjs") + .then(module => { + wrapInputStream = module.wrapInputStream; + }) + .catch(err => { + console.error("Failed to load wrapInputStream:", err); + }); +} // Fetch and render applist export async function fetchAppList() { @@ -44,6 +55,13 @@ export async function fetchAppList() { packageName }; } + if (typeof $packageManager !== 'undefined') { + const info = $packageManager.getApplicationInfo(packageName, 0, 0); + return { + appName: info.getLabel(), + packageName + }; + } return new Promise((resolve) => { const output = spawn('sh', [`${basePath}/common/get_extra.sh`, '--appname', packageName], { env: { PATH: `$PATH:${basePath}/common:/data/data/com.termux/files/usr/bin` } }); @@ -75,24 +93,43 @@ function renderAppList(data) { return (a.appName || "").localeCompare(b.appName || ""); }); - // Render + // Clear container appListContainer.innerHTML = ""; - sortedApps.forEach(({ appName, packageName }) => { + loadingIndicator.style.display = "none"; + hideFloatingBtn(false); + if (updateCard) appListContainer.appendChild(updateCard); + + // Append app + const appendApps = (index) => { + if (index >= sortedApps.length) { + document.querySelector('.uninstall-container').classList.remove('hidden-uninstall'); + toggleableCheckbox(); + setupRadioButtonListeners(); + setupModeMenu(); + updateCheckboxColor(); + applyRippleEffect(); + if (typeof $packageManager !== 'undefined') { + setupIconIntersectionObserver(); + } + return; + } + + const { appName, packageName } = sortedApps[index]; const appElement = document.importNode(appTemplate, true); const contentElement = appElement.querySelector(".content"); contentElement.setAttribute("data-package", packageName); - + // Set unique names for radio button groups const radioButtons = appElement.querySelectorAll('input[type="radio"]'); radioButtons.forEach((radio) => { radio.name = `mode-radio-${packageName}`; }); - + // Preselect the radio button based on the package name const generateRadio = appElement.querySelector('#generate-mode'); const hackRadio = appElement.querySelector('#hack-mode'); const normalRadio = appElement.querySelector('#normal-mode'); - + if (appsWithExclamation.includes(packageName)) { generateRadio.checked = true; } else if (appsWithQuestion.includes(packageName)) { @@ -100,9 +137,13 @@ function renderAppList(data) { } else { normalRadio.checked = true; } - + const nameElement = appElement.querySelector(".name"); nameElement.innerHTML = ` +
+
+ +
${appName}
${packageName}
@@ -111,19 +152,76 @@ function renderAppList(data) { const checkbox = appElement.querySelector(".checkbox"); checkbox.checked = targetList.includes(packageName); appListContainer.appendChild(appElement); + appendApps(index + 1); + }; + + appendApps(0); +} + +/** + * Sets up an IntersectionObserver to load app icons when they enter the viewport + */ +function setupIconIntersectionObserver() { + const observer = new IntersectionObserver((entries) => { + entries.forEach(entry => { + if (entry.isIntersecting) { + const container = entry.target; + const packageName = container.querySelector('.app-icon').getAttribute('data-package'); + if (packageName) { + loadIcons(packageName); + observer.unobserve(container); + } + } + }); + }, { + rootMargin: '100px', + threshold: 0.1 }); - loadingIndicator.style.display = "none"; - hideFloatingBtn(false); - document.querySelector('.uninstall-container').classList.remove('hidden-uninstall'); - toggleableCheckbox(); - if (appListContainer.firstChild !== updateCard) { - appListContainer.insertBefore(updateCard, appListContainer.firstChild); + const iconContainers = document.querySelectorAll('.app-icon-container'); + iconContainers.forEach(container => { + observer.observe(container); + }); +} + +const iconCache = new Map(); + +/** + * Load all app icons asynchronously after UI is rendered + * @param {Array} packageName - package names to load icons for + */ +function loadIcons(packageName) { + const imgElement = document.querySelector(`.app-icon[data-package="${packageName}"]`); + const loader = document.querySelector(`.loader[data-package="${packageName}"]`); + + if (iconCache.has(packageName)) { + imgElement.src = iconCache.get(packageName); + loader.style.display = 'none'; + imgElement.style.opacity = '1'; + } else { + const stream = $packageManager.getApplicationIcon(packageName, 0, 0); + wrapInputStream(stream) + .then(r => r.arrayBuffer()) + .then(buffer => { + const base64 = 'data:image/png;base64,' + arrayBufferToBase64(buffer); + iconCache.set(packageName, base64); + imgElement.src = base64; + loader.style.display = 'none'; + imgElement.style.opacity = '1'; + }) } - setupRadioButtonListeners(); - setupModeMenu(); - updateCheckboxColor(); - applyRippleEffect(); +} + +/** + * convert array buffer to base 64 + * @param {string} buffer + * @returns {string} + */ +function arrayBufferToBase64(buffer) { + const uint8Array = new Uint8Array(buffer); + let binary = ''; + uint8Array.forEach(byte => binary += String.fromCharCode(byte)); + return btoa(binary); } // Function to save app with ! and ? then process target list @@ -262,4 +360,4 @@ function updateCheckboxColor() { checkbox.classList.remove("checkbox-checked-generate", "checkbox-checked-hack"); } }); -} \ No newline at end of file +} diff --git a/module/webui/styles/applist.css b/module/webui/styles/applist.css index 4986a33..d333279 100644 --- a/module/webui/styles/applist.css +++ b/module/webui/styles/applist.css @@ -218,13 +218,54 @@ } .name { - display: inline-block; + display: flex; + align-items: center; margin: 0; font-size: 15.5px; max-width: calc(100% - 30px); + user-select: none; +} + +.app-info { + display: inline-block; overflow-wrap: break-word; word-break: break-word; - user-select: none; +} + +.app-icon-container { + flex-shrink: 0; + height: 3em; + width: 3em; + margin-right: 10px; + position: relative; +} + +.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; +} + +.app-icon { + height: 100%; + opacity: 0; + transition: opacity 1s ease; +} + +@keyframes shimmer { + 0% { + background-position: -200% 0; + } + + 100% { + background-position: 200% 0; + } } .checkbox-wrapper {