wip: optimizations

This commit is contained in:
2026-05-23 15:28:40 +02:00
parent 5bf506af03
commit 155fe20862
89 changed files with 7431 additions and 392 deletions

View File

@@ -14,6 +14,7 @@ import { environment } from '../../../../../environments/environment';
import { RealtimeSessionFacade } from '../../../../core/realtime';
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';
import type {
PluginRequirementSummary,
TojuPluginInstallScope,
@@ -72,6 +73,7 @@ export class PluginStoreService {
private readonly desktopState = inject(PluginDesktopStateService);
private readonly destroyRef = inject(DestroyRef);
private readonly host = inject(PluginHostService);
private readonly jsonStorage = jsonStorage;
private readonly pluginRequirements = inject(PluginRequirementService);
private readonly realtime = inject(RealtimeSessionFacade, { optional: true });
private readonly registry = inject(PluginRegistryService);
@@ -947,17 +949,16 @@ export class PluginStoreService {
}
private loadState(): PersistedPluginStoreState {
try {
const raw = localStorage.getItem(getUserScopedStorageKey(STORAGE_KEY_PLUGIN_STORE));
const parsed = this.jsonStorage.read<unknown>(
getUserScopedStorageKey(STORAGE_KEY_PLUGIN_STORE),
null
);
if (!raw) {
return createDefaultStoreState();
}
return normalizePersistedState(JSON.parse(raw) as unknown);
} catch {
if (!parsed) {
return createDefaultStoreState();
}
return normalizePersistedState(parsed);
}
private saveState(): void {
@@ -969,10 +970,7 @@ export class PluginStoreService {
sourceUrls: this.sourceUrls()
};
try {
localStorage.setItem(getUserScopedStorageKey(STORAGE_KEY_PLUGIN_STORE), JSON.stringify(state));
} catch {}
this.jsonStorage.write(getUserScopedStorageKey(STORAGE_KEY_PLUGIN_STORE), state);
void this.desktopState.writeJson(STORAGE_KEY_PLUGIN_STORE, state);
}
@@ -984,7 +982,7 @@ export class PluginStoreService {
}
const normalized = normalizePersistedState(state);
const sourceUrlsChanged = JSON.stringify(normalized.sourceUrls) !== JSON.stringify(this.sourceUrls());
const sourceUrlsChanged = !areStringArraysEqual(normalized.sourceUrls, this.sourceUrls());
if (sourceUrlsChanged) {
this.sourceUrlsSignal.set(normalized.sourceUrls);
@@ -1107,7 +1105,7 @@ function parsePluginEntry(sourceUrl: string, sourceTitle: string, value: unknown
githubUrl: resolveOptionalUrl(sourceUrl, readGithubUrl(value)),
homepageUrl: resolveOptionalUrl(sourceUrl, readString(value, 'homepage', 'homepageUrl', 'website')),
id,
imageUrl: normalizeImageUrl(resolveOptionalUrl(sourceUrl, readString(value, 'image', 'imageUrl', 'icon', 'iconUrl', 'banner'))),
imageUrl: resolveOptionalUrl(sourceUrl, readString(value, 'image', 'imageUrl', 'icon', 'iconUrl', 'banner')),
installUrl: resolveOptionalUrl(sourceUrl, readString(value, 'install', 'installUrl', 'manifest', 'manifestUrl')),
readmeUrl: resolveOptionalUrl(sourceUrl, readString(value, 'readme', 'readmeUrl')),
scope: readPluginInstallScope(value),
@@ -1128,6 +1126,24 @@ function readPluginInstallScope(record: Record<string, unknown>): TojuPluginInst
return scope === 'server' || scope === 'client' ? scope : undefined;
}
function areStringArraysEqual(first: readonly string[], second: readonly string[]): boolean {
if (first === second) {
return true;
}
if (first.length !== second.length) {
return false;
}
for (let arrayIndex = 0; arrayIndex < first.length; arrayIndex++) {
if (first[arrayIndex] !== second[arrayIndex]) {
return false;
}
}
return true;
}
function normalizePersistedState(value: unknown): PersistedPluginStoreState {
if (!isRecord(value)) {
return createDefaultStoreState();
@@ -1300,44 +1316,6 @@ function normalizeOptionalSourceUrl(rawUrl: string): string | undefined {
}
}
/**
* Rewrites human-friendly GitHub URLs so the browser can load the underlying
* binary asset. Users typically paste links copied from the GitHub web UI which
* point at the rendered HTML preview (`github.com/<owner>/<repo>/blob/...`) or
* the raw redirector (`github.com/<owner>/<repo>/raw/...`). Both forms must be
* mapped to `raw.githubusercontent.com` for `<img>` tags to work.
*/
function normalizeImageUrl(rawUrl: string | undefined): string | undefined {
if (!rawUrl) {
return undefined;
}
let url: URL;
try {
url = new URL(rawUrl);
} catch {
return rawUrl;
}
if (url.hostname !== 'github.com' && url.hostname !== 'www.github.com') {
return rawUrl;
}
const segments = url.pathname.split('/').filter(Boolean);
const kindIndex = segments.findIndex((segment) => segment === 'blob' || segment === 'raw');
if (kindIndex < 2 || kindIndex >= segments.length - 1) {
return rawUrl;
}
const owner = segments[0];
const repo = segments[1];
const ref = segments.slice(kindIndex + 1).join('/');
return `https://raw.githubusercontent.com/${owner}/${repo}/${ref}${url.search}`;
}
function resolveOptionalUrl(sourceUrl: string, rawUrl?: string): string | undefined {
if (!rawUrl) {
return undefined;

View File

@@ -255,7 +255,7 @@
@if (filteredPlugins().length > 0) {
<div class="grid gap-3">
@for (plugin of filteredPlugins(); track trackPlugin($index, plugin)) {
<article class="grid min-w-0 overflow-hidden rounded-lg border border-border bg-background sm:grid-cols-[5.5rem_minmax(0,1fr)]">
<article class="grid min-w-0 overflow-hidden rounded-lg border border-border bg-background sm:grid-cols-[5.5rem_minmax(0,1fr)] [content-visibility:auto] [contain-intrinsic-size:auto_140px]">
<div class="grid min-h-24 place-items-center bg-secondary text-muted-foreground sm:min-h-full">
@if (plugin.imageUrl && !hasBrokenImage(plugin)) {
<img

View File

@@ -8,9 +8,11 @@ import {
inject,
signal
} from '@angular/core';
import { toObservable, toSignal } from '@angular/core/rxjs-interop';
import { FormsModule } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { Store as NgRxStore } from '@ngrx/store';
import { debounceTime } from 'rxjs';
import { NgIcon, provideIcons } from '@ng-icons/core';
import {
lucideArrowLeft,
@@ -103,7 +105,7 @@ export class PluginStoreComponent implements OnInit {
readonly sourceErrors = computed(() => this.store.sources().filter((source) => !!source.error));
readonly installedIds = computed(() => new Set(this.store.installedPlugins().map((plugin) => plugin.manifest.id)));
readonly filteredPlugins = computed(() => {
const searchTerm = this.searchTerm().trim()
const searchTerm = this.debouncedSearchTerm().trim()
.toLowerCase();
const sourceFilter = this.selectedSourceUrl();
const showInstalled = this.showInstalledOnly();
@@ -157,6 +159,16 @@ export class PluginStoreComponent implements OnInit {
readonly serverInstallBusy = signal(false);
readonly brokenImageKeys = signal<Set<string>>(new Set());
/**
* Debounced search term used by `filteredPlugins`. Keeps each keystroke
* from forcing a full re-filter of the (potentially large) plugin catalog
* while still letting the input update its bound model instantly.
*/
protected readonly debouncedSearchTerm = toSignal(
toObservable(this.searchTerm).pipe(debounceTime(200)),
{ initialValue: '' }
);
private destroyed = false;
private readonly destroyRef = inject(DestroyRef);
private readonly externalLinks = inject(ExternalLinkService);