Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions apps/desktop/extensions/ai-sidebar/apps.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// New-tab "Apps" widget: Profullstack's apps on pwamart.com, one tap to install.
//
// Installing goes through `tron pwa` over native messaging (host
// dev.tronbrowser.pwa, registered by the launcher inside the profile). It puts
// the app in the system app drawer as a standalone TronBrowser window, which is
// the only real install the engine allows us without root. Where the host is
// not there (macOS, Windows, a Tor profile) the button opens the app in its own
// window instead, and says so.

import { fetchWithTimeout } from './net.js';

export const PWAMART = 'https://pwamart.com';
export const PUBLISHER = 'profullstack';
export const HOST = 'dev.tronbrowser.pwa';
// Pinned first, and installed for every new profile by the launcher.
export const DEFAULT_APPS = ['coinpay', 'logicsrc', 'tronbrowser'];
const TTL = 6 * 60 * 60 * 1000;

export function host(msg) {
return new Promise((resolve) => {
try {
chrome.runtime.sendNativeMessage(HOST, msg, (reply) => {
const err = chrome.runtime.lastError;
resolve(err ? { ok: false, error: err.message, noHost: true } : reply || { ok: false, error: 'no reply' });
});
} catch (e) {
resolve({ ok: false, error: String(e), noHost: true });
}
});
}

