Merge branch 'feat/overlay-indicator'

* feat/overlay-indicator:
  feat(overlayIndicator): implement basic overlay

# Conflicts:
#	electron/shared-with-frontend/ipc-events.const.ts
This commit is contained in:
Johannes Millan 2025-06-27 22:37:14 +02:00
commit 2fcd310ea1
21 changed files with 849 additions and 61 deletions

View file

@ -93,6 +93,11 @@ export const initDebug = (opts: any, isAddReload: boolean): void => {
}
app.on('browser-window-created', (event, win) => {
// Skip dev tools for overlay window
if (win.title === 'Super Productivity Overlay') {
return;
}
if (opts.showDevTools) {
win.webContents.once('devtools-opened', () => {
// Workaround for https://github.com/electron/electron/issues/13095

View file

@ -103,6 +103,8 @@ export interface ElectronAPI {
sendAppSettingsToElectron(globalCfg: GlobalConfigState): void;
sendSettingsUpdate(globalCfg: GlobalConfigState): void;
registerGlobalShortcuts(keyboardConfig: KeyboardConfig): void;
showFullScreenBlocker(args: { msg?: string; takeABreakCfg: TakeABreakConfig }): void;
@ -123,6 +125,8 @@ export interface ElectronAPI {
task: Task | null,
isPomodoroEnabled: boolean,
currentPomodoroSessionTime: number,
isFocusModeEnabled?: boolean,
currentFocusSessionTime?: number,
);
exec(command: string): void;

View file

@ -5,6 +5,11 @@ import { getWin } from './main-window';
import { GlobalConfigState } from '../src/app/features/config/global-config.model';
import { TaskCopy } from '../src/app/features/tasks/task.model';
import { release } from 'os';
import {
initOverlayIndicator,
updateOverlayEnabled,
updateOverlayTask,
} from './overlay-indicator/overlay-indicator';
let tray: Tray;
let DIR: string;
@ -43,6 +48,7 @@ export const initIndicator = ({
tray.on('click', () => {
showApp();
});
return tray;
};
@ -59,6 +65,23 @@ function initAppListeners(app: App): void {
// eslint-disable-next-line prefer-arrow/prefer-arrow-functions
function initListeners(): void {
let isOverlayEnabled = false;
// Listen for settings updates to handle overlay enable/disable
ipcMain.on(IPC.UPDATE_SETTINGS, (ev, settings: GlobalConfigState) => {
const isOverlayEnabledNew = settings?.misc?.isOverlayIndicatorEnabled || false;
if (isOverlayEnabledNew === isOverlayEnabled) {
return;
}
isOverlayEnabled = isOverlayEnabledNew;
updateOverlayEnabled(isOverlayEnabled);
// Initialize overlay without shortcut (overlay doesn't need shortcut, that's for focus mode)
if (isOverlayEnabled) {
initOverlayIndicator(isOverlayEnabled);
}
});
ipcMain.on(IPC.SET_PROGRESS_BAR, (ev, { progress }) => {
const suf = shouldUseDarkColors ? '-d' : '-l';
if (typeof progress === 'number' && progress > 0 && isFinite(progress)) {
@ -73,7 +96,22 @@ function initListeners(): void {
ipcMain.on(
IPC.CURRENT_TASK_UPDATED,
(ev, currentTask, isPomodoroEnabled, currentPomodoroSessionTime) => {
(
ev,
currentTask,
isPomodoroEnabled,
currentPomodoroSessionTime,
isFocusModeEnabled,
currentFocusSessionTime,
) => {
updateOverlayTask(
currentTask,
isPomodoroEnabled,
currentPomodoroSessionTime,
isFocusModeEnabled || false,
currentFocusSessionTime || 0,
);
const mainWin = getWin();
getSettings(mainWin, (settings: GlobalConfigState) => {
const isTrayShowCurrentTask = settings.misc.isTrayShowCurrentTask;

View file

@ -18,6 +18,10 @@ import { readFileSync, stat } from 'fs';
import { error, log } from 'electron-log/main';
import { GlobalConfigState } from '../src/app/features/config/global-config.model';
import { IS_MAC } from './common.const';
import {
showOverlayWindow,
hideOverlayWindow,
} from './overlay-indicator/overlay-indicator';
let mainWin: BrowserWindow;
@ -209,6 +213,24 @@ function initWinEventListeners(app: any): void {
// TODO refactor quitting mess
appCloseHandler(app);
appMinimizeHandler(app);
// Handle restore and show events to hide overlay
mainWin.on('restore', () => {
hideOverlayWindow();
});
mainWin.on('show', () => {
hideOverlayWindow();
});
mainWin.on('focus', () => {
hideOverlayWindow();
});
// Handle hide event to show overlay
mainWin.on('hide', () => {
showOverlayWindow();
});
}
// eslint-disable-next-line prefer-arrow/prefer-arrow-functions
@ -283,6 +305,7 @@ const appCloseHandler = (app: App): void => {
getSettings(mainWin, (appCfg: GlobalConfigState) => {
if (appCfg && appCfg.misc.isMinimizeToTray && !(app as any).isQuiting) {
mainWin.hide();
showOverlayWindow();
return;
}
@ -316,8 +339,13 @@ const appMinimizeHandler = (app: App): void => {
if (appCfg.misc.isMinimizeToTray) {
event.preventDefault();
mainWin.hide();
} else if (IS_MAC) {
app.dock.show();
showOverlayWindow();
} else {
// For regular minimize (not to tray), also show overlay
showOverlayWindow();
if (IS_MAC) {
app.dock.show();
}
}
});
});

View file

@ -0,0 +1,19 @@
interface OverlayContentData {
title: string;
time: string;
mode: 'pomodoro' | 'focus' | 'task' | 'idle';
}
interface OverlayAPI {
setIgnoreMouseEvents: (ignore: boolean) => void;
showMainWindow: () => void;
onUpdateContent: (callback: (data: OverlayContentData) => void) => void;
}
declare global {
interface Window {
overlayAPI: OverlayAPI;
}
}
export {};

View file

@ -0,0 +1,293 @@
import { BrowserWindow, ipcMain, screen, globalShortcut } from 'electron';
import { join } from 'path';
import { TaskCopy } from '../../src/app/features/tasks/task.model';
import { info } from 'electron-log/main';
import { IPC } from '../shared-with-frontend/ipc-events.const';
let overlayWindow: BrowserWindow | null = null;
let isOverlayEnabled = false;
let isOverlayVisible = true;
let currentTask: TaskCopy | null = null;
let isPomodoroEnabled = false;
let currentPomodoroSessionTime = 0;
let isFocusModeEnabled = false;
let currentFocusSessionTime = 0;
export const initOverlayIndicator = (enabled: boolean, shortcut?: string): void => {
isOverlayEnabled = enabled;
if (enabled) {
createOverlayWindow();
if (shortcut) {
registerShortcut(shortcut);
}
initListeners();
// Show overlay and request current task state
setTimeout(() => {
const mainWindow = BrowserWindow.getAllWindows().find(
(win) => win !== overlayWindow,
);
if (mainWindow) {
// Request current task state
mainWindow.webContents.send(IPC.REQUEST_CURRENT_TASK_FOR_OVERLAY);
}
// Always show overlay when initialized
showOverlayWindow();
}, 100);
}
};
export const updateOverlayEnabled = (isEnabled: boolean): void => {
isOverlayEnabled = isEnabled;
if (isEnabled && !overlayWindow) {
createOverlayWindow();
initListeners();
// Show overlay and request current task state immediately when enabling
setTimeout(() => {
const mainWindow = BrowserWindow.getAllWindows().find(
(win) => win !== overlayWindow,
);
if (mainWindow) {
mainWindow.webContents.send(IPC.REQUEST_CURRENT_TASK_FOR_OVERLAY);
}
// Always show overlay when enabled
showOverlayWindow();
}, 100);
} else if (!isEnabled && overlayWindow) {
destroyOverlayWindow();
}
};
export const updateOverlayTheme = (isDarkTheme: boolean): void => {
// No longer needed - using prefers-color-scheme CSS
};
export const destroyOverlayWindow = (): void => {
if (overlayWindow) {
overlayWindow.removeAllListeners('close');
overlayWindow.destroy();
overlayWindow = null;
}
};
const createOverlayWindow = (): void => {
if (overlayWindow) {
return;
}
const primaryDisplay = screen.getPrimaryDisplay();
const { width } = primaryDisplay.workAreaSize;
overlayWindow = new BrowserWindow({
width: 300,
height: 80,
x: width - 320,
y: 20,
title: 'Super Productivity Overlay',
frame: false,
transparent: true,
alwaysOnTop: true,
skipTaskbar: true,
// resizable: false,
minimizable: false,
maximizable: false,
hasShadow: false, // Disable shadow with transparent windows
autoHideMenuBar: true,
roundedCorners: false, // Disable rounded corners for better compatibility
webPreferences: {
preload: join(__dirname, 'overlay-preload.js'),
contextIsolation: true,
nodeIntegration: false,
disableDialogs: true,
webSecurity: true,
allowRunningInsecureContent: false,
},
});
overlayWindow.loadFile(join(__dirname, 'overlay.html'));
// Set visible on all workspaces immediately after creation
overlayWindow.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
overlayWindow.on('closed', () => {
overlayWindow = null;
});
overlayWindow.on('ready-to-show', () => {
// Show the overlay window
isOverlayVisible = true;
overlayWindow.show();
// Ensure window stays on all workspaces
overlayWindow.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
// Request current task state from main window
const mainWindow = BrowserWindow.getAllWindows().find((win) => win !== overlayWindow);
if (mainWindow) {
mainWindow.webContents.send(IPC.REQUEST_CURRENT_TASK_FOR_OVERLAY);
}
});
// Prevent context menu on right-click to avoid crashes
overlayWindow.webContents.on('context-menu', (e) => {
e.preventDefault();
});
// Prevent any window system menu
overlayWindow.on('system-context-menu', (e) => {
e.preventDefault();
});
// Prevent window close attempts that might cause issues
overlayWindow.on('close', (e) => {
if (isOverlayEnabled) {
e.preventDefault();
overlayWindow.hide();
isOverlayVisible = false;
}
});
// Don't make window click-through initially to allow dragging
// The renderer process will handle mouse events dynamically
// Update initial state
updateOverlayContent();
};
const registerShortcut = (shortcut: string): void => {
globalShortcut.register(shortcut, () => {
toggleOverlayVisibility();
});
};
const toggleOverlayVisibility = (): void => {
if (!overlayWindow) {
return;
}
isOverlayVisible = !isOverlayVisible;
if (isOverlayVisible) {
overlayWindow.show();
} else {
overlayWindow.hide();
}
};
export const showOverlayWindow = (): void => {
if (!overlayWindow || !isOverlayEnabled) {
info(
'Overlay show skipped: window=' + !!overlayWindow + ', enabled=' + isOverlayEnabled,
);
return;
}
// Only show if not already visible
if (!overlayWindow.isVisible()) {
info('Showing overlay window');
isOverlayVisible = true;
overlayWindow.show();
} else {
info('Overlay already visible');
}
};
export const hideOverlayWindow = (): void => {
if (!overlayWindow || !isOverlayEnabled) {
info(
'Overlay hide skipped: window=' + !!overlayWindow + ', enabled=' + isOverlayEnabled,
);
return;
}
// Only hide if currently visible
if (overlayWindow.isVisible()) {
info('Hiding overlay window');
isOverlayVisible = false;
overlayWindow.hide();
} else {
info('Overlay already hidden');
}
};
const initListeners = (): void => {
// Listen for show main window request
ipcMain.on('overlay-show-main-window', () => {
const mainWindow = BrowserWindow.getAllWindows().find((win) => win !== overlayWindow);
if (mainWindow) {
mainWindow.show();
mainWindow.focus();
hideOverlayWindow();
}
});
};
export const updateOverlayTask = (
task: TaskCopy | null,
pomodoroEnabled: boolean,
pomodoroTime: number,
focusModeEnabled: boolean,
focusTime: number,
): void => {
currentTask = task;
isPomodoroEnabled = pomodoroEnabled;
currentPomodoroSessionTime = pomodoroTime;
isFocusModeEnabled = focusModeEnabled;
currentFocusSessionTime = focusTime;
updateOverlayContent();
};
const updateOverlayContent = (): void => {
if (!overlayWindow || !isOverlayEnabled) {
return;
}
let title = '';
let timeStr = '';
let mode: 'pomodoro' | 'focus' | 'task' | 'idle' = 'idle';
if (currentTask && currentTask.title) {
title = currentTask.title;
if (title.length > 40) {
title = title.substring(0, 37) + '...';
}
if (isPomodoroEnabled) {
mode = 'pomodoro';
timeStr = formatTime(currentPomodoroSessionTime);
} else if (isFocusModeEnabled) {
mode = 'focus';
timeStr = formatTime(currentFocusSessionTime);
} else if (currentTask.timeEstimate) {
mode = 'task';
const remainingTime = Math.max(currentTask.timeEstimate - currentTask.timeSpent, 0);
timeStr = formatTime(remainingTime);
} else if (currentTask.timeSpent) {
mode = 'task';
timeStr = formatTime(currentTask.timeSpent);
}
}
overlayWindow.webContents.send('update-content', {
title,
time: timeStr,
mode,
});
};
const formatTime = (timeMs: number): string => {
const totalSeconds = Math.floor(timeMs / 1000);
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
if (hours > 0) {
return `${hours}:${minutes.toString().padStart(2, '0')}:${seconds
.toString()
.padStart(2, '0')}`;
}
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
};

View file

@ -0,0 +1,13 @@
import { contextBridge, ipcRenderer } from 'electron';
contextBridge.exposeInMainWorld('overlayAPI', {
setIgnoreMouseEvents: (ignore: boolean) => {
ipcRenderer.send('overlay-set-ignore-mouse', ignore);
},
showMainWindow: () => {
ipcRenderer.send('overlay-show-main-window');
},
onUpdateContent: (callback: (data: any) => void) => {
ipcRenderer.on('update-content', (event, data) => callback(data));
},
});

View file

@ -0,0 +1,158 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
overflow: hidden;
background: transparent;
user-select: none;
}
/* Prevent all auxiliary button interactions */
body,
body * {
pointer-events: auto;
}
/* Disable right-click context menu globally */
body {
-webkit-context-menu: none;
}
#overlay-container {
display: flex;
align-items: center;
height: 100vh;
padding: 8px;
background-color: rgba(255, 255, 255, 0.95);
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
transition:
background-color 0.3s,
color 0.3s;
-webkit-app-region: drag;
}
@media (prefers-color-scheme: dark) {
#overlay-container {
background-color: rgba(32, 32, 32, 0.95);
color: #e0e0e0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
}
}
#content {
flex: 1;
padding: 0 16px;
min-width: 0;
display: flex;
justify-content: center;
align-content: center;
}
#task-title {
font-size: 16px;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-bottom: 4px;
color: #333;
}
@media (prefers-color-scheme: dark) {
#task-title {
color: #e0e0e0;
}
}
#time-display {
font-size: 16px;
font-weight: 600;
font-variant-numeric: tabular-nums;
color: #666;
margin-left: auto;
padding-left: 16px;
}
@media (prefers-color-scheme: dark) {
#time-display {
color: #aaa;
}
}
/* Mode-specific colors */
.mode-pomodoro #time-display {
color: #dc3545;
}
.mode-focus #time-display {
color: #007bff;
}
.mode-task #time-display {
color: #28a745;
}
@media (prefers-color-scheme: dark) {
.mode-pomodoro #time-display {
color: #ff6b7a;
}
.mode-focus #time-display {
color: #5cb3ff;
}
.mode-task #time-display {
color: #5dd66f;
}
}
#show-main {
width: 32px;
height: 32px;
border: none;
background: rgba(0, 0, 0, 0.05);
border-radius: 4px;
cursor: pointer;
color: #666;
font-size: 12px;
transition: all 0.2s;
-webkit-app-region: no-drag;
}
@media (prefers-color-scheme: dark) {
#show-main {
background: rgba(255, 255, 255, 0.1);
color: #aaa;
}
}
#show-main:hover {
background: rgba(0, 0, 0, 0.1);
transform: scale(1.1);
}
@media (prefers-color-scheme: dark) {
#show-main:hover {
background: rgba(255, 255, 255, 0.2);
}
}
#show-main:active {
transform: scale(0.95);
}
/* Handle resize cursor on edges */
body {
resize: both;
}
/* Make sure content doesn't overflow on resize */
#overlay-container {
width: 100%;
height: 100%;
}

