From 036d11b0967d4f583e6f45dc4bca957ffb0bd5f3 Mon Sep 17 00:00:00 2001 From: Rohid Date: Thu, 22 Aug 2024 21:03:35 +0600 Subject: [PATCH 1/5] list open windows for tracking --- electron/electron-env.d.ts | 1 + electron/helpers/monitored-app.ts | 33 ++++-- electron/helpers/monitoring-manager.ts | 6 - electron/main.ts | 31 +++++ electron/preload.ts | 3 + electron/utils/constants.ts | 1 + electron/watchers/wakatime.ts | 12 +- electron/watchers/watcher.ts | 3 - package-lock.json | 27 +++++ package.json | 1 + src/components/ui/tabs.tsx | 30 ++--- src/components/ui/textarea.tsx | 16 +-- src/lib/query-client.ts | 3 + src/main.tsx | 9 +- src/monitored-apps.tsx | 7 +- src/pages/MonitoredApps.tsx | 153 +++++++++++++++++++++---- src/settings.tsx | 7 +- 17 files changed, 263 insertions(+), 80 deletions(-) create mode 100644 src/lib/query-client.ts diff --git a/electron/electron-env.d.ts b/electron/electron-env.d.ts index b7858e2..d615660 100644 --- a/electron/electron-env.d.ts +++ b/electron/electron-env.d.ts @@ -47,6 +47,7 @@ interface Window { setAutoUpdateEnabled: (autoUpdateEnabled: boolean) => void; codeTimeInStatusBar: () => boolean; setCodeTimeInStatusBar: (codeTimeInStatusBar: boolean) => void; + getOpenWindows: () => Promise; }; } diff --git a/electron/helpers/monitored-app.ts b/electron/helpers/monitored-app.ts index 52467fd..264d414 100644 --- a/electron/helpers/monitored-app.ts +++ b/electron/helpers/monitored-app.ts @@ -6,11 +6,21 @@ import { AppData } from "../utils/validators"; import { FilterManager } from "./filter-manager"; import { PropertiesManager } from "./properties-manager"; +export interface HeartbeatData { + entity: string; + category: Category | null; + language: string | null; + project: string | null; +} + export class MonitoredApp { - static heartbeatData(windowInfo: WindowInfo, app: AppData) { + static heartbeatData( + windowInfo: WindowInfo, + app?: AppData, + ): HeartbeatData | null { const entity = this.entity(windowInfo, app); - const category = this.category(app); - const language = this.language(app); + const category = app ? this.category(app) : null; + const language = app ? this.language(app) : null; const project = this.project(windowInfo); if (!entity) { return null; @@ -102,8 +112,8 @@ export class MonitoredApp { return null; } - static entity(windowInfo: WindowInfo, app: AppData) { - if (app.isBrowser) { + static entity(windowInfo: WindowInfo, app?: AppData) { + if (app?.isBrowser) { if (windowInfo.url && FilterManager.filterBrowsedSites(windowInfo.url)) { if ( PropertiesManager.domainPreference === DomainPreferenceType.domain @@ -115,7 +125,7 @@ export class MonitoredApp { return null; } - switch (app.id) { + switch (app?.id) { // TODO: Unimplemented feature case "canva": return null; @@ -127,8 +137,8 @@ export class MonitoredApp { } } - static title(windowInfo: WindowInfo, app: AppData) { - switch (app.id) { + static title(windowInfo: WindowInfo, app?: AppData) { + switch (app?.id) { case "arcbrowser": case "brave": case "canva": @@ -139,7 +149,7 @@ export class MonitoredApp { case "safaripreview": case "xcode": case "microsoft_edge": - return `${app.id} should never use window title as entity`; + return `${app?.id} should never use window title as entity`; case "figma": { const title = windowInfo.title.split(" - ")[0]; if (!title || title === "Figma" || title === "Drafts") { @@ -162,7 +172,10 @@ export class MonitoredApp { return title; } default: - return windowInfo.title.split(" - ")[0]; + if (windowInfo.title) { + return windowInfo.title.split(" - ")[0]; + } + return windowInfo.info.name; } } diff --git a/electron/helpers/monitoring-manager.ts b/electron/helpers/monitoring-manager.ts index 154209a..af08d34 100644 --- a/electron/helpers/monitoring-manager.ts +++ b/electron/helpers/monitoring-manager.ts @@ -11,9 +11,6 @@ export abstract class MonitoringManager { } static isMonitored(path: string) { - if (!AppsManager.instance().getApp(path)) { - return; - } const monitoringKey = this.monitoredKey(path); const file = getDesktopWakaTimeConfigFilePath(); const monitoring = ConfigFileReader.getBool( @@ -29,9 +26,6 @@ export abstract class MonitoringManager { } static set(path: string, monitor: boolean) { - if (!AppsManager.instance().getApp(path)) { - return; - } const file = getDesktopWakaTimeConfigFilePath(); const monitoringKey = this.monitoredKey(path); ConfigFileReader.setBool(file, "monitoring", monitoringKey, monitor); diff --git a/electron/main.ts b/electron/main.ts index 6227aaa..4a137a1 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -1,4 +1,5 @@ import path from "node:path"; +import { openWindowsAsync } from "@miniben90/x-win"; import { app, BrowserWindow, @@ -358,3 +359,33 @@ ipcMain.on(IpcKeys.getAllowlist, (event) => { ipcMain.on(IpcKeys.setAllowlist, (_, value: string) => { PropertiesManager.allowlist = value; }); + +ipcMain.on(IpcKeys.getOpenWindows, async (event) => { + const windows = await openWindowsAsync(); + event.returnValue = ( + await Promise.all( + windows.map(async (window) => { + const app = AppsManager.instance().apps.find( + (item) => item.path === window.info.path, + ); + + if (app) { + return null; + } + + const icon = (await window.getIconAsync()).data; + return { + id: window.info.path, + name: window.info.name + (window.title ? ` - ${window.title}` : ""), + path: window.info.path, + icon, + isBrowser: false, + isDefaultEnabled: false, + isElectronApp: false, + bundleId: null, + version: null, + }; + }), + ) + ).filter((item) => !!item); +}); diff --git a/electron/preload.ts b/electron/preload.ts index 8c17c63..6b4c8a9 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -68,4 +68,7 @@ contextBridge.exposeInMainWorld("ipcRenderer", { setShouldLogToFile(shouldLogToFile: boolean) { ipcRenderer.send(IpcKeys.setShouldLogToFile, shouldLogToFile); }, + getOpenWindows() { + return ipcRenderer.sendSync(IpcKeys.getOpenWindows); + }, }); diff --git a/electron/utils/constants.ts b/electron/utils/constants.ts index 5238649..98c55ca 100644 --- a/electron/utils/constants.ts +++ b/electron/utils/constants.ts @@ -23,6 +23,7 @@ export enum IpcKeys { setDenylist = "set_denylist", getAllowlist = "get_allowlist", setAllowlist = "set_allowlist", + getOpenWindows = "get_opened_windows", } export enum FilterType { diff --git a/electron/watchers/wakatime.ts b/electron/watchers/wakatime.ts index 9dafac2..789e0a9 100644 --- a/electron/watchers/wakatime.ts +++ b/electron/watchers/wakatime.ts @@ -116,7 +116,7 @@ export class Wakatime { } async sendHeartbeat(props: { - appData: AppData; + appData?: AppData; windowInfo: WindowInfo; entity: string; entityType: EntityType; @@ -140,13 +140,13 @@ export class Wakatime { if (!this.shouldSendHeartbeat(entity, time, isWrite, category)) { return; } - if (!MonitoringManager.isMonitored(appData.path)) { + if (!MonitoringManager.isMonitored(windowInfo.info.path)) { return; } - const appName = windowInfo?.info.name ?? appData.name; - const appVersion = appData.version; - if (!appName || !appVersion) { + const appName = windowInfo.info.name ?? appData?.name; + const appVersion = appData?.version; + if (!appName) { return; } @@ -162,7 +162,7 @@ export class Wakatime { "--category", category, "--plugin", - `${appName}/${appVersion} ${getPlatfrom()}-wakatime/${app.getVersion()}`, + `${appName}${appVersion ? `/${appVersion}` : ""} ${getPlatfrom()}-wakatime/${app.getVersion()}`, ]; if (project) { diff --git a/electron/watchers/watcher.ts b/electron/watchers/watcher.ts index eb410c9..4d1cfee 100644 --- a/electron/watchers/watcher.ts +++ b/electron/watchers/watcher.ts @@ -35,9 +35,6 @@ export class Watcher { // To ensure we always retrieve the most current window information, including the updated URL and title, we use the activeWindow function instead of relying on the previously stored this.activeApp. This approach addresses the issue where switching tabs in your browser does not trigger a window change event, leading to activeApp retaining outdated URL and title information. const window = activeWindow(); const app = AppsManager.instance().getApp(window.info.path); - if (!app) { - return; - } const heartbeatData = MonitoredApp.heartbeatData(window, app); if (!heartbeatData) { return; diff --git a/package-lock.json b/package-lock.json index 4ebab7b..e35afc8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-switch": "^1.1.0", "@radix-ui/react-tabs": "^1.1.0", + "@tanstack/react-query": "^5.52.0", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "date-fns": "^3.6.0", @@ -3982,6 +3983,32 @@ "node": ">=10" } }, + "node_modules/@tanstack/query-core": { + "version": "5.52.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.52.0.tgz", + "integrity": "sha512-U1DOEgltjUwalN6uWYTewSnA14b+tE7lSylOiASKCAO61ENJeCq9VVD/TXHA6O5u9+6v5+UgGYBSccTKDoyMqw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.52.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.52.0.tgz", + "integrity": "sha512-T8tLZdPEopSD3A1EBZ/sq7WkI76pKLKKiT82F486K8wf26EPgYCdeiSnJfuayssdQjWwLQMQVl/ROUBNmlWgCQ==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.52.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18.0.0" + } + }, "node_modules/@tootallnate/once": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", diff --git a/package.json b/package.json index c2a8631..9024b24 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-switch": "^1.1.0", "@radix-ui/react-tabs": "^1.1.0", + "@tanstack/react-query": "^5.52.0", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "date-fns": "^3.6.0", diff --git a/src/components/ui/tabs.tsx b/src/components/ui/tabs.tsx index 7a3d9dc..3a772cb 100644 --- a/src/components/ui/tabs.tsx +++ b/src/components/ui/tabs.tsx @@ -1,11 +1,11 @@ -"use client" +"use client"; -import * as React from "react" -import * as TabsPrimitive from "@radix-ui/react-tabs" +import * as React from "react"; +import * as TabsPrimitive from "@radix-ui/react-tabs"; -import { cn } from "~/utils" +import { cn } from "~/utils"; -const Tabs = TabsPrimitive.Root +const Tabs = TabsPrimitive.Root; const TabsList = React.forwardRef< React.ElementRef, @@ -15,12 +15,12 @@ const TabsList = React.forwardRef< ref={ref} className={cn( "inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground", - className + className, )} {...props} /> -)) -TabsList.displayName = TabsPrimitive.List.displayName +)); +TabsList.displayName = TabsPrimitive.List.displayName; const TabsTrigger = React.forwardRef< React.ElementRef, @@ -30,12 +30,12 @@ const TabsTrigger = React.forwardRef< ref={ref} className={cn( "inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm", - className + className, )} {...props} /> -)) -TabsTrigger.displayName = TabsPrimitive.Trigger.displayName +)); +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName; const TabsContent = React.forwardRef< React.ElementRef, @@ -45,11 +45,11 @@ const TabsContent = React.forwardRef< ref={ref} className={cn( "mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", - className + className, )} {...props} /> -)) -TabsContent.displayName = TabsPrimitive.Content.displayName +)); +TabsContent.displayName = TabsPrimitive.Content.displayName; -export { Tabs, TabsList, TabsTrigger, TabsContent } +export { Tabs, TabsList, TabsTrigger, TabsContent }; diff --git a/src/components/ui/textarea.tsx b/src/components/ui/textarea.tsx index 5fbebb8..f74ac29 100644 --- a/src/components/ui/textarea.tsx +++ b/src/components/ui/textarea.tsx @@ -1,6 +1,6 @@ -import * as React from "react" +import * as React from "react"; -import { cn } from "~/utils" +import { cn } from "~/utils"; export interface TextareaProps extends React.TextareaHTMLAttributes {} @@ -11,14 +11,14 @@ const Textarea = React.forwardRef(