Skip to content

Commit

Permalink
feat: add circular chart in Home page for users to see current progress
Browse files Browse the repository at this point in the history
  • Loading branch information
timeowilliams committed Oct 2, 2024
1 parent d50d1ae commit b350b8f
Show file tree
Hide file tree
Showing 13 changed files with 272 additions and 242 deletions.
43 changes: 3 additions & 40 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,18 +64,17 @@ Note, for running this app locally, you may run into issues with active-window.
- [x] Implement changelog using conventional commits
- [x] Add integration and automated tests
- [x] Implement user authentication and cloud-based data persistence
- [x] Implement progress bar for deep work visualization
- [x] Improve user onboarding experience
- [x] Use inspiration from debugtron to render the electron apps most commonly used. Use another API service to get the favicons of the top websites and include this in email and in the desktop app.
- [ ] Allow users to enter session goals and customize productive/unproductive sites
- [ ] Migrate from electron-storage to SQLite for improved data handling
- [ ] Implement progress bar for deep work visualization
- [ ] Enhance data analysis and insights
- [ ] Improve user onboarding experience
- [ ] Develop comprehensive test suite for main and renderer processes
- [ ] Create cloud synchronization for user data and preferences
- [ ] Implement secure user authentication system
- [ ] Use inspiration from debugtron to render the electron apps most commonly used. Use another API service to get the favicons of the top websites and include this in email and in the desktop app.
- [ ] Collect each site visited. Show users all sites visited in the past day at the end of the day/next day and ask them to label them as productive or unproductive.
- [ ] To do list like functionality? Have people add tasks to their list and mark as productive or not productive. Then, at the end of the day, they can see a list of tasks and see how productive they were.
- [ ] Add a light

## Philosophy

Expand All @@ -101,39 +100,3 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
## Contact