View file

@ -0,0 +1,26 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<title>Super Productivity Overlay</title>
<link
rel="stylesheet"
href="overlay.css"
/>
</head>
<body>
<div id="overlay-container">
<div id="content">
<div id="task-title">No active task</div>
<div id="time-display">--:--</div>
</div>
<button
id="show-main"
title="Show main window"
>
</button>
</div>
<script src="overlay.js"></script>
</body>
</html>

View file

@ -0,0 +1,67 @@
// Get elements
const showMainBtn = document.getElementById('show-main') as HTMLButtonElement;
const container = document.getElementById('overlay-container') as HTMLDivElement;
const taskTitle = document.getElementById('task-title') as HTMLDivElement;
const timeDisplay = document.getElementById('time-display') as HTMLDivElement;
// Prevent all right-click events
document.addEventListener(
'contextmenu',
(e) => {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
return false;
},
true,
);
// Also prevent mousedown for right-click
document.addEventListener(
'mousedown',
(e) => {
if (e.button === 2) {
// Right mouse button
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
return false;
}
},
true,
);
// Prevent mouseup for right-click
document.addEventListener(
'mouseup',
(e) => {
if (e.button === 2) {
// Right mouse button
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
return false;
}
},
true,
);
// Handle show main button
showMainBtn.addEventListener('click', () => {
window.overlayAPI.showMainWindow();
});
// Listen for content updates
window.overlayAPI.onUpdateContent((data) => {
// Clear existing mode classes
container.classList.remove('mode-pomodoro', 'mode-focus', 'mode-task', 'mode-idle');
// Update mode
if (data.mode) {
container.classList.add(`mode-${data.mode}`);
}
// Update content
taskTitle.textContent = data.title || 'No active task';
timeDisplay.textContent = data.time || '--:--';
});

