diff --git a/src/gui/src/UI/Dashboard/TabApps.js b/src/gui/src/UI/Dashboard/TabApps.js index 13d32e714..1d33a85d9 100644 --- a/src/gui/src/UI/Dashboard/TabApps.js +++ b/src/gui/src/UI/Dashboard/TabApps.js @@ -17,83 +17,89 @@ * along with this program. If not, see . */ -function buildAppsSection () { - let apps_str = ''; - if ( window.launch_apps?.recommended?.length > 0 ) { - apps_str += '
'; - for ( let index = 0; index < window.launch_apps.recommended.length; index++ ) { - const app_info = window.launch_apps.recommended[index]; - apps_str += `
`; - apps_str += `
`; - apps_str += ``; - apps_str += `${html_encode(app_info.title)}`; - apps_str += '
'; - apps_str += '
'; - } - apps_str += '
'; +function buildAppsGrid (apps) { + if ( !apps || apps.length === 0 ) { + let h = '
'; + h += ''; + h += ''; + h += ''; + h += ''; + h += '

No apps installed yet

'; + h += '
'; + return h; } - // No apps message - if ( (!window.launch_apps?.recent || window.launch_apps.recent.length === 0) && - (!window.launch_apps?.recommended || window.launch_apps.recommended.length === 0) ) { - apps_str += '

No apps available yet.

'; - } + let h = '
'; + for ( const app of apps ) { + const title = (app.title || app.name || '').trim(); + const iconUrl = app.iconUrl || window.icons['app.svg']; - return apps_str; + h += `
`; + h += '
'; + h += ``; + h += '
'; + h += `${html_encode(title)}`; + h += '
'; + } + h += '
'; + return h; } const TabApps = { id: 'apps', - label: 'My Apps', - icon: ``, + label: 'Apps', + icon: '', + + _apps: null, html () { - return '
'; + let h = '
'; + h += '
'; + h += '
Loading apps...
'; + h += '
'; + h += '
'; + return h; }, init ($el_window) { - // Load apps initially this.loadApps($el_window); - // Handle app clicks - open in new browser tab - $el_window.on('click', '.dashboard-apps-container .start-app', function (e) { + // Handle app tile clicks + $el_window.on('click', '.myapps-tile', function (e) { e.preventDefault(); e.stopPropagation(); - const appName = $(this).attr('data-app-name'); if ( appName ) { - const appUrl = `/app/${appName}`; - window.open(appUrl, '_blank'); + window.open(`/app/${appName}`, '_blank'); } }); }, async loadApps ($el_window) { - // If launch_apps is not populated yet, fetch from server - if ( !window.launch_apps || !window.launch_apps.recent || window.launch_apps.recent.length === 0 ) { - try { - window.launch_apps = await $.ajax({ - url: `${window.api_origin}/get-launch-apps?icon_size=64`, - type: 'GET', - async: true, - contentType: 'application/json', + const $container = $el_window.find('.myapps-container'); + + try { + const res = await fetch( + `${window.api_origin}/installedApps?orderBy=name&limit=100`, + { headers: { - 'Authorization': `Bearer ${window.auth_token}`, + 'Authorization': `Bearer ${puter.authToken}`, }, - }); - } catch (e) { - console.error('Failed to load launch apps:', e); - } + method: 'GET', + }, + ); + const apps = await res.json(); + this._apps = apps; + $container.html(buildAppsGrid(apps)); + } catch (e) { + console.error('Failed to load installed apps:', e); + $container.html('

Failed to load apps

'); } - // Populate the apps container - $el_window.find('.dashboard-apps-container').html(buildAppsSection()); }, onActivate ($el_window) { - // Refresh apps when navigating to apps section this.loadApps($el_window); }, }; export default TabApps; - diff --git a/src/gui/src/UI/Dashboard/UIDashboard.js b/src/gui/src/UI/Dashboard/UIDashboard.js index d1911564a..e179b1b72 100644 --- a/src/gui/src/UI/Dashboard/UIDashboard.js +++ b/src/gui/src/UI/Dashboard/UIDashboard.js @@ -54,7 +54,7 @@ import TabSecurity from './TabSecurity.js'; // Registry of built-in tabs const builtinTabs = [ TabHome, - // TabApps, + TabApps, TabFiles, TabUsage, TabAccount, @@ -95,7 +95,7 @@ async function UIDashboard (options) { for ( let i = 0; i < tabs.length; i++ ) { const tab = tabs[i]; const isActive = i === 0 ? ' active' : ''; - const isBeta = tab.label === 'Files'; + const isBeta = tab.label === 'Apps'; h += `
`; h += tab.icon; h += tab.label; diff --git a/src/gui/src/css/dashboard.css b/src/gui/src/css/dashboard.css index a61806dac..754ce63e1 100644 --- a/src/gui/src/css/dashboard.css +++ b/src/gui/src/css/dashboard.css @@ -433,6 +433,112 @@ body { padding: 24px 0; } +/* My Apps tab */ + +.myapps-tab { + max-width: 900px; + padding: 32px 40px; +} + +.myapps-tab h2 { + font-size: 26px; + font-weight: 700; + color: var(--dashboard-text-heading); + margin: 0 0 24px 0; +} + +.myapps-container { + min-height: 200px; +} + +.myapps-loading { + color: var(--dashboard-text-tertiary); + font-size: 14px; + padding: 40px 0; + text-align: center; +} + +.myapps-empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 60px 0; + color: var(--dashboard-text-tertiary); +} + +.myapps-empty svg { + width: 48px; + height: 48px; + margin-bottom: 12px; + opacity: 0.4; +} + +.myapps-empty p { + font-size: 15px; + margin: 0; +} + +.myapps-grid { + display: grid; + grid-template-columns: repeat(auto-fill, 80px); + gap: 24px 20px; + justify-content: start; +} + +.myapps-tile { + display: flex; + flex-direction: column; + align-items: center; + cursor: pointer; + width: 80px; + -webkit-user-select: none; + user-select: none; +} + +.myapps-tile-icon { + width: 60px; + height: 60px; + border-radius: 14px; + overflow: hidden; + background: var(--dashboard-card-background); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08), 0 0 0 0.5px rgba(0, 0, 0, 0.04); + transition: transform 0.15s ease, box-shadow 0.15s ease; + display: flex; + align-items: center; + justify-content: center; +} + +.myapps-tile-icon img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.myapps-tile:hover .myapps-tile-icon { + transform: scale(1.08); + box-shadow: 0 3px 8px rgba(0, 0, 0, 0.12), 0 0 0 0.5px rgba(0, 0, 0, 0.06); +} + +.myapps-tile:active .myapps-tile-icon { + transform: scale(0.95); +} + +.myapps-tile-label { + margin-top: 6px; + font-size: 12px; + color: var(--dashboard-text-heading); + text-align: center; + line-height: 1.3; + max-width: 80px; + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + word-break: break-word; +} + /* Dashboard files */ .dashboard-content.files { @@ -1548,6 +1654,30 @@ body { width: 40px; height: 40px; } + + .myapps-tab { + padding: 20px 16px; + } + + .myapps-tab h2 { + font-size: 22px; + margin-bottom: 16px; + } + + .myapps-grid { + grid-template-columns: repeat(auto-fill, 72px); + gap: 18px 14px; + } + + .myapps-tile { + width: 72px; + } + + .myapps-tile-icon { + width: 52px; + height: 52px; + border-radius: 12px; + } } /* Desktop: Make metadata wrapper transparent */