import { html, render, keyed } from "/js/lib/lit-html.js";
import { pageEffect, bindPageTitle } from "/js/router.js";
import { headerTemplate } from "/js/templates/header.template.js";
import "/js/components/app-icon.js";
import { confirmModal } from "/js/modals/confirm.modal.js";
import { showToast } from "/js/toasts.js";
import { Signal, SignalSet, ReactiveStore } from "/js/signals.js";
import { PermissionsDeclinedError } from "/js/plugins/pluginService.js";
import { linkToPluginSettings, linkToCommunityPlugin } from "/js/navigation.js";
import "/js/components/toggle-switch.js";
export default async function installedPluginsView({
root,
router,
context: { auth, pluginService },
}) {
await auth.requireAuth();
const state = new ReactiveStore("installedPluginsView");
state.$uninstallingIds = new SignalSet();
state.$enablingIds = new SignalSet();
state.$disablingIds = new SignalSet();
state.$updatingIds = new SignalSet();
state.$reloading = new Signal.State(false);
state.$checkingForUpdates = new Signal.State(false);
state.$updatingAll = new Signal.State(false);
async function uninstallPlugin(plugin) {
const confirmed = await confirmModal(
`"${plugin.name}" will be uninstalled and its settings will be deleted.`,
{
title: "Uninstall plugin?",
confirmButtonStyle: "danger",
confirmButtonText: "Uninstall",
},
);
if (!confirmed) return;
state.$uninstallingIds.add(plugin.id);
try {
await pluginService.uninstallPlugin(plugin.id);
showToast(`Uninstalled ${plugin.name}`);
} finally {
state.$uninstallingIds.delete(plugin.id);
}
}
async function reloadPlugins() {
if (state.$reloading.get()) return;
state.$reloading.set(true);
try {
await pluginService.reloadPlugins();
showToast("Reloaded plugins");
} catch (e) {
console.error(e);
showToast("Failed to reload plugins", { style: "error" });
} finally {
state.$reloading.set(false);
}
}
async function checkForUpdates() {
if (state.$checkingForUpdates.get()) return;
state.$checkingForUpdates.set(true);
try {
const updates = await pluginService.checkForUpdates();
if (updates.size === 0) {
showToast("All plugins are up to date", { style: "success" });
} else {
showToast(
`${updates.size} update${updates.size === 1 ? "" : "s"} available`,
);
}
} catch (e) {
showToast("Failed to check for updates", { style: "error" });
} finally {
state.$checkingForUpdates.set(false);
}
}
async function updatePlugin(plugin) {
state.$updatingIds.add(plugin.id);
try {
const result = await pluginService.updatePlugin(plugin.id);
if (result.updated) {
showToast(`Updated ${plugin.name} to v${result.version}`, {
style: "success",
});
}
} catch (e) {
if (e instanceof PermissionsDeclinedError) {
// User declined the permission prompt; nothing to report.
} else {
console.error(e);
showToast(`Failed to update ${plugin.name}`, {
style: "error",
});
}
} finally {
state.$updatingIds.delete(plugin.id);
}
}
async function updateAllPlugins() {
if (state.$updatingAll.get()) return;
state.$updatingAll.set(true);
try {
const { updated, failed } = await pluginService.updateAllPlugins();
if (failed.length > 0) {
showToast(`Updated ${updated.length}, failed ${failed.length}`, {
style: "error",
});
} else {
showToast(
`Updated ${updated.length} plugin${updated.length === 1 ? "" : "s"}`,
{ style: "success" },
);
}
} finally {
state.$updatingAll.set(false);
}
}
async function togglePlugin(plugin) {
const pendingIds = plugin.enabled
? state.$disablingIds
: state.$enablingIds;
pendingIds.add(plugin.id);
try {
if (plugin.enabled) {
await pluginService.disablePlugin(plugin.id);
showToast(`Disabled ${plugin.name}`);
} else {
try {
await pluginService.enablePlugin(plugin.id);
showToast(`Enabled ${plugin.name}`, { style: "success" });
} catch (e) {
const message = e.message ?? String(e);
showToast(`Error when loading ${plugin.name} - ${message}`, {
style: "error",
});
}
}
} finally {
pendingIds.delete(plugin.id);
}
}
bindPageTitle(root, () => "Plugins");
pageEffect(root, () => {
const reloading = state.$reloading.get();
const checkingForUpdates = state.$checkingForUpdates.get();
const updatingAll = state.$updatingAll.get();
const pluginsInfo = pluginService.$pluginsInfo.get();
const availableUpdates = pluginService.$availableUpdates.get();
const hasAvailableUpdates =
availableUpdates !== null && availableUpdates.size > 0;
render(
html`
${headerTemplate({
title: "Plugins",
})}
${!pluginsInfo
? html`Loading…
`
: pluginsInfo.length === 0
? html`
No plugins installed
Browse the community registry to find and install plugins.
`
: html`
${pluginsInfo.map((plugin) => {
const hasUpdate =
availableUpdates?.has(plugin.id) ?? false;
const isUpdating =
state.$updatingIds.has(plugin.id) ||
(updatingAll && hasUpdate);
const isPending =
state.$uninstallingIds.has(plugin.id) ||
state.$enablingIds.has(plugin.id) ||
state.$disablingIds.has(plugin.id) ||
isUpdating;
const isLocal = plugin.id.endsWith("__LOCAL");
const infoContent = html`
${plugin.name}
${isLocal
? html`local`
: ""}
${plugin.description
? html`
${plugin.description}
`
: ""}
Version: ${plugin.version}
By ${plugin.author}
`;
// Keyed to avoid flipping animation when plugins are added / removed
return keyed(
plugin.id,
html`
-
${isLocal
? html`
${infoContent}
`
: html`
${infoContent}
`}
${hasUpdate
? html`
`
: ""}
${plugin.enabled &&
(plugin.hasSettings || plugin.hasSystemSettings)
? html`
`
: ""}
togglePlugin(plugin)}
>
`,
);
})}
`}
`,
root,
);
});
}