View file

@ -83,6 +83,7 @@ const ea: ElectronAPI = {
sendAppSettingsToElectron: (globalCfg) =>
_send('TRANSFER_SETTINGS_TO_ELECTRON', globalCfg),
sendSettingsUpdate: (globalCfg) => _send('UPDATE_SETTINGS', globalCfg),
registerGlobalShortcuts: (keyboardCfg) =>
_send('REGISTER_GLOBAL_SHORTCUTS', keyboardCfg),
showFullScreenBlocker: (args) => _send('FULL_SCREEN_BLOCKER', args),
@ -92,8 +93,21 @@ const ea: ElectronAPI = {
backupAppData: (appData) => _send('BACKUP', appData),
updateCurrentTask: (task, isPomodoroEnabled, currentPomodoroSessionTime) =>
_send('CURRENT_TASK_UPDATED', task, isPomodoroEnabled, currentPomodoroSessionTime),
updateCurrentTask: (
task,
isPomodoroEnabled,
currentPomodoroSessionTime,
isFocusModeEnabled?,
currentFocusSessionTime?,
) =>
_send(
'CURRENT_TASK_UPDATED',
task,
isPomodoroEnabled,
currentPomodoroSessionTime,
isFocusModeEnabled,
currentFocusSessionTime,
),
exec: (command: string) => _send('EXEC', command),

View file

@ -18,6 +18,7 @@ export enum IPC {
APP_READY = 'APP_READY',
ERROR = 'ELECTRON_ERROR',
CURRENT_TASK_UPDATED = 'CURRENT_TASK_UPDATED',
REQUEST_CURRENT_TASK_FOR_OVERLAY = 'REQUEST_CURRENT_TASK_FOR_OVERLAY',
TASK_MARK_AS_DONE = 'TASK_MARK_AS_DONE',
TASK_START = 'TASK_START',
TASK_TOGGLE_START = 'TASK_TOGGLE_START',
@ -72,6 +73,8 @@ export enum IPC {
// Plugin Node Execution
PLUGIN_EXEC_NODE_SCRIPT = 'PLUGIN_EXEC_NODE_SCRIPT',
UPDATE_SETTINGS = 'UPDATE_SETTINGS',
// maybe_UPDATE_CURRENT_TASK = 'UPDATE_CURRENT_TASK',
// maybe_IS_IDLE = 'IS_IDLE',
// maybe_IS_BUSY = 'IS_BUSY',

View file

@ -21,6 +21,7 @@ import { initIndicator } from './indicator';
import { quitApp, showOrFocus } from './various-shared';
import { createWindow } from './main-window';
import { IdleTimeHandler } from './idle-time-handler';
import { destroyOverlayWindow } from './overlay-indicator/overlay-indicator';
const ICONS_FOLDER = __dirname + '/assets/icons/';
const IS_MAC = process.platform === 'darwin';
@ -253,6 +254,11 @@ export const startApp = (): void => {
globalShortcut.unregisterAll();
});
appIN.on('before-quit', () => {
// Clean up overlay window before quitting
destroyOverlayWindow();
});
appIN.on('window-all-closed', () => {
log('Quit after all windows being closed');
// if (!IS_MAC) {

View file

@ -1,6 +1,7 @@
import { app, BrowserWindow } from 'electron';
import { info } from 'electron-log/main';
import { getWin } from './main-window';
import { hideOverlayWindow } from './overlay-indicator/overlay-indicator';
// eslint-disable-next-line prefer-arrow/prefer-arrow-functions
export function quitApp(): void {
@ -28,6 +29,9 @@ export function showOrFocus(passedWin: BrowserWindow): void {
win.show();
}
// Hide overlay when main window is shown
hideOverlayWindow();
// focus window afterwards always
setTimeout(() => {
win.focus();

View file

@ -31,6 +31,7 @@ export const DEFAULT_GLOBAL_CONFIG: GlobalConfigState = {
**Why do I want it?**
`,
isOverlayIndicatorEnabled: false,
},
shortSyntax: {
isEnableProject: true,

View file

@ -146,5 +146,13 @@ export const MISC_SETTINGS_FORM_CFG: ConfigFormSection<MiscConfig> = {
label: T.GCF.MISC.IS_TRAY_SHOW_CURRENT_COUNTDOWN,
},
},
{
key: 'isOverlayIndicatorEnabled',
type: 'checkbox',
templateOptions: {
label: T.GCF.MISC.IS_OVERLAY_INDICATOR_ENABLED,
},
hideExpression: (model: any) => !model?.isTrayShowCurrentTask,
},
],
};

View file

@ -22,6 +22,7 @@ export type MiscConfig = Readonly<{
// optional because it was added later
isShowTipLonger?: boolean;
isTrayShowCurrentCountdown?: boolean;
isOverlayIndicatorEnabled?: boolean;
}>;
export type ShortSyntaxConfig = Readonly<{

View file

@ -1,6 +1,6 @@
import { inject, Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { filter, pairwise, tap } from 'rxjs/operators';
import { filter, pairwise, tap, withLatestFrom } from 'rxjs/operators';
import { Store } from '@ngrx/store';
import { IS_ELECTRON, LanguageCode } from '../../../app.constants';
import { T } from '../../../t.const';
@ -145,4 +145,34 @@ export class GlobalConfigEffects {
),
{ dispatch: false },
);
notifyElectronAboutCfgChange: any =
IS_ELECTRON &&
createEffect(
() =>
this._actions$.pipe(
ofType(updateGlobalConfigSection),
withLatestFrom(this._store.select('globalConfig')),
tap(([action, globalConfig]) => {
// Send the entire settings object to electron for overlay initialization
window.ea.sendSettingsUpdate(globalConfig);
}),
),
{ dispatch: false },
);
notifyElectronAboutCfgChangeInitially: any =
IS_ELECTRON &&
createEffect(
() =>
this._actions$.pipe(
ofType(loadAllData),
tap(({ appDataComplete }) => {
const cfg = appDataComplete.globalConfig || DEFAULT_GLOBAL_CONFIG;
// Send initial settings to electron for overlay initialization
window.ea.sendSettingsUpdate(cfg);
}),
),
{ dispatch: false },
);
}

View file

@ -2,13 +2,23 @@ import { inject, Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { setCurrentTask, unsetCurrentTask } from './task.actions';
import { select, Store } from '@ngrx/store';
import { filter, tap, withLatestFrom } from 'rxjs/operators';
import { filter, startWith, take, tap, withLatestFrom } from 'rxjs/operators';
import { selectCurrentTask } from './task.selectors';
import { IS_ELECTRON } from '../../../app.constants';
import { GlobalConfigService } from '../../config/global-config.service';
import { selectIsFocusOverlayShown } from '../../focus-mode/store/focus-mode.selectors';
import { PomodoroService } from '../../pomodoro/pomodoro.service';
import { TimeTrackingActions } from '../../time-tracking/store/time-tracking.actions';
import { FocusModeService } from '../../focus-mode/focus-mode.service';
import {
cancelFocusSession,
focusSessionDone,
hideFocusOverlay,
pauseFocusSession,
showFocusOverlay,
startFocusSession,
unPauseFocusSession,
} from '../../focus-mode/store/focus-mode.actions';
import { IPC } from '../../../../../electron/shared-with-frontend/ipc-events.const';
// TODO send message to electron when current task changes here
@ -18,71 +28,129 @@ export class TaskElectronEffects {
private _store$ = inject<Store<any>>(Store);
private _configService = inject(GlobalConfigService);
private _pomodoroService = inject(PomodoroService);
private _focusModeService = inject(FocusModeService);
taskChangeElectron$: any = createEffect(
() =>
this._actions$.pipe(
ofType(setCurrentTask, unsetCurrentTask, TimeTrackingActions.addTimeSpent),
withLatestFrom(this._store$.pipe(select(selectCurrentTask))),
withLatestFrom(
this._store$.pipe(select(selectCurrentTask)),
this._pomodoroService.isEnabled$,
this._pomodoroService.currentSessionTime$,
),
tap(([action, current, isPomodoroEnabled, currentPomodoroSessionTime]) => {
if (IS_ELECTRON) {
// -----------------------------------------------------------------------------------
// NOTE: IS_ELECTRON checks not necessary, since we check before importing this module
// -----------------------------------------------------------------------------------
constructor() {
// Listen for overlay request and send current task state
window.ea.on(IPC.REQUEST_CURRENT_TASK_FOR_OVERLAY, () => {
this._store$
.pipe(
select(selectCurrentTask),
withLatestFrom(
this._pomodoroService.isEnabled$,
this._pomodoroService.currentSessionTime$,
this._store$.pipe(select(selectIsFocusOverlayShown)),
this._focusModeService.currentSessionTime$,
),
// Only take the first value and complete
take(1),
)
.subscribe(
([
current,
isPomodoroEnabled,
currentPomodoroSessionTime,
isFocusModeEnabled,
currentFocusSessionTime,
]) => {
window.ea.updateCurrentTask(
current,
isPomodoroEnabled,
currentPomodoroSessionTime,
isFocusModeEnabled,
currentFocusSessionTime,
);
},
);
});
}
taskChangeElectron$: any = createEffect(
() =>
this._actions$.pipe(
ofType(
setCurrentTask,
unsetCurrentTask,
TimeTrackingActions.addTimeSpent,
showFocusOverlay,
hideFocusOverlay,
startFocusSession,
cancelFocusSession,
pauseFocusSession,
unPauseFocusSession,
focusSessionDone,
),
withLatestFrom(
this._store$.pipe(select(selectCurrentTask)),
this._pomodoroService.isEnabled$,
this._pomodoroService.currentSessionTime$,
this._store$.pipe(select(selectIsFocusOverlayShown)),
this._focusModeService.currentSessionTime$.pipe(startWith(0)),
),
tap(
([
action,
current,
isPomodoroEnabled,
currentPomodoroSessionTime,
isFocusModeEnabled,
currentFocusSessionTime,
]) => {
window.ea.updateCurrentTask(
current,
isPomodoroEnabled,
currentPomodoroSessionTime,
isFocusModeEnabled,
currentFocusSessionTime,
);
},
),
),
{ dispatch: false },
);
setTaskBarNoProgress$ = createEffect(
() =>
this._actions$.pipe(
ofType(setCurrentTask),
tap(({ id }) => {
if (!id) {
window.ea.setProgressBar({
progress: 0,
progressBarMode: 'pause',
});
}
}),
),
{ dispatch: false },
);
setTaskBarNoProgress$ =
IS_ELECTRON &&
createEffect(
() =>
this._actions$.pipe(
ofType(setCurrentTask),
tap(({ id }) => {
if (!id) {
window.ea.setProgressBar({
progress: 0,
progressBarMode: 'pause',
});
}
}),
setTaskBarProgress$: any = createEffect(
() =>
this._actions$.pipe(
ofType(TimeTrackingActions.addTimeSpent),
withLatestFrom(
this._configService.cfg$,
this._store$.select(selectIsFocusOverlayShown),
),
{ dispatch: false },
);
setTaskBarProgress$: any =
IS_ELECTRON &&
createEffect(
() =>
this._actions$.pipe(
ofType(TimeTrackingActions.addTimeSpent),
withLatestFrom(
this._configService.cfg$,
this._store$.select(selectIsFocusOverlayShown),
),
// we display pomodoro progress for pomodoro
filter(
([a, cfg, isFocusSessionRunning]) =>
!isFocusSessionRunning && (!cfg || !cfg.pomodoro.isEnabled),
),
tap(([{ task }]) => {
const progress = task.timeSpent / task.timeEstimate;
window.ea.setProgressBar({
progress,
progressBarMode: 'normal',
});
}),
// we display pomodoro progress for pomodoro
filter(
([a, cfg, isFocusSessionRunning]) =>
!isFocusSessionRunning && (!cfg || !cfg.pomodoro.isEnabled),
),
{ dispatch: false },
);
tap(([{ task }]) => {
const progress = task.timeSpent / task.timeEstimate;
window.ea.setProgressBar({
progress,
progressBarMode: 'normal',
});
}),
),
{ dispatch: false },
);
}

View file

@ -1654,6 +1654,7 @@ const T = {
IS_DISABLE_ANIMATIONS: 'GCF.MISC.IS_DISABLE_ANIMATIONS',
IS_HIDE_NAV: 'GCF.MISC.IS_HIDE_NAV',
IS_MINIMIZE_TO_TRAY: 'GCF.MISC.IS_MINIMIZE_TO_TRAY',
IS_OVERLAY_INDICATOR_ENABLED: 'GCF.MISC.IS_OVERLAY_INDICATOR_ENABLED',
IS_SHOW_TIP_LONGER: 'GCF.MISC.IS_SHOW_TIP_LONGER',
IS_TRAY_SHOW_CURRENT_COUNTDOWN: 'GCF.MISC.IS_TRAY_SHOW_CURRENT_COUNTDOWN',
IS_TRAY_SHOW_CURRENT_TASK: 'GCF.MISC.IS_TRAY_SHOW_CURRENT_TASK',

View file

@ -1631,6 +1631,7 @@
"IS_SHOW_TIP_LONGER": "Show productivity tip on app start a little longer",
"IS_TRAY_SHOW_CURRENT_COUNTDOWN": "Show current countdown in the tray / status menu (desktop mac only)",
"IS_TRAY_SHOW_CURRENT_TASK": "Show current task in the tray / status menu (desktop mac/windows only)",
"IS_OVERLAY_INDICATOR_ENABLED": "Enable overlay indicator window (desktop linux/gnome)",
"IS_TURN_OFF_MARKDOWN": "Turn off markdown parsing for task notes",
"IS_USE_MINIMAL_SIDE_NAV": "Use minimal navigation bar (show only icons)",
"START_OF_NEXT_DAY": "Start time of the next day",