mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-01-23 02:36:05 +00:00
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:
commit
2fcd310ea1
21 changed files with 849 additions and 61 deletions
|
|
@ -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
|
||||
|
|
|
|||
4
electron/electronAPI.d.ts
vendored
4
electron/electronAPI.d.ts
vendored
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
19
electron/overlay-indicator/overlay-api.d.ts
vendored
Normal file
19
electron/overlay-indicator/overlay-api.d.ts
vendored
Normal 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 {};
|
||||
293
electron/overlay-indicator/overlay-indicator.ts
Normal file
293
electron/overlay-indicator/overlay-indicator.ts
Normal 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')}`;
|
||||
};
|
||||
13
electron/overlay-indicator/overlay-preload.ts
Normal file
13
electron/overlay-indicator/overlay-preload.ts
Normal 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));
|
||||
},
|
||||
});
|
||||
158
electron/overlay-indicator/overlay.css
Normal file
158
electron/overlay-indicator/overlay.css
Normal 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%;
|
||||
}
|
||||
26
electron/overlay-indicator/overlay.html
Normal file
26
electron/overlay-indicator/overlay.html
Normal 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>
|
||||
67
electron/overlay-indicator/overlay.ts
Normal file
67
electron/overlay-indicator/overlay.ts
Normal 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 || '--:--';
|
||||
});
|
||||
|
|
@ -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),
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ export const DEFAULT_GLOBAL_CONFIG: GlobalConfigState = {
|
|||
|
||||
**Why do I want it?**
|
||||
`,
|
||||
isOverlayIndicatorEnabled: false,
|
||||
},
|
||||
shortSyntax: {
|
||||
isEnableProject: true,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ export type MiscConfig = Readonly<{
|
|||
// optional because it was added later
|
||||
isShowTipLonger?: boolean;
|
||||
isTrayShowCurrentCountdown?: boolean;
|
||||
isOverlayIndicatorEnabled?: boolean;
|
||||
}>;
|
||||
|
||||
export type ShortSyntaxConfig = Readonly<{
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue