Skip to content

Commit 810ab57

Browse files
authored
Merge branch 'johannesjo:master' into openproject-filter-fix
2 parents 7d8b54e + 74827cc commit 810ab57

14 files changed

+170
-19
lines changed

.github/ISSUE_TEMPLATE/BUG_REPORT.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,11 @@ assignees: ''
4949

5050
<!--- For the desktop versions, there is also an error log file in case there is no console output.
5151
Usually, you can find it here:
52-
on Linux: ~/.config/superProductivity/logs/main.log
5352
on macOS: ~/Library/Logs/superProductivity/main.log
5453
on Windows: %USERPROFILE%\AppData\Roaming\superProductivity\logs\main.log
54+
on Linux: ~/.config/superProductivity/logs/main.log
55+
on Linux (snap): ~/snap/superproductivity/common/.config/superProductivity/logs
56+
on Linux (flatpak): ~/.var/app/com.super_productivity/SuperProductivity/config/superProductivity/logs
5557
5658
If you don't feel comfortable posting your logs here in public you can also send to me via email: [email protected]
5759
. -->

src/app/features/config/config-sound-form/config-sound-form.component.html

+8
Original file line numberDiff line numberDiff line change
@@ -53,5 +53,13 @@
5353
}
5454
</mat-select>
5555
</mat-form-field>
56+
<mat-form-field class="width100">
57+
<mat-label>{{ 'GCF.SOUND.TRACK_TIME_SOUND' | translate }}</mat-label>
58+
<mat-select formControlName="trackTimeSound">
59+
@for (sound of soundOpts; track sound) {
60+
<mat-option [value]="sound.value">{{ sound.label }}</mat-option>
61+
}
62+
</mat-select>
63+
</mat-form-field>
5664
</div>
5765
</collapsible>

src/app/features/config/config-sound-form/config-sound-form.component.ts

+7
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export class ConfigSoundFormComponent {
5555
volume: new FormControl<number>(0),
5656
doneSound: new FormControl<string | null>(null),
5757
breakReminderSound: new FormControl<string | null>(null),
58+
trackTimeSound: new FormControl<string | null>(null),
5859
isIncreaseDoneSoundPitch: new FormControl<boolean>(false),
5960
});
6061

