diff --git a/src/app/core/confetti/confetti.model.ts b/src/app/core/confetti/confetti.model.ts new file mode 100644 index 0000000000..5adc3471f5 --- /dev/null +++ b/src/app/core/confetti/confetti.model.ts @@ -0,0 +1,25 @@ +export interface ConfettiConfig { + particleCount?: number; + angel?: number; + spread?: number; + startVelocity?: number; + decay?: number; + gravity?: number; + drift?: number; + flat?: boolean; + ticks?: number; + origin?: { x?: number; y?: number }; + colors?: string[]; + shapes?: (string | Shape)[]; + scalar?: number; + zIndex?: number; +} + +interface Shape { + type: 'path' | 'bitmap'; + path?: string; + matrix: DOMMatrix; + bitmap?: ImageBitmap; +} + +export type CanvasConfetti = (props: ConfettiConfig) => Promise | null; diff --git a/src/app/core/confetti/confetti.service.ts b/src/app/core/confetti/confetti.service.ts new file mode 100644 index 0000000000..26b607638b --- /dev/null +++ b/src/app/core/confetti/confetti.service.ts @@ -0,0 +1,22 @@ +import { inject, Injectable } from '@angular/core'; + +import { GlobalConfigService } from '../../features/config/global-config.service'; +import { ConfettiConfig } from './confetti.model'; + +@Injectable({ + providedIn: 'root', +}) +export class ConfettiService { + private readonly _configService = inject(GlobalConfigService); + + async createConfetti(props: ConfettiConfig): Promise { + const misc = this._configService.misc(); + + if (misc && misc.isDisableAnimations) { + return; + } + + const confettiModule = await import('canvas-confetti'); + confettiModule.default(props); + } +} diff --git a/src/app/features/focus-mode/focus-mode-task-done/focus-mode-task-done.component.ts b/src/app/features/focus-mode/focus-mode-task-done/focus-mode-task-done.component.ts index 3f91116ef9..61d4b0978d 100644 --- a/src/app/features/focus-mode/focus-mode-task-done/focus-mode-task-done.component.ts +++ b/src/app/features/focus-mode/focus-mode-task-done/focus-mode-task-done.component.ts @@ -1,26 +1,30 @@ +import { AsyncPipe } from '@angular/common'; import { AfterViewInit, ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { MatButton } from '@angular/material/button'; + +import { of } from 'rxjs'; +import { map, switchMap, take } from 'rxjs/operators'; +import { T } from 'src/app/t.const'; + import { Store } from '@ngrx/store'; +import { TranslatePipe } from '@ngx-translate/core'; + +import { ConfettiService } from '../../../core/confetti/confetti.service'; +import { MsToStringPipe } from '../../../ui/duration/ms-to-string.pipe'; +import { + selectCurrentTask, + selectLastCurrentTask, +} from '../../tasks/store/task.selectors'; +import { FocusModePage } from '../focus-mode.const'; import { cancelFocusSession, hideFocusOverlay, setFocusSessionActivePage, } from '../store/focus-mode.actions'; -import { FocusModePage } from '../focus-mode.const'; -import { - selectCurrentTask, - selectLastCurrentTask, -} from '../../tasks/store/task.selectors'; import { selectFocusModeMode, selectLastSessionTotalDurationOrTimeElapsedFallback, } from '../store/focus-mode.selectors'; -import { map, switchMap, take } from 'rxjs/operators'; -import { of } from 'rxjs'; -import { T } from 'src/app/t.const'; -import { MatButton } from '@angular/material/button'; -import { AsyncPipe } from '@angular/common'; -import { MsToStringPipe } from '../../../ui/duration/ms-to-string.pipe'; -import { TranslatePipe } from '@ngx-translate/core'; @Component({ selector: 'focus-mode-task-done', @@ -31,6 +35,7 @@ import { TranslatePipe } from '@ngx-translate/core'; }) export class FocusModeTaskDoneComponent implements AfterViewInit { private _store = inject(Store); + private readonly _confettiService = inject(ConfettiService); mode$ = this._store.select(selectFocusModeMode); currentTask$ = this._store.select(selectCurrentTask); @@ -49,20 +54,16 @@ export class FocusModeTaskDoneComponent implements AfterViewInit { T: typeof T = T; async ngAfterViewInit(): Promise { - // Lazy load confetti library only when task is done - const confettiModule = await import('canvas-confetti'); - const confetti = confettiModule.default; - const defaults = { startVelocity: 80, spread: 720, ticks: 600, zIndex: 0 }; const particleCount = 200; // since particles fall down, start a bit higher than random - confetti({ + this._confettiService.createConfetti({ ...defaults, particleCount, origin: { x: 0.5, y: 1 }, }); - confetti({ + this._confettiService.createConfetti({ ...defaults, particleCount, origin: { x: 0.5, y: 1 }, diff --git a/src/app/features/simple-counter/store/simple-counter.effects.ts b/src/app/features/simple-counter/store/simple-counter.effects.ts index 10bcbfa8b1..5fffe74dcb 100644 --- a/src/app/features/simple-counter/store/simple-counter.effects.ts +++ b/src/app/features/simple-counter/store/simple-counter.effects.ts @@ -1,24 +1,28 @@ import { inject, Injectable } from '@angular/core'; + +import { EMPTY, Observable } from 'rxjs'; +import { map, mergeMap, switchMap, tap } from 'rxjs/operators'; +import { DateService } from 'src/app/core/date/date.service'; + import { Actions, createEffect, ofType } from '@ngrx/effects'; import { select, Store } from '@ngrx/store'; -import confetti from 'canvas-confetti'; +import { TranslateService } from '@ngx-translate/core'; + +import { ConfettiService } from '../../../core/confetti/confetti.service'; +import { GlobalTrackingIntervalService } from '../../../core/global-tracking-interval/global-tracking-interval.service'; +import { SnackService } from '../../../core/snack/snack.service'; +import { PfapiService } from '../../../pfapi/pfapi.service'; +import { T } from '../../../t.const'; +import { getDbDateStr } from '../../../util/get-db-date-str'; +import { GlobalConfigService } from '../../config/global-config.service'; +import { getSimpleCounterStreakDuration } from '../get-simple-counter-streak-duration'; +import { SimpleCounterType } from '../simple-counter.model'; +import { SimpleCounterService } from '../simple-counter.service'; import { increaseSimpleCounterCounterToday, updateAllSimpleCounters, } from './simple-counter.actions'; -import { map, mergeMap, switchMap, tap } from 'rxjs/operators'; import { selectSimpleCounterById } from './simple-counter.reducer'; -import { SimpleCounterType } from '../simple-counter.model'; -import { GlobalTrackingIntervalService } from '../../../core/global-tracking-interval/global-tracking-interval.service'; -import { SimpleCounterService } from '../simple-counter.service'; -import { EMPTY, Observable } from 'rxjs'; -import { T } from '../../../t.const'; -import { SnackService } from '../../../core/snack/snack.service'; -import { DateService } from 'src/app/core/date/date.service'; -import { getDbDateStr } from '../../../util/get-db-date-str'; -import { getSimpleCounterStreakDuration } from '../get-simple-counter-streak-duration'; -import { TranslateService } from '@ngx-translate/core'; -import { PfapiService } from '../../../pfapi/pfapi.service'; @Injectable() export class SimpleCounterEffects { @@ -30,6 +34,8 @@ export class SimpleCounterEffects { private _simpleCounterService = inject(SimpleCounterService); private _snackService = inject(SnackService); private _translateService = inject(TranslateService); + private _configService = inject(GlobalConfigService); + private readonly _confettiService = inject(ConfettiService); successFullCountersMap: { [key: string]: boolean } = {}; @@ -97,6 +103,7 @@ export class SimpleCounterEffects { msg, }); this.successFullCountersMap[sc.id] = true; + this._celebrate(); } // else if ( @@ -120,7 +127,7 @@ export class SimpleCounterEffects { ); private _celebrate(): void { - confetti({ + this._confettiService.createConfetti({ particleCount: 100, spread: 70, origin: { y: 0.6 }, diff --git a/src/app/pages/daily-summary/daily-summary.component.ts b/src/app/pages/daily-summary/daily-summary.component.ts index 1e02e9e8e8..d4d55c05c8 100644 --- a/src/app/pages/daily-summary/daily-summary.component.ts +++ b/src/app/pages/daily-summary/daily-summary.component.ts @@ -1,5 +1,4 @@ -import confetti from 'canvas-confetti'; - +import { AsyncPipe } from '@angular/common'; import { AfterViewInit, ChangeDetectionStrategy, @@ -12,13 +11,16 @@ import { OnInit, Signal, } from '@angular/core'; -import { TaskService } from '../../features/tasks/task.service'; -import { ActivatedRoute, Router, RouterLink } from '@angular/router'; -import { IS_ELECTRON } from '../../app.constants'; +import { toSignal } from '@angular/core/rxjs-interop'; +import { MatAnchor, MatButton, MatIconButton } from '@angular/material/button'; import { MatDialog } from '@angular/material/dialog'; +import { MatIcon } from '@angular/material/icon'; +import { MatProgressSpinner } from '@angular/material/progress-spinner'; +import { MatTab, MatTabGroup } from '@angular/material/tabs'; +import { MatTooltip } from '@angular/material/tooltip'; +import { ActivatedRoute, Router, RouterLink } from '@angular/router'; + import { combineLatest, from, merge, Observable, Subject } from 'rxjs'; -import { DialogConfirmComponent } from '../../ui/dialog-confirm/dialog-confirm.component'; -import { GlobalConfigService } from '../../features/config/global-config.service'; import { delay, filter, @@ -31,47 +33,47 @@ import { takeUntil, withLatestFrom, } from 'rxjs/operators'; -import { T } from '../../t.const'; -import { WorkContextService } from '../../features/work-context/work-context.service'; -import { Task, TaskWithSubTasks } from '../../features/tasks/task.model'; -import { SyncWrapperService } from '../../imex/sync/sync-wrapper.service'; -import { isToday, isYesterday } from '../../util/is-today.util'; -import { WorklogService } from '../../features/worklog/worklog.service'; -import { WorkContextType } from '../../features/work-context/work-context.model'; -import { EntityState } from '@ngrx/entity'; -import { TODAY_TAG } from '../../features/tag/tag.const'; -import { shareReplayUntil } from '../../util/share-replay-until'; import { DateService } from 'src/app/core/date/date.service'; + +import { EntityState } from '@ngrx/entity'; import { Action } from '@ngrx/store'; -import { BeforeFinishDayService } from '../../features/before-finish-day/before-finish-day.service'; -import { MatAnchor, MatButton, MatIconButton } from '@angular/material/button'; -import { MatIcon } from '@angular/material/icon'; -import { InlineInputComponent } from '../../ui/inline-input/inline-input.component'; -import { MatTab, MatTabGroup } from '@angular/material/tabs'; -import { MatProgressSpinner } from '@angular/material/progress-spinner'; -import { PlanTasksTomorrowComponent } from './plan-tasks-tomorrow/plan-tasks-tomorrow.component'; -import { MatTooltip } from '@angular/material/tooltip'; -import { AsyncPipe } from '@angular/common'; -import { MomentFormatPipe } from '../../ui/pipes/moment-format.pipe'; -import { MsToClockStringPipe } from '../../ui/duration/ms-to-clock-string.pipe'; import { TranslatePipe } from '@ngx-translate/core'; -import { TaskSummaryTablesComponent } from '../../features/tasks/task-summary-tables/task-summary-tables.component'; -import { TasksByTagComponent } from '../../features/tasks/tasks-by-tag/tasks-by-tag.component'; + +import { IS_ELECTRON } from '../../app.constants'; +import { ConfettiService } from '../../core/confetti/confetti.service'; +import { Log } from '../../core/log'; +import { BeforeFinishDayService } from '../../features/before-finish-day/before-finish-day.service'; +import { GlobalConfigService } from '../../features/config/global-config.service'; import { EvaluationSheetComponent } from '../../features/metric/evaluation-sheet/evaluation-sheet.component'; -import { WorklogWeekComponent } from '../../features/worklog/worklog-week/worklog-week.component'; -import { InlineMarkdownComponent } from '../../ui/inline-markdown/inline-markdown.component'; -import { unToggleCheckboxesInMarkdownTxt } from '../../util/untoggle-checkboxes-in-markdown-txt'; -import { expandAnimation } from '../../ui/animations/expand.ani'; -import { SimpleCounterService } from '../../features/simple-counter/simple-counter.service'; -import { toSignal } from '@angular/core/rxjs-interop'; import { getSimpleCounterStreakDuration } from '../../features/simple-counter/get-simple-counter-streak-duration'; +import { SimpleCounterService } from '../../features/simple-counter/simple-counter.service'; +import { TODAY_TAG } from '../../features/tag/tag.const'; +import { TaskSummaryTablesComponent } from '../../features/tasks/task-summary-tables/task-summary-tables.component'; +import { Task, TaskWithSubTasks } from '../../features/tasks/task.model'; +import { TaskService } from '../../features/tasks/task.service'; +import { TasksByTagComponent } from '../../features/tasks/tasks-by-tag/tasks-by-tag.component'; +import { TaskArchiveService } from '../../features/time-tracking/task-archive.service'; +import { WorkContextType } from '../../features/work-context/work-context.model'; +import { WorkContextService } from '../../features/work-context/work-context.service'; +import { WorklogWeekComponent } from '../../features/worklog/worklog-week/worklog-week.component'; +import { WorklogService } from '../../features/worklog/worklog.service'; +import { SyncWrapperService } from '../../imex/sync/sync-wrapper.service'; +import { T } from '../../t.const'; +import { expandAnimation } from '../../ui/animations/expand.ani'; +import { DialogConfirmComponent } from '../../ui/dialog-confirm/dialog-confirm.component'; +import { MsToClockStringPipe } from '../../ui/duration/ms-to-clock-string.pipe'; +import { InlineInputComponent } from '../../ui/inline-input/inline-input.component'; +import { InlineMarkdownComponent } from '../../ui/inline-markdown/inline-markdown.component'; +import { MomentFormatPipe } from '../../ui/pipes/moment-format.pipe'; +import { isToday, isYesterday } from '../../util/is-today.util'; +import { IS_TOUCH_ONLY } from '../../util/is-touch-only'; +import { shareReplayUntil } from '../../util/share-replay-until'; +import { unToggleCheckboxesInMarkdownTxt } from '../../util/untoggle-checkboxes-in-markdown-txt'; +import { PlanTasksTomorrowComponent } from './plan-tasks-tomorrow/plan-tasks-tomorrow.component'; import { SimpleCounterSummaryItem, SimpleCounterSummaryItemComponent, } from './simple-counter-summary-item/simple-counter-summary-item.component'; -import { TaskArchiveService } from '../../features/time-tracking/task-archive.service'; -import { IS_TOUCH_ONLY } from '../../util/is-touch-only'; -import { Log } from '../../core/log'; const SUCCESS_ANIMATION_DURATION = 500; const MAGIC_YESTERDAY_MARGIN = 4 * 60 * 60 * 1000; @@ -108,6 +110,7 @@ const MAGIC_YESTERDAY_MARGIN = 4 * 60 * 60 * 1000; }) export class DailySummaryComponent implements OnInit, OnDestroy, AfterViewInit { readonly configService = inject(GlobalConfigService); + private readonly _confettiService = inject(ConfettiService); readonly workContextService = inject(WorkContextService); private readonly _taskService = inject(TaskService); private readonly _router = inject(Router); @@ -285,6 +288,10 @@ export class DailySummaryComponent implements OnInit, OnDestroy, AfterViewInit { } ngAfterViewInit(): void { + if (this.configService.misc()?.isDisableAnimations) { + return; + } + this._startCelebrationTimeout = window.setTimeout( () => { this._celebrate(); @@ -560,12 +567,12 @@ export class DailySummaryComponent implements OnInit, OnDestroy, AfterViewInit { const particleCount = 50 * (timeLeft / duration); // since particles fall down, start a bit higher than random - confetti({ + this._confettiService.createConfetti({ ...defaults, particleCount, origin: { x: randomInRange(0.1, 0.3), y: Math.random() - 0.2 }, }); - confetti({ + this._confettiService.createConfetti({ ...defaults, particleCount, origin: { x: randomInRange(0.7, 0.9), y: Math.random() - 0.2 },