mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-18 00:46:45 +00:00
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:
commit
e6536d06df
5 changed files with 135 additions and 73 deletions
25
src/app/core/confetti/confetti.model.ts
Normal file
25
src/app/core/confetti/confetti.model.ts
Normal 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;
|
||||
22
src/app/core/confetti/confetti.service.ts
Normal file
22
src/app/core/confetti/confetti.service.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue