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:
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user