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

This commit is contained in:
2026-06-05 17:13:03 +02:00
parent 8ecfc9a1fe
commit ee293d7daf
301 changed files with 8247 additions and 2218 deletions

View File

@@ -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>

View File

@@ -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);
});