246 lines
8.9 KiB
TypeScript
246 lines
8.9 KiB
TypeScript
import { CommonModule } from '@angular/common';
|
|
import {
|
|
Component,
|
|
EventEmitter,
|
|
Output,
|
|
computed,
|
|
inject,
|
|
input,
|
|
signal
|
|
} from '@angular/core';
|
|
import { Router } from '@angular/router';
|
|
import { NgIcon, provideIcons } from '@ng-icons/core';
|
|
import {
|
|
lucideArrowLeft,
|
|
lucideBug,
|
|
lucideCheck,
|
|
lucidePackage,
|
|
lucidePlay,
|
|
lucideRefreshCw,
|
|
lucideSettings,
|
|
lucideShield,
|
|
lucideStore,
|
|
lucideX
|
|
} from '@ng-icons/lucide';
|
|
import type { PluginCapabilityId, TojuPluginInstallScope } from '../../../../shared-kernel';
|
|
import { PluginCapabilityService } from '../../application/services/plugin-capability.service';
|
|
import { PluginHostService } from '../../application/services/plugin-host.service';
|
|
import { PluginLoggerService } from '../../application/services/plugin-logger.service';
|
|
import { PluginRegistryService } from '../../application/services/plugin-registry.service';
|
|
import { PluginRequirementStateService } from '../../application/services/plugin-requirement-state.service';
|
|
import { PluginUiRegistryService } from '../../application/services/plugin-ui-registry.service';
|
|
import { getPluginInstallScope } from '../../domain/logic/plugin-install-scope.logic';
|
|
import type { RegisteredPlugin } from '../../domain/models/plugin-runtime.models';
|
|
import { PluginRenderHostComponent } from '../plugin-render-host/plugin-render-host.component';
|
|
|
|
type PluginManagerTab = 'docs' | 'extensions' | 'installed' | 'logs' | 'requirements' | 'settings';
|
|
|
|
@Component({
|
|
selector: 'app-plugin-manager',
|
|
standalone: true,
|
|
imports: [
|
|
CommonModule,
|
|
NgIcon,
|
|
PluginRenderHostComponent
|
|
],
|
|
templateUrl: './plugin-manager.component.html',
|
|
viewProviders: [
|
|
provideIcons({
|
|
lucideArrowLeft,
|
|
lucideBug,
|
|
lucideCheck,
|
|
lucidePackage,
|
|
lucidePlay,
|
|
lucideRefreshCw,
|
|
lucideSettings,
|
|
lucideShield,
|
|
lucideStore,
|
|
lucideX
|
|
})
|
|
]
|
|
})
|
|
export class PluginManagerComponent {
|
|
@Output() readonly closed = new EventEmitter<void>();
|
|
@Output() readonly storeOpened = new EventEmitter<void>();
|
|
|
|
readonly scope = input<TojuPluginInstallScope>('client');
|
|
|
|
readonly capabilities = inject(PluginCapabilityService);
|
|
readonly host = inject(PluginHostService);
|
|
readonly logger = inject(PluginLoggerService);
|
|
readonly registry = inject(PluginRegistryService);
|
|
readonly requirementState = inject(PluginRequirementStateService);
|
|
readonly router = inject(Router);
|
|
readonly uiRegistry = inject(PluginUiRegistryService);
|
|
readonly activeTab = signal<PluginManagerTab>('installed');
|
|
readonly busyPluginId = signal<string | null>(null);
|
|
readonly busyAll = signal(false);
|
|
readonly selectedPluginId = signal<string | null>(null);
|
|
readonly allEntries = this.registry.entries;
|
|
readonly entries = computed(() => this.allEntries().filter((entry) => this.entryBelongsToScope(entry)));
|
|
readonly managerTitle = computed(() => this.scope() === 'server' ? 'Server plugins' : 'Client plugins');
|
|
readonly managerDescription = computed(() => this.scope() === 'server'
|
|
? 'Plugins installed for the current chat server.'
|
|
: 'Global client plugins installed on this device.');
|
|
readonly selectedPlugin = computed(() => {
|
|
const selectedPluginId = this.selectedPluginId();
|
|
|
|
return this.entries().find((entry) => entry.manifest.id === selectedPluginId) ?? this.entries()[0] ?? null;
|
|
});
|
|
readonly missingCapabilities = computed(() => {
|
|
const selectedPlugin = this.selectedPlugin();
|
|
|
|
return selectedPlugin ? this.capabilities.missing(selectedPlugin.manifest) : [];
|
|
});
|
|
readonly selectedLogs = computed(() => {
|
|
const selectedPlugin = this.selectedPlugin();
|
|
|
|
return selectedPlugin ? this.logger.entries().filter((entry) => entry.pluginId === selectedPlugin.manifest.id)
|
|
.slice(-20) : [];
|
|
});
|
|
readonly extensionCounts = computed(() => ({
|
|
appPages: this.uiRegistry.appPageRecords().filter((record) => this.hasVisiblePlugin(record.pluginId)).length,
|
|
channelSections: this.uiRegistry.channelSectionRecords().filter((record) => this.hasVisiblePlugin(record.pluginId)).length,
|
|
composerActions: this.uiRegistry.composerActionRecords().filter((record) => this.hasVisiblePlugin(record.pluginId)).length,
|
|
embeds: this.uiRegistry.embedRecords().filter((record) => this.hasVisiblePlugin(record.pluginId)).length,
|
|
profileActions: this.uiRegistry.profileActionRecords().filter((record) => this.hasVisiblePlugin(record.pluginId)).length,
|
|
settingsPages: this.uiRegistry.settingsPageRecords().filter((record) => this.hasVisiblePlugin(record.pluginId)).length,
|
|
sidePanels: this.uiRegistry.sidePanelRecords().filter((record) => this.hasVisiblePlugin(record.pluginId)).length,
|
|
toolbarActions: this.uiRegistry.toolbarActionRecords().filter((record) => this.hasVisiblePlugin(record.pluginId)).length
|
|
}));
|
|
readonly requirementComparisons = computed(() => this.scope() === 'server' ? this.requirementState.comparisons() : []);
|
|
readonly uiConflicts = computed(() => this.uiRegistry.conflicts()
|
|
.filter((conflict) => conflict.pluginIds.some((pluginId) => this.hasVisiblePlugin(pluginId))));
|
|
readonly selectedRequirement = computed(() => {
|
|
const selectedPlugin = this.selectedPlugin();
|
|
|
|
return selectedPlugin ? this.requirementState.comparisonFor(selectedPlugin.manifest.id) : null;
|
|
});
|
|
readonly selectedSettingsSchema = computed(() => this.selectedPlugin()?.manifest.settings ?? null);
|
|
readonly selectedSettingsPages = computed(() => {
|
|
const selectedPlugin = this.selectedPlugin();
|
|
|
|
return selectedPlugin
|
|
? this.uiRegistry.settingsPageRecords().filter((record) => record.pluginId === selectedPlugin.manifest.id)
|
|
: [];
|
|
});
|
|
readonly emptyTitle = computed(() => this.scope() === 'server' ? 'No server plugins installed.' : 'No client plugins installed.');
|
|
readonly emptyBody = computed(() => this.scope() === 'server'
|
|
? 'Server-scoped plugins use scope: server in toju-plugin.json.'
|
|
: 'Client-scoped plugins use scope: client or omit scope in toju-plugin.json.');
|
|
readonly selectedDocs = computed(() => {
|
|
const manifest = this.selectedPlugin()?.manifest;
|
|
|
|
if (!manifest) {
|
|
return [];
|
|
}
|
|
|
|
return [
|
|
{ label: 'Readme', url: manifest.readme },
|
|
{ label: 'Homepage', url: manifest.homepage },
|
|
{ label: 'Changelog', url: manifest.changelog },
|
|
{ label: 'Support', url: manifest.bugs }
|
|
].filter((item): item is { label: string; url: string } => typeof item.url === 'string' && item.url.length > 0);
|
|
});
|
|
|
|
setTab(tab: PluginManagerTab): void {
|
|
this.activeTab.set(tab);
|
|
}
|
|
|
|
openStore(): void {
|
|
const returnUrl = this.router.url.startsWith('/plugin-store') ? '/dashboard' : this.router.url;
|
|
|
|
this.storeOpened.emit();
|
|
void this.router.navigate(['/plugin-store'], { queryParams: { returnUrl } });
|
|
}
|
|
|
|
selectPlugin(pluginId: string): void {
|
|
this.selectedPluginId.set(pluginId);
|
|
}
|
|
|
|
grantAll(entry: RegisteredPlugin): void {
|
|
this.capabilities.grantAll(entry.manifest);
|
|
}
|
|
|
|
toggleCapability(entry: RegisteredPlugin, capability: PluginCapabilityId): void {
|
|
if (this.capabilities.has(entry.manifest.id, capability)) {
|
|
this.capabilities.revoke(entry.manifest.id, capability);
|
|
return;
|
|
}
|
|
|
|
this.capabilities.grant(entry.manifest.id, capability);
|
|
}
|
|
|
|
async activateAll(): Promise<void> {
|
|
this.busyAll.set(true);
|
|
|
|
try {
|
|
await this.host.activateReadyPlugins();
|
|
} finally {
|
|
this.busyAll.set(false);
|
|
}
|
|
}
|
|
|
|
async reload(entry: RegisteredPlugin): Promise<void> {
|
|
this.busyPluginId.set(entry.manifest.id);
|
|
|
|
try {
|
|
await this.host.reloadPlugin(entry.manifest.id);
|
|
} finally {
|
|
this.busyPluginId.set(null);
|
|
}
|
|
}
|
|
|
|
async activate(entry: RegisteredPlugin): Promise<void> {
|
|
this.busyPluginId.set(entry.manifest.id);
|
|
|
|
try {
|
|
await this.host.activatePluginById(entry.manifest.id);
|
|
} finally {
|
|
this.busyPluginId.set(null);
|
|
}
|
|
}
|
|
|
|
async unload(entry: RegisteredPlugin): Promise<void> {
|
|
this.busyPluginId.set(entry.manifest.id);
|
|
|
|
try {
|
|
await this.host.deactivatePlugin(entry.manifest.id, { forgetActivation: true });
|
|
} finally {
|
|
this.busyPluginId.set(null);
|
|
}
|
|
}
|
|
|
|
setEnabled(entry: RegisteredPlugin, enabled: boolean): void {
|
|
this.registry.setEnabled(entry.manifest.id, enabled);
|
|
}
|
|
|
|
isSelected(entry: RegisteredPlugin): boolean {
|
|
return this.selectedPlugin()?.manifest.id === entry.manifest.id;
|
|
}
|
|
|
|
isActive(entry: RegisteredPlugin): boolean {
|
|
return this.host.isPluginActive(entry.manifest.id);
|
|
}
|
|
|
|
close(): void {
|
|
this.closed.emit();
|
|
}
|
|
|
|
trackEntry(index: number, entry: RegisteredPlugin): string {
|
|
return entry.manifest.id;
|
|
}
|
|
|
|
trackCapability(index: number, capability: PluginCapabilityId): string {
|
|
return capability;
|
|
}
|
|
|
|
private entryBelongsToScope(entry: RegisteredPlugin): boolean {
|
|
return getPluginInstallScope(entry.manifest) === this.scope();
|
|
}
|
|
|
|
private hasVisiblePlugin(pluginId: string): boolean {
|
|
return this.entries().some((entry) => entry.manifest.id === pluginId);
|
|
}
|
|
}
|