Merge pull request #4949 from notsecret808/fix/confetti-celebration-does-not-respect-is-disable-animations-config

Fix #4736: confetti celebration does not respect the "Disable All Animations" anymore
This commit is contained in:
Johannes Millan 2025-08-13 19:36:14 +02:00 committed by GitHub
commit e6536d06df
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 135 additions and 73 deletions

View file

@ -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<void> | null;

View file

@ -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<void> {
const misc = this._configService.misc();
if (misc && misc.isDisableAnimations) {
return;
}
const confettiModule = await import('canvas-confetti');
confettiModule.default(props);
}
}

View file

@ -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<void> {
// 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 },

View file

@ -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 },

View file

@ -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 },