diff --git a/package.json b/package.json index 8f93d6f..d350f40 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "deepFocus", "productName": "Deep Focus", - "version": "2.4.4", + "version": "2.4.5", "main": ".vite/build/main.js", "scripts": { "start": "DEBUG=electron* electron-forge start", diff --git a/src/main.ts b/src/main.ts index b0f0bff..b99a622 100644 --- a/src/main.ts +++ b/src/main.ts @@ -16,14 +16,7 @@ import fs from 'fs' import schedule from 'node-schedule' import dotenv from 'dotenv' import Store from 'electron-store' -import { - StoreSchema, - SiteTimeTracker, - DeepWorkHours, - MessageType, - User, - AppIcon, -} from './types' +import { StoreSchema, SiteTimeTracker, DeepWorkHours, MessageType, User, AppIcon } from './types' import { updateSiteTimeTracker, getBrowserURL, @@ -47,7 +40,7 @@ export interface TypedStore extends Store { } const store = new Store() as TypedStore -let currentSiteTimeTrackers: SiteTimeTracker[] = [] +let currentSiteTimeTrackers: SiteTimeTracker[] = store.get('siteTimeTrackers', []) let monitoringInterval: NodeJS.Timeout | null = null let deepWorkHours = { Monday: 0, @@ -60,7 +53,7 @@ let deepWorkHours = { } as DeepWorkHours let currentDeepWork = 0 -let user: User | null = null +let user: User | null = store.get('user', null) // change back to null let iconPath = '' let mainWindow: BrowserWindow | null = null let tray: Tray | null = null @@ -72,22 +65,19 @@ const resourcesPath: string = setupEnvironment() log.info('Set up Environment') // Initialize environment variables based on the environment -function setupEnvironment() { +function setupEnvironment(): string { try { log.info('Setting up environment...') - // Initialize resourcesPath within this function const resourcesPath = app.isPackaged ? path.join(process.resourcesPath) : path.join(__dirname, 'resources') log.info('app.isPackaged:', app.isPackaged) log.info('resourcesPath:', resourcesPath) - // Set up the path to the .env file const envPath = path.join(resourcesPath, '.env') log.info('Looking for .env at:', envPath) - // Load environment variables from the .env file if it exists if (fs.existsSync(envPath)) { dotenv.config({ path: envPath }) log.info('Loaded .env file from:', envPath) @@ -119,7 +109,7 @@ export function handleUserData(user: User, store: TypedStore): User { } // Load user data if available on app start -export function loadUserData() { +export function loadUserData(): User | null { const savedUser: User | null = store.get('user') || null if (savedUser) { schedulerWorker.postMessage({ type: MessageType.SET_USER_INFO, user: savedUser }) @@ -136,23 +126,24 @@ export function loadUserData() { return savedUser } -async function storeData() { +async function storeData(): Promise { const today = dayjs().format('dddd') as keyof typeof deepWorkHours log.info( 'Periodic save triggered (updating siteTimeTrackers, deepWorkHours, currentDeepWork and icon): ' ) + log.info('Loaded API_BASE_URL:', process.env.VITE_SERVER_URL_PROD) store.set('siteTimeTrackers', currentSiteTimeTrackers) store.set('deepWorkHours', deepWorkHours) - currentDeepWork = deepWorkHours[today] || 0 - deepWorkHours[today] = 0 } -export async function resetCounters(type: 'daily' | 'weekly') { +export async function resetCounters(type: 'daily' | 'weekly'): Promise { const now = dayjs() log.info('Invoked resetCounters') stopActivityMonitoring() + log.info('Loaded API_BASE_URL:', process.env.VITE_SERVER_URL_PROD) + checkAndSendMissedEmails() if (type === 'daily') { currentSiteTimeTrackers?.forEach((tracker) => { @@ -188,7 +179,7 @@ export async function resetCounters(type: 'daily' | 'weekly') { } // Periodic saving of time trackers, deep work hours, and icon progress every 2 minutes -function setupPeriodicSave() { +function setupPeriodicSave(): void { if (!monitoringInterval) { setInterval( () => { @@ -212,7 +203,7 @@ function setupPeriodicSave() { } } -export function startActivityMonitoring() { +export function startActivityMonitoring(): void { if (!monitoringInterval) { const today = dayjs() monitoringInterval = setInterval(async () => { @@ -255,7 +246,7 @@ export function startActivityMonitoring() { } // Handles both periodicSave & activity monitoring -function stopActivityMonitoring() { +function stopActivityMonitoring(): void { if (monitoringInterval) { clearInterval(monitoringInterval) // Clear the interval monitoringInterval = null // Reset the interval ID @@ -345,51 +336,38 @@ app.whenReady().then(async () => { const image = nativeImage.createFromPath(iconPath) tray = new Tray(image) tray.setToolTip('Deep Focus. Get more done.') + createTrayMenu() - const trayMenu = Menu.buildFromTemplate([ - { - label: 'Total Deep Work', - click: () => { - const today = dayjs().format('dddd') as keyof typeof deepWorkHours - const totalDeepWorkHours = getDeepWorkHours()[today] - new Notification({ - title: 'DeepFocus', - body: `Total Deep Work: ${totalDeepWorkHours} hours`, - icon: iconPath - }).show() - } - }, - { - label: 'Reset Data', - click: () => { - handleDailyReset() // Replace with your function to reset data - new Notification({ - title: 'DeepFocus', - body: 'Daily data has been reset.', - icon: iconPath - }).show() - } - }, - { type: 'separator' }, - { - label: 'Quit', - click: () => { - app.quit() - } - } - ]) + function createTrayMenu() { + const today = dayjs().format('dddd') as keyof typeof deepWorkHours + const totalDeepWorkHours = getDeepWorkHours()[today] + log.info('totalDeepWorkHours', totalDeepWorkHours) - tray.setContextMenu(trayMenu) + // Update the label directly with the latest deep work hours + const trayMenu = Menu.buildFromTemplate([ + { + label: `Total Deep Work: ${totalDeepWorkHours} hours` + }, + { type: 'separator' }, + { + label: 'Quit', + click: () => { + app.quit() + } + } + ]) - tray.on('click', () => { - if (mainWindow) { - mainWindow.isVisible() ? mainWindow.hide() : mainWindow.show() - } - }) + tray?.setContextMenu(trayMenu) + } + // TODO: Undecided if we want to show app on tray click + // tray.on('click', () => { + // if (mainWindow) { + // mainWindow.isVisible() ? mainWindow.hide() : mainWindow.show() + // } + // }) }) app.on('ready', () => { - checkForUpdates() }) @@ -470,6 +448,7 @@ export function getSiteTrackers(): SiteTimeTracker[] { schedule.scheduleJob('0 0 0 * * *', () => { log.info('Scheduled daily reset at midnight') resetCounters('daily') + sendDailyEmail() log.info('new reset date is ', store.get('lastResetDate')) }) @@ -639,7 +618,6 @@ export function handleDailyReset() { log.info('Performing daily reset. Previous reset date:', lastResetDate.format('YYYY-MM-DD')) resetCounters('daily') log.info(`Daily reset performed. New reset date stored: ${now.format('YYYY-MM-DD')}`) - // Check if we need to do a full weekly reset (if last reset was more than a week ago) if (now.diff(lastResetDate, 'week') >= 1) { log.info('Performing full weekly reset for the previous week.') @@ -653,33 +631,38 @@ export function handleDailyReset() { } } -async function sendDailyEmail(username, date, deepWorkHours, siteTimeTrackers) { - if (!username || siteTimeTrackers.length === 0) { - console.log('No data to send in email.') - return - } +async function sendDailyEmail() { + const now = dayjs() + log.info('currentSiteTimeTrackers:', currentSiteTimeTrackers) - const MIN_TIME_THRESHOLD = 60 - const filteredTrackers = siteTimeTrackers.filter( + const MIN_TIME_THRESHOLD = 10 + const filteredTrackers = currentSiteTimeTrackers.filter( (tracker) => tracker.timeSpent >= MIN_TIME_THRESHOLD ) - - const emailData = { - username, - date, - deepWorkHours, - trackers: filteredTrackers.map((tracker) => ({ + const today = now.format('dddd') // needs to be number to access deepWorkHours + const deepWorkHours = getDeepWorkHours() + const workToday = deepWorkHours[today] as number + log.info('workToday:', workToday) + const lastResetDate = now.toISOString() + store?.set('lastResetDate', lastResetDate) + + const dailyData = { + username: user.username, + date: today, + workToday, + trackers: filteredTrackers.map((tracker: SiteTimeTracker) => ({ title: tracker.title, url: tracker.url, - timeSpent: tracker.timeSpent + timeSpent: tracker.timeSpent, + iconUrl: tracker.iconUrl })) } try { - const response = await fetch(`${process.env.VITE_SERVER_URL_PROD}/api/v1/email/send-daily`, { + const response = await fetch(`${process.env.VITE_SERVER_URL_PROD}/api/v1/activity/send-daily`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(emailData) + body: JSON.stringify(dailyData) }) console.log('Email sent status:', response.status) } catch (error) { @@ -687,9 +670,8 @@ async function sendDailyEmail(username, date, deepWorkHours, siteTimeTrackers) { } } - -async function checkAndSendMissedEmails() { - const lastEmailDate = dayjs(store.get(LAST_EMAIL_DATE_KEY, null) || dayjs().subtract(1, 'day')) +async function checkAndSendMissedEmails(): Promise { + const lastEmailDate = dayjs(store.get(lastEmailDate, null) || dayjs().subtract(1, 'day')) const today = dayjs().startOf('day') if (!lastEmailDate.isSame(today, 'day')) { @@ -698,16 +680,9 @@ async function checkAndSendMissedEmails() { while (dateToProcess.isBefore(today) || dateToProcess.isSame(today, 'day')) { const formattedDate = dateToProcess.format('YYYY-MM-DD') console.log(`Sending missed email for date: ${formattedDate}`) - - const username = store.get('user')?.username - const deepWorkHours = store.get('deepWorkHours') - const siteTimeTrackers = store.get('siteTimeTrackers', []) - - await sendDailyEmail(username, formattedDate, deepWorkHours, siteTimeTrackers) - + await sendDailyEmail() // Update the last email date after each successful send - store.set(LAST_EMAIL_DATE_KEY, dateToProcess.toISOString()) - + store.set(lastEmailDate, dateToProcess.toISOString()) dateToProcess = dateToProcess.add(1, 'day') } } diff --git a/src/productivityUtils.ts b/src/productivityUtils.ts index f017a38..c7cf266 100755 --- a/src/productivityUtils.ts +++ b/src/productivityUtils.ts @@ -12,6 +12,9 @@ import { TypedStore } from './main' import { exec } from 'child_process' import dayjs from 'dayjs' import log from 'electron-log/node.js' +import path from 'path' +import { app } from 'electron' +import fs from 'fs' export function getUrlFromResult(result: Result): string | undefined { if ('url' in result) { @@ -86,6 +89,26 @@ export function formatTime(milliseconds: number): string { return `${seconds}s` } } + +// Dynamically get the user's data path for icon storage +const ICONS_BASE_PATH = path.join(app.getPath('userData'), 'icons') + +function findBestIconMatch(appName: string): string | null { + const sanitizedAppName = appName.replace(/[^a-zA-Z0-9]/g, '-').toLowerCase() + const icons = fs.readdirSync(ICONS_BASE_PATH) + const matchedIcon = icons.find((icon) => icon.toLowerCase().includes(sanitizedAppName)) + if (matchedIcon) { + log.info(`Found matching icon: ${matchedIcon} for app: ${appName}`) + return path.join(ICONS_BASE_PATH, matchedIcon) + } + return null +} + +function getBase64Icon(iconPath: string): string { + const iconBuffer = fs.readFileSync(iconPath) + return `data:image/png;base64,${iconBuffer.toString('base64')}` +} + export function updateSiteTimeTracker( appName: string, timeTrackers: SiteTimeTracker[], @@ -96,17 +119,30 @@ export function updateSiteTimeTracker( let trackerKey = '' let trackerTitle = '' let trackerType: TrackerType + let iconUrl = '' if (url && isValidURL(url)) { // For URLs, use the base URL as the tracker key and the title as the URL's base domain trackerKey = url trackerTitle = url trackerType = TrackerType.Website + iconUrl = `https://www.google.com/s2/favicons?sz=64&domain=${trackerTitle}` } else { // If it's a desktop app (no valid URL), use the app path and name for the tracker trackerKey = appName || 'Unknown App' trackerTitle = appName || 'Unknown App' trackerType = TrackerType.App + + // Attempt to find the cached icon for this app + const iconPath = findBestIconMatch(appName); + + if (fs.existsSync(iconPath)) { + iconUrl = getBase64Icon(iconPath) // Use Base64 data URI for the icon + log.info(`Using cached icon: ${iconPath}`) + } else { + iconUrl = 'https://cdn-icons-png.freepik.com/512/7022/7022186.png' + log.info(`Using default icon for app: ${appName}`) + } } // Find an existing tracker or create a new one @@ -115,13 +151,15 @@ export function updateSiteTimeTracker( log.info('Updating existing tracker', tracker.title, tracker.timeSpent) tracker.timeSpent += 5 tracker.lastActiveTimestamp = currentTime + tracker.iconUrl = iconUrl } else { tracker = { url: trackerKey, title: trackerTitle, timeSpent: 0, lastActiveTimestamp: currentTime, - type: trackerType + type: trackerType, + iconUrl } timeTrackers.push(tracker) } diff --git a/src/renderer/src/Analytics.tsx b/src/renderer/src/Analytics.tsx index 1591af5..5b97ef0 100644 --- a/src/renderer/src/Analytics.tsx +++ b/src/renderer/src/Analytics.tsx @@ -103,7 +103,7 @@ const Analytics = () => { transition={{ duration: 0.5, easing: "ease-in-out" }} >
-
+
diff --git a/src/renderer/src/BarChart.tsx b/src/renderer/src/BarChart.tsx index e600a99..61e8144 100755 --- a/src/renderer/src/BarChart.tsx +++ b/src/renderer/src/BarChart.tsx @@ -89,7 +89,7 @@ const BarChart = () => { - +
) } diff --git a/src/types.ts b/src/types.ts index bcea39e..e5d9ea5 100755 --- a/src/types.ts +++ b/src/types.ts @@ -23,6 +23,7 @@ export interface StoreSchema { siteTimeTrackers: SiteTimeTracker[] user?: User lastResetDate?: string + lastEmailDate?: string unproductiveUrls?: string[] deepWorkHours?: { Monday: number