// Defaults first (in their order), then the rest by pwamart's own order.
export function orderApps(apps) {
const rank = (a) => {
const i = DEFAULT_APPS.indexOf(a.slug);
return i === -1 ? DEFAULT_APPS.length : i;
};
return apps
.filter((a) => a && /^[a-z0-9][a-z0-9-]{0,58}[a-z0-9]$/.test(a.slug) && /^https:\/\//.test(a.start_url || a.url || ''))
.map((a, i) => ({ a, i }))
.sort((x, y) => rank(x.a) - rank(y.a) || x.i - y.i)
.map(({ a }) => a);
}

export async function loadApps() {
const { pwamartApps } = await chrome.storage.local.get('pwamartApps');
if (pwamartApps && Date.now() - pwamartApps.at < TTL) return pwamartApps.apps;
try {
const res = await fetchWithTimeout(`${PWAMART}/api/v1/publishers/${PUBLISHER}`, {}, 10000);
// A 402 (pwamart's bot gate answers headless browsers with x402) or any
// other failure must not be cached as "no apps" for the next six hours.
if (!res.ok) throw new Error(`pwamart ${res.status}`);
const data = await res.json();
const apps = orderApps(data.apps || []).map((a) => ({
slug: a.slug, name: a.name, summary: a.summary || '', icon: a.icon || '', start_url: a.start_url || a.url,
}));
if (apps.length) await chrome.storage.local.set({ pwamartApps: { at: Date.now(), apps } });
return apps;
} catch {
return pwamartApps ? pwamartApps.apps : [];
}
}
3 changes: 2 additions & 1 deletion apps/desktop/extensions/ai-sidebar/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
"notifications",
"alarms",
"contentSettings",
"declarativeNetRequestWithHostAccess"
"declarativeNetRequestWithHostAccess",
"nativeMessaging"
],
"host_permissions": [
"https://api.openai.com/*",
Expand Down
17 changes: 17 additions & 0 deletions apps/desktop/extensions/ai-sidebar/newtab.css
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,23 @@ body {
border-radius: 10px; padding: 14px 16px; white-space: pre-wrap; font-size: 13px; color: var(--fg); }
.motd-body a { color: var(--cyan); text-decoration: none; }

/* Apps widget */
.apps-q { margin-left: auto; background: #04060c; color: var(--fg); border: 1px solid var(--line);
border-radius: 6px; padding: 3px 10px; font: inherit; font-size: 12px; width: min(220px, 40vw); }
.apps-q:focus { outline: none; border-color: var(--cyan); }
.apps-note { color: var(--muted); font-size: 12px; margin: 8px 0 0; }
.appgrid { margin-top: 12px; display: grid; gap: 10px; grid-template-columns: repeat(auto-fill, minmax(118px, 1fr));
max-height: 340px; overflow-y: auto; }
.app { background: var(--panel); border: 1px solid var(--line); border-radius: 10px; padding: 12px 8px;
display: flex; flex-direction: column; align-items: center; gap: 6px; text-align: center; min-width: 0; }
.app.def { border-color: color-mix(in srgb, var(--cyan) 45%, var(--line)); }
.app img, .app-noicon { width: 40px; height: 40px; border-radius: 10px; object-fit: contain; background: #04060c; }
.app b { font-size: 12px; color: #fff; font-weight: 600; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.app button { background: var(--cyan); color: #04060c; border: 0; border-radius: 999px; padding: 3px 12px;
font: inherit; font-size: 12px; font-weight: 700; cursor: pointer; }
.app.on button { background: transparent; color: var(--cyan); border: 1px solid var(--line); }
.app button:disabled { opacity: .5; cursor: progress; }

/* Embeddable player modal */
.modal { position: fixed; inset: 0; z-index: 100; background: rgba(0,0,0,.82); display: grid; place-items: center; }
.modal[hidden] { display: none; }
Expand Down
10 changes: 10 additions & 0 deletions apps/desktop/extensions/ai-sidebar/newtab.html
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,16 @@
</nav>
</main>

<section class="strip" id="apps-sec" hidden>
<div class="strip-head">
<h2>Apps</h2>
<input id="apps-q" class="apps-q" type="search" placeholder="Filter apps" autocomplete="off" />
<a class="edit" href="https://pwamart.com/publishers/profullstack" target="_blank" rel="noopener">pwamart</a>
</div>
<p id="apps-note" class="apps-note" hidden></p>
<div id="apps" class="appgrid"></div>
</section>

<section class="strip motd" id="motd-sec" hidden>
<div class="strip-head"><h2>Message of the Day</h2></div>
<div id="motd" class="motd-body"></div>
Expand Down
54 changes: 54 additions & 0 deletions apps/desktop/extensions/ai-sidebar/newtab.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { fetchMotd } from './motd.js';
import { getToken as btrToken, BTR_BASE } from './bittorrented.js';
import { fetchWithTimeout, storageGet } from './net.js';
import { w3bsUrl } from './w3bs.js';
import { loadApps, host, DEFAULT_APPS } from './apps.js';

const el = (id) => document.getElementById(id);

Expand Down Expand Up @@ -406,6 +407,59 @@ window.addEventListener('message', (e) => {
});
document.addEventListener('keydown', (e) => { if (e.key === 'Escape') closePlayer(); });

// --- Apps: Profullstack's pwamart listings, one tap to install ---
let installed = new Set();
let hasHost = false;
let allApps = [];

function appTile(a) {
const on = installed.has(a.slug);
const label = on || !hasHost ? 'Open' : 'Install';
const icon = /^https:\/\//.test(a.icon)
? `<img src="${escAttr(a.icon)}" alt="" loading="lazy" decoding="async" referrerpolicy="no-referrer" width="40" height="40">`
: '<span class="app-noicon"></span>';
return `<div class="app${on ? ' on' : ''}${DEFAULT_APPS.includes(a.slug) ? ' def' : ''}" title="${escAttr(a.summary)}">` +
`${icon}<b>${escapeHtml(a.name)}</b>` +
`<button data-app="${escAttr(a.slug)}">${on ? '✓ ' : ''}${label}</button></div>`;
}

function drawApps() {
const q = el('apps-q').value.trim().toLowerCase();
const list = q ? allApps.filter((a) => (a.name + ' ' + a.slug + ' ' + a.summary).toLowerCase().includes(q)) : allApps;
el('apps').innerHTML = list.map(appTile).join('') || '<p class="muted">No apps match.</p>';
}

async function renderApps() {
allApps = await loadApps();
if (!allApps.length) return;
const r = await host({ cmd: 'list' });
hasHost = !!r.ok;
installed = new Set(r.ok ? r.installed : []);
el('apps-note').hidden = hasHost;
el('apps-note').textContent = hasHost ? '' : 'Installing needs the TronBrowser launcher on Linux; here apps open in their own window.';
el('apps-sec').hidden = false;
drawApps();
}

el('apps-q').addEventListener('input', drawApps);
el('apps').addEventListener('click', async (e) => {
const b = e.target.closest('button[data-app]');
if (!b) return;
const slug = b.getAttribute('data-app');
const app = allApps.find((a) => a.slug === slug);
if (!app) return;
if (!hasHost) { chrome.windows.create({ url: app.start_url, type: 'popup', width: 420, height: 820 }); return; }
b.disabled = true;
const r = await host({ cmd: installed.has(slug) ? 'open' : 'add', slug });
if (r.ok && r.installed) installed = new Set(r.installed);
drawApps();
if (!r.ok) {
const again = el('apps').querySelector(`button[data-app="${slug}"]`);
if (again) { again.textContent = 'Retry'; again.title = r.error || 'failed'; }
}
});

renderApps();
renderMotd();
renderBtr();
renderFeeds();
Expand Down
Loading
Loading