From c640247d1d0e3966ee4130400fc94ddb1e0c3484 Mon Sep 17 00:00:00 2001 From: Johannes Millan Date: Wed, 4 Mar 2026 19:56:40 +0100 Subject: [PATCH 1/2] fix(build): fix tag resolution and add error handling in bump-android-version MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous two-tag approach (`currentTag` → `prevTag`) would resolve the wrong changelog range during `npm version` since the new tag doesn't exist yet. Simplified to a single `lastTag...HEAD` range which correctly captures all commits since the last release. Added try-catch with fallback to last 20 commits when no tags exist, and a fallback message for empty changelogs. --- tools/bump-android-version.js | 37 +++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/tools/bump-android-version.js b/tools/bump-android-version.js index 0c80cae555..e3a2d18a8b 100644 --- a/tools/bump-android-version.js +++ b/tools/bump-android-version.js @@ -80,20 +80,27 @@ const outputDir = path.join( ); const outputFilePath = path.join(outputDir, `${versionCodeDroid}.txt`); -// Get commit messages between previous tag and HEAD -const currentTag = execFileSync('git', ['describe', '--tags', '--abbrev=0', 'HEAD'], { - encoding: 'utf8', -}).trim(); -const prevTag = execFileSync( - 'git', - ['describe', '--tags', '--abbrev=0', `${currentTag}~1`], - { encoding: 'utf8' }, -).trim(); -const gitLog = execFileSync( - 'git', - ['log', `${prevTag}...HEAD`, '--no-merges', '--pretty=format:- %s'], - { encoding: 'utf8' }, -); +// Get commit messages since the last tag. +// During `npm version`, the new tag does not exist yet, so +// `git describe --tags --abbrev=0 HEAD` gives us the previous release tag, +// which is exactly the start of the range we want. +let gitLog; +try { + const lastTag = execFileSync('git', ['describe', '--tags', '--abbrev=0', 'HEAD'], { + encoding: 'utf8', + }).trim(); + gitLog = execFileSync( + 'git', + ['log', `${lastTag}...HEAD`, '--no-merges', '--pretty=format:- %s'], + { encoding: 'utf8' }, + ); +} catch (err) { + console.warn(`Could not generate changelog from git tags: ${err.message}`); + console.warn('Falling back to last 20 commits'); + gitLog = execFileSync('git', ['log', '-20', '--no-merges', '--pretty=format:- %s'], { + encoding: 'utf8', + }); +} // Strip conventional-commit prefixes (e.g. "feat(tasks): " → "") let latestChanges = gitLog.replace(/^- \w+(\([^)]*\))?!?:\s*/gm, '- '); // Truncate to 500 chars at line boundaries for Play Store limit @@ -103,7 +110,7 @@ for (const line of lines) { if ((truncated + line + '\n').length > 500) break; truncated += line + '\n'; } -latestChanges = truncated.trimEnd(); +latestChanges = truncated.trimEnd() || 'Bug fixes and improvements'; // Ensure the output directory exists if (!fs.existsSync(outputDir)) { From 8adcbb2cc53441919fdfbbf4d66e1d716f8dd1ab Mon Sep 17 00:00:00 2001 From: Johannes Millan Date: Fri, 6 Mar 2026 15:44:09 +0100 Subject: [PATCH 2/2] fix(play-button): replace infinite CSS animation with periodic finite pulses The pulse circle behind the play button used `animation: pulse 2s infinite`, which forces the browser compositor to run at 60fps continuously. This caused ~5-10% CPU and ~30% GPU usage while time tracking was active (#6076). Replace with a finite CSS animation (`animation-iteration-count: 1`) triggered every 5s via lazySetInterval. The compositor goes fully idle between pulses. Combined with `will-change: transform` (own GPU layer), the animation does not cause repaints of surrounding elements. Closes #6076 --- .../play-button/play-button.component.ts | 71 ++++++++++++++++--- src/app/util/lazy-set-interval.ts | 19 +++++ 2 files changed, 82 insertions(+), 8 deletions(-) create mode 100644 src/app/util/lazy-set-interval.ts diff --git a/src/app/core-ui/main-header/play-button/play-button.component.ts b/src/app/core-ui/main-header/play-button/play-button.component.ts index 5d70119447..7aa94b0ad3 100644 --- a/src/app/core-ui/main-header/play-button/play-button.component.ts +++ b/src/app/core-ui/main-header/play-button/play-button.component.ts @@ -3,6 +3,7 @@ import { ChangeDetectorRef, Component, computed, + effect, ElementRef, inject, input, @@ -25,6 +26,7 @@ import { TaskService } from '../../../features/tasks/task.service'; import { animationFrameScheduler, Subscription } from 'rxjs'; import { distinctUntilChanged, observeOn } from 'rxjs/operators'; import { NavigateToTaskService } from '../../navigate-to-task/navigate-to-task.service'; +import { lazySetInterval } from '../../../util/lazy-set-interval'; @Component({ selector: 'play-button', @@ -51,7 +53,10 @@ import { NavigateToTaskService } from '../../navigate-to-task/navigate-to-task.s } @if (currentTaskId()) { -
+
} @if (hasTimeEstimate) { @@ -98,14 +103,16 @@ import { NavigateToTaskService } from '../../navigate-to-task/navigate-to-task.s display: contents; } - @keyframes pulse { + /* Finite pulse animation played once per trigger (not infinite). + * An infinite CSS animation forces the compositor to run at 60fps + * continuously, causing ~5-10% CPU and ~30% GPU even when idle. + * Instead, this finite animation is triggered periodically via JS + * (see effect() in the component class). */ + @keyframes pulse-once { 0% { transform: scale(0.7); } - 25% { - transform: scale(1); - } - 50% { + 40% { transform: scale(1); } 100% { @@ -127,10 +134,17 @@ import { NavigateToTaskService } from '../../navigate-to-task/navigate-to-task.s bottom: 0; border-radius: 50%; margin: auto; - transform: scale(1, 1); - animation: pulse 2s infinite; + transform: scale(0.7); background: var(--c-accent); opacity: 0.6; + + /* Promote to own GPU layer so the pulse animation does not + * trigger repaints of the button or icon above it. */ + will-change: transform; + + &.do-pulse { + animation: pulse-once 2s ease-in-out 1; + } } .circle-svg { @@ -227,6 +241,7 @@ export class PlayButtonComponent implements OnInit, OnDestroy { readonly currentTaskContext = input(); readonly hasTrackableTasks = input(true); readonly circleSvg = viewChild>('circleSvg'); + readonly pulseCircle = viewChild>('pulseCircle'); readonly isDisabled = computed( () => !this.currentTaskId() && !this.hasTrackableTasks(), @@ -238,6 +253,46 @@ export class PlayButtonComponent implements OnInit, OnDestroy { private _subs = new Subscription(); private circumference = 10 * 2 * Math.PI; // ~62.83 protected hasTimeEstimate = false; + constructor() { + // Intermittent pulse animation to indicate active time tracking. + // + // Performance note (see https://github.com/super-productivity/super-productivity/issues/6076): + // CSS `animation: infinite` forces the browser compositor to run at 60fps + // continuously, even for a simple scale transform. This caused ~5-10% CPU + // and ~30% GPU usage while tracking. + // + // Fix: use a finite CSS animation (`animation-iteration-count: 1`) triggered + // periodically via lazySetInterval. The compositor goes fully idle between + // pulses. Combined with `will-change: transform` on the pulse element (which + // promotes it to its own GPU layer), the animation does not cause repaints of + // surrounding elements (button, icon). + effect((onCleanup) => { + const el = this.pulseCircle()?.nativeElement; + const isTracking = !!this.currentTaskId(); + + if (el && isTracking) { + // Trigger an initial pulse immediately + el.classList.add('do-pulse'); + + // Remove the class when the animation finishes so it can be re-triggered + const onAnimEnd = (): void => el.classList.remove('do-pulse'); + el.addEventListener('animationend', onAnimEnd); + + // Re-trigger the pulse every 5s (animation itself is 2s, leaving 3s idle) + const stopInterval = lazySetInterval(() => { + el.classList.add('do-pulse'); + }, 5000); + + // Angular calls onCleanup automatically when the effect re-runs or + // the component is destroyed — no manual ngOnDestroy cleanup needed. + onCleanup(() => { + stopInterval(); + el.removeEventListener('animationend', onAnimEnd); + el.classList.remove('do-pulse'); + }); + } + }); + } ngOnInit(): void { // Subscribe to current task to track if it has a time estimate diff --git a/src/app/util/lazy-set-interval.ts b/src/app/util/lazy-set-interval.ts new file mode 100644 index 0000000000..a8ffb4f117 --- /dev/null +++ b/src/app/util/lazy-set-interval.ts @@ -0,0 +1,19 @@ +// Avoids the performance issues caused by normal setInterval when the user +// is not at the computer for some time. Uses chained setTimeout instead. +export const lazySetInterval = ( + func: () => void, + intervalDuration: number, +): (() => void) => { + let lastTimeoutId: ReturnType; + + const interval = (): void => { + lastTimeoutId = setTimeout(interval, intervalDuration); + func.call(null); + }; + + lastTimeoutId = setTimeout(interval, intervalDuration); + + return () => { + clearTimeout(lastTimeoutId); + }; +};