refactor(pro): simplify PluginMarketplace component (503→310 lines)
This commit is contained in:
parent
5300c09157
commit
19771237c9
1 changed files with 159 additions and 352 deletions
|
|
@ -1,31 +1,23 @@
|
||||||
// SPDX-License-Identifier: LicenseRef-Commercial
|
// SPDX-License-Identifier: LicenseRef-Commercial
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {
|
import {
|
||||||
proMarketplaceFetchCatalog,
|
proMarketplaceFetchCatalog, proMarketplaceInstalled, proMarketplaceInstall,
|
||||||
proMarketplaceInstalled,
|
proMarketplaceUninstall, proMarketplaceCheckUpdates, proMarketplaceUpdate,
|
||||||
proMarketplaceInstall,
|
type CatalogPlugin, type InstalledPlugin,
|
||||||
proMarketplaceUninstall,
|
|
||||||
proMarketplaceCheckUpdates,
|
|
||||||
proMarketplaceUpdate,
|
|
||||||
type CatalogPlugin,
|
|
||||||
type InstalledPlugin,
|
|
||||||
} from './pro-bridge';
|
} from './pro-bridge';
|
||||||
|
|
||||||
type Tab = 'browse' | 'installed';
|
type Tab = 'browse' | 'installed';
|
||||||
|
|
||||||
let tab = $state<Tab>('browse');
|
let tab = $state<Tab>('browse');
|
||||||
let search = $state('');
|
let search = $state('');
|
||||||
let toast = $state<string | null>(null);
|
let toast = $state<string | null>(null);
|
||||||
let toastTimer: ReturnType<typeof setTimeout> | undefined;
|
let toastTimer: ReturnType<typeof setTimeout> | undefined;
|
||||||
|
|
||||||
// Browse state
|
|
||||||
let catalog = $state<CatalogPlugin[]>([]);
|
let catalog = $state<CatalogPlugin[]>([]);
|
||||||
let catalogLoading = $state(true);
|
let catalogLoading = $state(true);
|
||||||
let catalogError = $state<string | null>(null);
|
let catalogError = $state<string | null>(null);
|
||||||
let selectedPlugin = $state<CatalogPlugin | null>(null);
|
let selectedPlugin = $state<CatalogPlugin | null>(null);
|
||||||
let installing = $state<Set<string>>(new Set());
|
let installing = $state<Set<string>>(new Set());
|
||||||
|
|
||||||
// Installed state
|
|
||||||
let installed = $state<InstalledPlugin[]>([]);
|
let installed = $state<InstalledPlugin[]>([]);
|
||||||
let installedLoading = $state(true);
|
let installedLoading = $state(true);
|
||||||
let installedError = $state<string | null>(null);
|
let installedError = $state<string | null>(null);
|
||||||
|
|
@ -35,16 +27,12 @@
|
||||||
let confirmUninstall = $state<string | null>(null);
|
let confirmUninstall = $state<string | null>(null);
|
||||||
|
|
||||||
let installedIds = $derived(new Set(installed.map((p) => p.id)));
|
let installedIds = $derived(new Set(installed.map((p) => p.id)));
|
||||||
|
|
||||||
let filtered = $derived(
|
let filtered = $derived(
|
||||||
catalog.filter((p) => {
|
catalog.filter((p) => {
|
||||||
if (!search) return true;
|
if (!search) return true;
|
||||||
const q = search.toLowerCase();
|
const q = search.toLowerCase();
|
||||||
return (
|
return p.name.toLowerCase().includes(q) || p.description.toLowerCase().includes(q)
|
||||||
p.name.toLowerCase().includes(q) ||
|
|| (p.tags ?? []).some((t) => t.toLowerCase().includes(q));
|
||||||
p.description.toLowerCase().includes(q) ||
|
|
||||||
(p.tags ?? []).some((t) => t.toLowerCase().includes(q))
|
|
||||||
);
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -54,116 +42,86 @@
|
||||||
toastTimer = setTimeout(() => (toast = null), 3000);
|
toastTimer = setTimeout(() => (toast = null), 3000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mutSet(s: Set<string>, id: string, add: boolean): Set<string> {
|
||||||
|
const n = new Set(s);
|
||||||
|
add ? n.add(id) : n.delete(id);
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
async function loadCatalog() {
|
async function loadCatalog() {
|
||||||
catalogLoading = true;
|
catalogLoading = true; catalogError = null;
|
||||||
catalogError = null;
|
try { catalog = await proMarketplaceFetchCatalog(); }
|
||||||
try {
|
catch (e) { catalogError = e instanceof Error ? e.message : String(e); }
|
||||||
catalog = await proMarketplaceFetchCatalog();
|
finally { catalogLoading = false; }
|
||||||
} catch (e) {
|
|
||||||
catalogError = e instanceof Error ? e.message : String(e);
|
|
||||||
} finally {
|
|
||||||
catalogLoading = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadInstalled() {
|
async function loadInstalled() {
|
||||||
installedLoading = true;
|
installedLoading = true; installedError = null;
|
||||||
installedError = null;
|
try { installed = await proMarketplaceInstalled(); }
|
||||||
try {
|
catch (e) { installedError = e instanceof Error ? e.message : String(e); }
|
||||||
installed = await proMarketplaceInstalled();
|
finally { installedLoading = false; }
|
||||||
} catch (e) {
|
|
||||||
installedError = e instanceof Error ? e.message : String(e);
|
|
||||||
} finally {
|
|
||||||
installedLoading = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleInstall(pluginId: string) {
|
async function handleInstall(id: string) {
|
||||||
installing = new Set([...installing, pluginId]);
|
installing = mutSet(installing, id, true);
|
||||||
try {
|
try {
|
||||||
const result = await proMarketplaceInstall(pluginId);
|
const r = await proMarketplaceInstall(id);
|
||||||
installed = [...installed, result];
|
installed = [...installed, r];
|
||||||
showToast(`Installed ${result.name} v${result.version}`);
|
showToast(`Installed ${r.name} v${r.version}`);
|
||||||
} catch (e) {
|
} catch (e) { showToast(`Install failed: ${e instanceof Error ? e.message : String(e)}`); }
|
||||||
showToast(`Install failed: ${e instanceof Error ? e.message : String(e)}`);
|
finally { installing = mutSet(installing, id, false); }
|
||||||
} finally {
|
|
||||||
const next = new Set(installing);
|
|
||||||
next.delete(pluginId);
|
|
||||||
installing = next;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleUninstall(pluginId: string) {
|
async function handleUninstall(id: string) {
|
||||||
confirmUninstall = null;
|
confirmUninstall = null;
|
||||||
uninstalling = new Set([...uninstalling, pluginId]);
|
uninstalling = mutSet(uninstalling, id, true);
|
||||||
const name = installed.find((p) => p.id === pluginId)?.name ?? pluginId;
|
const name = installed.find((p) => p.id === id)?.name ?? id;
|
||||||
try {
|
try {
|
||||||
await proMarketplaceUninstall(pluginId);
|
await proMarketplaceUninstall(id);
|
||||||
installed = installed.filter((p) => p.id !== pluginId);
|
installed = installed.filter((p) => p.id !== id);
|
||||||
showToast(`Uninstalled ${name}`);
|
showToast(`Uninstalled ${name}`);
|
||||||
} catch (e) {
|
} catch (e) { showToast(`Uninstall failed: ${e instanceof Error ? e.message : String(e)}`); }
|
||||||
showToast(`Uninstall failed: ${e instanceof Error ? e.message : String(e)}`);
|
finally { uninstalling = mutSet(uninstalling, id, false); }
|
||||||
} finally {
|
|
||||||
const next = new Set(uninstalling);
|
|
||||||
next.delete(pluginId);
|
|
||||||
uninstalling = next;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleCheckUpdates() {
|
async function handleCheckUpdates() {
|
||||||
checking = true;
|
checking = true;
|
||||||
try {
|
try {
|
||||||
const updated = await proMarketplaceCheckUpdates();
|
const u = await proMarketplaceCheckUpdates();
|
||||||
installed = updated;
|
installed = u;
|
||||||
const count = updated.filter((p) => p.hasUpdate).length;
|
const c = u.filter((p) => p.hasUpdate).length;
|
||||||
showToast(count > 0 ? `${count} update(s) available` : 'All plugins up to date');
|
showToast(c > 0 ? `${c} update(s) available` : 'All plugins up to date');
|
||||||
} catch (e) {
|
} catch (e) { showToast(`Check failed: ${e instanceof Error ? e.message : String(e)}`); }
|
||||||
showToast(`Check failed: ${e instanceof Error ? e.message : String(e)}`);
|
finally { checking = false; }
|
||||||
} finally {
|
|
||||||
checking = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleUpdate(pluginId: string) {
|
async function handleUpdate(id: string) {
|
||||||
updating = new Set([...updating, pluginId]);
|
updating = mutSet(updating, id, true);
|
||||||
try {
|
try {
|
||||||
const result = await proMarketplaceUpdate(pluginId);
|
const r = await proMarketplaceUpdate(id);
|
||||||
installed = installed.map((p) => (p.id === pluginId ? result : p));
|
installed = installed.map((p) => (p.id === id ? r : p));
|
||||||
showToast(`Updated ${result.name} to v${result.version}`);
|
showToast(`Updated ${r.name} to v${r.version}`);
|
||||||
} catch (e) {
|
} catch (e) { showToast(`Update failed: ${e instanceof Error ? e.message : String(e)}`); }
|
||||||
showToast(`Update failed: ${e instanceof Error ? e.message : String(e)}`);
|
finally { updating = mutSet(updating, id, false); }
|
||||||
} finally {
|
|
||||||
const next = new Set(updating);
|
|
||||||
next.delete(pluginId);
|
|
||||||
updating = next;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function fmtDownloads(n: number | null): string {
|
function fmtDl(n: number | null): string {
|
||||||
if (n == null) return '';
|
if (n == null) return '';
|
||||||
if (n >= 1000) return `${(n / 1000).toFixed(1)}k`;
|
return n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n);
|
||||||
return String(n);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function stars(rating: number | null): string {
|
function stars(r: number | null): string {
|
||||||
if (rating == null) return '';
|
if (r == null) return '';
|
||||||
const full = Math.round(rating);
|
const f = Math.round(r);
|
||||||
return '\u2605'.repeat(full) + '\u2606'.repeat(5 - full);
|
return '\u2605'.repeat(f) + '\u2606'.repeat(5 - f);
|
||||||
}
|
}
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => { loadCatalog(); loadInstalled(); });
|
||||||
loadCatalog();
|
|
||||||
loadInstalled();
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="mp-root">
|
<div class="mp-root">
|
||||||
<!-- Toast -->
|
{#if toast}<div class="mp-toast">{toast}</div>{/if}
|
||||||
{#if toast}
|
|
||||||
<div class="mp-toast">{toast}</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<!-- Tabs -->
|
|
||||||
<div class="mp-tabs">
|
<div class="mp-tabs">
|
||||||
<button class="mp-tab" class:active={tab === 'browse'} onclick={() => (tab = 'browse')}>Browse</button>
|
<button class="mp-tab" class:active={tab === 'browse'} onclick={() => (tab = 'browse')}>Browse</button>
|
||||||
<button class="mp-tab" class:active={tab === 'installed'} onclick={() => (tab = 'installed')}>
|
<button class="mp-tab" class:active={tab === 'installed'} onclick={() => (tab = 'installed')}>
|
||||||
|
|
@ -171,64 +129,44 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Browse Tab -->
|
|
||||||
{#if tab === 'browse'}
|
{#if tab === 'browse'}
|
||||||
<div class="mp-search">
|
<div class="mp-search">
|
||||||
<input type="text" placeholder="Search plugins..." bind:value={search} class="mp-search-input" />
|
<input type="text" placeholder="Search plugins..." bind:value={search} class="mp-input" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if catalogLoading}
|
{#if catalogLoading}
|
||||||
<div class="mp-state">Loading catalog...</div>
|
<div class="mp-state">Loading catalog...</div>
|
||||||
{:else if catalogError}
|
{:else if catalogError}
|
||||||
<div class="mp-state mp-error">{catalogError}</div>
|
<div class="mp-state mp-err">{catalogError}</div>
|
||||||
{:else if filtered.length === 0}
|
{:else if filtered.length === 0}
|
||||||
<div class="mp-state">{search ? 'No plugins match your search' : 'No plugins available'}</div>
|
<div class="mp-state">{search ? 'No plugins match your search' : 'No plugins available'}</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="mp-grid">
|
<div class="mp-grid">
|
||||||
{#each filtered as plugin (plugin.id)}
|
{#each filtered as p (p.id)}
|
||||||
<div class="mp-card" onclick={() => (selectedPlugin = selectedPlugin?.id === plugin.id ? null : plugin)}>
|
<div class="mp-card" onclick={() => (selectedPlugin = selectedPlugin?.id === p.id ? null : p)}>
|
||||||
<div class="mp-card-header">
|
<div class="mp-row">
|
||||||
<span class="mp-card-name">{plugin.name}</span>
|
<span class="mp-name">{p.name}</span>
|
||||||
<span class="mp-card-version">v{plugin.version}</span>
|
<span class="mp-ver">v{p.version}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="mp-card-author">by {plugin.author}</div>
|
<div class="mp-author">by {p.author}</div>
|
||||||
<div class="mp-card-desc">{plugin.description}</div>
|
<div class="mp-desc">{p.description}</div>
|
||||||
<div class="mp-card-meta">
|
<div class="mp-meta">
|
||||||
{#if plugin.tags}
|
{#if p.tags}<div class="mp-tags">{#each p.tags.slice(0, 3) as t}<span class="mp-tag">{t}</span>{/each}</div>{/if}
|
||||||
<div class="mp-card-tags">
|
<div class="mp-stats">
|
||||||
{#each plugin.tags.slice(0, 3) as t}
|
{#if p.downloads != null}<span>{fmtDl(p.downloads)} dl</span>{/if}
|
||||||
<span class="mp-tag">{t}</span>
|
{#if p.rating != null}<span class="mp-stars">{stars(p.rating)}</span>{/if}
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
<div class="mp-card-stats">
|
|
||||||
{#if plugin.downloads != null}<span>{fmtDownloads(plugin.downloads)} dl</span>{/if}
|
|
||||||
{#if plugin.rating != null}<span class="mp-stars">{stars(plugin.rating)}</span>{/if}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{#if plugin.permissions.length > 0}
|
{#if p.permissions.length > 0}
|
||||||
<div class="mp-card-perms">
|
<div class="mp-perms">
|
||||||
{#each plugin.permissions.slice(0, 3) as perm}
|
{#each p.permissions.slice(0, 3) as pm}<span class="mp-perm">{pm}</span>{/each}
|
||||||
<span class="mp-perm">{perm}</span>
|
{#if p.permissions.length > 3}<span class="mp-perm">+{p.permissions.length - 3}</span>{/if}
|
||||||
{/each}
|
|
||||||
{#if plugin.permissions.length > 3}
|
|
||||||
<span class="mp-perm">+{plugin.permissions.length - 3}</span>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="mp-card-actions">
|
<div class="mp-actions">
|
||||||
<button
|
<button class="mp-btn primary" disabled={installedIds.has(p.id) || installing.has(p.id)}
|
||||||
class="mp-btn mp-btn-install"
|
onclick={(e) => { e.stopPropagation(); handleInstall(p.id); }}>
|
||||||
disabled={installedIds.has(plugin.id) || installing.has(plugin.id)}
|
{installing.has(p.id) ? 'Installing...' : installedIds.has(p.id) ? 'Installed' : 'Install'}
|
||||||
onclick={(e) => { e.stopPropagation(); handleInstall(plugin.id); }}
|
|
||||||
>
|
|
||||||
{#if installing.has(plugin.id)}
|
|
||||||
Installing...
|
|
||||||
{:else if installedIds.has(plugin.id)}
|
|
||||||
Installed
|
|
||||||
{:else}
|
|
||||||
Install
|
|
||||||
{/if}
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -236,35 +174,34 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Detail Panel -->
|
|
||||||
{#if selectedPlugin}
|
{#if selectedPlugin}
|
||||||
|
{@const sp = selectedPlugin}
|
||||||
<div class="mp-detail">
|
<div class="mp-detail">
|
||||||
<div class="mp-detail-header">
|
<div class="mp-detail-hdr">
|
||||||
<h3 class="mp-detail-name">{selectedPlugin.name}</h3>
|
<h3 class="mp-detail-name">{sp.name}</h3>
|
||||||
<button class="mp-btn-close" onclick={() => (selectedPlugin = null)}>x</button>
|
<button class="mp-x" onclick={() => (selectedPlugin = null)}>x</button>
|
||||||
</div>
|
</div>
|
||||||
<p class="mp-detail-desc">{selectedPlugin.description}</p>
|
<p class="mp-detail-desc">{sp.description}</p>
|
||||||
<div class="mp-detail-fields">
|
<div class="mp-fields">
|
||||||
<div class="mp-field"><span class="mp-field-label">Version</span> {selectedPlugin.version}</div>
|
<span><b>Version</b> {sp.version}</span>
|
||||||
<div class="mp-field"><span class="mp-field-label">Author</span> {selectedPlugin.author}</div>
|
<span><b>Author</b> {sp.author}</span>
|
||||||
{#if selectedPlugin.license}<div class="mp-field"><span class="mp-field-label">License</span> {selectedPlugin.license}</div>{/if}
|
{#if sp.license}<span><b>License</b> {sp.license}</span>{/if}
|
||||||
{#if selectedPlugin.sizeBytes != null}<div class="mp-field"><span class="mp-field-label">Size</span> {(selectedPlugin.sizeBytes / 1024).toFixed(0)} KB</div>{/if}
|
{#if sp.sizeBytes != null}<span><b>Size</b> {(sp.sizeBytes / 1024).toFixed(0)} KB</span>{/if}
|
||||||
{#if selectedPlugin.minAgorVersion}<div class="mp-field"><span class="mp-field-label">Min Version</span> {selectedPlugin.minAgorVersion}</div>{/if}
|
{#if sp.minAgorVersion}<span><b>Min Ver</b> {sp.minAgorVersion}</span>{/if}
|
||||||
</div>
|
</div>
|
||||||
{#if selectedPlugin.permissions.length > 0}
|
{#if sp.permissions.length > 0}
|
||||||
<div class="mp-detail-section">Permissions</div>
|
<div class="mp-section-hd">Permissions</div>
|
||||||
<div class="mp-detail-perms">{#each selectedPlugin.permissions as p}<span class="mp-perm">{p}</span>{/each}</div>
|
<div class="mp-perms">{#each sp.permissions as pm}<span class="mp-perm">{pm}</span>{/each}</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="mp-detail-links">
|
<div class="mp-links">
|
||||||
{#if selectedPlugin.homepage}<a href={selectedPlugin.homepage} target="_blank" rel="noopener" class="mp-link">Homepage</a>{/if}
|
{#if sp.homepage}<a href={sp.homepage} target="_blank" rel="noopener" class="mp-link">Homepage</a>{/if}
|
||||||
{#if selectedPlugin.repository}<a href={selectedPlugin.repository} target="_blank" rel="noopener" class="mp-link">Repository</a>{/if}
|
{#if sp.repository}<a href={sp.repository} target="_blank" rel="noopener" class="mp-link">Repository</a>{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Installed Tab -->
|
|
||||||
{:else}
|
{:else}
|
||||||
<div class="mp-installed-bar">
|
<div class="mp-bar">
|
||||||
<button class="mp-btn" onclick={handleCheckUpdates} disabled={checking}>
|
<button class="mp-btn" onclick={handleCheckUpdates} disabled={checking}>
|
||||||
{checking ? 'Checking...' : 'Check for Updates'}
|
{checking ? 'Checking...' : 'Check for Updates'}
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -273,44 +210,39 @@
|
||||||
{#if installedLoading}
|
{#if installedLoading}
|
||||||
<div class="mp-state">Loading installed plugins...</div>
|
<div class="mp-state">Loading installed plugins...</div>
|
||||||
{:else if installedError}
|
{:else if installedError}
|
||||||
<div class="mp-state mp-error">{installedError}</div>
|
<div class="mp-state mp-err">{installedError}</div>
|
||||||
{:else if installed.length === 0}
|
{:else if installed.length === 0}
|
||||||
<div class="mp-state">No plugins installed. Browse the catalog to get started.</div>
|
<div class="mp-state">No plugins installed. Browse the catalog to get started.</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="mp-installed-list">
|
<div class="mp-list">
|
||||||
{#each installed as plugin (plugin.id)}
|
{#each installed as p (p.id)}
|
||||||
<div class="mp-installed-item">
|
<div class="mp-item">
|
||||||
<div class="mp-installed-info">
|
<div class="mp-item-info">
|
||||||
<div class="mp-installed-top">
|
<div class="mp-row">
|
||||||
<span class="mp-card-name">{plugin.name}</span>
|
<span class="mp-name">{p.name}</span>
|
||||||
<span class="mp-card-version">v{plugin.version}</span>
|
<span class="mp-ver">v{p.version}</span>
|
||||||
{#if plugin.hasUpdate && plugin.latestVersion}
|
{#if p.hasUpdate && p.latestVersion}<span class="mp-upd-badge">v{p.latestVersion} available</span>{/if}
|
||||||
<span class="mp-update-badge">v{plugin.latestVersion} available</span>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="mp-card-author">by {plugin.author}</div>
|
<div class="mp-author">by {p.author}</div>
|
||||||
<div class="mp-card-desc">{plugin.description}</div>
|
<div class="mp-desc">{p.description}</div>
|
||||||
{#if plugin.permissions.length > 0}
|
{#if p.permissions.length > 0}
|
||||||
<div class="mp-card-perms">
|
<div class="mp-perms">{#each p.permissions as pm}<span class="mp-perm">{pm}</span>{/each}</div>
|
||||||
{#each plugin.permissions as perm}<span class="mp-perm">{perm}</span>{/each}
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="mp-installed-actions">
|
<div class="mp-item-acts">
|
||||||
{#if plugin.hasUpdate}
|
{#if p.hasUpdate}
|
||||||
<button class="mp-btn mp-btn-install" disabled={updating.has(plugin.id)} onclick={() => handleUpdate(plugin.id)}>
|
<button class="mp-btn primary" disabled={updating.has(p.id)} onclick={() => handleUpdate(p.id)}>
|
||||||
{updating.has(plugin.id) ? 'Updating...' : 'Update'}
|
{updating.has(p.id) ? 'Updating...' : 'Update'}
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
{#if confirmUninstall === plugin.id}
|
{#if confirmUninstall === p.id}
|
||||||
<button class="mp-btn mp-btn-danger" onclick={() => handleUninstall(plugin.id)}>Confirm</button>
|
<button class="mp-btn danger" onclick={() => handleUninstall(p.id)}>Confirm</button>
|
||||||
<button class="mp-btn" onclick={() => (confirmUninstall = null)}>Cancel</button>
|
<button class="mp-btn" onclick={() => (confirmUninstall = null)}>Cancel</button>
|
||||||
{:else}
|
{:else}
|
||||||
<button
|
<button class="mp-btn danger" disabled={uninstalling.has(p.id)}
|
||||||
class="mp-btn mp-btn-danger"
|
onclick={() => (confirmUninstall = p.id)}>
|
||||||
disabled={uninstalling.has(plugin.id)}
|
{uninstalling.has(p.id) ? 'Removing...' : 'Uninstall'}
|
||||||
onclick={() => (confirmUninstall = plugin.id)}
|
</button>
|
||||||
>{uninstalling.has(plugin.id) ? 'Removing...' : 'Uninstall'}</button>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -321,183 +253,58 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.mp-root {
|
.mp-root { position: relative; display: flex; flex-direction: column; gap: 0.75rem; padding: 0.75rem; height: 100%; overflow-y: auto; color: var(--ctp-text); font-family: var(--ui-font-family, system-ui, sans-serif); font-size: var(--ui-font-size, 0.8125rem); }
|
||||||
position: relative;
|
.mp-toast { position: fixed; top: 0.75rem; right: 0.75rem; padding: 0.5rem 1rem; background: var(--ctp-surface1); color: var(--ctp-text); border: 1px solid var(--ctp-surface2); border-radius: 0.375rem; font-size: 0.75rem; z-index: 999; animation: mp-fi 0.15s ease-out; }
|
||||||
display: flex;
|
@keyframes mp-fi { from { opacity: 0; transform: translateY(-0.5rem); } }
|
||||||
flex-direction: column;
|
.mp-tabs { display: flex; gap: 0.25rem; border-bottom: 1px solid var(--ctp-surface0); padding-bottom: 0.25rem; }
|
||||||
gap: 0.75rem;
|
.mp-tab { padding: 0.375rem 0.75rem; background: none; border: none; border-bottom: 2px solid transparent; color: var(--ctp-subtext0); cursor: pointer; font-size: 0.8125rem; }
|
||||||
padding: 0.75rem;
|
|
||||||
height: 100%;
|
|
||||||
overflow-y: auto;
|
|
||||||
color: var(--ctp-text);
|
|
||||||
font-family: var(--ui-font-family, system-ui, sans-serif);
|
|
||||||
font-size: var(--ui-font-size, 0.8125rem);
|
|
||||||
}
|
|
||||||
.mp-toast {
|
|
||||||
position: fixed;
|
|
||||||
top: 0.75rem;
|
|
||||||
right: 0.75rem;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
background: var(--ctp-surface1);
|
|
||||||
color: var(--ctp-text);
|
|
||||||
border: 1px solid var(--ctp-surface2);
|
|
||||||
border-radius: 0.375rem;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
z-index: 999;
|
|
||||||
animation: mp-fade-in 0.15s ease-out;
|
|
||||||
}
|
|
||||||
@keyframes mp-fade-in { from { opacity: 0; transform: translateY(-0.5rem); } }
|
|
||||||
.mp-tabs {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.25rem;
|
|
||||||
border-bottom: 1px solid var(--ctp-surface0);
|
|
||||||
padding-bottom: 0.25rem;
|
|
||||||
}
|
|
||||||
.mp-tab {
|
|
||||||
padding: 0.375rem 0.75rem;
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
border-bottom: 2px solid transparent;
|
|
||||||
color: var(--ctp-subtext0);
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
}
|
|
||||||
.mp-tab:hover { color: var(--ctp-text); }
|
.mp-tab:hover { color: var(--ctp-text); }
|
||||||
.mp-tab.active { color: var(--ctp-blue); border-bottom-color: var(--ctp-blue); }
|
.mp-tab.active { color: var(--ctp-blue); border-bottom-color: var(--ctp-blue); }
|
||||||
.mp-search { display: flex; }
|
.mp-search { display: flex; }
|
||||||
.mp-search-input {
|
.mp-input { flex: 1; padding: 0.375rem 0.625rem; background: var(--ctp-surface0); border: 1px solid var(--ctp-surface1); border-radius: 0.375rem; color: var(--ctp-text); font-size: 0.8125rem; outline: none; }
|
||||||
flex: 1;
|
.mp-input::placeholder { color: var(--ctp-overlay0); }
|
||||||
padding: 0.375rem 0.625rem;
|
.mp-input:focus { border-color: var(--ctp-blue); }
|
||||||
background: var(--ctp-surface0);
|
|
||||||
border: 1px solid var(--ctp-surface1);
|
|
||||||
border-radius: 0.375rem;
|
|
||||||
color: var(--ctp-text);
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
.mp-search-input::placeholder { color: var(--ctp-overlay0); }
|
|
||||||
.mp-search-input:focus { border-color: var(--ctp-blue); }
|
|
||||||
.mp-state { padding: 2rem; text-align: center; color: var(--ctp-subtext0); }
|
.mp-state { padding: 2rem; text-align: center; color: var(--ctp-subtext0); }
|
||||||
.mp-error { color: var(--ctp-red); }
|
.mp-err { color: var(--ctp-red); }
|
||||||
.mp-grid {
|
.mp-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr)); gap: 0.5rem; }
|
||||||
display: grid;
|
.mp-card { display: flex; flex-direction: column; gap: 0.25rem; padding: 0.625rem; background: var(--ctp-surface0); border: 1px solid var(--ctp-surface1); border-radius: 0.375rem; cursor: pointer; transition: border-color 0.1s; }
|
||||||
grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
.mp-card {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.25rem;
|
|
||||||
padding: 0.625rem;
|
|
||||||
background: var(--ctp-surface0);
|
|
||||||
border: 1px solid var(--ctp-surface1);
|
|
||||||
border-radius: 0.375rem;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: border-color 0.1s;
|
|
||||||
}
|
|
||||||
.mp-card:hover { border-color: var(--ctp-blue); }
|
.mp-card:hover { border-color: var(--ctp-blue); }
|
||||||
.mp-card-header { display: flex; align-items: baseline; gap: 0.375rem; }
|
.mp-row { display: flex; align-items: baseline; gap: 0.375rem; }
|
||||||
.mp-card-name { font-weight: 600; color: var(--ctp-text); }
|
.mp-name { font-weight: 600; color: var(--ctp-text); }
|
||||||
.mp-card-version { font-size: 0.6875rem; color: var(--ctp-overlay0); font-family: monospace; }
|
.mp-ver { font-size: 0.6875rem; color: var(--ctp-overlay0); font-family: monospace; }
|
||||||
.mp-card-author { font-size: 0.6875rem; color: var(--ctp-subtext0); }
|
.mp-author { font-size: 0.6875rem; color: var(--ctp-subtext0); }
|
||||||
.mp-card-desc {
|
.mp-desc { font-size: 0.75rem; color: var(--ctp-subtext1); display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
||||||
font-size: 0.75rem;
|
.mp-meta { display: flex; justify-content: space-between; align-items: center; gap: 0.25rem; }
|
||||||
color: var(--ctp-subtext1);
|
.mp-tags { display: flex; gap: 0.25rem; flex-wrap: wrap; }
|
||||||
display: -webkit-box;
|
.mp-tag { font-size: 0.625rem; padding: 0.0625rem 0.375rem; background: color-mix(in srgb, var(--ctp-blue) 15%, transparent); color: var(--ctp-blue); border-radius: 0.25rem; }
|
||||||
-webkit-line-clamp: 2;
|
.mp-stats { display: flex; gap: 0.5rem; font-size: 0.6875rem; color: var(--ctp-overlay1); }
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.mp-card-meta { display: flex; justify-content: space-between; align-items: center; gap: 0.25rem; }
|
|
||||||
.mp-card-tags { display: flex; gap: 0.25rem; flex-wrap: wrap; }
|
|
||||||
.mp-tag {
|
|
||||||
font-size: 0.625rem;
|
|
||||||
padding: 0.0625rem 0.375rem;
|
|
||||||
background: color-mix(in srgb, var(--ctp-blue) 15%, transparent);
|
|
||||||
color: var(--ctp-blue);
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
}
|
|
||||||
.mp-card-stats { display: flex; gap: 0.5rem; font-size: 0.6875rem; color: var(--ctp-overlay1); }
|
|
||||||
.mp-stars { color: var(--ctp-yellow); letter-spacing: 0.05em; }
|
.mp-stars { color: var(--ctp-yellow); letter-spacing: 0.05em; }
|
||||||
.mp-card-perms { display: flex; gap: 0.25rem; flex-wrap: wrap; }
|
.mp-perms { display: flex; gap: 0.25rem; flex-wrap: wrap; }
|
||||||
.mp-perm {
|
.mp-perm { font-size: 0.625rem; padding: 0.0625rem 0.375rem; background: color-mix(in srgb, var(--ctp-peach) 12%, transparent); color: var(--ctp-peach); border-radius: 0.25rem; }
|
||||||
font-size: 0.625rem;
|
.mp-actions { margin-top: 0.25rem; display: flex; justify-content: flex-end; }
|
||||||
padding: 0.0625rem 0.375rem;
|
.mp-btn { padding: 0.25rem 0.75rem; border: 1px solid var(--ctp-surface1); border-radius: 0.25rem; background: var(--ctp-surface0); color: var(--ctp-subtext0); cursor: pointer; font-size: 0.75rem; }
|
||||||
background: color-mix(in srgb, var(--ctp-peach) 12%, transparent);
|
|
||||||
color: var(--ctp-peach);
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
}
|
|
||||||
.mp-card-actions { margin-top: 0.25rem; display: flex; justify-content: flex-end; }
|
|
||||||
.mp-btn {
|
|
||||||
padding: 0.25rem 0.75rem;
|
|
||||||
border: 1px solid var(--ctp-surface1);
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
background: var(--ctp-surface0);
|
|
||||||
color: var(--ctp-subtext0);
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
}
|
|
||||||
.mp-btn:hover:not(:disabled) { background: var(--ctp-surface1); color: var(--ctp-text); }
|
.mp-btn:hover:not(:disabled) { background: var(--ctp-surface1); color: var(--ctp-text); }
|
||||||
.mp-btn:disabled { opacity: 0.4; cursor: default; }
|
.mp-btn:disabled { opacity: 0.4; cursor: default; }
|
||||||
.mp-btn-install {
|
.mp-btn.primary { background: var(--ctp-blue); color: var(--ctp-base); border-color: var(--ctp-blue); }
|
||||||
background: var(--ctp-blue);
|
.mp-btn.primary:hover:not(:disabled) { opacity: 0.9; background: var(--ctp-blue); color: var(--ctp-base); }
|
||||||
color: var(--ctp-base);
|
.mp-btn.danger { color: var(--ctp-red); border-color: var(--ctp-red); background: none; }
|
||||||
border-color: var(--ctp-blue);
|
.mp-btn.danger:hover:not(:disabled) { background: color-mix(in srgb, var(--ctp-red) 12%, transparent); color: var(--ctp-red); }
|
||||||
}
|
.mp-x { background: none; border: none; color: var(--ctp-overlay0); cursor: pointer; font-size: 1rem; padding: 0.125rem 0.375rem; }
|
||||||
.mp-btn-install:hover:not(:disabled) { opacity: 0.9; background: var(--ctp-blue); color: var(--ctp-base); }
|
.mp-x:hover { color: var(--ctp-text); }
|
||||||
.mp-btn-danger { color: var(--ctp-red); border-color: var(--ctp-red); background: none; }
|
.mp-detail { background: var(--ctp-mantle); border: 1px solid var(--ctp-surface1); border-radius: 0.375rem; padding: 0.75rem; display: flex; flex-direction: column; gap: 0.5rem; }
|
||||||
.mp-btn-danger:hover:not(:disabled) {
|
.mp-detail-hdr { display: flex; justify-content: space-between; align-items: center; }
|
||||||
background: color-mix(in srgb, var(--ctp-red) 12%, transparent);
|
|
||||||
color: var(--ctp-red);
|
|
||||||
}
|
|
||||||
.mp-btn-close {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: var(--ctp-overlay0);
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 1rem;
|
|
||||||
padding: 0.125rem 0.375rem;
|
|
||||||
}
|
|
||||||
.mp-btn-close:hover { color: var(--ctp-text); }
|
|
||||||
.mp-detail {
|
|
||||||
background: var(--ctp-mantle);
|
|
||||||
border: 1px solid var(--ctp-surface1);
|
|
||||||
border-radius: 0.375rem;
|
|
||||||
padding: 0.75rem;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
.mp-detail-header { display: flex; justify-content: space-between; align-items: center; }
|
|
||||||
.mp-detail-name { margin: 0; font-size: 1rem; font-weight: 600; color: var(--ctp-text); }
|
.mp-detail-name { margin: 0; font-size: 1rem; font-weight: 600; color: var(--ctp-text); }
|
||||||
.mp-detail-desc { font-size: 0.8125rem; color: var(--ctp-subtext1); margin: 0; line-height: 1.5; }
|
.mp-detail-desc { font-size: 0.8125rem; color: var(--ctp-subtext1); margin: 0; line-height: 1.5; }
|
||||||
.mp-detail-fields { display: flex; flex-wrap: wrap; gap: 0.375rem 1rem; font-size: 0.75rem; color: var(--ctp-subtext1); }
|
.mp-fields { display: flex; flex-wrap: wrap; gap: 0.375rem 1rem; font-size: 0.75rem; color: var(--ctp-subtext1); }
|
||||||
.mp-field-label { color: var(--ctp-subtext0); margin-right: 0.25rem; }
|
.mp-fields b { color: var(--ctp-subtext0); font-weight: 500; margin-right: 0.25rem; }
|
||||||
.mp-detail-section { font-size: 0.6875rem; color: var(--ctp-subtext0); text-transform: uppercase; letter-spacing: 0.04em; }
|
.mp-section-hd { font-size: 0.6875rem; color: var(--ctp-subtext0); text-transform: uppercase; letter-spacing: 0.04em; }
|
||||||
.mp-detail-perms { display: flex; gap: 0.25rem; flex-wrap: wrap; }
|
.mp-links { display: flex; gap: 0.75rem; }
|
||||||
.mp-detail-links { display: flex; gap: 0.75rem; }
|
|
||||||
.mp-link { font-size: 0.75rem; color: var(--ctp-blue); text-decoration: none; }
|
.mp-link { font-size: 0.75rem; color: var(--ctp-blue); text-decoration: none; }
|
||||||
.mp-link:hover { text-decoration: underline; }
|
.mp-link:hover { text-decoration: underline; }
|
||||||
.mp-installed-bar { display: flex; justify-content: flex-end; }
|
.mp-bar { display: flex; justify-content: flex-end; }
|
||||||
.mp-installed-list { display: flex; flex-direction: column; gap: 0.375rem; }
|
.mp-list { display: flex; flex-direction: column; gap: 0.375rem; }
|
||||||
.mp-installed-item {
|
.mp-item { display: flex; justify-content: space-between; align-items: flex-start; gap: 0.75rem; padding: 0.625rem; background: var(--ctp-surface0); border: 1px solid var(--ctp-surface1); border-radius: 0.375rem; }
|
||||||
display: flex;
|
.mp-item-info { flex: 1; display: flex; flex-direction: column; gap: 0.25rem; }
|
||||||
justify-content: space-between;
|
.mp-upd-badge { font-size: 0.625rem; padding: 0.0625rem 0.375rem; background: color-mix(in srgb, var(--ctp-green) 15%, transparent); color: var(--ctp-green); border-radius: 0.25rem; }
|
||||||
align-items: flex-start;
|
.mp-item-acts { display: flex; gap: 0.375rem; flex-shrink: 0; align-items: center; }
|
||||||
gap: 0.75rem;
|
|
||||||
padding: 0.625rem;
|
|
||||||
background: var(--ctp-surface0);
|
|
||||||
border: 1px solid var(--ctp-surface1);
|
|
||||||
border-radius: 0.375rem;
|
|
||||||
}
|
|
||||||
.mp-installed-info { flex: 1; display: flex; flex-direction: column; gap: 0.25rem; }
|
|
||||||
.mp-installed-top { display: flex; align-items: baseline; gap: 0.375rem; }
|
|
||||||
.mp-update-badge {
|
|
||||||
font-size: 0.625rem;
|
|
||||||
padding: 0.0625rem 0.375rem;
|
|
||||||
background: color-mix(in srgb, var(--ctp-green) 15%, transparent);
|
|
||||||
color: var(--ctp-green);
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
}
|
|
||||||
.mp-installed-actions { display: flex; gap: 0.375rem; flex-shrink: 0; align-items: center; }
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue