Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/improsocial/impro
An extensible Bluesky client for web impro.social
Something went wrong. Try again.
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340import { 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`<div id="installed-plugins-view"> ${headerTemplate({ title: "Plugins", })} <main> <a class="community-plugins-link" href="/plugins/community"> <span class="community-plugins-link-icon" ><app-icon icon="globe-grid-line"></app-icon ></span> <span class="community-plugins-link-text"> <span class="community-plugins-link-title" >Browse community plugins</span > <span class="community-plugins-link-subtitle" >Discover plugins built by the community</span > </span> <span class="community-plugins-link-arrow" ><app-icon icon="chevron-right-line"></app-icon ></span> </a>
${!pluginsInfo ? html`<p class="plugin-list-loading">Loading…</p>` : pluginsInfo.length === 0 ? html`<div class="plugins-empty-state"> <div class="plugins-empty-state-title"> No plugins installed </div> <p class="plugins-empty-state-message"> Browse the community registry to find and install plugins. </p> </div>` : html`<div class="installed-plugins-header"> <h2>Installed plugins</h2> <div class="installed-plugins-header-actions"> <button class="plugin-check-updates-button rounded-button rounded-button-primary" ?disabled=${checkingForUpdates || updatingAll} @click=${() => hasAvailableUpdates ? updateAllPlugins() : checkForUpdates()} > ${checkingForUpdates || updatingAll ? html`${hasAvailableUpdates ? "Updating..." : "Checking..."} <div class="loading-spinner" data-testid="loading-spinner" ></div>` : hasAvailableUpdates ? "Update all" : "Check for updates"} </button> <button class="plugin-reload-button icon-button" aria-label="Reload plugins" ?disabled=${reloading} @click=${() => reloadPlugins()} > <app-icon icon="reload-line"></app-icon> </button> </div> </div> <ul class="plugin-list"> ${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` <div class="plugin-list-item-name"> ${plugin.name} ${isLocal ? html`<span class="plugin-local-badge" >local</span >` : ""} </div> ${plugin.description ? html`<div class="plugin-list-item-description"> ${plugin.description} </div>` : ""} <div class="plugin-list-item-version"> Version: ${plugin.version} </div> <div class="plugin-list-item-author"> By ${plugin.author} </div> `; // Keyed to avoid flipping animation when plugins are added / removed return keyed( plugin.id, html` <li class="plugin-list-item ${state.$uninstallingIds.has( plugin.id, ) ? "uninstalling" : ""}" ?inert=${isPending} > ${isLocal ? html`<div class="plugin-list-item-info"> ${infoContent} </div>` : html`<a class="plugin-list-item-info" href=${linkToCommunityPlugin(plugin.id)} > ${infoContent} </a>`} <div class="plugin-list-item-controls"> ${hasUpdate ? html`<button class="plugin-update-button rounded-button rounded-button-primary" @click=${() => updatePlugin(plugin)} ?disabled=${isUpdating} > ${isUpdating ? html`Updating <div class="loading-spinner" data-testid="loading-spinner" ></div>` : "Update"} </button>` : ""} ${plugin.enabled && (plugin.hasSettings || plugin.hasSystemSettings) ? html`<a class="plugin-settings-link icon-button" href=${linkToPluginSettings(plugin.id)} aria-label="Settings for ${plugin.name}" > <app-icon icon="settings-cog-line" ></app-icon> </a>` : ""} <button class="plugin-uninstall-button icon-button" aria-label="Uninstall ${plugin.name}" @click=${() => uninstallPlugin(plugin)} > <app-icon icon="delete-bin-line"></app-icon> </button> <toggle-switch class="plugin-toggle" label="Enable ${plugin.name}" ?checked=${state.$enablingIds.has(plugin.id) ? true : state.$disablingIds.has(plugin.id) ? false : plugin.enabled} ?disabled=${state.$enablingIds.has(plugin.id) || state.$disablingIds.has(plugin.id)} @change=${() => togglePlugin(plugin)} ></toggle-switch> </div> </li> `, ); })} </ul>`} </main> </div>`, root, ); });}