@@ -72,6 +73,7 @@ export class ConfigSoundFormComponent {
7273
volume: data.volume ?? 0,
7374
doneSound: data.doneSound ?? null,
7475
breakReminderSound: data.breakReminderSound ?? null,
76+
trackTimeSound: data.trackTimeSound ?? null,
7577
isIncreaseDoneSoundPitch: data.isIncreaseDoneSoundPitch ?? false,
7678
});
7779
}
@@ -92,6 +94,9 @@ export class ConfigSoundFormComponent {
9294
this.soundForm
9395
.get('breakReminderSound')!
9496
.patchValue(this.config.breakReminderSound, { emitEvent: false, onlySelf: true });
97+
this.soundForm
98+
.get('trackTimeSound')!
99+
.patchValue(this.config.trackTimeSound, { emitEvent: false, onlySelf: true });
95100
this.soundForm
96101
.get('isIncreaseDoneSoundPitch')!
97102
.patchValue(this.config.isIncreaseDoneSoundPitch, {
@@ -112,6 +117,8 @@ export class ConfigSoundFormComponent {
112117
cfg.breakReminderSound
113118
) {
114119
playSound(cfg.breakReminderSound, cfg.volume);
120+
} else if (cfg.trackTimeSound !== this.config?.trackTimeSound && cfg.trackTimeSound) {
121+
playSound(cfg.trackTimeSound, cfg.volume);
115122
} else {
116123
playDoneSound(cfg);
117124
}

src/app/features/config/default-global-config.const.ts

+3
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ export const DEFAULT_GLOBAL_CONFIG: GlobalConfigState = {
140140
isIncreaseDoneSoundPitch: true,
141141
doneSound: 'done2.mp3',
142142
breakReminderSound: null,
143+
trackTimeSound: null,
143144
},
144145
timeTracking: {
145146
trackingInterval: TRACKING_INTERVAL,
@@ -150,6 +151,8 @@ export const DEFAULT_GLOBAL_CONFIG: GlobalConfigState = {
150151
isTrackingReminderEnabled: false,
151152
isTrackingReminderShowOnMobile: false,
152153
trackingReminderMinTime: 5 * minute,
154+
isTrackingReminderNotify: false, // Show desktop notification when tracking reminder is triggered
155+
isTrackingReminderFocusWindow: false, // Focus the application window when tracking reminder is triggered
153156
},
154157
reminder: {
155158
isCountdownBannerEnabled: true,

src/app/features/config/form-cfgs/sound-form.const.ts

+9
Original file line numberDiff line numberDiff line change
@@ -57,5 +57,14 @@ export const SOUND_FORM_CFG: ConfigFormSection<SoundConfig> = {
5757
change: ({ model }) => playSound(model.breakReminderSound, model.volume),
5858
},
5959
},
60+
{
61+
key: 'trackTimeSound',
62+
type: 'select',
63+
templateOptions: {
64+
label: T.GCF.SOUND.TRACK_TIME_SOUND,
65+
options: SOUND_OPTS,
66+
change: ({ model }) => playSound(model.trackTimeSound, model.volume),
67+
},
68+
},
6069
],
6170
};

src/app/features/config/form-cfgs/time-tracking-form.const.ts

+16
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,22 @@ export const TIME_TRACKING_FORM_CFG: ConfigFormSection<TimeTrackingConfig> = {
5454
label: T.GCF.TIME_TRACKING.L_IS_TRACKING_REMINDER_SHOW_ON_MOBILE,
5555
},
5656
},
57+
{
58+
key: 'isTrackingReminderNotify',
59+
type: 'checkbox',
60+
hideExpression: (model) => !model.isTrackingReminderEnabled,
61+
templateOptions: {
62+
label: T.GCF.TIME_TRACKING.L_IS_TRACKING_REMINDER_NOTIFY,
63+
},
64+
},
65+
{
66+
key: 'isTrackingReminderFocusWindow',
67+
type: 'checkbox',
68+
hideExpression: (model) => !model.isTrackingReminderEnabled,
69+
templateOptions: {
70+
label: T.GCF.TIME_TRACKING.L_IS_TRACKING_REMINDER_FOCUS_WINDOW,
71+
},
72+
},
5773
{
5874
key: 'trackingReminderMinTime',
5975
type: 'duration',

src/app/features/config/global-config.model.ts

+3
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ export type TimeTrackingConfig = Readonly<{
4040
isTrackingReminderEnabled: boolean;
4141
isTrackingReminderShowOnMobile: boolean;
4242
trackingReminderMinTime: number;
43+
isTrackingReminderNotify: boolean;
44+
isTrackingReminderFocusWindow: boolean;
4345
}>;
4446

4547
export type EvaluationConfig = Readonly<{
@@ -112,6 +114,7 @@ export type SoundConfig = Readonly<{
112114
isIncreaseDoneSoundPitch: boolean;
113115
doneSound: string | null;
114116
breakReminderSound: string | null;
117+
trackTimeSound: string | null;
115118
volume: number;
116119
}>;
117120

src/app/features/tasks/task/task.component.ts

+6-8
Original file line numberDiff line numberDiff line change
@@ -739,7 +739,7 @@ export class TaskComponent implements OnDestroy, AfterViewInit {
739739

740740
moveToToday(): void {
741741
const t = this.task();
742-
if (t.projectId && !t.parentId) {
742+
if (t.projectId) {
743743
this._projectService.moveTaskToTodayList(t.id, t.projectId);
744744
this.addToMyDay();
745745
}
@@ -899,13 +899,11 @@ export class TaskComponent implements OnDestroy, AfterViewInit {
899899
}
900900

901901
if (checkKeyCombo(ev, keys.moveToTodaysTasks) && t.projectId) {
902-
if (!t.parentId) {
903-
ev.preventDefault();
904-
// same default shortcut as schedule so we stop propagation
905-
ev.stopPropagation();
906-
this.focusNext(true, true);
907-
this.moveToToday();
908-
}
902+
ev.preventDefault();
903+
// same default shortcut as schedule so we stop propagation
904+
ev.stopPropagation();
905+
this.focusNext(true, true);
906+
this.moveToToday();
909907
}
910908

911909
// collapse sub tasks

src/app/features/tracking-reminder/tracking-reminder.service.ts

+67-1
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@ import { combineLatest, EMPTY, merge, Observable, of, Subject } from 'rxjs';
66
import {
77
distinctUntilChanged,
88
filter,
9+
first,
910
map,
1011
shareReplay,
1112
switchMap,
13+
throttleTime,
1214
withLatestFrom,
1315
} from 'rxjs/operators';
1416
import { realTimer$ } from '../../util/real-timer';
@@ -22,8 +24,13 @@ import { T } from '../../t.const';
2224
import { TranslateService } from '@ngx-translate/core';
2325
import { TimeTrackingConfig } from '../config/global-config.model';
2426
import { IS_TOUCH_ONLY } from '../../util/is-touch-only';
25-
import { DateService } from 'src/app/core/date/date.service';
27+
import { DateService } from '../../core/date/date.service';
2628
import { TakeABreakService } from '../take-a-break/take-a-break.service';
29+
import { NotifyService } from '../../core/notify/notify.service';
30+
import { UiHelperService } from '../ui-helper/ui-helper.service';
31+
import { playSound } from '../../util/play-sound';
32+
33+
const DESKTOP_NOTIFICATION_THROTTLE = 60 * 1000;
2734

2835
@Injectable({
2936
providedIn: 'root',
@@ -37,6 +44,8 @@ export class TrackingReminderService {
3744
private _translateService = inject(TranslateService);
3845
private _dateService = inject(DateService);
3946
private _takeABreakService = inject(TakeABreakService);
47+
private _notifyService = inject(NotifyService);
48+
private _uiHelperService = inject(UiHelperService);
4049

4150
_cfg$: Observable<TimeTrackingConfig> = this._globalConfigService.cfg$.pipe(
4251
map((cfg) => cfg?.timeTracking),
@@ -73,6 +82,9 @@ export class TrackingReminderService {
7382
shareReplay(),
7483
);
7584

85+
// New throttled notification stream
86+
private _throttledNotificationTrigger$ = new Subject<string>();
87+
7688
init(): void {
7789
this.remindCounter$.subscribe((count) => {
7890
this._triggerBanner(count);
@@ -81,6 +93,30 @@ export class TrackingReminderService {
8193
this._hideTrigger$.subscribe((v) => {
8294
this._hideBanner();
8395
});
96+
97+
// Set up throttled notification handling
98+
this._throttledNotificationTrigger$
99+
.pipe(
100+
throttleTime(DESKTOP_NOTIFICATION_THROTTLE),
101+
// Get all needed config in one go
102+
withLatestFrom(
103+
this._globalConfigService.sound$,
104+
this._globalConfigService.cfg$.pipe(map((cfg) => cfg.timeTracking)),
105+
),
106+
)
107+
.subscribe(([durationStr, soundCfg, timeTrackingCfg]) => {
108+
this._showNotification(durationStr);
109+
110+
// Play sound if configured
111+
if (soundCfg.trackTimeSound) {
112+
playSound(soundCfg.trackTimeSound as string);
113+
}
114+
115+
// Focus window if enabled
116+
if (timeTrackingCfg.isTrackingReminderFocusWindow) {
117+
this._focusWindow();
118+
}
119+
});
84120
}
85121

86122
private _hideBanner(): void {
@@ -109,6 +145,20 @@ export class TrackingReminderService {
109145
fn: () => this._dismissBanner(),
110146
},
111147
});
148+
149+
// Handle desktop notification if enabled
150+
this._globalConfigService.cfg$
151+
.pipe(
152+
filter((cfg) => !!cfg),
153+
first(),
154+
)
155+
.subscribe((cfg) => {
156+
// Show desktop notification if enabled
157+
if (cfg.timeTracking.isTrackingReminderNotify) {
158+
// Instead of showing directly, queue it through the throttled subject
159+
this._throttledNotificationTrigger$.next(durationStr);
160+
}
161+
});
112162
}
113163

114164
private _openDialog(): void {
@@ -153,4 +203,20 @@ export class TrackingReminderService {
153203
this._bannerService.dismiss(BannerId.StartTrackingReminder);
154204
this._manualReset$.next();
155205
}
206+
207+
private _showNotification(durationStr: string): void {
208+
this._notifyService.notify({
209+
title: this._translateService.instant(
210+
T.F.TIME_TRACKING.D_TRACKING_REMINDER.NOTIFICATION_TITLE,
211+
),
212+
body: this._translateService.instant(T.F.TIME_TRACKING.B_TTR.MSG, {
213+
time: durationStr,
214+
}),
215+
requireInteraction: true,
216+
});
217+
}
218+
219+
private _focusWindow(): void {
220+
this._uiHelperService.focusApp();
221+
}
156222
}

src/app/features/work-view/work-view.component.ts

+24-2
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ import {
66
ElementRef,
77
inject,
88
input,
9+
OnChanges,
910
OnDestroy,
1011
OnInit,
12+
SimpleChanges,
1113
ViewChild,
1214
} from '@angular/core';
1315
import { TaskService } from '../tasks/task.service';
@@ -25,7 +27,7 @@ import {
2527
zip,
2628
} from 'rxjs';
2729
import { TaskWithSubTasks } from '../tasks/task.model';
28-
import { delay, filter, map, switchMap } from 'rxjs/operators';
30+
import { delay, filter, map, switchMap, take } from 'rxjs/operators';
2931
import { fadeAnimation } from '../../ui/animations/fade.ani';
3032
import { PlanningModeService } from '../planning-mode/planning-mode.service';
3133
import { T } from '../../t.const';
@@ -85,7 +87,7 @@ import { TranslatePipe } from '@ngx-translate/core';
8587
TranslatePipe,
8688
],
8789
})
88-
export class WorkViewComponent implements OnInit, OnDestroy, AfterContentInit {
90+
export class WorkViewComponent implements OnInit, OnChanges, OnDestroy, AfterContentInit {
8991
taskService = inject(TaskService);
9092
takeABreakService = inject(TakeABreakService);
9193
planningModeService = inject(PlanningModeService);
@@ -179,6 +181,26 @@ export class WorkViewComponent implements OnInit, OnDestroy, AfterContentInit {
179181
);
180182
}
181183

184+
ngOnChanges(changes: SimpleChanges): void {
185+
const undoneChanges = changes['undoneTasks'];
186+
const doneChanges = changes['doneTasks'];
187+
188+
if ((undoneChanges || doneChanges) && this.taskService.selectedTaskId$) {
189+
const undoneArr = this.undoneTasks();
190+
const doneArr = this.doneTasks();
191+
const allTasks = [...undoneArr, ...doneArr];
192+
193+
this.taskService.selectedTaskId$.pipe(take(1)).subscribe((selectedTaskId) => {
194+
if (selectedTaskId) {
195+
const isSelectedStillPresent = allTasks.some((t) => t.id === selectedTaskId);
196+
if (!isSelectedStillPresent) {
197+
this.taskService.setSelectedId(null);
198+
}
199+
}
200+
});
201+
}
202+
}
203+
182204
ngOnDestroy(): void {
183205
if (this._switchListAnimationTimeout) {
184206
window.clearTimeout(this._switchListAnimationTimeout);

src/app/t.const.ts

+5
Original file line numberDiff line numberDiff line change
@@ -1289,6 +1289,7 @@ const T = {
12891289
TASK: 'F.TIME_TRACKING.D_TRACKING_REMINDER.TASK',
12901290
TRACK_TO: 'F.TIME_TRACKING.D_TRACKING_REMINDER.TRACK_TO',
12911291
UNTRACKED_TIME: 'F.TIME_TRACKING.D_TRACKING_REMINDER.UNTRACKED_TIME',
1292+
NOTIFICATION_TITLE: 'F.TIME_TRACKING.D_TRACKING_REMINDER.NOTIFICATION_TITLE',
12921293
},
12931294
},
12941295
WORKLOG: {
@@ -1574,6 +1575,7 @@ const T = {
15741575
IS_INCREASE_DONE_PITCH: 'GCF.SOUND.IS_INCREASE_DONE_PITCH',
15751576
TITLE: 'GCF.SOUND.TITLE',
15761577
VOLUME: 'GCF.SOUND.VOLUME',
1578+
TRACK_TIME_SOUND: 'GCF.SOUND.TRACK_TIME_SOUND',
15771579
},
15781580
TAKE_A_BREAK: {
15791581
ADD_NEW_IMG: 'GCF.TAKE_A_BREAK.ADD_NEW_IMG',
@@ -1603,6 +1605,9 @@ const T = {
16031605
L_TRACKING_INTERVAL: 'GCF.TIME_TRACKING.L_TRACKING_INTERVAL',
16041606
L_TRACKING_REMINDER_MIN_TIME: 'GCF.TIME_TRACKING.L_TRACKING_REMINDER_MIN_TIME',
16051607
TITLE: 'GCF.TIME_TRACKING.TITLE',
1608+
L_IS_TRACKING_REMINDER_NOTIFY: 'GCF.TIME_TRACKING.L_IS_TRACKING_REMINDER_NOTIFY',
1609+
L_IS_TRACKING_REMINDER_FOCUS_WINDOW:
1610+
'GCF.TIME_TRACKING.L_IS_TRACKING_REMINDER_FOCUS_WINDOW',
16061611
},
16071612
},
16081613
GLOBAL_SNACK: {

src/assets/i18n/en.json

+6-2
Original file line numberDiff line numberDiff line change
@@ -1267,7 +1267,8 @@
12671267
"IDLE_FOR": "You have been idle for:",
12681268
"TASK": "Task",
12691269
"TRACK_TO": "Track to:",
1270-
"UNTRACKED_TIME": "Untracked time:"
1270+
"UNTRACKED_TIME": "Untracked time:",
1271+
"NOTIFICATION_TITLE": "Track your time!"
12711272
}
12721273
},
12731274
"WORKLOG": {
@@ -1548,6 +1549,7 @@
15481549
},
15491550
"SOUND": {
15501551
"BREAK_REMINDER_SOUND": "Take a break reminder sound",
1552+
"TRACK_TIME_SOUND": "Track time reminder sound",
15511553
"DONE_SOUND": "Task marked done sound",
15521554
"IS_INCREASE_DONE_PITCH": "Increase pitch for every task done",
15531555
"TITLE": "Sound",
@@ -1578,7 +1580,9 @@
15781580
"L_IS_TRACKING_REMINDER_SHOW_ON_MOBILE": "Show tracking reminder on mobile app",
15791581
"L_TRACKING_INTERVAL": "Time tracking interval (EXPERIMENTAL)",
15801582
"L_TRACKING_REMINDER_MIN_TIME": "Time to wait before showing tracking reminder Banner",
1581-
"TITLE": "Time Tracking"
1583+
"TITLE": "Time Tracking",
1584+
"L_IS_TRACKING_REMINDER_NOTIFY": "Notify when time tracking reminder is shown",
1585+
"L_IS_TRACKING_REMINDER_FOCUS_WINDOW": "Focus app window when reminder is active (desktop only)"
15821586
}
15831587
},
15841588
"GLOBAL_SNACK": {

0 commit comments

Comments
 (0)