193 lines
8 KiB
JavaScript
193 lines
8 KiB
JavaScript
|
|
/**
|
|||
|
|
* pc-live | Software Store App
|
|||
|
|
*/
|
|||
|
|
const StoreApp = (() => {
|
|||
|
|
const WIN_ID = 'app-store';
|
|||
|
|
let _apps = [];
|
|||
|
|
let _appsMap = {}; // app_id -> app (for dependency lookups)
|
|||
|
|
let _filter = { category: 'all', search: '', installedOnly: false };
|
|||
|
|
|
|||
|
|
// Category definitions – icon + label.
|
|||
|
|
// Order here determines sidebar order.
|
|||
|
|
const CATEGORY_META = {
|
|||
|
|
all: { label: 'All Apps', icon: '🗂' },
|
|||
|
|
system: { label: 'System', icon: '🖥' },
|
|||
|
|
utility: { label: 'Utility', icon: '🔧' },
|
|||
|
|
communication: { label: 'Communication', icon: '📡' },
|
|||
|
|
security: { label: 'Security', icon: '🔐' },
|
|||
|
|
business: { label: 'Business', icon: '💼' },
|
|||
|
|
government: { label: 'Government', icon: '🏛' },
|
|||
|
|
entertainment: { label: 'Entertainment', icon: '🎮' },
|
|||
|
|
darknet: { label: 'Darknet', icon: '🌑' },
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
/* ── Helpers ─────────────────────────────────────────────── */
|
|||
|
|
|
|||
|
|
// Only show categories that actually have apps in the current list
|
|||
|
|
function getActiveCategories() {
|
|||
|
|
const present = new Set(['all']);
|
|||
|
|
for (const app of _apps) {
|
|||
|
|
if (app.category) present.add(app.category);
|
|||
|
|
}
|
|||
|
|
return Object.keys(CATEGORY_META).filter(c => present.has(c));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function filtered() {
|
|||
|
|
let list = _apps;
|
|||
|
|
if (_filter.installedOnly) list = list.filter(a => a.installed);
|
|||
|
|
if (_filter.category !== 'all') list = list.filter(a => a.category === _filter.category);
|
|||
|
|
if (_filter.search) {
|
|||
|
|
const q = _filter.search.toLowerCase();
|
|||
|
|
list = list.filter(a =>
|
|||
|
|
(a.name || '').toLowerCase().includes(q) ||
|
|||
|
|
(a.description || '').toLowerCase().includes(q)
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
return list;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* ── Render ─────────────────────────────────────────────── */
|
|||
|
|
|
|||
|
|
function renderSidebar() {
|
|||
|
|
const cats = getActiveCategories();
|
|||
|
|
return `
|
|||
|
|
<div class="app-sidebar">
|
|||
|
|
<div class="sidebar-section">Categories</div>
|
|||
|
|
${cats.map(c => {
|
|||
|
|
const m = CATEGORY_META[c] || { icon: '📂', label: c };
|
|||
|
|
return `
|
|||
|
|
<div class="sidebar-item ${_filter.category === c ? 'active' : ''}"
|
|||
|
|
onclick="StoreApp._setCategory('${c}')">
|
|||
|
|
${m.icon} ${m.label}
|
|||
|
|
</div>`;
|
|||
|
|
}).join('')}
|
|||
|
|
</div>`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function renderCard(app) {
|
|||
|
|
const icon = app.icon || '📦';
|
|||
|
|
const priceLabel = app.price > 0 ? `$${app.price.toLocaleString()}` : 'Free';
|
|||
|
|
const priceClass = app.price > 0 ? '' : 'free';
|
|||
|
|
const catMeta = CATEGORY_META[app.category] || { icon: '📂' };
|
|||
|
|
|
|||
|
|
// Resolve missing dependencies
|
|||
|
|
const missingDeps = (app.dependencies || [])
|
|||
|
|
.filter(depId => { const d = _appsMap[depId]; return !d || !d.installed; })
|
|||
|
|
.map(depId => { const d = _appsMap[depId]; return d ? d.name : depId; });
|
|||
|
|
|
|||
|
|
// Action buttons
|
|||
|
|
let action = '';
|
|||
|
|
if (app.installed) {
|
|||
|
|
if (app.update_available) {
|
|||
|
|
action = `<button class="btn-primary btn-sm" onclick="StoreApp._install('${app.app_id}')">↑ Update</button>`;
|
|||
|
|
} else {
|
|||
|
|
action = `<button class="btn-ghost btn-sm" style="cursor:default;opacity:.5" disabled>✓ Installed</button>`;
|
|||
|
|
}
|
|||
|
|
if (!app.default) {
|
|||
|
|
action += `<button class="btn-danger btn-sm" onclick="StoreApp._uninstall('${app.app_id}')">✕</button>`;
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
const locked = missingDeps.length > 0;
|
|||
|
|
action = `<button class="btn-primary btn-sm"
|
|||
|
|
${locked ? 'style="opacity:.4" title="Missing: ' + missingDeps.join(', ') + '"' : ''}
|
|||
|
|
onclick="StoreApp._install('${app.app_id}')">Install</button>`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Info badges
|
|||
|
|
let badges = `<span class="tag">${catMeta.icon} ${app.category || 'other'}</span>`;
|
|||
|
|
badges += `<span class="tag">v${app.version || '?'}</span>`;
|
|||
|
|
if (app.installed) badges += `<span class="tag tag-accent">✓</span>`;
|
|||
|
|
if (app.update_available) badges += `<span class="tag" style="color:var(--warning);border-color:rgba(245,158,11,.4)">↑ Update</span>`;
|
|||
|
|
if (app.permissions?.job) badges += `<span class="tag" style="color:var(--text-dim)">🔑 ${app.permissions.job}</span>`;
|
|||
|
|
if (missingDeps.length) badges += `<span class="tag" style="color:var(--error);border-color:rgba(239,68,68,.3)">⚠ Needs: ${missingDeps.join(', ')}</span>`;
|
|||
|
|
|
|||
|
|
return `
|
|||
|
|
<div class="store-card ${app.installed ? 'store-card--installed' : ''}">
|
|||
|
|
<div class="store-card-icon">${icon}</div>
|
|||
|
|
<div class="store-card-name">${app.name || app.app_id}</div>
|
|||
|
|
<div class="store-card-desc">${app.description || ''}</div>
|
|||
|
|
<div class="store-card-footer"><div style="display:flex;gap:4px;flex-wrap:wrap">${badges}</div></div>
|
|||
|
|
<div class="store-card-footer">
|
|||
|
|
<span class="store-price ${priceClass}">${priceLabel}</span>
|
|||
|
|
<div style="display:flex;gap:4px">${action}</div>
|
|||
|
|
</div>
|
|||
|
|
</div>`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function _refresh() {
|
|||
|
|
const body = WindowManager.getBody(WIN_ID);
|
|||
|
|
if (!body) return;
|
|||
|
|
|
|||
|
|
const list = filtered();
|
|||
|
|
const instCount = _apps.filter(a => a.installed).length;
|
|||
|
|
const totalCount = _apps.length;
|
|||
|
|
|
|||
|
|
body.innerHTML = `
|
|||
|
|
<div class="app-layout" style="height:100%">
|
|||
|
|
${renderSidebar()}
|
|||
|
|
<div style="flex:1;display:flex;flex-direction:column;overflow:hidden">
|
|||
|
|
<div class="app-content" style="flex:1">
|
|||
|
|
<div class="search-bar">
|
|||
|
|
<input type="text" placeholder="Search apps…"
|
|||
|
|
value="${(_filter.search || '').replace(/"/g, '"')}"
|
|||
|
|
oninput="StoreApp._setSearch(this.value)" id="store-search"/>
|
|||
|
|
<button class="btn-sm ${_filter.installedOnly ? 'btn-primary' : 'btn-ghost'}"
|
|||
|
|
onclick="StoreApp._toggleInstalled()" title="Show installed only">
|
|||
|
|
✓ Installed
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
${list.length === 0
|
|||
|
|
? `<div class="empty-state"><div class="empty-icon">📦</div><p>No apps found</p></div>`
|
|||
|
|
: `<div class="store-grid">${list.map(renderCard).join('')}</div>`
|
|||
|
|
}
|
|||
|
|
</div>
|
|||
|
|
<div class="store-statusbar">
|
|||
|
|
📦 <strong>${instCount}</strong> installed · ${totalCount} available
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* ── Filter actions ─────────────────────────────────────── */
|
|||
|
|
function _setCategory(cat) { _filter.category = cat; _refresh(); }
|
|||
|
|
function _setSearch(q) { _filter.search = q; _refresh(); }
|
|||
|
|
function _toggleInstalled() {
|
|||
|
|
_filter.installedOnly = !_filter.installedOnly;
|
|||
|
|
_refresh();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* ── Install / Uninstall ─────────────────────────────────── */
|
|||
|
|
function _install(appId) { fetchNui('installApp', { appId }); }
|
|||
|
|
function _uninstall(appId) { fetchNui('uninstallApp', { appId }); }
|
|||
|
|
|
|||
|
|
/* ── NUI event – store data received ────────────────────── */
|
|||
|
|
function onStoreData(apps) {
|
|||
|
|
_apps = apps || [];
|
|||
|
|
_appsMap = {};
|
|||
|
|
for (const a of _apps) _appsMap[a.app_id] = a;
|
|||
|
|
_refresh();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* ── Open ───────────────────────────────────────────────── */
|
|||
|
|
function open() {
|
|||
|
|
const created = WindowManager.create({
|
|||
|
|
id: WIN_ID,
|
|||
|
|
title: 'Software Store',
|
|||
|
|
icon: '🛒',
|
|||
|
|
width: 860,
|
|||
|
|
height: 560,
|
|||
|
|
content: '',
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
if (created) {
|
|||
|
|
_apps = [];
|
|||
|
|
_appsMap = {};
|
|||
|
|
_filter = { category: 'all', search: '', installedOnly: false };
|
|||
|
|
_refresh();
|
|||
|
|
fetchNui('getStore', {});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return { open, _setCategory, _setSearch, _toggleInstalled, _install, _uninstall, onStoreData };
|
|||
|
|
})();
|