feat: Rename to Toju and add translation
Some checks failed
Deploy Web Apps / deploy (push) Successful in 5m52s
Build Android APK / build-android-apk (push) Failing after 23m15s
Queue Release Build / prepare (push) Successful in 1m42s
Queue Release Build / build-linux (push) Failing after 9m33s
Queue Release Build / build-windows (push) Successful in 26m5s
Queue Release Build / finalize (push) Has been skipped
Some checks failed
Deploy Web Apps / deploy (push) Successful in 5m52s
Build Android APK / build-android-apk (push) Failing after 23m15s
Queue Release Build / prepare (push) Successful in 1m42s
Queue Release Build / build-linux (push) Failing after 9m33s
Queue Release Build / build-windows (push) Successful in 26m5s
Queue Release Build / finalize (push) Has been skipped
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import { Injector } from '@angular/core';
|
||||
import { provideTranslateService } from '@ngx-translate/core';
|
||||
import { environment } from '../../../../../environments/environment';
|
||||
import { AppI18nService } from '../../../../core/i18n';
|
||||
import type { TojuPluginManifest } from '../../../../shared-kernel';
|
||||
import { ElectronBridgeService } from '../../../../core/platform/electron/electron-bridge.service';
|
||||
import { PluginStoreService } from './plugin-store.service';
|
||||
@@ -259,6 +261,11 @@ function createService(
|
||||
): PluginStoreService {
|
||||
const injector = Injector.create({
|
||||
providers: [
|
||||
provideTranslateService({
|
||||
fallbackLang: 'en',
|
||||
lang: 'en'
|
||||
}),
|
||||
AppI18nService,
|
||||
PluginStoreService,
|
||||
{
|
||||
provide: ElectronBridgeService,
|
||||
@@ -299,7 +306,10 @@ function createService(
|
||||
]
|
||||
});
|
||||
|
||||
return injector.get(PluginStoreService);
|
||||
const service = injector.get(PluginStoreService);
|
||||
injector.get(AppI18nService).initialize();
|
||||
|
||||
return service;
|
||||
}
|
||||
|
||||
function toBase64(value: string): string {
|
||||
|
||||
@@ -12,6 +12,7 @@ import { Store } from '@ngrx/store';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { environment } from '../../../../../environments/environment';
|
||||
import { RealtimeSessionFacade } from '../../../../core/realtime';
|
||||
import { AppI18nService } from '../../../../core/i18n';
|
||||
import { getUserScopedStorageKey } from '../../../../core/storage/current-user-storage';
|
||||
import { ElectronBridgeService } from '../../../../core/platform/electron/electron-bridge.service';
|
||||
import { jsonStorage } from '../../../../infrastructure/persistence/json-storage.service';
|
||||
@@ -79,6 +80,7 @@ export class PluginStoreService {
|
||||
private readonly registry = inject(PluginRegistryService);
|
||||
private readonly serverDirectory = inject(ServerDirectoryFacade, { optional: true });
|
||||
private readonly store = inject(Store, { optional: true });
|
||||
private readonly appI18n = inject(AppI18nService);
|
||||
private readonly currentRoom = this.store?.selectSignal(selectCurrentRoom) ?? null;
|
||||
private readonly currentRoomId = this.store?.selectSignal(selectCurrentRoomId) ?? null;
|
||||
private readonly currentRoomName = this.store?.selectSignal(selectCurrentRoomName) ?? null;
|
||||
@@ -442,19 +444,25 @@ export class PluginStoreService {
|
||||
: 'installed';
|
||||
}
|
||||
|
||||
getActionLabel(plugin: PluginStoreEntry): PluginStoreActionLabel {
|
||||
getActionLabel(plugin: PluginStoreEntry): string {
|
||||
const state = this.getInstallState(plugin);
|
||||
const serverScoped = getStoreEntryInstallScope(plugin) === 'server';
|
||||
|
||||
if (state === 'updateAvailable') {
|
||||
return serverScoped ? 'Update Server' : 'Update';
|
||||
return serverScoped
|
||||
? this.appI18n.instant('plugins.store.actions.updateServer')
|
||||
: this.appI18n.instant('plugins.store.actions.update');
|
||||
}
|
||||
|
||||
if (state === 'installed') {
|
||||
return serverScoped ? 'Remove from Server' : 'Uninstall';
|
||||
return serverScoped
|
||||
? this.appI18n.instant('plugins.store.actions.removeFromServer')
|
||||
: this.appI18n.instant('plugins.store.actions.uninstall');
|
||||
}
|
||||
|
||||
return serverScoped ? 'Install to Server' : 'Install';
|
||||
return serverScoped
|
||||
? this.appI18n.instant('plugins.store.actions.installToServer')
|
||||
: this.appI18n.instant('plugins.store.actions.install');
|
||||
}
|
||||
|
||||
canInstallPlugin(plugin: PluginStoreEntry): boolean {
|
||||
|
||||
@@ -2,19 +2,21 @@
|
||||
appThemeNode="contextMenuSurface"
|
||||
class="w-80 rounded-lg border border-border bg-card p-3 shadow-xl"
|
||||
role="menu"
|
||||
aria-label="Plugin actions"
|
||||
[attr.aria-label]="'plugins.actionMenu.menuAria' | translate"
|
||||
style="animation: profile-card-in 120ms cubic-bezier(0.2, 0, 0, 1) both"
|
||||
>
|
||||
<div class="mb-3 flex items-center justify-between gap-3">
|
||||
<div class="min-w-0">
|
||||
<p class="text-sm font-semibold text-foreground">Plugins</p>
|
||||
<p class="truncate text-xs text-muted-foreground">{{ actions().length }} available actions</p>
|
||||
<p class="text-sm font-semibold text-foreground">{{ 'plugins.actionMenu.title' | translate }}</p>
|
||||
<p class="truncate text-xs text-muted-foreground">
|
||||
{{ 'plugins.actionMenu.availableActions' | translate: { count: actions().length } }}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="grid h-7 w-7 shrink-0 place-items-center rounded-md text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground"
|
||||
aria-label="Close plugin menu"
|
||||
title="Close"
|
||||
[attr.aria-label]="'plugins.actionMenu.closeAria' | translate"
|
||||
[title]="'plugins.actionMenu.close' | translate"
|
||||
(click)="close()"
|
||||
>
|
||||
<ng-icon
|
||||
@@ -56,7 +58,7 @@
|
||||
</div>
|
||||
} @else {
|
||||
<p class="rounded-md border border-dashed border-border bg-background/40 px-3 py-4 text-center text-sm text-muted-foreground">
|
||||
No plugin actions available.
|
||||
{{ 'plugins.actionMenu.empty' | translate }}
|
||||
</p>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -16,6 +16,7 @@ import { PluginRegistryService } from '../../application/services/plugin-registr
|
||||
import type { PluginUiContributionRecord } from '../../application/services/plugin-ui-registry.service';
|
||||
import { PluginUiRegistryService } from '../../application/services/plugin-ui-registry.service';
|
||||
import { ThemeNodeDirective } from '../../../theme';
|
||||
import { APP_TRANSLATE_IMPORTS } from '../../../../core/i18n';
|
||||
|
||||
@Component({
|
||||
selector: 'app-plugin-action-menu',
|
||||
@@ -23,7 +24,8 @@ import { ThemeNodeDirective } from '../../../theme';
|
||||
imports: [
|
||||
CommonModule,
|
||||
NgIcon,
|
||||
ThemeNodeDirective
|
||||
ThemeNodeDirective,
|
||||
...APP_TRANSLATE_IMPORTS
|
||||
],
|
||||
viewProviders: [provideIcons({ lucideX })],
|
||||
templateUrl: './plugin-action-menu.component.html'
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex h-11 w-11 flex-shrink-0 items-center justify-center rounded-md text-muted-foreground hover:bg-muted hover:text-foreground md:h-8 md:w-8"
|
||||
aria-label="Back to settings"
|
||||
aria-label="{{ 'plugins.manager.backToSettings' | translate }}"
|
||||
(click)="close()"
|
||||
>
|
||||
<ng-icon
|
||||
@@ -32,7 +32,7 @@
|
||||
name="lucidePlay"
|
||||
size="16"
|
||||
/>
|
||||
Activate ready plugins
|
||||
{{ 'plugins.manager.activateReady' | translate }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -43,14 +43,14 @@
|
||||
name="lucideStore"
|
||||
size="16"
|
||||
/>
|
||||
Open Plugin Store
|
||||
{{ 'plugins.manager.openStore' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<nav
|
||||
class="no-scrollbar flex gap-2 overflow-x-auto border-b border-border px-3 py-2 md:px-4"
|
||||
aria-label="Plugin manager sections"
|
||||
aria-label="{{ 'plugins.manager.sectionsAria' | translate }}"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
@@ -62,7 +62,7 @@
|
||||
name="lucidePackage"
|
||||
size="16"
|
||||
/>
|
||||
Installed
|
||||
{{ 'plugins.manager.tabs.installed' | translate }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -74,7 +74,7 @@
|
||||
name="lucideSettings"
|
||||
size="16"
|
||||
/>
|
||||
Extension points
|
||||
{{ 'plugins.manager.tabs.extensions' | translate }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -86,7 +86,7 @@
|
||||
name="lucideShield"
|
||||
size="16"
|
||||
/>
|
||||
Requirements
|
||||
{{ 'plugins.manager.tabs.requirements' | translate }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -98,7 +98,7 @@
|
||||
name="lucideSettings"
|
||||
size="16"
|
||||
/>
|
||||
Settings
|
||||
{{ 'plugins.manager.tabs.settings' | translate }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -110,7 +110,7 @@
|
||||
name="lucidePackage"
|
||||
size="16"
|
||||
/>
|
||||
Docs
|
||||
{{ 'plugins.manager.tabs.docs' | translate }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -122,7 +122,7 @@
|
||||
name="lucideBug"
|
||||
size="16"
|
||||
/>
|
||||
Logs
|
||||
{{ 'plugins.manager.tabs.logs' | translate }}
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
@@ -134,20 +134,7 @@
|
||||
class="grid gap-3 md:grid-cols-2 xl:grid-cols-4"
|
||||
data-testid="plugin-extension-counts"
|
||||
>
|
||||
@for (
|
||||
item of [
|
||||
{ label: 'Settings pages', value: extensionCounts().settingsPages },
|
||||
{ label: 'App pages', value: extensionCounts().appPages },
|
||||
{ label: 'Side panels', value: extensionCounts().sidePanels },
|
||||
{ label: 'Channel sections', value: extensionCounts().channelSections },
|
||||
{ label: 'Composer actions', value: extensionCounts().composerActions },
|
||||
{ label: 'Profile actions', value: extensionCounts().profileActions },
|
||||
{ label: 'Toolbar actions', value: extensionCounts().toolbarActions },
|
||||
{ label: 'Slash commands', value: extensionCounts().slashCommands },
|
||||
{ label: 'Embed renderers', value: extensionCounts().embeds }
|
||||
];
|
||||
track item.label
|
||||
) {
|
||||
@for (item of extensionCountItems(); track item.label) {
|
||||
<article class="rounded-lg border border-border bg-card p-3">
|
||||
<p class="text-sm text-muted-foreground">{{ item.label }}</p>
|
||||
<p class="mt-2 text-2xl font-semibold">{{ item.value }}</p>
|
||||
@@ -159,17 +146,19 @@
|
||||
class="rounded-lg border border-border bg-card p-4"
|
||||
data-testid="plugin-conflict-diagnostics"
|
||||
>
|
||||
<h3 class="text-sm font-semibold">Conflict diagnostics</h3>
|
||||
<h3 class="text-sm font-semibold">{{ 'plugins.manager.conflicts.title' | translate }}</h3>
|
||||
@if (uiConflicts().length === 0) {
|
||||
<p class="mt-2 text-sm text-muted-foreground">
|
||||
No duplicate route, action, embed, channel, panel, or settings contribution ids detected.
|
||||
{{ 'plugins.manager.conflicts.none' | translate }}
|
||||
</p>
|
||||
} @else {
|
||||
<div class="mt-3 space-y-2">
|
||||
@for (conflict of uiConflicts(); track conflict.kind + conflict.contributionId) {
|
||||
<div class="rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm">
|
||||
<span class="font-medium">{{ conflict.kind }} / {{ conflict.contributionId }}</span>
|
||||
<span class="text-muted-foreground"> conflicts in {{ conflict.pluginIds.join(', ') }}</span>
|
||||
<span class="text-muted-foreground">
|
||||
{{ 'plugins.manager.conflicts.conflictsIn' | translate: { plugins: conflict.pluginIds.join(', ') } }}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -184,7 +173,7 @@
|
||||
>
|
||||
@if (requirementComparisons().length === 0) {
|
||||
<p class="rounded-lg border border-border bg-card p-4 text-sm text-muted-foreground">
|
||||
No server plugin requirements for the current room.
|
||||
{{ 'plugins.manager.requirements.empty' | translate }}
|
||||
</p>
|
||||
} @else {
|
||||
@for (comparison of requirementComparisons(); track comparison.pluginId) {
|
||||
@@ -197,9 +186,13 @@
|
||||
<span class="rounded bg-muted px-2 py-1 text-xs text-muted-foreground">{{ comparison.status }}</span>
|
||||
</div>
|
||||
@if (comparison.requirement) {
|
||||
<p class="mt-3 text-sm text-muted-foreground">Server status: {{ comparison.requirement.status }}</p>
|
||||
<p class="mt-3 text-sm text-muted-foreground">
|
||||
{{ 'plugins.manager.requirements.serverStatus' | translate: { status: comparison.requirement.status } }}
|
||||
</p>
|
||||
@if (comparison.requirement.versionRange) {
|
||||
<p class="mt-1 text-sm text-muted-foreground">Version range: {{ comparison.requirement.versionRange }}</p>
|
||||
<p class="mt-1 text-sm text-muted-foreground">
|
||||
{{ 'plugins.manager.requirements.versionRange' | translate: { range: comparison.requirement.versionRange } }}
|
||||
</p>
|
||||
}
|
||||
@if (comparison.requirement.reason) {
|
||||
<p class="mt-1 text-sm text-muted-foreground">{{ comparison.requirement.reason }}</p>
|
||||
@@ -229,7 +222,7 @@
|
||||
</div>
|
||||
<section class="rounded-lg border border-border bg-card p-3 md:p-4">
|
||||
@if (selectedPlugin(); as plugin) {
|
||||
<h3 class="text-sm font-semibold">{{ plugin.manifest.title }} settings</h3>
|
||||
<h3 class="text-sm font-semibold">{{ plugin.manifest.title }} {{ 'plugins.manager.settings.settingsSuffix' | translate }}</h3>
|
||||
@if (selectedSettingsPages().length > 0) {
|
||||
<div class="mt-4 space-y-3">
|
||||
@for (page of selectedSettingsPages(); track page.id) {
|
||||
@@ -243,7 +236,7 @@
|
||||
@if (selectedSettingsSchema()) {
|
||||
<pre class="mt-3 max-h-[420px] overflow-auto rounded-md bg-muted p-3 text-xs">{{ selectedSettingsSchema() | json }}</pre>
|
||||
} @else {
|
||||
<p class="mt-2 text-sm text-muted-foreground">This plugin does not declare a settings schema.</p>
|
||||
<p class="mt-2 text-sm text-muted-foreground">{{ 'plugins.manager.settings.noSchema' | translate }}</p>
|
||||
}
|
||||
}
|
||||
</section>
|
||||
@@ -289,7 +282,7 @@
|
||||
@case ('logs') {
|
||||
<div class="space-y-3">
|
||||
@if (!selectedPlugin()) {
|
||||
<p class="text-sm text-muted-foreground">No plugins installed.</p>
|
||||
<p class="text-sm text-muted-foreground">{{ 'plugins.manager.logs.noPlugins' | translate }}</p>
|
||||
} @else {
|
||||
<div class="flex flex-wrap gap-2">
|
||||
@for (entry of entries(); track trackEntry($index, entry)) {
|
||||
@@ -305,7 +298,7 @@
|
||||
</div>
|
||||
<div class="rounded-lg border border-border bg-card">
|
||||
@if (selectedLogs().length === 0) {
|
||||
<p class="p-4 text-sm text-muted-foreground">No logs for selected plugin.</p>
|
||||
<p class="p-4 text-sm text-muted-foreground">{{ 'plugins.manager.logs.noLogs' | translate }}</p>
|
||||
} @else {
|
||||
@for (log of selectedLogs(); track log.timestamp) {
|
||||
<div class="border-b border-border px-4 py-3 last:border-b-0">
|
||||
@@ -360,7 +353,7 @@
|
||||
class="inline-flex min-h-11 items-center justify-center gap-1.5 rounded-md border border-border px-2 text-sm hover:bg-muted md:h-8 md:min-h-0"
|
||||
(click)="selectPlugin(entry.manifest.id)"
|
||||
>
|
||||
Select
|
||||
{{ 'plugins.manager.installed.select' | translate }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -371,7 +364,7 @@
|
||||
[name]="entry.enabled ? 'lucideX' : 'lucideCheck'"
|
||||
size="14"
|
||||
/>
|
||||
{{ entry.enabled ? 'Disable' : 'Enable' }}
|
||||
{{ (entry.enabled ? 'plugins.manager.installed.disable' : 'plugins.manager.installed.enable') | translate }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -383,7 +376,7 @@
|
||||
name="lucidePlay"
|
||||
size="14"
|
||||
/>
|
||||
Activate
|
||||
{{ 'plugins.manager.installed.activate' | translate }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -395,7 +388,7 @@
|
||||
name="lucideRefreshCw"
|
||||
size="14"
|
||||
/>
|
||||
Reload
|
||||
{{ 'plugins.manager.installed.reload' | translate }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -407,7 +400,7 @@
|
||||
name="lucideX"
|
||||
size="14"
|
||||
/>
|
||||
Unload
|
||||
{{ 'plugins.manager.installed.unload' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -426,17 +419,17 @@
|
||||
name="lucideShield"
|
||||
size="18"
|
||||
/>
|
||||
<h3 class="text-sm font-semibold">Capabilities</h3>
|
||||
<h3 class="text-sm font-semibold">{{ 'plugins.manager.capabilities.title' | translate }}</h3>
|
||||
</div>
|
||||
@if ((plugin.manifest.capabilities?.length ?? 0) === 0) {
|
||||
<p class="mt-3 text-sm text-muted-foreground">Plugin requests no capabilities.</p>
|
||||
<p class="mt-3 text-sm text-muted-foreground">{{ 'plugins.manager.capabilities.none' | translate }}</p>
|
||||
} @else {
|
||||
<button
|
||||
type="button"
|
||||
class="mt-3 min-h-11 rounded-md border border-border px-3 text-sm hover:bg-muted md:h-8 md:min-h-0"
|
||||
(click)="grantAll(plugin)"
|
||||
>
|
||||
Grant all requested
|
||||
{{ 'plugins.manager.capabilities.grantAll' | translate }}
|
||||
</button>
|
||||
<div class="mt-3 space-y-2">
|
||||
@for (capability of plugin.manifest.capabilities; track trackCapability($index, capability)) {
|
||||
@@ -453,7 +446,9 @@
|
||||
</div>
|
||||
}
|
||||
@if (missingCapabilities().length > 0) {
|
||||
<p class="mt-3 text-xs text-muted-foreground">Missing: {{ missingCapabilities().join(', ') }}</p>
|
||||
<p class="mt-3 text-xs text-muted-foreground">
|
||||
{{ 'plugins.manager.capabilities.missing' | translate: { capabilities: missingCapabilities().join(', ') } }}
|
||||
</p>
|
||||
}
|
||||
}
|
||||
</aside>
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
lucideX
|
||||
} from '@ng-icons/lucide';
|
||||
import type { PluginCapabilityId, TojuPluginInstallScope } from '../../../../shared-kernel';
|
||||
import { AppI18nService, APP_TRANSLATE_IMPORTS } from '../../../../core/i18n';
|
||||
import { PluginCapabilityService } from '../../application/services/plugin-capability.service';
|
||||
import { PluginHostService } from '../../application/services/plugin-host.service';
|
||||
import { PluginLoggerService } from '../../application/services/plugin-logger.service';
|
||||
@@ -41,7 +42,8 @@ type PluginManagerTab = 'docs' | 'extensions' | 'installed' | 'logs' | 'requirem
|
||||
imports: [
|
||||
CommonModule,
|
||||
NgIcon,
|
||||
PluginRenderHostComponent
|
||||
PluginRenderHostComponent,
|
||||
...APP_TRANSLATE_IMPORTS
|
||||
],
|
||||
templateUrl: './plugin-manager.component.html',
|
||||
viewProviders: [
|
||||
@@ -72,16 +74,19 @@ export class PluginManagerComponent {
|
||||
readonly requirementState = inject(PluginRequirementStateService);
|
||||
readonly router = inject(Router);
|
||||
readonly uiRegistry = inject(PluginUiRegistryService);
|
||||
private readonly appI18n = inject(AppI18nService);
|
||||
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 managerTitle = computed(() => this.scope() === 'server'
|
||||
? this.appI18n.instant('plugins.manager.serverTitle')
|
||||
: this.appI18n.instant('plugins.manager.clientTitle'));
|
||||
readonly managerDescription = computed(() => this.scope() === 'server'
|
||||
? 'Plugins installed for the current chat server.'
|
||||
: 'Global client plugins installed on this device.');
|
||||
? this.appI18n.instant('plugins.manager.serverDescription')
|
||||
: this.appI18n.instant('plugins.manager.clientDescription'));
|
||||
readonly selectedPlugin = computed(() => {
|
||||
const selectedPluginId = this.selectedPluginId();
|
||||
|
||||
@@ -109,6 +114,21 @@ export class PluginManagerComponent {
|
||||
slashCommands: this.uiRegistry.slashCommandRecords().filter((record) => this.hasVisiblePlugin(record.pluginId)).length,
|
||||
toolbarActions: this.uiRegistry.toolbarActionRecords().filter((record) => this.hasVisiblePlugin(record.pluginId)).length
|
||||
}));
|
||||
readonly extensionCountItems = computed(() => {
|
||||
const counts = this.extensionCounts();
|
||||
|
||||
return [
|
||||
{ label: this.appI18n.instant('plugins.manager.extensionCounts.settingsPages'), value: counts.settingsPages },
|
||||
{ label: this.appI18n.instant('plugins.manager.extensionCounts.appPages'), value: counts.appPages },
|
||||
{ label: this.appI18n.instant('plugins.manager.extensionCounts.sidePanels'), value: counts.sidePanels },
|
||||
{ label: this.appI18n.instant('plugins.manager.extensionCounts.channelSections'), value: counts.channelSections },
|
||||
{ label: this.appI18n.instant('plugins.manager.extensionCounts.composerActions'), value: counts.composerActions },
|
||||
{ label: this.appI18n.instant('plugins.manager.extensionCounts.profileActions'), value: counts.profileActions },
|
||||
{ label: this.appI18n.instant('plugins.manager.extensionCounts.toolbarActions'), value: counts.toolbarActions },
|
||||
{ label: this.appI18n.instant('plugins.manager.extensionCounts.slashCommands'), value: counts.slashCommands },
|
||||
{ label: this.appI18n.instant('plugins.manager.extensionCounts.embedRenderers'), value: counts.embeds }
|
||||
];
|
||||
});
|
||||
readonly requirementComparisons = computed(() => this.scope() === 'server' ? this.requirementState.comparisons() : []);
|
||||
readonly uiConflicts = computed(() => this.uiRegistry.conflicts()
|
||||
.filter((conflict) => conflict.pluginIds.some((pluginId) => this.hasVisiblePlugin(pluginId))));
|
||||
@@ -125,10 +145,12 @@ export class PluginManagerComponent {
|
||||
? 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 emptyTitle = computed(() => this.scope() === 'server'
|
||||
? this.appI18n.instant('plugins.manager.empty.serverTitle')
|
||||
: this.appI18n.instant('plugins.manager.empty.clientTitle'));
|
||||
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.');
|
||||
? this.appI18n.instant('plugins.manager.empty.serverBody')
|
||||
: this.appI18n.instant('plugins.manager.empty.clientBody'));
|
||||
readonly selectedDocs = computed(() => {
|
||||
const manifest = this.selectedPlugin()?.manifest;
|
||||
|
||||
@@ -137,10 +159,10 @@ export class PluginManagerComponent {
|
||||
}
|
||||
|
||||
return [
|
||||
{ label: 'Readme', url: manifest.readme },
|
||||
{ label: 'Homepage', url: manifest.homepage },
|
||||
{ label: 'Changelog', url: manifest.changelog },
|
||||
{ label: 'Support', url: manifest.bugs }
|
||||
{ label: this.appI18n.instant('plugins.manager.docs.readme'), url: manifest.readme },
|
||||
{ label: this.appI18n.instant('plugins.manager.docs.homepage'), url: manifest.homepage },
|
||||
{ label: this.appI18n.instant('plugins.manager.docs.changelog'), url: manifest.changelog },
|
||||
{ label: this.appI18n.instant('plugins.manager.docs.support'), url: manifest.bugs }
|
||||
].filter((item): item is { label: string; url: string } => typeof item.url === 'string' && item.url.length > 0);
|
||||
});
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<a
|
||||
routerLink="/dashboard"
|
||||
class="text-sm text-muted-foreground hover:text-foreground"
|
||||
>Back</a
|
||||
>{{ 'plugins.pageHost.back' | translate }}</a
|
||||
>
|
||||
@if (page(); as pageRecord) {
|
||||
<section class="mx-auto mt-6 max-w-5xl">
|
||||
@@ -14,8 +14,8 @@
|
||||
</section>
|
||||
} @else {
|
||||
<section class="mx-auto mt-6 max-w-2xl rounded-lg border border-border bg-card p-8 text-center">
|
||||
<h1 class="text-xl font-semibold">Plugin page unavailable</h1>
|
||||
<p class="mt-2 text-sm text-muted-foreground">The plugin page is not registered or the plugin is not loaded.</p>
|
||||
<h1 class="text-xl font-semibold">{{ 'plugins.pageHost.unavailableTitle' | translate }}</h1>
|
||||
<p class="mt-2 text-sm text-muted-foreground">{{ 'plugins.pageHost.unavailableBody' | translate }}</p>
|
||||
</section>
|
||||
}
|
||||
</main>
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
import { ActivatedRoute, RouterLink } from '@angular/router';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { PluginUiRegistryService } from '../../application/services/plugin-ui-registry.service';
|
||||
import { APP_TRANSLATE_IMPORTS } from '../../../../core/i18n';
|
||||
import { PluginRenderHostComponent } from '../plugin-render-host/plugin-render-host.component';
|
||||
|
||||
@Component({
|
||||
@@ -16,7 +17,8 @@ import { PluginRenderHostComponent } from '../plugin-render-host/plugin-render-h
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterLink,
|
||||
PluginRenderHostComponent
|
||||
PluginRenderHostComponent,
|
||||
...APP_TRANSLATE_IMPORTS
|
||||
],
|
||||
templateUrl: './plugin-page-host.component.html'
|
||||
})
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
type="button"
|
||||
(click)="goBack()"
|
||||
class="grid h-8 w-8 shrink-0 place-items-center rounded-lg border border-border text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground"
|
||||
title="Back to app"
|
||||
[title]="'plugins.store.backToApp' | translate"
|
||||
>
|
||||
<ng-icon
|
||||
name="lucideArrowLeft"
|
||||
@@ -25,9 +25,18 @@
|
||||
</div>
|
||||
|
||||
<div class="min-w-0">
|
||||
<h1 class="truncate text-xl font-semibold leading-7">Plugin Store</h1>
|
||||
<h1 class="truncate text-xl font-semibold leading-7">{{ 'plugins.store.title' | translate }}</h1>
|
||||
<p class="truncate text-sm text-muted-foreground">
|
||||
{{ installedCount() }} installed for {{ store.installScopeLabel() }} · {{ totalSourcePlugins() }} available · {{ sourceCount() }} sources
|
||||
{{
|
||||
'plugins.store.summary'
|
||||
| translate
|
||||
: {
|
||||
installed: installedCount(),
|
||||
scope: store.installScopeLabel(),
|
||||
available: totalSourcePlugins(),
|
||||
sources: sourceCount()
|
||||
}
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -42,7 +51,7 @@
|
||||
name="lucideSettings"
|
||||
class="h-4 w-4"
|
||||
/>
|
||||
Manage Plugins
|
||||
{{ 'plugins.store.manage' | translate }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -55,7 +64,7 @@
|
||||
class="h-4 w-4"
|
||||
[class.animate-spin]="store.isLoading()"
|
||||
/>
|
||||
Refresh
|
||||
{{ 'plugins.store.refresh' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
@@ -67,8 +76,8 @@
|
||||
type="text"
|
||||
[(ngModel)]="newSourceUrl"
|
||||
(keyup.enter)="addSourceUrl()"
|
||||
placeholder="https://example.com/plugins.json or /home/me/plugins/source.json"
|
||||
aria-label="Plugin source manifest URL"
|
||||
[placeholder]="'plugins.store.sourcePlaceholder' | translate"
|
||||
[attr.aria-label]="'plugins.store.sourceAria' | translate"
|
||||
class="min-h-9 w-full rounded-lg border border-border bg-secondary px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
/>
|
||||
</label>
|
||||
@@ -82,7 +91,7 @@
|
||||
name="lucidePlus"
|
||||
class="h-4 w-4"
|
||||
/>
|
||||
Add Source
|
||||
{{ 'plugins.store.addSource' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -97,11 +106,11 @@
|
||||
>
|
||||
<aside
|
||||
class="grid gap-3 xl:sticky xl:top-3"
|
||||
aria-label="Plugin sources and filters"
|
||||
[attr.aria-label]="'plugins.store.sourcesAndFiltersAria' | translate"
|
||||
>
|
||||
<section class="grid min-w-0 gap-1 rounded-lg border border-border bg-card p-3">
|
||||
<div class="mb-1 flex items-center justify-between gap-2">
|
||||
<h2 class="text-xs font-bold uppercase text-foreground">Sources</h2>
|
||||
<h2 class="text-xs font-bold uppercase text-foreground">{{ 'plugins.store.sources' | translate }}</h2>
|
||||
<span class="rounded-full bg-secondary px-2 py-0.5 text-xs text-muted-foreground">{{ sourceCount() }}</span>
|
||||
</div>
|
||||
|
||||
@@ -112,7 +121,7 @@
|
||||
[class.text-foreground]="selectedSourceUrl() === null"
|
||||
(click)="selectSource(null)"
|
||||
>
|
||||
<span class="truncate">All sources</span>
|
||||
<span class="truncate">{{ 'plugins.store.allSources' | translate }}</span>
|
||||
<strong class="rounded-full bg-background px-2 py-0.5 text-xs text-muted-foreground">{{ totalSourcePlugins() }}</strong>
|
||||
</button>
|
||||
|
||||
@@ -132,7 +141,7 @@
|
||||
type="button"
|
||||
(click)="removeSourceUrl(source.url)"
|
||||
class="grid h-8 w-8 shrink-0 place-items-center rounded-lg border border-border text-muted-foreground transition-colors hover:bg-destructive/10 hover:text-destructive"
|
||||
title="Remove source"
|
||||
[title]="'plugins.store.removeSource' | translate"
|
||||
>
|
||||
<ng-icon
|
||||
name="lucideTrash2"
|
||||
@@ -161,7 +170,7 @@
|
||||
type="button"
|
||||
(click)="removeSourceUrl(sourceUrl)"
|
||||
class="grid h-8 w-8 shrink-0 place-items-center rounded-lg border border-border text-muted-foreground transition-colors hover:bg-destructive/10 hover:text-destructive"
|
||||
title="Remove source"
|
||||
[title]="'plugins.store.removeSource' | translate"
|
||||
>
|
||||
<ng-icon
|
||||
name="lucideTrash2"
|
||||
@@ -174,7 +183,7 @@
|
||||
|
||||
<section class="grid min-w-0 gap-2 rounded-lg border border-border bg-card p-3">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<h2 class="text-xs font-bold uppercase text-foreground">Filters</h2>
|
||||
<h2 class="text-xs font-bold uppercase text-foreground">{{ 'plugins.store.filters' | translate }}</h2>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
@@ -183,14 +192,14 @@
|
||||
[class.text-foreground]="showInstalledOnly()"
|
||||
(click)="toggleInstalledOnly()"
|
||||
>
|
||||
<span>Installed only</span>
|
||||
<span>{{ 'plugins.store.installedOnly' | translate }}</span>
|
||||
<strong class="rounded-full bg-background px-2 py-0.5 text-xs text-muted-foreground">{{ installedCount() }}</strong>
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<section class="grid min-w-0 gap-2 rounded-lg border border-border bg-card p-3">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<h2 class="text-xs font-bold uppercase text-foreground">Install server</h2>
|
||||
<h2 class="text-xs font-bold uppercase text-foreground">{{ 'plugins.store.installServer' | translate }}</h2>
|
||||
<span class="rounded-full bg-secondary px-2 py-0.5 text-xs text-muted-foreground">{{ manageableServers().length }}</span>
|
||||
</div>
|
||||
|
||||
@@ -209,13 +218,15 @@
|
||||
></span>
|
||||
<span class="min-w-0">
|
||||
<span class="block truncate text-sm font-semibold text-foreground">{{ server.name }}</span>
|
||||
<span class="block truncate text-xs text-muted-foreground">{{ server.sourceUrl || 'Default endpoint' }}</span>
|
||||
<span class="block truncate text-xs text-muted-foreground">{{
|
||||
server.sourceUrl || ('plugins.store.defaultEndpoint' | translate)
|
||||
}}</span>
|
||||
</span>
|
||||
</button>
|
||||
}
|
||||
} @else {
|
||||
<p class="rounded-md border border-border bg-secondary/40 px-2 py-2 text-xs leading-5 text-muted-foreground">
|
||||
No server is available for plugin installs. Owner or Manage Server access is required.
|
||||
{{ 'plugins.store.noServerForInstall' | translate }}
|
||||
</p>
|
||||
}
|
||||
</section>
|
||||
@@ -223,7 +234,7 @@
|
||||
|
||||
<section
|
||||
class="grid min-w-0 gap-3 rounded-lg border border-border bg-card p-3"
|
||||
aria-label="Available plugins"
|
||||
[attr.aria-label]="'plugins.store.availablePluginsAria' | translate"
|
||||
>
|
||||
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<label class="relative flex min-w-0 flex-1 sm:max-w-xl">
|
||||
@@ -235,13 +246,13 @@
|
||||
type="search"
|
||||
[ngModel]="searchTerm()"
|
||||
(ngModelChange)="searchTerm.set($event)"
|
||||
placeholder="Search plugins, authors, ids"
|
||||
aria-label="Search plugins"
|
||||
[placeholder]="'plugins.store.searchPlaceholder' | translate"
|
||||
[attr.aria-label]="'plugins.store.searchAria' | translate"
|
||||
class="min-h-9 w-full rounded-lg border border-border bg-secondary py-2 pl-9 pr-3 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div class="text-sm text-muted-foreground">{{ filteredPlugins().length }} shown</div>
|
||||
<div class="text-sm text-muted-foreground">{{ 'plugins.store.shown' | translate: { count: filteredPlugins().length } }}</div>
|
||||
</div>
|
||||
|
||||
@if (actionError()) {
|
||||
@@ -280,19 +291,21 @@
|
||||
<div class="grid min-w-0 grid-cols-[minmax(0,1fr)_auto] gap-2">
|
||||
<div class="min-w-0">
|
||||
<h2 class="truncate text-base font-semibold leading-6">{{ plugin.title }}</h2>
|
||||
<p class="truncate text-sm text-muted-foreground">{{ plugin.author || 'Unknown author' }} · v{{ plugin.version }}</p>
|
||||
<p class="truncate text-sm text-muted-foreground">
|
||||
{{ plugin.author || ('plugins.store.unknownAuthor' | translate) }} · v{{ plugin.version }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@if (getPluginInstallState(plugin) === 'updateAvailable') {
|
||||
<span
|
||||
class="self-start whitespace-nowrap rounded-full bg-primary/10 px-2 py-0.5 text-[11px] font-semibold uppercase tracking-wide text-primary"
|
||||
>Update</span
|
||||
>{{ 'plugins.store.updateBadge' | translate }}</span
|
||||
>
|
||||
}
|
||||
@if (getPluginInstallState(plugin) === 'installed') {
|
||||
<span
|
||||
class="self-start whitespace-nowrap rounded-full bg-emerald-600/10 px-2 py-0.5 text-[11px] font-semibold uppercase tracking-wide text-emerald-600"
|
||||
>Installed</span
|
||||
>{{ 'plugins.store.installedBadge' | translate }}</span
|
||||
>
|
||||
}
|
||||
</div>
|
||||
@@ -312,11 +325,10 @@
|
||||
[title]="serverInstallButtonTitle(plugin)"
|
||||
class="inline-flex min-h-8 items-center justify-center gap-2 rounded-lg border border-primary bg-primary px-3 py-1.5 text-sm font-semibold text-primary-foreground transition-colors hover:bg-primary/90 disabled:cursor-not-allowed disabled:opacity-55"
|
||||
[ngClass]="{
|
||||
'border-destructive/35':
|
||||
getPrimaryActionLabel(plugin) === 'Uninstall' || getPrimaryActionLabel(plugin) === 'Remove from Server',
|
||||
'bg-destructive/10': getPrimaryActionLabel(plugin) === 'Uninstall' || getPrimaryActionLabel(plugin) === 'Remove from Server'
|
||||
'border-destructive/35': isDestructivePrimaryAction(plugin),
|
||||
'bg-destructive/10': isDestructivePrimaryAction(plugin)
|
||||
}"
|
||||
[class.text-destructive]="getPrimaryActionLabel(plugin) === 'Uninstall' || getPrimaryActionLabel(plugin) === 'Remove from Server'"
|
||||
[class.text-destructive]="isDestructivePrimaryAction(plugin)"
|
||||
>
|
||||
<ng-icon
|
||||
[name]="primaryActionIcon(plugin)"
|
||||
@@ -331,9 +343,9 @@
|
||||
type="button"
|
||||
(click)="loadReadme(plugin)"
|
||||
class="inline-flex min-h-8 items-center justify-center rounded-lg px-3 py-1.5 text-sm font-semibold text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground"
|
||||
title="Load readme"
|
||||
[title]="'plugins.store.loadReadme' | translate"
|
||||
>
|
||||
{{ isReadmeLoading(plugin) ? 'Loading' : 'Readme' }}
|
||||
{{ (isReadmeLoading(plugin) ? 'plugins.store.loadingReadme' : 'plugins.store.readme') | translate }}
|
||||
</button>
|
||||
}
|
||||
|
||||
@@ -342,7 +354,7 @@
|
||||
type="button"
|
||||
(click)="openExternal(plugin.githubUrl)"
|
||||
class="grid h-8 w-8 place-items-center rounded-lg border border-border text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground"
|
||||
title="Open GitHub"
|
||||
[title]="'plugins.store.openGitHub' | translate"
|
||||
>
|
||||
<ng-icon
|
||||
name="lucideExternalLink"
|
||||
@@ -362,9 +374,9 @@
|
||||
name="lucidePackage"
|
||||
class="h-7 w-7 text-muted-foreground"
|
||||
/>
|
||||
<h2 class="text-base font-semibold">No plugins found</h2>
|
||||
<h2 class="text-base font-semibold">{{ 'plugins.store.emptyTitle' | translate }}</h2>
|
||||
<p class="max-w-md text-sm text-muted-foreground">
|
||||
{{ sourceCount() ? 'Adjust filters or add another source manifest.' : 'Add a plugin source manifest URL to populate the catalog.' }}
|
||||
{{ (sourceCount() ? 'plugins.store.emptyWithSources' : 'plugins.store.emptyNoSources') | translate }}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
@@ -374,14 +386,16 @@
|
||||
@if (readme()) {
|
||||
<aside
|
||||
class="grid min-w-0 gap-3 rounded-lg border border-border bg-card p-3 xl:sticky xl:top-3 xl:max-h-[calc(100vh-6rem)]"
|
||||
aria-label="Plugin readme"
|
||||
[attr.aria-label]="'plugins.store.readmePanelAria' | translate"
|
||||
>
|
||||
<div class="grid grid-cols-[minmax(0,1fr)_auto] gap-3">
|
||||
<div class="min-w-0">
|
||||
<p class="mb-1 text-xs font-bold uppercase text-primary">Readme</p>
|
||||
<p class="mb-1 text-xs font-bold uppercase text-primary">{{ 'plugins.store.readmeLabel' | translate }}</p>
|
||||
<h2 class="truncate text-base font-semibold">{{ readme()!.title }}</h2>
|
||||
@if (selectedReadmePlugin(); as plugin) {
|
||||
<span class="block truncate text-sm text-muted-foreground">{{ plugin.author || 'Unknown author' }} · v{{ plugin.version }}</span>
|
||||
<span class="block truncate text-sm text-muted-foreground">
|
||||
{{ plugin.author || ('plugins.store.unknownAuthor' | translate) }} · v{{ plugin.version }}
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
<div class="flex items-start gap-2">
|
||||
@@ -390,13 +404,13 @@
|
||||
(click)="toggleReadmeRawMode()"
|
||||
class="inline-flex min-h-8 items-center justify-center rounded-lg border border-border bg-secondary px-3 py-1.5 text-xs font-semibold text-foreground transition-colors hover:bg-secondary/80"
|
||||
>
|
||||
{{ readmeRawMode() ? 'Parsed' : 'Raw' }}
|
||||
{{ (readmeRawMode() ? 'plugins.store.parsed' : 'plugins.store.raw') | translate }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
(click)="closeReadme()"
|
||||
class="grid h-8 w-8 place-items-center rounded-lg border border-border text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground"
|
||||
title="Close readme"
|
||||
[title]="'plugins.store.closeReadme' | translate"
|
||||
>
|
||||
<ng-icon
|
||||
name="lucideX"
|
||||
@@ -427,7 +441,7 @@
|
||||
name="lucideExternalLink"
|
||||
class="h-4 w-4"
|
||||
/>
|
||||
Open source readme
|
||||
{{ 'plugins.store.openSourceReadme' | translate }}
|
||||
</button>
|
||||
</aside>
|
||||
}
|
||||
@@ -446,7 +460,7 @@
|
||||
>
|
||||
<header class="flex items-start justify-between gap-4 border-b border-border p-4">
|
||||
<div class="min-w-0">
|
||||
<p class="text-sm text-muted-foreground">Server plugin install</p>
|
||||
<p class="text-sm text-muted-foreground">{{ 'plugins.store.serverInstall.title' | translate }}</p>
|
||||
<h2
|
||||
id="server-plugin-install-title"
|
||||
class="mt-1 truncate text-lg font-semibold"
|
||||
@@ -458,7 +472,7 @@
|
||||
type="button"
|
||||
(click)="closeServerInstallDialog()"
|
||||
class="grid h-8 w-8 shrink-0 place-items-center rounded-lg border border-border text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground"
|
||||
title="Cancel install"
|
||||
[title]="'plugins.store.serverInstall.cancelInstall' | translate"
|
||||
>
|
||||
<ng-icon
|
||||
name="lucideX"
|
||||
@@ -469,7 +483,7 @@
|
||||
|
||||
<div class="grid min-h-0 gap-4 overflow-auto p-4">
|
||||
<label class="grid gap-2">
|
||||
<span class="text-sm text-muted-foreground">Install to server</span>
|
||||
<span class="text-sm text-muted-foreground">{{ 'plugins.store.serverInstall.installToServer' | translate }}</span>
|
||||
<select
|
||||
[value]="dialog.selectedServerId"
|
||||
[disabled]="serverInstallBusy()"
|
||||
@@ -489,12 +503,12 @@
|
||||
[disabled]="serverInstallBusy()"
|
||||
(change)="serverInstallOptional.set($any($event.target).checked)"
|
||||
/>
|
||||
<span>Optional for server members</span>
|
||||
<span>{{ 'plugins.store.serverInstall.optionalForMembers' | translate }}</span>
|
||||
</label>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<h3 class="text-sm font-semibold">Capabilities</h3>
|
||||
<h3 class="text-sm font-semibold">{{ 'plugins.store.serverInstall.capabilities' | translate }}</h3>
|
||||
<span class="rounded-full bg-secondary px-2 py-0.5 text-xs text-muted-foreground">{{ dialog.manifest.capabilities?.length ?? 0 }}</span>
|
||||
</div>
|
||||
|
||||
@@ -511,7 +525,7 @@
|
||||
</label>
|
||||
}
|
||||
} @else {
|
||||
<p class="text-sm text-muted-foreground">This plugin requests no capabilities.</p>
|
||||
<p class="text-sm text-muted-foreground">{{ 'plugins.store.serverInstall.noCapabilities' | translate }}</p>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -527,7 +541,7 @@
|
||||
[disabled]="serverInstallBusy()"
|
||||
class="inline-flex min-h-8 items-center justify-center rounded-lg border border-border bg-card px-3 py-1.5 text-sm font-semibold transition-colors hover:bg-secondary disabled:cursor-not-allowed disabled:opacity-55"
|
||||
>
|
||||
Cancel
|
||||
{{ 'plugins.store.serverInstall.cancel' | translate }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -540,7 +554,7 @@
|
||||
class="h-4 w-4"
|
||||
[class.animate-spin]="serverInstallBusy()"
|
||||
/>
|
||||
Install and Activate
|
||||
{{ 'plugins.store.serverInstall.installAndActivate' | translate }}
|
||||
</button>
|
||||
</footer>
|
||||
</section>
|
||||
|
||||
@@ -39,6 +39,7 @@ import type {
|
||||
import { selectCurrentRoom, selectSavedRooms } from '../../../../store/rooms/rooms.selectors';
|
||||
import { selectCurrentUser } from '../../../../store/users/users.selectors';
|
||||
import { ModalBackdropComponent } from '../../../../shared';
|
||||
import { AppI18nService, APP_TRANSLATE_IMPORTS } from '../../../../core/i18n';
|
||||
import { PluginCapabilityService } from '../../application/services/plugin-capability.service';
|
||||
import { PluginStoreService } from '../../application/services/plugin-store.service';
|
||||
import type {
|
||||
@@ -62,7 +63,8 @@ interface ServerPluginInstallDialog {
|
||||
FormsModule,
|
||||
ChatMessageMarkdownComponent,
|
||||
NgIcon,
|
||||
ModalBackdropComponent
|
||||
ModalBackdropComponent,
|
||||
...APP_TRANSLATE_IMPORTS
|
||||
],
|
||||
viewProviders: [
|
||||
provideIcons({
|
||||
@@ -83,6 +85,7 @@ interface ServerPluginInstallDialog {
|
||||
export class PluginStoreComponent implements OnInit {
|
||||
readonly store = inject(PluginStoreService);
|
||||
readonly capabilities = inject(PluginCapabilityService);
|
||||
private readonly appI18n = inject(AppI18nService);
|
||||
readonly ngrxStore = inject(NgRxStore);
|
||||
readonly savedRooms = this.ngrxStore.selectSignal(selectSavedRooms);
|
||||
readonly currentRoom = this.ngrxStore.selectSignal(selectCurrentRoom);
|
||||
@@ -237,7 +240,7 @@ export class PluginStoreComponent implements OnInit {
|
||||
return;
|
||||
}
|
||||
|
||||
this.sourceError.set(error instanceof Error ? error.message : 'Unable to add plugin source');
|
||||
this.sourceError.set(error instanceof Error ? error.message : this.appI18n.instant('plugins.store.errors.addSource'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -255,7 +258,7 @@ export class PluginStoreComponent implements OnInit {
|
||||
return;
|
||||
}
|
||||
|
||||
this.sourceError.set(error instanceof Error ? error.message : 'Unable to remove plugin source');
|
||||
this.sourceError.set(error instanceof Error ? error.message : this.appI18n.instant('plugins.store.errors.removeSource'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -269,22 +272,24 @@ export class PluginStoreComponent implements OnInit {
|
||||
return;
|
||||
}
|
||||
|
||||
this.sourceError.set(error instanceof Error ? error.message : 'Unable to refresh plugin sources');
|
||||
this.sourceError.set(error instanceof Error ? error.message : this.appI18n.instant('plugins.store.errors.refreshSources'));
|
||||
}
|
||||
}
|
||||
|
||||
async runPrimaryAction(plugin: PluginStoreEntry): Promise<void> {
|
||||
const action = this.getPrimaryActionLabel(plugin);
|
||||
const state = this.getPluginInstallState(plugin);
|
||||
|
||||
this.actionError.set(null);
|
||||
this.actionBusyPluginId.set(plugin.id);
|
||||
|
||||
try {
|
||||
if (action === 'Uninstall') {
|
||||
await this.store.uninstallPlugin(plugin.id, plugin.scope);
|
||||
} else if (action === 'Remove from Server') {
|
||||
await this.store.uninstallPlugin(plugin.id, plugin.scope, { serverId: this.selectedStoreServerId() ?? undefined });
|
||||
await this.refreshSelectedServerInstalledPlugins();
|
||||
if (state === 'installed') {
|
||||
if (this.isServerScopedPlugin(plugin)) {
|
||||
await this.store.uninstallPlugin(plugin.id, plugin.scope, { serverId: this.selectedStoreServerId() ?? undefined });
|
||||
await this.refreshSelectedServerInstalledPlugins();
|
||||
} else {
|
||||
await this.store.uninstallPlugin(plugin.id, plugin.scope);
|
||||
}
|
||||
} else if (this.isServerScopedPlugin(plugin)) {
|
||||
await this.openServerInstallDialog(plugin);
|
||||
} else {
|
||||
@@ -295,7 +300,7 @@ export class PluginStoreComponent implements OnInit {
|
||||
return;
|
||||
}
|
||||
|
||||
this.actionError.set(error instanceof Error ? error.message : 'Unable to update plugin installation');
|
||||
this.actionError.set(error instanceof Error ? error.message : this.appI18n.instant('plugins.store.errors.updateInstallation'));
|
||||
} finally {
|
||||
if (!this.destroyed) {
|
||||
this.actionBusyPluginId.set(null);
|
||||
@@ -321,7 +326,7 @@ export class PluginStoreComponent implements OnInit {
|
||||
return;
|
||||
}
|
||||
|
||||
this.readmeError.set(error instanceof Error ? error.message : 'Unable to load readme');
|
||||
this.readmeError.set(error instanceof Error ? error.message : this.appI18n.instant('plugins.store.errors.loadReadme'));
|
||||
} finally {
|
||||
if (!this.destroyed) {
|
||||
this.readmeLoadingPluginId.set(null);
|
||||
@@ -348,7 +353,7 @@ export class PluginStoreComponent implements OnInit {
|
||||
const selectedServerId = this.selectedStoreServerId();
|
||||
|
||||
if (!selectedServerId) {
|
||||
throw new Error('You need owner or Manage Server access on a chat server before installing server plugins');
|
||||
throw new Error(this.appI18n.instant('plugins.store.errors.noServerAccess'));
|
||||
}
|
||||
|
||||
this.selectedCapabilityIds.set(new Set(manifest.capabilities ?? []));
|
||||
@@ -359,7 +364,7 @@ export class PluginStoreComponent implements OnInit {
|
||||
return;
|
||||
}
|
||||
|
||||
this.actionError.set(error instanceof Error ? error.message : 'Unable to prepare server plugin install');
|
||||
this.actionError.set(error instanceof Error ? error.message : this.appI18n.instant('plugins.store.errors.prepareServerInstall'));
|
||||
} finally {
|
||||
if (!this.destroyed) {
|
||||
this.actionBusyPluginId.set(null);
|
||||
@@ -441,7 +446,7 @@ export class PluginStoreComponent implements OnInit {
|
||||
return;
|
||||
}
|
||||
|
||||
this.serverInstallError.set(error instanceof Error ? error.message : 'Unable to install server plugin');
|
||||
this.serverInstallError.set(error instanceof Error ? error.message : this.appI18n.instant('plugins.store.errors.installServerPlugin'));
|
||||
} finally {
|
||||
if (!this.destroyed) {
|
||||
this.serverInstallBusy.set(false);
|
||||
@@ -521,24 +526,20 @@ export class PluginStoreComponent implements OnInit {
|
||||
const state = this.getPluginInstallState(plugin);
|
||||
|
||||
if (state === 'updateAvailable') {
|
||||
return 'Update Server';
|
||||
return this.appI18n.instant('plugins.store.actions.updateServer');
|
||||
}
|
||||
|
||||
return state === 'installed' ? 'Remove from Server' : 'Install to Server';
|
||||
return state === 'installed'
|
||||
? this.appI18n.instant('plugins.store.actions.removeFromServer')
|
||||
: this.appI18n.instant('plugins.store.actions.installToServer');
|
||||
}
|
||||
|
||||
isDestructivePrimaryAction(plugin: PluginStoreEntry): boolean {
|
||||
return this.getPluginInstallState(plugin) === 'installed';
|
||||
}
|
||||
|
||||
primaryActionIcon(plugin: PluginStoreEntry): string {
|
||||
const action = this.getPrimaryActionLabel(plugin);
|
||||
|
||||
if (action === 'Uninstall') {
|
||||
return 'lucideTrash2';
|
||||
}
|
||||
|
||||
if (action === 'Remove from Server') {
|
||||
return 'lucideTrash2';
|
||||
}
|
||||
|
||||
return 'lucidePlus';
|
||||
return this.isDestructivePrimaryAction(plugin) ? 'lucideTrash2' : 'lucidePlus';
|
||||
}
|
||||
|
||||
trackPlugin(index: number, plugin: PluginStoreEntry): string {
|
||||
@@ -577,7 +578,7 @@ export class PluginStoreComponent implements OnInit {
|
||||
|
||||
serverInstallButtonTitle(plugin: PluginStoreEntry): string {
|
||||
return this.isServerScopedPlugin(plugin) && this.manageableServers().length === 0
|
||||
? 'Requires owner or Manage Server access on a chat server'
|
||||
? this.appI18n.instant('plugins.store.errors.requiresServerAccess')
|
||||
: this.getPrimaryActionLabel(plugin);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user