feat: ingest icon/banner/screenshots from source repo
This commit is contained in:
@@ -18,6 +18,8 @@ interface Extension {
|
|||||||
minAppVersion: string | null;
|
minAppVersion: string | null;
|
||||||
latestVersion: string | null;
|
latestVersion: string | null;
|
||||||
installed: boolean;
|
installed: boolean;
|
||||||
|
iconUrl: string | null;
|
||||||
|
bannerUrl: string | null;
|
||||||
author: {
|
author: {
|
||||||
displayName: string;
|
displayName: string;
|
||||||
githubLogin: string;
|
githubLogin: string;
|
||||||
@@ -259,10 +261,27 @@ function ExtensionCard({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="group relative border border-border rounded-lg overflow-hidden hover:border-ring/30 transition-colors">
|
<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">
|
<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="flex items-start gap-3">
|
||||||
<div className="w-10 h-10 rounded-md bg-muted flex items-center justify-center shrink-0">
|
<div className="w-10 h-10 rounded-md bg-muted flex items-center justify-center shrink-0 overflow-hidden">
|
||||||
{isPlugin ? (
|
{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" />
|
<Puzzle className="w-5 h-5 text-muted-foreground" />
|
||||||
) : (
|
) : (
|
||||||
<SwatchBook 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;
|
githubRepo: string | null;
|
||||||
license: string | null;
|
license: string | null;
|
||||||
minAppVersion: string | null;
|
minAppVersion: string | null;
|
||||||
|
iconUrl: string | null;
|
||||||
|
bannerUrl: string | null;
|
||||||
author: {
|
author: {
|
||||||
displayName: string;
|
displayName: string;
|
||||||
githubLogin: string;
|
githubLogin: string;
|
||||||
@@ -209,11 +211,30 @@ export default function MarketplacePreviewPage() {
|
|||||||
<ArrowLeft className="w-4 h-4" /> Back to Marketplace
|
<ArrowLeft className="w-4 h-4" /> Back to Marketplace
|
||||||
</Link>
|
</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 */}
|
{/* Header */}
|
||||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-start">
|
<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="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">
|
<div className="w-14 h-14 rounded-lg bg-muted flex items-center justify-center shrink-0 overflow-hidden">
|
||||||
{isPlugin ? (
|
{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" />
|
<Puzzle className="w-7 h-7 text-muted-foreground" />
|
||||||
) : (
|
) : (
|
||||||
<SwatchBook 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(
|
return NextResponse.json(
|
||||||
{
|
{
|
||||||
extension: {
|
extension: {
|
||||||
@@ -184,6 +189,8 @@ export async function GET(
|
|||||||
githubRepo: extension.githubRepo ?? null,
|
githubRepo: extension.githubRepo ?? null,
|
||||||
license: extension.license ?? null,
|
license: extension.license ?? null,
|
||||||
minAppVersion: extension.minAppVersion ?? null,
|
minAppVersion: extension.minAppVersion ?? null,
|
||||||
|
iconUrl: fileUrl(extension.iconPath),
|
||||||
|
bannerUrl: fileUrl(extension.bannerPath),
|
||||||
author: extension.author ?? null,
|
author: extension.author ?? null,
|
||||||
latestVersion,
|
latestVersion,
|
||||||
versions,
|
versions,
|
||||||
|
|||||||
@@ -60,9 +60,16 @@ export async function GET(request: NextRequest) {
|
|||||||
const installedPlugins = new Set(pluginRegistry.plugins.map(p => p.id));
|
const installedPlugins = new Set(pluginRegistry.plugins.map(p => p.id));
|
||||||
const installedThemes = new Set(themeRegistry.themes.map(t => t.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) {
|
if (data.data) {
|
||||||
data.data = data.data.map((ext: Record<string, unknown>) => ({
|
data.data = data.data.map((ext: Record<string, unknown>) => ({
|
||||||
...ext,
|
...ext,
|
||||||
|
iconUrl: fileUrl(ext.iconPath),
|
||||||
|
bannerUrl: fileUrl(ext.bannerPath),
|
||||||
installed: ext.type === 'theme'
|
installed: ext.type === 'theme'
|
||||||
? installedThemes.has(ext.slug as string)
|
? installedThemes.has(ext.slug as string)
|
||||||
: installedPlugins.has(ext.slug as string),
|
: installedPlugins.has(ext.slug as string),
|
||||||
|
|||||||
@@ -53,7 +53,23 @@ export interface ThemeManifest {
|
|||||||
author: string;
|
author: string;
|
||||||
description: string;
|
description: string;
|
||||||
type: 'theme';
|
type: 'theme';
|
||||||
|
/** @deprecated kept as alias for `banner` so existing themes still work. */
|
||||||
preview?: string;
|
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[];
|
variants: ThemeVariant[];
|
||||||
minAppVersion?: string;
|
minAppVersion?: string;
|
||||||
|
|
||||||
@@ -100,6 +116,25 @@ export interface PluginManifest {
|
|||||||
* Validated at install time and merged into the host CSP `frame-src`.
|
* Validated at install time and merged into the host CSP `frame-src`.
|
||||||
*/
|
*/
|
||||||
frameOrigins?: string[];
|
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 {
|
export interface SettingFieldSchema {
|
||||||
|
|||||||
Reference in New Issue
Block a user