feat: ingest icon/banner/screenshots from source repo
This commit is contained in:
@@ -18,6 +18,8 @@ interface Extension {
|
||||
minAppVersion: string | null;
|
||||
latestVersion: string | null;
|
||||
installed: boolean;
|
||||
iconUrl: string | null;
|
||||
bannerUrl: string | null;
|
||||
author: {
|
||||
displayName: string;
|
||||
githubLogin: string;
|
||||
@@ -259,10 +261,27 @@ function ExtensionCard({
|
||||
|
||||
return (
|
||||
<div className="group relative border border-border rounded-lg overflow-hidden hover:border-ring/30 transition-colors">
|
||||
{extension.bannerUrl && (
|
||||
<Link href={previewHref} className="block focus:outline-none">
|
||||
<img
|
||||
src={extension.bannerUrl}
|
||||
alt=""
|
||||
className="block h-24 w-full object-cover border-b border-border"
|
||||
loading="lazy"
|
||||
/>
|
||||
</Link>
|
||||
)}
|
||||
<Link href={previewHref} className="block p-4 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring/40 rounded-lg">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="w-10 h-10 rounded-md bg-muted flex items-center justify-center shrink-0">
|
||||
{isPlugin ? (
|
||||
<div className="w-10 h-10 rounded-md bg-muted flex items-center justify-center shrink-0 overflow-hidden">
|
||||
{extension.iconUrl ? (
|
||||
<img
|
||||
src={extension.iconUrl}
|
||||
alt=""
|
||||
className="w-10 h-10 object-cover"
|
||||
loading="lazy"
|
||||
/>
|
||||
) : isPlugin ? (
|
||||
<Puzzle className="w-5 h-5 text-muted-foreground" />
|
||||
) : (
|
||||
<SwatchBook className="w-5 h-5 text-muted-foreground" />
|
||||
|
||||
@@ -37,6 +37,8 @@ interface PreviewData {
|
||||
githubRepo: string | null;
|
||||
license: string | null;
|
||||
minAppVersion: string | null;
|
||||
iconUrl: string | null;
|
||||
bannerUrl: string | null;
|
||||
author: {
|
||||
displayName: string;
|
||||
githubLogin: string;
|
||||
@@ -209,11 +211,30 @@ export default function MarketplacePreviewPage() {
|
||||
<ArrowLeft className="w-4 h-4" /> Back to Marketplace
|
||||
</Link>
|
||||
|
||||
{/* Banner / hero */}
|
||||
{ext.bannerUrl && (
|
||||
<div className="mb-6 overflow-hidden rounded-lg border border-border bg-muted">
|
||||
<img
|
||||
src={ext.bannerUrl}
|
||||
alt=""
|
||||
className="block w-full max-h-64 object-cover"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Header */}
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-start">
|
||||
<div className="flex items-start gap-4 flex-1 min-w-0">
|
||||
<div className="w-14 h-14 rounded-lg bg-muted flex items-center justify-center shrink-0">
|
||||
{isPlugin ? (
|
||||
<div className="w-14 h-14 rounded-lg bg-muted flex items-center justify-center shrink-0 overflow-hidden">
|
||||
{ext.iconUrl ? (
|
||||
<img
|
||||
src={ext.iconUrl}
|
||||
alt=""
|
||||
className="w-14 h-14 object-cover"
|
||||
loading="lazy"
|
||||
/>
|
||||
) : isPlugin ? (
|
||||
<Puzzle className="w-7 h-7 text-muted-foreground" />
|
||||
) : (
|
||||
<SwatchBook className="w-7 h-7 text-muted-foreground" />
|
||||
|
||||
@@ -168,6 +168,11 @@ export async function GET(
|
||||
}))
|
||||
: [];
|
||||
|
||||
const fileUrl = (path: unknown): string | null =>
|
||||
typeof path === 'string' && path
|
||||
? new URL(`/api/v1/files/${path}`, DIRECTORY_URL).toString()
|
||||
: null;
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
extension: {
|
||||
@@ -184,6 +189,8 @@ export async function GET(
|
||||
githubRepo: extension.githubRepo ?? null,
|
||||
license: extension.license ?? null,
|
||||
minAppVersion: extension.minAppVersion ?? null,
|
||||
iconUrl: fileUrl(extension.iconPath),
|
||||
bannerUrl: fileUrl(extension.bannerPath),
|
||||
author: extension.author ?? null,
|
||||
latestVersion,
|
||||
versions,
|
||||
|
||||
@@ -60,9 +60,16 @@ export async function GET(request: NextRequest) {
|
||||
const installedPlugins = new Set(pluginRegistry.plugins.map(p => p.id));
|
||||
const installedThemes = new Set(themeRegistry.themes.map(t => t.id));
|
||||
|
||||
const fileUrl = (path: unknown): string | null =>
|
||||
typeof path === 'string' && path
|
||||
? new URL(`/api/v1/files/${path}`, DIRECTORY_URL).toString()
|
||||
: null;
|
||||
|
||||
if (data.data) {
|
||||
data.data = data.data.map((ext: Record<string, unknown>) => ({
|
||||
...ext,
|
||||
iconUrl: fileUrl(ext.iconPath),
|
||||
bannerUrl: fileUrl(ext.bannerPath),
|
||||
installed: ext.type === 'theme'
|
||||
? installedThemes.has(ext.slug as string)
|
||||
: installedPlugins.has(ext.slug as string),
|
||||
|
||||
@@ -53,7 +53,23 @@ export interface ThemeManifest {
|
||||
author: string;
|
||||
description: string;
|
||||
type: 'theme';
|
||||
/** @deprecated kept as alias for `banner` so existing themes still work. */
|
||||
preview?: string;
|
||||
/**
|
||||
* Path inside the source repo (relative to manifest.json) to a square
|
||||
* brand icon shown in marketplace cards and the host's theme picker.
|
||||
*/
|
||||
icon?: string;
|
||||
/**
|
||||
* Path to a wide promo image shown as the hero on the theme detail
|
||||
* page. PNG/JPG/WebP, ≤512 KB.
|
||||
*/
|
||||
banner?: string;
|
||||
/**
|
||||
* Up to 6 screenshot paths shown in the gallery on the detail page.
|
||||
* Themes typically use this to show light + dark variants.
|
||||
*/
|
||||
screenshots?: string[];
|
||||
variants: ThemeVariant[];
|
||||
minAppVersion?: string;
|
||||
|
||||
@@ -100,6 +116,25 @@ export interface PluginManifest {
|
||||
* Validated at install time and merged into the host CSP `frame-src`.
|
||||
*/
|
||||
frameOrigins?: string[];
|
||||
|
||||
// ─── Marketplace media (NOT shipped in the runtime zip) ──────
|
||||
/**
|
||||
* Path inside the source repo (relative to manifest.json) to a square
|
||||
* brand icon. PNG/SVG/WebP, ≤256 KB, 128×128 or larger recommended.
|
||||
* The extension directory ingests this from git and serves it on
|
||||
* marketplace cards and the host's plugin admin UI.
|
||||
*/
|
||||
icon?: string;
|
||||
/**
|
||||
* Path to a wide promo image (16:9 recommended), shown as the hero on
|
||||
* the extension detail page. PNG/JPG/WebP, ≤512 KB.
|
||||
*/
|
||||
banner?: string;
|
||||
/**
|
||||
* Up to 6 screenshot paths shown in the gallery on the detail page.
|
||||
* Each ≤512 KB; total ≤2 MB. Order is preserved.
|
||||
*/
|
||||
screenshots?: string[];
|
||||
}
|
||||
|
||||
export interface SettingFieldSchema {
|
||||
|
||||
Reference in New Issue
Block a user