Timeo Williams [@timeowilliams](https://twitter.com/timeowilliams) - [email protected]

Problems:

1. UI for Analytics doesn't send a request to get fresh data after 24 hours if
the user stays on the same Analytics page. Perhaps a refresh button would be
better? Or force a refresh after a certain amount of time?

2. Need to allow customers to add websites that are productive
so that we can track the time spent on those sites.

As a default, the Login Window and Settings page should be hidden.
windowInfo {
owner: {
name: 'loginwindow',
processId: 186,
bundleId: 'com.apple.loginwindow',
path: '/System/Library/CoreServices/loginwindow.app'
},
bounds: { width: 30000, y: -15000, height: 30000, x: -15000 },
memoryUsage: 18672,
title: '',
platform: 'macos',
id: 23597
}
windowInfo {
owner: {
processId: 516,
bundleId: 'com.apple.finder',
name: 'Finder',
path: '/System/Library/CoreServices/Finder.app'
},
}

Honestly, if any of the bundleIDs contain com.apple, it's probably not productive.

Last, but not least, let's sign this App up for the Apple Developer Program.
19 changes: 12 additions & 7 deletions src/main/childProcess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@ import fs from 'fs'
import path from 'path'
import plist from 'simple-plist'

interface MacosAppInfo {
CFBundleIdentifier: string
CFBundleName: string
CFBundleExecutable: string
CFBundleIconFile: string
}

// Helper to read the Info.plist file
async function readPlistFile(filePath: string) {
return new Promise<MacosAppInfo>((resolve, reject) => {
Expand All @@ -22,7 +29,7 @@ async function readIcnsAsImageUri(file: string) {
if (!buf) return ''

const totalSize = buf.readInt32BE(4) - 8
const icons = []
const icons: { type: string; size: number; data: Buffer }[] = []
let start = 0
const buffer = buf.subarray(8)

Expand All @@ -41,15 +48,13 @@ async function readIcnsAsImageUri(file: string) {
}
return '' // No valid image data
} catch (error) {
console.error(`Error reading .icns file: ${error.message}`)
console.error(`Error reading .icns file: ${error}`)
return '' // Return an empty string or fallback icon
}
}

// Updated getInstalledApps function
export async function getInstalledApps(): Promise<
{ name: string; path: string; icon: string | null }[]
> {
export async function getInstalledApps(): Promise<{ name: string; path: string; icon: string }[]> {
const dir = '/Applications'
const appPaths = await fs.promises.readdir(dir)

Expand All @@ -61,7 +66,7 @@ export async function getInstalledApps(): Promise<
const info = await readPlistFile(plistPath)
const iconPath = path.join(fullAppPath, 'Contents/Resources', info.CFBundleIconFile)

let icon = null
let icon: string | null = null
if (fs.existsSync(iconPath)) {
icon = await readIcnsAsImageUri(iconPath)
} else {
Expand All @@ -82,5 +87,5 @@ export async function getInstalledApps(): Promise<
})

const apps = await Promise.all(appPromises)
return apps.filter(Boolean) // Remove null results
return apps.filter(Boolean) as { name: string; path: string; icon: string }[]
}
12 changes: 7 additions & 5 deletions src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import Store from 'electron-store'
import { StoreSchema, SiteTimeTracker, DeepWorkHours, MessageType, User } from './types'
import { updateSiteTimeTracker } from './productivityUtils'
import { getInstalledApps } from './childProcess'
import { get } from 'http'

export interface TypedStore extends Store<StoreSchema> {
get<K extends keyof StoreSchema>(key: K): StoreSchema[K]
Expand All @@ -21,10 +20,9 @@ export interface TypedStore extends Store<StoreSchema> {
}

const store = new Store<StoreSchema>() as TypedStore
store.clear()
let currentSiteTimeTrackers: SiteTimeTracker[] = []
let currentDeepWork = 0
const deepWorkTarget = store.get('deepWorkTarget', 4) as number // Default to 4 hours if not set
const deepWorkTarget = store.get('deepWorkTarget', 4) as number
let mainWindow: BrowserWindow | null = null

setupEnvironment()
Expand Down Expand Up @@ -140,8 +138,12 @@ function startActivityMonitoring() {
try {
const windowInfo = await activeWindow()
if (windowInfo && windowInfo!.platform === 'macos') {
console.log('windowInfo', windowInfo)
updateSiteTimeTracker(windowInfo, currentSiteTimeTrackers)
if (!windowInfo.owner.bundleId.includes('com.apple')) {
console.log('windowInfo', windowInfo)
updateSiteTimeTracker(windowInfo, currentSiteTimeTrackers)
} else {
console.log('Ignoring Apple App', windowInfo.owner.bundleId)
}
}
} catch (error) {
console.error('Error getting active window:', error)
Expand Down
24 changes: 15 additions & 9 deletions src/main/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ function updateDeepWorkHours(siteTrackers: SiteTimeTracker[], deepWorkHours: Dee
return deepWorkHours
}

//TODO: Add when logic is added in frontend + Determine if current activity is considered deep work
function isDeepWork(item: string) {
// You can customize this condition based on specific apps, sites, or window titles
// Check if the tracker is a deep work app (e.g., VSCode, GitHub, etc.)
Expand Down Expand Up @@ -111,19 +110,26 @@ async function persistDailyData(
return
}

const MIN_TIME_THRESHOLD = 60

// Filter out sites/apps with time spent less than the threshold
const filteredTrackers = workerSiteTimeTrackers.filter(
(tracker) => tracker.timeSpent >= MIN_TIME_THRESHOLD * 1000 // timeSpent is in milliseconds
)

if (filteredTrackers.length === 0) {
console.log('No site time trackers met the minimum time threshold to persist.')
return
}

const today = dayjs().format('dddd') // Get back Monday, Tuesday, etc.
const dailyData: {
username: string
url: string
title: string
timeSpent: number
date: string
}[] = workerSiteTimeTrackers.map((tracker) => ({

const dailyData = filteredTrackers.map((tracker) => ({
username: currentUsername,
url: tracker.url,
title: tracker.title,
timeSpent: tracker.timeSpent,
date: dayjs().format('YYYY-MM-DD') // Get back 2023-09-27
date: dayjs().format('YYYY-MM-DD') // Get back 2024-09-30
}))

try {
Expand Down
3 changes: 2 additions & 1 deletion src/renderer/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import './assets/main.css'
import logo from './assets/deepWork.svg'
import { IconSettings } from './components/ui/icons'
import { Button } from './components/ui/button'
import Onboarding from './Onboarding'

// Lazy load the components
const Login = lazy(() => import('./Login'))
Expand All @@ -17,6 +16,8 @@ const Home = lazy(() => import('./Home'))
const HelloWorld = () => <h1>Hello World!</h1>
const BarChart = lazy(() => import('./BarChart'))

const Onboarding = lazy(() => import('./Onboarding'))

const Settings = lazy(() => import('./Settings'))

const App = (props: ComponentProps<typeof Router>) => {
Expand Down
67 changes: 36 additions & 31 deletions src/renderer/src/BarChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
Legend
} from 'chart.js'
import { Bar } from 'solid-chartjs'
import { Button } from './components/ui/button'

const BarChart = () => {
const [chartData, setChartData] = createSignal({
Expand All @@ -24,41 +25,41 @@ const BarChart = () => {
}
]
})
onMount(() => {
ChartJS.register(BarController, CategoryScale, BarElement, LinearScale, Tooltip, Title, Legend)

// Send the initial request to fetch deep work data
window?.electron.ipcRenderer.send('fetch-deep-work-data')
let refreshIntervalId

// Listen for the deep work data response and update the chart
const deepWorkDataHandler = (event, data) => {
if (data && data.length) {
console.log('Retrieved Data! ', data)
const fetchDeepWorkData = () => {
window?.electron?.ipcRenderer.send('fetch-deep-work-data')
}

setChartData((prevData) => ({
...prevData,
datasets: [
{
...prevData.datasets[0],
data: data
}
]
}))
} else {
console.log('No data found for deep work hours. Using default data.')
}
const handleDataResponse = (event, data) => {
if (data && data.length) {
console.log('Retrieved Data! ', data)
setChartData((prevData) => ({
...prevData,
datasets: [{ ...prevData.datasets[0], data }]
}))
} else {
console.log('No data found for deep work hours.')
}
}

window?.electron.ipcRenderer.on('deep-work-data-response', deepWorkDataHandler)
onMount(() => {
ChartJS.register(BarController, CategoryScale, BarElement, LinearScale, Tooltip, Title, Legend)
fetchDeepWorkData()

window?.electron.ipcRenderer.on('deep-work-data-response', handleDataResponse)

refreshIntervalId = setInterval(() => fetchDeepWorkData(), 14400000) // Refresh every 4 hours

// Clean up the event listener on unmount
onCleanup(() => {
window?.electron.ipcRenderer.removeListener('deep-work-data-response', deepWorkDataHandler)
window?.electron.ipcRenderer.removeListener('deep-work-data-response', handleDataResponse)
clearInterval(refreshIntervalId)
})
})

const chartOptions = {
responsive: false,
responsive: true,
maintainAspectRatio: true,
scales: {
y: {
Expand All @@ -70,21 +71,25 @@ const BarChart = () => {
}
},
plugins: {
title: {
display: true,
text: 'Deep Work Hours'
},
title: { display: true, text: 'Deep Work Hours' },
tooltip: {
callbacks: {
label: function (context) {
const value = context.raw
return value === 0 ? 'Data coming soon' : `${value} hours`
return context.raw === 0 ? 'Data coming soon' : `${context.raw} hours`
}
}
}
}
}
return <Bar data={chartData()} options={chartOptions} width={700} height={700} />

return (
<div>
<Button class="mb-4 p-2 bg-blue-500 text-white" onClick={fetchDeepWorkData}>
Refresh Data
</Button>
<Bar data={chartData()} options={chartOptions} width={500} height={500} />
</div>
)
}

export default BarChart
34 changes: 34 additions & 0 deletions src/renderer/src/CircularProgress.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { createEffect } from 'solid-js'
const CircularProgress = (props) => {
const radius = 45
const circumference = 2 * Math.PI * radius

console.log('Progress value inside CircularProgress:', props.progress)
createEffect(() => {
console.log('Progress value inside CircularProgress createEffect:', props.progress)
})

return (
<div class="circular-progress">
<svg height="100" width="100">
<circle stroke="grey" fill="transparent" r={radius} cx="50" cy="50" stroke-width="10" />
<circle
stroke="white"
fill="transparent"
r={radius}
cx="50"
cy="50"
stroke-width="10"
stroke-dasharray={String(circumference)}
stroke-dashoffset={(1 - props.progress) * circumference}
style={{ transition: 'stroke-dashoffset 0.5s ease' }}
/>
</svg>
<div class="text-center mt-2">
<p>Progress: {Math.round(props.progress * 100)}%</p>
</div>
</div>
)
}

export default CircularProgress
Loading

0 comments on commit b350b8f

Please sign in to comment.