mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-17 16:37:43 +00:00
feat(focus-mode): show pomodoro timer in browser tab title (#7579)
* feat(focus-mode): show pomodoro timer in browser tab title * fix(focus-mode): handle overtime and translate browser title labels
This commit is contained in:
parent
deab927bb7
commit
f7f8056c50
5 changed files with 302 additions and 0 deletions
|
|
@ -77,6 +77,7 @@ import { OnboardingPresetSelectionComponent } from './features/onboarding/onboar
|
|||
import { OnboardingHintComponent } from './features/onboarding/onboarding-hint.component';
|
||||
import { OnboardingHintService } from './features/onboarding/onboarding-hint.service';
|
||||
import { MaterialIconsLoaderService } from './ui/material-icons-loader.service';
|
||||
import { BrowserTitleService } from './core/browser-title/browser-title.service';
|
||||
|
||||
const ONBOARDING_PRESET_EXIT_DELAY = 1000;
|
||||
const ONBOARDING_ENTRANCE_COMPLETE_DELAY = 2000;
|
||||
|
|
@ -153,6 +154,7 @@ export class AppComponent implements OnDestroy, AfterViewInit {
|
|||
readonly globalThemeService = inject(GlobalThemeService);
|
||||
readonly _store = inject(Store);
|
||||
private _sectionService = inject(SectionService);
|
||||
private _browserTitleService = inject(BrowserTitleService);
|
||||
readonly T = T;
|
||||
readonly TODAY_TAG_ID = TODAY_TAG.id;
|
||||
readonly isShowMobileButtonNav = this.layoutService.isShowMobileBottomNav;
|
||||
|
|
|
|||
231
src/app/core/browser-title/browser-title.service.spec.ts
Normal file
231
src/app/core/browser-title/browser-title.service.spec.ts
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
import { signal } from '@angular/core';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { BrowserTitleService } from './browser-title.service';
|
||||
import { FocusModeMode } from '../../features/focus-mode/focus-mode.model';
|
||||
import { FocusModeService } from '../../features/focus-mode/focus-mode.service';
|
||||
|
||||
describe('BrowserTitleService', () => {
|
||||
let service: BrowserTitleService;
|
||||
let titleService: jasmine.SpyObj<Title>;
|
||||
let translateService: jasmine.SpyObj<TranslateService>;
|
||||
let focusModeServiceMock: any;
|
||||
|
||||
beforeEach(() => {
|
||||
titleService = jasmine.createSpyObj('Title', ['setTitle']);
|
||||
|
||||
translateService = jasmine.createSpyObj('TranslateService', ['instant']);
|
||||
|
||||
translateService.instant.and.callFake((key: string) => {
|
||||
if (key.includes('BREAK')) {
|
||||
return 'Break';
|
||||
}
|
||||
|
||||
if (key.includes('PAUSED')) {
|
||||
return 'Paused';
|
||||
}
|
||||
|
||||
return key;
|
||||
});
|
||||
|
||||
focusModeServiceMock = {
|
||||
mode: signal(FocusModeMode.Pomodoro),
|
||||
timeRemaining: signal(1500000),
|
||||
timeElapsed: signal(0),
|
||||
isBreakActive: signal(false),
|
||||
isRunning: signal(false),
|
||||
isSessionPaused: signal(false),
|
||||
isInOvertime: signal(false),
|
||||
};
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
BrowserTitleService,
|
||||
{ provide: Title, useValue: titleService },
|
||||
{ provide: TranslateService, useValue: translateService },
|
||||
{ provide: FocusModeService, useValue: focusModeServiceMock },
|
||||
],
|
||||
});
|
||||
|
||||
service = TestBed.inject(BrowserTitleService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('_getTitle', () => {
|
||||
it('should return base title when not in Pomodoro mode', () => {
|
||||
const result = (service as any)._getTitle(
|
||||
FocusModeMode.Flowtime,
|
||||
1500000,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
0,
|
||||
);
|
||||
|
||||
expect(result).toBe('Super Productivity');
|
||||
});
|
||||
|
||||
it('should return base title when in Pomodoro but not running or paused', () => {
|
||||
const result = (service as any)._getTitle(
|
||||
FocusModeMode.Pomodoro,
|
||||
1500000,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
0,
|
||||
);
|
||||
|
||||
expect(result).toBe('Super Productivity');
|
||||
});
|
||||
|
||||
it('should show remaining time when running', () => {
|
||||
const result = (service as any)._getTitle(
|
||||
FocusModeMode.Pomodoro,
|
||||
1500000,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
0,
|
||||
);
|
||||
|
||||
expect(result).toBe('(25:00) Super Productivity');
|
||||
});
|
||||
|
||||
it('should show "Paused" when paused', () => {
|
||||
const result = (service as any)._getTitle(
|
||||
FocusModeMode.Pomodoro,
|
||||
1500000,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
0,
|
||||
);
|
||||
|
||||
expect(result).toBe('(Paused 25:00) Super Productivity');
|
||||
});
|
||||
|
||||
it('should show "Break" when break is active', () => {
|
||||
const result = (service as any)._getTitle(
|
||||
FocusModeMode.Pomodoro,
|
||||
300000,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
0,
|
||||
);
|
||||
|
||||
expect(result).toBe('(05:00 Break) Super Productivity');
|
||||
});
|
||||
|
||||
it('should show both "Paused" and "Break" when both are active', () => {
|
||||
const result = (service as any)._getTitle(
|
||||
FocusModeMode.Pomodoro,
|
||||
300000,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
0,
|
||||
);
|
||||
|
||||
expect(result).toBe('(Paused 05:00 Break) Super Productivity');
|
||||
});
|
||||
|
||||
it('should show elapsed time during overtime', () => {
|
||||
const result = (service as any)._getTitle(
|
||||
FocusModeMode.Pomodoro,
|
||||
0,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
1560000,
|
||||
);
|
||||
|
||||
expect(result).toBe('(26:00) Super Productivity');
|
||||
});
|
||||
|
||||
it('should show elapsed break time during overtime', () => {
|
||||
const result = (service as any)._getTitle(
|
||||
FocusModeMode.Pomodoro,
|
||||
0,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
360000,
|
||||
);
|
||||
|
||||
expect(result).toBe('(06:00 Break) Super Productivity');
|
||||
});
|
||||
|
||||
it('should pad single digit minutes correctly', () => {
|
||||
const result = (service as any)._getTitle(
|
||||
FocusModeMode.Pomodoro,
|
||||
300000,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
0,
|
||||
);
|
||||
|
||||
expect(result).toBe('(05:00) Super Productivity');
|
||||
});
|
||||
});
|
||||
|
||||
describe('effect integration', () => {
|
||||
it('should update document title when signals change', () => {
|
||||
focusModeServiceMock.isRunning.set(true);
|
||||
focusModeServiceMock.timeRemaining.set(1499000);
|
||||
|
||||
TestBed.flushEffects();
|
||||
|
||||
expect(titleService.setTitle).toHaveBeenCalledWith('(24:59) Super Productivity');
|
||||
|
||||
focusModeServiceMock.isBreakActive.set(true);
|
||||
focusModeServiceMock.timeRemaining.set(299000);
|
||||
|
||||
TestBed.flushEffects();
|
||||
|
||||
expect(titleService.setTitle).toHaveBeenCalledWith(
|
||||
'(04:59 Break) Super Productivity',
|
||||
);
|
||||
|
||||
focusModeServiceMock.isRunning.set(false);
|
||||
focusModeServiceMock.isSessionPaused.set(true);
|
||||
|
||||
TestBed.flushEffects();
|
||||
|
||||
expect(titleService.setTitle).toHaveBeenCalledWith(
|
||||
'(Paused 04:59 Break) Super Productivity',
|
||||
);
|
||||
|
||||
focusModeServiceMock.isSessionPaused.set(false);
|
||||
focusModeServiceMock.isRunning.set(true);
|
||||
focusModeServiceMock.isInOvertime.set(true);
|
||||
focusModeServiceMock.timeElapsed.set(1560000);
|
||||
|
||||
TestBed.flushEffects();
|
||||
|
||||
expect(titleService.setTitle).toHaveBeenCalledWith(
|
||||
'(26:00 Break) Super Productivity',
|
||||
);
|
||||
|
||||
focusModeServiceMock.mode.set(FocusModeMode.Flowtime);
|
||||
|
||||
TestBed.flushEffects();
|
||||
|
||||
expect(titleService.setTitle).toHaveBeenCalledWith('Super Productivity');
|
||||
});
|
||||
});
|
||||
});
|
||||
65
src/app/core/browser-title/browser-title.service.ts
Normal file
65
src/app/core/browser-title/browser-title.service.ts
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import { effect, inject, Injectable } from '@angular/core';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
import { FocusModeService } from '../../features/focus-mode/focus-mode.service';
|
||||
import { msToMinuteClockString } from '../../ui/duration/ms-to-minute-clock-string.pipe';
|
||||
import { FocusModeMode } from '../../features/focus-mode/focus-mode.model';
|
||||
import { T } from 'src/app/t.const';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class BrowserTitleService {
|
||||
private _titleService = inject(Title);
|
||||
private _focusModeService = inject(FocusModeService);
|
||||
private _translateService = inject(TranslateService);
|
||||
|
||||
private readonly _baseTitle = 'Super Productivity';
|
||||
|
||||
constructor() {
|
||||
effect(() => {
|
||||
this._titleService.setTitle(
|
||||
this._getTitle(
|
||||
this._focusModeService.mode(),
|
||||
this._focusModeService.timeRemaining(),
|
||||
this._focusModeService.isBreakActive(),
|
||||
this._focusModeService.isRunning(),
|
||||
this._focusModeService.isSessionPaused(),
|
||||
this._focusModeService.isInOvertime(),
|
||||
this._focusModeService.timeElapsed(),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private _getTitle(
|
||||
mode: FocusModeMode,
|
||||
timeRemaining: number,
|
||||
isBreakActive: boolean,
|
||||
isRunning: boolean,
|
||||
isSessionPaused: boolean,
|
||||
isInOvertime: boolean,
|
||||
timeElapsed: number,
|
||||
): string {
|
||||
if (mode === FocusModeMode.Pomodoro && (isRunning || isSessionPaused)) {
|
||||
const displayTime = isInOvertime ? timeElapsed : timeRemaining;
|
||||
|
||||
const timeStr = msToMinuteClockString(displayTime);
|
||||
|
||||
const [minutes, seconds] = timeStr.split(':');
|
||||
const formattedTime = `${minutes.padStart(2, '0')}:${seconds}`;
|
||||
|
||||
const breakStr = isBreakActive
|
||||
? ` ${this._translateService.instant(T.F.FOCUS_MODE.BROWSER_TITLE_BREAK)}`
|
||||
: '';
|
||||
|
||||
const pausedStr = isSessionPaused
|
||||
? `${this._translateService.instant(T.F.FOCUS_MODE.BROWSER_TITLE_PAUSED)} `
|
||||
: '';
|
||||
|
||||
return `(${pausedStr}${formattedTime}${breakStr}) ${this._baseTitle}`;
|
||||
}
|
||||
|
||||
return this._baseTitle;
|
||||
}
|
||||
}
|
||||
|
|
@ -278,6 +278,8 @@ const T = {
|
|||
},
|
||||
BACK_TO_PLANNING: 'F.FOCUS_MODE.BACK_TO_PLANNING',
|
||||
BREAK_RELAX_MSG: 'F.FOCUS_MODE.BREAK_RELAX_MSG',
|
||||
BROWSER_TITLE_BREAK: 'F.FOCUS_MODE.BROWSER_TITLE_BREAK',
|
||||
BROWSER_TITLE_PAUSED: 'F.FOCUS_MODE.BROWSER_TITLE_PAUSED',
|
||||
CLICK_TO_EDIT_DURATION: 'F.FOCUS_MODE.CLICK_TO_EDIT_DURATION',
|
||||
COMPLETE_FOCUS_SESSION: 'F.FOCUS_MODE.COMPLETE_FOCUS_SESSION',
|
||||
CONTINUE_TO_NEXT_SESSION: 'F.FOCUS_MODE.CONTINUE_TO_NEXT_SESSION',
|
||||
|
|
|
|||
|
|
@ -277,6 +277,8 @@
|
|||
},
|
||||
"BACK_TO_PLANNING": "Back to Planning",
|
||||
"BREAK_RELAX_MSG": "Take a moment to relax",
|
||||
"BROWSER_TITLE_BREAK": "Break",
|
||||
"BROWSER_TITLE_PAUSED": "Paused",
|
||||
"CLICK_TO_EDIT_DURATION": "Click to edit duration",
|
||||
"COMPLETE_FOCUS_SESSION": "Complete focus session",
|
||||
"CONTINUE_TO_NEXT_SESSION": "Continue to next session",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue