wip: optimizations
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user