feat: plugin install/uninstall lifecycle + UI buttons

- AppConfig: add InstalledPlugins []string
- Manager.Discover(): no config dependency, all plugins start inactive
- Manager.SyncConfig(): apply installed/enabled state from AppConfig
- Manager.Enable(): works for plugins without on_install hook
- Manager.Install/Uninstall(): run on_install/on_uninstall hooks
- ActivatePlugin: skip if HasInstall && !Installed
- ReloadPlugins: Discover → SyncConfig → InitRuntimes
- Bindings: InstallPlugin, UninstallPlugin
- SettingsPlugins: install/uninstall buttons, toggle only after install
- Calendar: migration moved from on_init to on_install, on_uninstall drops tables
- Tests: all 12 pass (manager + runtime + calendar)
This commit is contained in:
2026-06-07 15:28:37 +08:00
parent b80941f908
commit e99ff984b1
10 changed files with 1358 additions and 161 deletions
+93 -9
View File
@@ -35,6 +35,31 @@
}
}
async function install(p) {
error = ''
try {
await wailsCall('InstallPlugin', p.name)
p.installed = true
// Refresh list to reflect new state
plugins = await wailsCall('ListPlugins') || []
} catch (e) {
error = String(e)
}
}
async function uninstall(p) {
if (!confirm(`Удалить плагин «${p.name}»? Все данные плагина будут очищены. Файлы плагина сохранятся.`)) return
error = ''
try {
await wailsCall('UninstallPlugin', p.name)
p.installed = false
p.active = false
plugins = await wailsCall('ListPlugins') || []
} catch (e) {
error = String(e)
}
}
$: sidebarCount = plugins.reduce((n, p) => n + (p.uiContribs?.sidebarItems?.length || 0), 0)
$: tabCount = plugins.reduce((n, p) => n + (p.uiContribs?.nodeTabs?.length || 0), 0)
</script>
@@ -82,15 +107,36 @@
</div>
</div>
<div class="plugin-toggle">
<button
class="toggle-btn"
class:active={p.active}
on:click={() => toggle(p)}
role="switch"
aria-checked={p.active}
>
<span class="toggle-knob"></span>
</button>
{#if p.hasInstall && !p.installed}
<button class="install-btn" on:click={() => install(p)}>
📦 Установить
</button>
{:else if p.hasInstall && p.installed}
<div class="toggle-group">
<button
class="toggle-btn"
class:active={p.active}
on:click={() => toggle(p)}
role="switch"
aria-checked={p.active}
>
<span class="toggle-knob"></span>
</button>
<button class="uninstall-btn btn-sm" on:click={() => uninstall(p)} title="Удалить плагин (очистить данные)">
🗑
</button>
</div>
{:else}
<button
class="toggle-btn"
class:active={p.active}
on:click={() => toggle(p)}
role="switch"
aria-checked={p.active}
>
<span class="toggle-knob"></span>
</button>
{/if}
</div>
</div>
{/each}
@@ -201,4 +247,42 @@
.toggle-btn.active .toggle-knob {
left: 22px;
}
.toggle-group {
display: flex;
align-items: center;
gap: 6px;
}
.install-btn {
padding: 6px 14px;
background: #6366f1;
color: #fff;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
white-space: nowrap;
}
.install-btn:hover { filter: brightness(1.15); }
.uninstall-btn {
width: 28px;
height: 28px;
background: var(--surface, #1a1a2e);
color: #ef4444;
border: 1px solid rgba(239,68,68,0.3);
border-radius: 6px;
cursor: pointer;
font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
opacity: 0.6;
transition: all 0.15s;
}
.uninstall-btn:hover {
opacity: 1;
background: rgba(239,68,68,0.1);
}
</style>