mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-11 02:46:16 -08:00
fix(settings): keep the settings search following an inconclusive Embedded MPV answer
The settings page asks the search service for Embedded MPV support once, when its search facade is created. After an inconclusive answer the service only probed again on its next call, so with the page left open the Embedded MPV rows stayed unsearchable after the login shell answered, while the player option on the same page already updated. The service now follows the answer with `watchEmbeddedMpvSupport()` until it is final, which updates the open page and the command palette alike. A call made while the answer is still inconclusive restarts the watch, so it asks at once as before. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
dad9217363
commit
98eb267daa
3 files changed
+108
-20
No files matched your search
@@ -3,6 +3,7 @@ import { Router } from '@angular/router';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { VodSourceDiscoveryService } from '@iptvnator/portal/shared/data-access';
|
||||
import { RuntimeCapabilitiesService } from '@iptvnator/services';
|
||||
import { EMBEDDED_MPV_SUPPORT_RECHECK_MS } from '@iptvnator/shared/interfaces';
|
||||
import { SETTINGS_SEARCH_ENTRIES } from './settings-search-entries';
|
||||
import { SETTINGS_SECTION_DEFINITIONS } from './settings-search-sections';
|
||||
import { SettingsSearchService } from './settings-search.service';
|
||||
@@ -213,6 +214,64 @@ describe('SettingsSearchService', () => {
|
||||
expect(getEmbeddedMpvSupport).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
describe('while the settings page stays open', () => {
|
||||
beforeEach(() => jest.useFakeTimers());
|
||||
afterEach(() => jest.useRealTimers());
|
||||
|
||||
it('follows an inconclusive answer without being asked again', async () => {
|
||||
const getEmbeddedMpvSupport = probeWith({
|
||||
supported: false,
|
||||
inconclusive: true,
|
||||
});
|
||||
const { service } = setup({
|
||||
...DESKTOP,
|
||||
supportsEmbeddedMpv: true,
|
||||
});
|
||||
const entries = () =>
|
||||
service.visibleEntries().map(({ id }) => id);
|
||||
|
||||
// The page asks once, when it is created.
|
||||
await service.ensureEmbeddedMpvSupportLoaded();
|
||||
expect(entries()).not.toContain('embedded-mpv-extra-options');
|
||||
|
||||
getEmbeddedMpvSupport.mockResolvedValue({
|
||||
platform: 'linux',
|
||||
supported: true,
|
||||
});
|
||||
await jest.advanceTimersByTimeAsync(
|
||||
EMBEDDED_MPV_SUPPORT_RECHECK_MS
|
||||
);
|
||||
|
||||
expect(entries()).toContain('embedded-mpv-extra-options');
|
||||
expect(
|
||||
service.ensureEmbeddedMpvSupportLoaded()
|
||||
).toBeUndefined();
|
||||
await jest.advanceTimersByTimeAsync(
|
||||
EMBEDDED_MPV_SUPPORT_RECHECK_MS * 20
|
||||
);
|
||||
expect(getEmbeddedMpvSupport).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('stops following once the service is destroyed', async () => {
|
||||
const getEmbeddedMpvSupport = probeWith({
|
||||
supported: false,
|
||||
inconclusive: true,
|
||||
});
|
||||
const { service } = setup({
|
||||
...DESKTOP,
|
||||
supportsEmbeddedMpv: true,
|
||||
});
|
||||
await service.ensureEmbeddedMpvSupportLoaded();
|
||||
|
||||
TestBed.resetTestingModule();
|
||||
await jest.advanceTimersByTimeAsync(
|
||||
EMBEDDED_MPV_SUPPORT_RECHECK_MS * 20
|
||||
);
|
||||
|
||||
expect(getEmbeddedMpvSupport).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
it('does not probe where the runtime has no embedded MPV bridge', () => {
|
||||
const getEmbeddedMpvSupport = probeWith({ supported: true });
|
||||
const { service } = setup(DESKTOP);
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { inject, Injectable, signal } from '@angular/core';
|
||||
import { DestroyRef, inject, Injectable, signal } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { VodSourceDiscoveryService } from '@iptvnator/portal/shared/data-access';
|
||||
import { RuntimeCapabilitiesService } from '@iptvnator/services';
|
||||
import { EmbeddedMpvSupport } from '@iptvnator/shared/interfaces';
|
||||
import {
|
||||
EmbeddedMpvSupport,
|
||||
watchEmbeddedMpvSupport,
|
||||
} from '@iptvnator/shared/interfaces';
|
||||
import { SETTINGS_SEARCH_ENTRIES } from './settings-search-entries';
|
||||
import { rankSearchMatch, tokenizeSearchQuery } from './settings-search-rank';
|
||||
import {
|
||||
@@ -39,6 +42,13 @@ export class SettingsSearchService {
|
||||
);
|
||||
private embeddedMpvSupportChecked = false;
|
||||
private embeddedMpvSupportLoad: Promise<void> | undefined;
|
||||
private stopEmbeddedMpvSupportWatch: (() => void) | undefined;
|
||||
|
||||
constructor() {
|
||||
inject(DestroyRef).onDestroy(() =>
|
||||
this.stopEmbeddedMpvSupportWatch?.()
|
||||
);
|
||||
}
|
||||
|
||||
/** Row the settings page should scroll to and highlight next. */
|
||||
readonly pendingReveal = this.revealRequest.asReadonly();
|
||||
@@ -62,12 +72,13 @@ export class SettingsSearchService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Probes embedded MPV support once so rows that need it become
|
||||
* searchable. Returns the pending probe, or `undefined` when there is
|
||||
* nothing to wait for. Call it lazily (palette open, settings page),
|
||||
* never from shell bootstrap: supported desktop builds may load the
|
||||
* native addon while answering. An inconclusive answer is used until
|
||||
* the next call, which probes again.
|
||||
* Probes embedded MPV support so rows that need it become searchable.
|
||||
* Returns the pending probe, or `undefined` when there is nothing to
|
||||
* wait for. Call it lazily (palette open, settings page), never from
|
||||
* shell bootstrap: supported desktop builds may load the native addon
|
||||
* while answering. A final answer is kept. An inconclusive one keeps
|
||||
* being followed, so an open settings page updates by itself, and the
|
||||
* next call asks again at once.
|
||||
*/
|
||||
ensureEmbeddedMpvSupportLoaded(): Promise<void> | undefined {
|
||||
if (this.embeddedMpvSupportChecked) {
|
||||
@@ -84,18 +95,32 @@ export class SettingsSearchService {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
this.embeddedMpvSupportLoad ??= electron
|
||||
.getEmbeddedMpvSupport()
|
||||
.then((support) => this.embeddedMpvSupport.set(support))
|
||||
.catch(() => this.embeddedMpvSupport.set(null))
|
||||
.finally(() => {
|
||||
this.embeddedMpvSupportChecked =
|
||||
!this.embeddedMpvSupport()?.inconclusive;
|
||||
this.embeddedMpvSupportLoad = undefined;
|
||||
});
|
||||
this.embeddedMpvSupportLoad ??= this.followEmbeddedMpvSupport(() =>
|
||||
electron.getEmbeddedMpvSupport()
|
||||
);
|
||||
return this.embeddedMpvSupportLoad;
|
||||
}
|
||||
|
||||
/** Follows the answer afresh; resolves with its first one. */
|
||||
private followEmbeddedMpvSupport(
|
||||
getSupport: () => Promise<EmbeddedMpvSupport>
|
||||
): Promise<void> {
|
||||
this.stopEmbeddedMpvSupportWatch?.();
|
||||
return new Promise<void>((answered) => {
|
||||
const take = (support: EmbeddedMpvSupport | null) => {
|
||||
this.embeddedMpvSupport.set(support);
|
||||
this.embeddedMpvSupportChecked = !support?.inconclusive;
|
||||
this.embeddedMpvSupportLoad = undefined;
|
||||
answered();
|
||||
};
|
||||
this.stopEmbeddedMpvSupportWatch = watchEmbeddedMpvSupport(
|
||||
getSupport,
|
||||
take,
|
||||
() => take(null)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs `listener` synchronously before every reveal navigation, so the
|
||||
* shell can drop a search keystroke still waiting for its debounce: if
|
||||
|
||||
Reference in new issue
Block a user