diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 3f873892..87ebd18e 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -29,6 +29,8 @@ import { ToastService } from './core/services/toast.service'; import { AppNetworkInitService, IBootstrapIssue } from './core/services/app-initNetwork.service'; import { SsoRedirectService } from './core/services/sso-redirect.service'; import { DashboardHistorySeriesSyncService } from './core/services/dashboard-history-series-sync.service'; +import { Title } from '@angular/platform-browser'; +import { resolveBrowserTabTitle } from './core/utils/browser-tab-title.util'; @Component({ selector: 'app-root', @@ -64,6 +66,8 @@ export class AppComponent implements OnInit, AfterViewInit, OnDestroy { private readonly _uiEvent = inject(uiEventService); private readonly _dialog = inject(DialogService); public readonly settings = inject(SettingsService); + private readonly _titleService = inject(Title); + private readonly _browserTabTitle = toSignal(this.settings.getBrowserTabTitleAsO(), { initialValue: 'SKip' }); private readonly _responsive = inject(BreakpointObserver); private readonly _destroyRef = inject(DestroyRef); private readonly _notificationOverlay = inject(NotificationOverlayService); @@ -92,6 +96,11 @@ export class AppComponent implements OnInit, AfterViewInit, OnDestroy { private readonly _hotkeyHandler = (key: string) => this.handleKeyDown(key); constructor() { + // Keep the browser tab title (document.title) in sync with the user setting (#1055). + effect(() => { + this._titleService.setTitle(resolveBrowserTabTitle(this._browserTabTitle())); + }); + effect(() => { if (this.settings.configUpgrade()) { const liveVersion = this.settings.getConfigVersion(); diff --git a/src/app/core/components/options/display/display.component.html b/src/app/core/components/options/display/display.component.html index 46a2927c..f0da44ea 100644 --- a/src/app/core/components/options/display/display.component.html +++ b/src/app/core/components/options/display/display.component.html @@ -55,6 +55,26 @@ Enable light theme for brighter display settings. + + + + Browser Tab + + @if (!isPhonePortrait().matches) { + + Set the browser tab title to tell multiple SKip instances apart. + + } + + + Browser tab title + + + diff --git a/src/app/core/components/options/display/display.component.ts b/src/app/core/components/options/display/display.component.ts index c7a14f67..4fcd016a 100644 --- a/src/app/core/components/options/display/display.component.ts +++ b/src/app/core/components/options/display/display.component.ts @@ -57,6 +57,7 @@ export class SettingsDisplayComponent implements OnInit { protected isLightTheme = model(false); protected isRemoteControl = model(false); protected instanceName = model(''); + protected browserTabTitle = model('SKip'); protected splitShellEnabled = model(false); protected splitShellSide = model<'left' | 'right'>('left'); protected splitShellSwipeDisabled = model(false); @@ -78,6 +79,7 @@ export class SettingsDisplayComponent implements OnInit { this.isRedNightMode.set(this.settings.getRedNightMode()); this.isRemoteControl.set(this.settings.getIsRemoteControl()); this.instanceName.set(this.settings.getInstanceName()); + this.browserTabTitle.set(this.settings.getBrowserTabTitle()); this.splitShellEnabled.set(this.settings.getSplitShellEnabled()); this.splitShellSide.set(this.settings.getSplitShellSide()); this.splitShellSwipeDisabled.set(this.settings.getSplitShellSwipeDisabled()); @@ -127,6 +129,7 @@ export class SettingsDisplayComponent implements OnInit { this.settings.setSplitShellSide(this.splitShellSide()); this.settings.setSplitShellSwipeDisabled(this.splitShellSwipeDisabled()); this.settings.setWidgetHistoryDisabled(this.widgetHistoryDisabled()); + this.settings.setBrowserTabTitle(this.browserTabTitle()); this.settings.setDisablePathValidation(this.isPathValidationDisabled()); // Await the server write; setKipPluginConfig() returns a Promise, so the old // `if (!this.setKipPluginConfig())` was always false (a Promise is truthy) — diff --git a/src/app/core/interfaces/app-settings.interfaces.ts b/src/app/core/interfaces/app-settings.interfaces.ts index 27aff150..903986f9 100644 --- a/src/app/core/interfaces/app-settings.interfaces.ts +++ b/src/app/core/interfaces/app-settings.interfaces.ts @@ -49,6 +49,7 @@ export interface IAppConfig { splitShellSwipeDisabled?: boolean; splitShellWidth?: number; widgetHistoryDisabled?: boolean; + browserTabTitle?: string; } export interface IThemeConfig { diff --git a/src/app/core/services/settings.service.ts b/src/app/core/services/settings.service.ts index 01e0f0fb..3fec2596 100644 --- a/src/app/core/services/settings.service.ts +++ b/src/app/core/services/settings.service.ts @@ -40,6 +40,7 @@ export class SettingsService { private nightModeBrightness: BehaviorSubject = new BehaviorSubject(1); private isRemoteControl: BehaviorSubject = new BehaviorSubject(false); private instanceName: BehaviorSubject = new BehaviorSubject(''); + private browserTabTitle: BehaviorSubject = new BehaviorSubject('SKip'); private splitShellEnabled: BehaviorSubject = new BehaviorSubject(false); private splitShellSide: BehaviorSubject<'left' | 'right'> = new BehaviorSubject<'left' | 'right'>('left'); private splitShellSwipeDisabled: BehaviorSubject = new BehaviorSubject(false); @@ -248,6 +249,12 @@ export class SettingsService { this._dashboards = this.activeConfig.dashboards; } + if (this.activeConfig.app.browserTabTitle === undefined) { + this.browserTabTitle.next('SKip'); + } else { + this.browserTabTitle.next(this.activeConfig.app.browserTabTitle); + } + if (this.activeConfig.app.splitShellEnabled === undefined) { this.setSplitShellEnabled(false); } else { @@ -457,6 +464,26 @@ export class SettingsService { this.saveConnectionConfigToLocalStorage(); } + // Browser tab title (document.title) + public getBrowserTabTitleAsO() { + return this.browserTabTitle.asObservable(); + } + + public getBrowserTabTitle(): string { + return this.browserTabTitle.getValue(); + } + + public setBrowserTabTitle(title: string) { + this.browserTabTitle.next(title); + const appConf = this.buildAppStorageObject(); + + if (this.useSharedConfig) { + this.storage.patchConfig('IAppConfig', appConf); + } else { + this.saveAppConfigToLocalStorage(); + } + } + public getDisablePathValidation(): boolean { return this.disablePathValidation; } @@ -726,7 +753,8 @@ export class SettingsService { splitShellSide: this.splitShellSide.getValue() ?? 'right', splitShellWidth: this.splitShellWidth.getValue() ?? 0.5, splitShellSwipeDisabled: this.splitShellSwipeDisabled.getValue(), - widgetHistoryDisabled: this.widgetHistoryDisabled.getValue() + widgetHistoryDisabled: this.widgetHistoryDisabled.getValue(), + browserTabTitle: this.browserTabTitle.getValue() } return storageObject; } diff --git a/src/app/core/utils/browser-tab-title.util.spec.ts b/src/app/core/utils/browser-tab-title.util.spec.ts new file mode 100644 index 00000000..94dce47a --- /dev/null +++ b/src/app/core/utils/browser-tab-title.util.spec.ts @@ -0,0 +1,16 @@ +import { describe, it, expect } from 'vitest'; +import { resolveBrowserTabTitle } from './browser-tab-title.util'; + +describe('resolveBrowserTabTitle (#1055)', () => { + it('defaults to SKip when the value is missing or blank', () => { + expect(resolveBrowserTabTitle(undefined)).toBe('SKip'); + expect(resolveBrowserTabTitle(null)).toBe('SKip'); + expect(resolveBrowserTabTitle('')).toBe('SKip'); + expect(resolveBrowserTabTitle(' ')).toBe('SKip'); + }); + + it('uses the trimmed configured value when set', () => { + expect(resolveBrowserTabTitle('Mast-SKip')).toBe('Mast-SKip'); + expect(resolveBrowserTabTitle(' Port Engine ')).toBe('Port Engine'); + }); +}); diff --git a/src/app/core/utils/browser-tab-title.util.ts b/src/app/core/utils/browser-tab-title.util.ts new file mode 100644 index 00000000..93fd7422 --- /dev/null +++ b/src/app/core/utils/browser-tab-title.util.ts @@ -0,0 +1,8 @@ +/** + * Resolves the browser tab title (document.title) from the user-configured value. + * Falls back to 'SKip' when the value is empty/whitespace so the tab is never blank (#1055). + */ +export function resolveBrowserTabTitle(value?: string | null): string { + const trimmed = (value ?? '').trim(); + return trimmed.length ? trimmed : 'SKip'; +} diff --git a/src/default-config/config.blank.const.ts b/src/default-config/config.blank.const.ts index 7e5db14d..d2e2cbb7 100644 --- a/src/default-config/config.blank.const.ts +++ b/src/default-config/config.blank.const.ts @@ -14,7 +14,8 @@ export const DefaultAppConfig: IAppConfig = { "splitShellEnabled": true, "splitShellSide": "left", "splitShellSwipeDisabled": false, - "splitShellWidth": 0.5 + "splitShellWidth": 0.5, + "browserTabTitle": "SKip" } export const DefaultThemeConfig: IThemeConfig = {