From d50d1aec6791a4665934f7aa706cb367bad309a6 Mon Sep 17 00:00:00 2001 From: Timeo Williams Date: Wed, 2 Oct 2024 00:19:30 -0400 Subject: [PATCH] feat: show website and desktop icons & add onboarding view for new users to immediately set productive and unproductive apps and sites --- .github/workflows/release.yml | 8 +- README.md | 36 +++++++++ electron-builder.yml | 4 + electron.vite.config.ts | 3 +- package.json | 1 + pnpm-lock.yaml | 39 +++++++++ src/main/childProcess.ts | 86 ++++++++++++++++++++ src/main/index.ts | 51 +++++++++--- src/main/types.ts | 1 + src/main/worker.ts | 20 +++-- src/renderer/src/App.tsx | 28 ++++--- src/renderer/src/Navbar.tsx | 57 +++++++++++++ src/renderer/src/Onboarding.tsx | 40 ++++++++++ src/renderer/src/Signup.tsx | 4 +- src/renderer/src/UnproductiveApps.tsx | 94 ++++++++++++++++++++++ src/renderer/src/UnproductiveWebsites.tsx | 97 +++++++++++++++++++++++ src/renderer/src/lib/AuthContext.tsx | 3 +- 17 files changed, 535 insertions(+), 37 deletions(-) create mode 100644 src/main/childProcess.ts create mode 100644 src/renderer/src/Navbar.tsx create mode 100644 src/renderer/src/Onboarding.tsx create mode 100644 src/renderer/src/UnproductiveApps.tsx create mode 100644 src/renderer/src/UnproductiveWebsites.tsx diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 366cb68..ba622db 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -49,16 +49,16 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: npx semantic-release -# This step will retrieve the latest created tag + # This step will retrieve the latest created tag - name: Set Release Tag id: set_tag run: echo "RELEASE_TAG=$(git describe --tags $(git rev-list --tags --max-count=1))" >> $GITHUB_ENV -# Now, upload the DMG to the GitHub release + # Now, upload the DMG to the GitHub release - name: Upload DMG to GitHub Release uses: softprops/action-gh-release@v2.0.8 with: - tag_name: ${{ env.RELEASE_TAG }} + tag_name: ${{ env.RELEASE_TAG }} name: ${{ env.RELEASE_TAG }} env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/README.md b/README.md index 7347dd6..0b66a89 100644 --- a/README.md +++ b/README.md @@ -101,3 +101,39 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file ## Contact Timeo Williams [@timeowilliams](https://twitter.com/timeowilliams) - timeo.williams@gmail.com + +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. diff --git a/electron-builder.yml b/electron-builder.yml index f516de2..a1c1e68 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -15,6 +15,10 @@ extraResources: to: 'resources/icon_green.png' - from: './resources/icon_red.png' to: 'resources/icon_red.png' + - from: './resources/icon_yellow.png' + to: 'resources/icon_yellow.png' + - from: './resources/icon.png' + to: 'resources/icon.png' asar: true asarUnpack: diff --git a/electron.vite.config.ts b/electron.vite.config.ts index 1e9e7fc..d5ea142 100644 --- a/electron.vite.config.ts +++ b/electron.vite.config.ts @@ -14,7 +14,8 @@ export default defineConfig({ rollupOptions: { input: { main: resolve(__dirname, 'src/main/index.ts'), - worker: resolve(__dirname, 'src/main/worker.ts') + worker: resolve(__dirname, 'src/main/worker.ts'), + childProcess: resolve(__dirname, 'src/main/childProcess.ts') }, output: { format: 'es', diff --git a/package.json b/package.json index d727bac..464d9b2 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "gsap": "^3.12.5", "node-addon-api": "^8.2.0", "node-schedule": "^2.1.1", + "simple-plist": "^1.3.1", "solid-chartjs": "^1.3.11", "solid-js": "^1.9.1", "tailwind-merge": "^2.5.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index babb626..f2d64c1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -56,6 +56,9 @@ importers: node-schedule: specifier: ^2.1.1 version: 2.1.1 + simple-plist: + specifier: ^1.3.1 + version: 1.3.1 solid-chartjs: specifier: ^1.3.11 version: 1.3.11(chart.js@4.4.4)(solid-js@1.9.1) @@ -1791,6 +1794,10 @@ packages: before-after-hook@3.0.2: resolution: {integrity: sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==} + big-integer@1.6.52: + resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==} + engines: {node: '>=0.6'} + binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} @@ -1817,6 +1824,13 @@ packages: resolution: {integrity: sha512-2hCgjEmP8YLWQ130n2FerGv7rYpfBmnmp9Uy2Le1vge6X3gZIfSmEzP5QTDElFxcvVcXlEn8Aq6MU/PZygIOog==} engines: {node: '>=14.16'} + bplist-creator@0.1.0: + resolution: {integrity: sha512-sXaHZicyEEmY86WyueLTQesbeoH/mquvarJaQNbjuOQO+7gbFcDEWqKmcWA4cOTLzFlfgvkiVxolk1k5bBIpmg==} + + bplist-parser@0.3.1: + resolution: {integrity: sha512-PyJxiNtA5T2PlLIeBot4lbp7rj4OadzjnMZD/G5zuBNt8ei/yCU7+wW0h2bag9vr8c+/WuRWmSxbqAl9hL1rBA==} + engines: {node: '>= 5.10.0'} + brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} @@ -4449,6 +4463,9 @@ packages: resolution: {integrity: sha512-iuh+gPf28RkltuJC7W5MRi6XAjTDCAPC/prJUpQoG4vIP3MJZ+GTydVnodXA7pwvTKb2cA0m9OFZW/cdWy/I/w==} engines: {node: '>=6'} + simple-plist@1.3.1: + resolution: {integrity: sha512-iMSw5i0XseMnrhtIzRb7XpQEXepa9xhWxGUojHBL43SIpQuDQkh3Wpy67ZbDzZVr6EKxvwVChnVpdl8hEVLDiw==} + simple-update-notifier@2.0.0: resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==} engines: {node: '>=10'} @@ -4573,6 +4590,10 @@ packages: std-env@3.7.0: resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==} + stream-buffers@2.2.0: + resolution: {integrity: sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg==} + engines: {node: '>= 0.10.0'} + stream-combiner2@1.1.1: resolution: {integrity: sha512-3PnJbYgS56AeWgtKF5jtJRT6uFJe56Z0Hc5Ngg/6sI6rIt8iiMBTa9cvdyFfpMQjaVHr8dusbNeFGIIonxOvKw==} @@ -6918,6 +6939,8 @@ snapshots: before-after-hook@3.0.2: {} + big-integer@1.6.52: {} + binary-extensions@2.3.0: {} bindings@1.5.0: @@ -6952,6 +6975,14 @@ snapshots: widest-line: 4.0.1 wrap-ansi: 8.1.0 + bplist-creator@0.1.0: + dependencies: + stream-buffers: 2.2.0 + + bplist-parser@0.3.1: + dependencies: + big-integer: 1.6.52 + brace-expansion@1.1.11: dependencies: balanced-match: 1.0.2 @@ -9845,6 +9876,12 @@ snapshots: figures: 2.0.0 pkg-conf: 2.1.0 + simple-plist@1.3.1: + dependencies: + bplist-creator: 0.1.0 + bplist-parser: 0.3.1 + plist: 3.1.0 + simple-update-notifier@2.0.0: dependencies: semver: 7.6.3 @@ -9972,6 +10009,8 @@ snapshots: std-env@3.7.0: {} + stream-buffers@2.2.0: {} + stream-combiner2@1.1.1: dependencies: duplexer2: 0.1.4 diff --git a/src/main/childProcess.ts b/src/main/childProcess.ts new file mode 100644 index 0000000..93cd0e3 --- /dev/null +++ b/src/main/childProcess.ts @@ -0,0 +1,86 @@ +import fs from 'fs' +import path from 'path' +import plist from 'simple-plist' + +// Helper to read the Info.plist file +async function readPlistFile(filePath: string) { + return new Promise((resolve, reject) => { + plist.readFile(filePath, (error, data) => { + if (error || !data) { + reject(error) + } else { + resolve(data) + } + }) + }) +} + +// Helper to read and convert .icns file to base64 PNG +async function readIcnsAsImageUri(file: string) { + try { + const buf = await fs.promises.readFile(file) + if (!buf) return '' + + const totalSize = buf.readInt32BE(4) - 8 + const icons = [] + let start = 0 + const buffer = buf.subarray(8) + + while (start < totalSize) { + const type = buffer.subarray(start, start + 4).toString() + const size = buffer.readInt32BE(start + 4) + const data = buffer.subarray(start + 8, start + size) + icons.push({ type, size, data }) + start += size + } + + icons.sort((a, b) => b.size - a.size) + const imageData = icons[0]?.data + if (imageData?.subarray(1, 4).toString() === 'PNG') { + return 'data:image/png;base64,' + imageData.toString('base64') + } + return '' // No valid image data + } catch (error) { + console.error(`Error reading .icns file: ${error.message}`) + return '' // Return an empty string or fallback icon + } +} + +// Updated getInstalledApps function +export async function getInstalledApps(): Promise< + { name: string; path: string; icon: string | null }[] +> { + const dir = '/Applications' + const appPaths = await fs.promises.readdir(dir) + + const appPromises = appPaths.map(async (appPath) => { + const fullAppPath = path.join(dir, appPath) + const plistPath = path.join(fullAppPath, 'Contents/Info.plist') + + if (fs.existsSync(plistPath)) { + const info = await readPlistFile(plistPath) + const iconPath = path.join(fullAppPath, 'Contents/Resources', info.CFBundleIconFile) + + let icon = null + if (fs.existsSync(iconPath)) { + icon = await readIcnsAsImageUri(iconPath) + } else { + console.warn(`Icon not found for ${info.CFBundleName}, using fallback`) + // Provide a default/fallback icon if iconPath doesn't exist + icon = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAABIVJREFUWIXNVztvJEUQ/qqnex67M2ffYowgJrqQE+h+AhIJKSHB/Y/7D0hkCIkACZDQkR0BOiEkogsvICFw5jut117Ps98EM9Me27uLMRioYNU7U931VdVX1TXAfyz0D+nsEv+XD1dKvR9F0WMi+oCI3gLwJoDoFsbX3vtXAF46577inP+wU/v58+fcWvu59975OxDn3NPj4+P51mhYaz+7C8NXQHw/jXxYNE3zKMuyX8dnylisqhpNpyC1gfceB3s5Ts7rrREkIiyKGZbrCkSEWESYJQmKNEGeJUHPGPOhEOLHKQAyxnwRRdGno/Gzurlhmm8mszgOIKy133DOPwHg+QV4ejQqr6oajAjLdRUOSGOBPEtgrIPUGtpYON8TnBFB8AiJEOARQ9VKdEqHqLxxb46ykwEAEb03OB8AMCJ6ZzRWdxJFlsJ5DyJCkaWIeV8Eq3JzCpSxaKXGwV6OWRKDRxHKtgMGkI2UQZeIDgEwAI4PSIiIslFBGxeUiywFZ4RVWeNgL0fEGLJEIBEcEbE+pN5BahO8XpU19ucZiixF1XY9QG2nAPZGuyEFAOJRwbkeQBYLxDzCqqxDuBfFHFJrlI2Ecf2hnEVIY477eV9hznuc1S0WxRxpLAAA3l/rRzT+EADhvQ8xer0uN4b578rhXjGNQgJAs02KI/mMdViuK6zKekwlOqXhnEfZSqzKGquyRtlKOOfRKQOgT/uqrLFcVzC2j+a28t0IYAx3pzSc90hjAan7/MacY1XVaKWCsQ7GOrRSYVXVgahSa6SxgPM+7NuQgu0ARlGmz3EiePCu6npvr4F2HrVUA3CDRPT00sZe070xAOf78EXEAuGUMVv19fDOOBsqxG3x/EYA/g3ZCYBN6pyzPr8x51v1xfCOswh2iB6j3ePETgAXpDJI4/7wPE3A2PVDGSPMk76VpDGH1GYAtXuM2AlgJFIrNRIhAphF3jcYxgiMEdJYYJHPAz8SIdBKHda75P/diMqmg3Mey3WF12c9KO/7/nBWtVieV1ieVzirWnRKh2b1+qzEcl3BOY+qlZfOvCpTAKFeaCBOqzSUsdifZ4FM471QzBIcFDkOihzFLIHzHqdV3+0YEfbnGZSxl67lTRIo7ZxbM8b2Mck9AJRthyJLsSj6i8Y6h6qVwbNL3gxGFsUcyliUbRcmnikZvfftVQDee78EsA8AWRJPlXHetMhigWKW4n4+g9QGytjQqBgxxDwKN1/dSbRXPJ+nF2c6516NEefDwiulXmZZ9i4AFFmCRioc7l+QZhTBoz8trWKWopill57Nk0sz4W+j3TEC7vT09Mssyz7GUOsEoGwlaimhtA1D6UjGTcKIcLCXh6FU8AizJEaeJrg3AVRV1dcA3HQvAYibpnl612N513U/P3jwIN70UcSePHmyKMvyuzs0/suzZ8/e3tYAafj8yl68ePHRycnJt13X/W6MKW9r0DkntdbHZVn+dHR09Pjhw4ezwfj1D5PJfxrIKYY5kV/ddEPxQ54NAAVAD2t/qeds2TwCYZP1bcRPgPhNX8p/AEIA9v67Ae68AAAAAElFTkSuQmCC' + } + + return { + name: info.CFBundleName, + path: fullAppPath, + icon + } + } else { + return null // Skip non-app directories + } + }) + + const apps = await Promise.all(appPromises) + return apps.filter(Boolean) // Remove null results +} diff --git a/src/main/index.ts b/src/main/index.ts index 505f7e7..fe86905 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -9,6 +9,8 @@ const { activeWindow } = await import('@deepfocus/get-windows') 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 { get(key: K): StoreSchema[K] @@ -19,6 +21,7 @@ export interface TypedStore extends Store { } const store = new Store() 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 @@ -43,15 +46,16 @@ function updateIconBasedOnProgress() { let iconPath if (currentDeepWork >= deepWorkTarget) { - iconPath = getIconPath('icon_green.png') // Change to green icon when the target is hit - } else if (currentDeepWork > 0) { + iconPath = getIconPath('icon_green.png') + } else if (currentDeepWork > 0 && currentDeepWork < Math.floor(deepWorkTarget / 2)) { console.log('Greater than 1. Setting to blue icon') - iconPath = getIconPath('icon_blue.png') // Change to yellow icon when there is progress + iconPath = getIconPath('icon_yellow.png') + } else if (currentDeepWork > 0 && currentDeepWork > Math.floor(deepWorkTarget / 2)) { + iconPath = getIconPath('icon_blue.png') } else { - iconPath = getIconPath('icon_red.png') // Default red icon + iconPath = getIconPath('icon_red.png') } - mainWindow.setIcon(iconPath) app.dock.setIcon(iconPath) } @@ -102,7 +106,6 @@ function resetCounters(type: 'daily' | 'weekly') { }) as DeepWorkHours deepWorkHours[now.format('dddd')] = 0 - resetCounters('daily') store.set('deepWorkHours', deepWorkHours) store.set('siteTimeTrackers', currentSiteTimeTrackers) } else if (type === 'weekly') { @@ -159,6 +162,7 @@ async function createWindow(): Promise { sandbox: false } }) + app.dock.setIcon(getIconPath('icon.png')) mainWindow.on('ready-to-show', async () => { mainWindow?.show() @@ -180,7 +184,7 @@ async function createWindow(): Promise { // Main app ready event app.whenReady().then(async () => { - electronApp.setAppUserModelId('com.electron') + electronApp.setAppUserModelId('com.deepfocus.app') await createWindow().then(() => { loadUserData() @@ -205,19 +209,38 @@ function setupIPCListeners() { } }) ipcMain.on('logout-user', () => handleUserLogout()) + + // Fetch Unproductive URLs ipcMain.on('fetch-unproductive-urls', (event) => { const urls = store.get('unproductiveUrls', []) event.reply('unproductive-urls-response', urls) }) + + // Fetch Unproductive Apps + ipcMain.on('fetch-unproductive-apps', (event) => { + const apps = store.get('unproductiveApps', []) + event.reply('unproductive-apps-response', apps) + }) + + // Add or update Unproductive URLs and persist them ipcMain.on('add-unproductive-url', (event, urls) => { store.set('unproductiveUrls', urls) console.log('Unproductive URLs updated:', urls, event.processId) + event.reply('unproductive-urls-response', urls) // Send updated URLs back }) - // Event for removing unproductive URLs + // Update Unproductive Apps and persist them + ipcMain.on('update-unproductive-apps', (event, apps) => { + store.set('unproductiveApps', apps) + console.log('Updated unproductive apps:', apps) + event.reply('unproductive-apps-response', apps) // Send updated apps back + }) + + // Remove specific unproductive URL and persist ipcMain.on('remove-unproductive-url', (event, urls) => { store.set('unproductiveUrls', urls) console.log('Unproductive URLs updated:', urls, event.processId) + event.reply('unproductive-urls-response', urls) // Send updated URLs back }) ipcMain.on('fetch-deep-work-data', (event) => { @@ -232,6 +255,7 @@ function setupIPCListeners() { Saturday: 0, Sunday: 0 }) as DeepWorkHours + currentSiteTimeTrackers = store.get('siteTimeTrackers', []) schedulerWorker.postMessage({ type: MessageType.UPDATE_DATA, data: { deepWorkHours, currentSiteTimeTrackers } @@ -247,9 +271,18 @@ function setupIPCListeners() { deepWorkHours?.Saturday || 0, deepWorkHours?.Sunday || 0 ] - // Send the data back to the renderer process event.reply('deep-work-data-response', chartData) }) + ipcMain.on('fetch-app-icons', async (event) => { + try { + const apps = await getInstalledApps() + console.log('Apps are ', apps) + event.reply('app-icons-response', apps) + } catch (error) { + console.error('Error fetching app icons:', error) + event.reply('app-icons-response', []) + } + }) } function handleDailyReset() { const now = dayjs() diff --git a/src/main/types.ts b/src/main/types.ts index b52129c..72613be 100644 --- a/src/main/types.ts +++ b/src/main/types.ts @@ -19,6 +19,7 @@ export enum MessageType { export interface StoreSchema { unproductiveSites?: string[] + unproductiveApps?: string[] siteTimeTrackers: SiteTimeTracker[] user?: User lastResetDate?: string diff --git a/src/main/worker.ts b/src/main/worker.ts index 4304d31..bc9a2b6 100644 --- a/src/main/worker.ts +++ b/src/main/worker.ts @@ -17,11 +17,13 @@ function updateDeepWorkHours(siteTrackers: SiteTimeTracker[], deepWorkHours: Dee const focusIntervals: FocusInterval[] = [] siteTrackers.forEach((tracker) => { - if (tracker) { + if (tracker && isDeepWork(tracker.title)) { focusIntervals.push({ start: tracker.lastActiveTimestamp - tracker.timeSpent, end: tracker.lastActiveTimestamp }) + } else { + console.log('Not deep work', tracker.title) } }) @@ -30,8 +32,10 @@ function updateDeepWorkHours(siteTrackers: SiteTimeTracker[], deepWorkHours: Dee const totalDeepWorkTime = mergedIntervals.reduce((acc, interval) => { return acc + (interval.end - interval.start) }, 0) + console.log('totalDeepWorkTime', totalDeepWorkTime) const timeSpentInHours = totalDeepWorkTime / (1000 * 60 * 60) + console.log('timeSpentInHours', timeSpentInHours) deepWorkHours[today] = parseFloat(timeSpentInHours.toFixed(2)) console.log(`Deep work hours for ${today}: ${deepWorkHours[today]} hours`) @@ -39,18 +43,12 @@ function updateDeepWorkHours(siteTrackers: SiteTimeTracker[], deepWorkHours: Dee } //TODO: Add when logic is added in frontend + Determine if current activity is considered deep work -function isDeepWork(windowInfo) { +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.) - - const deepWorkSites = [ - 'vscode', - 'settings', - 'notion', - 'https://github.com', - 'https://chatgpt.com' - ] - return deepWorkSites.includes(windowInfo?.title?.toLowerCase()) + const formattedItem = item.replaceAll(' ', '').toLowerCase() + const deepWorkSites = ['code', 'notion', 'github', 'chatgpt', 'leetcode', 'linkedin', 'electron'] + return deepWorkSites.filter((site) => formattedItem.includes(site)).length } // Listen for messages from the main thread diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index da94f68..88468c0 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -1,5 +1,5 @@ import { lazy, Suspense, onMount, createSignal, ComponentProps } from 'solid-js' -import { Router, Route, A, useLocation } from '@solidjs/router' +import { Router, Route, A, useLocation, HashRouter } from '@solidjs/router' import { render } from 'solid-js/web' import { AuthProvider, useAuth } from './lib/AuthContext' @@ -8,6 +8,7 @@ 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')) @@ -23,7 +24,6 @@ const App = (props: ComponentProps) => { const [isNewUser, setIsNewUser] = createSignal(true) const location = useLocation() - // Check for the token in localStorage on component mount onMount(() => { const token = localStorage.getItem('token') as string const user = localStorage.getItem('user') @@ -40,7 +40,7 @@ const App = (props: ComponentProps) => { setIsNewUser(false) } - return ( + const NavBar = () => ( <>
@@ -86,23 +86,33 @@ const App = (props: ComponentProps) => { )}
+ + ) + return ( + <> + {props.children} ) } +export default App + render( () => ( Loading...}> - - - - - - + <> + + +

Hello World!

} /> + + + + +
diff --git a/src/renderer/src/Navbar.tsx b/src/renderer/src/Navbar.tsx new file mode 100644 index 0000000..f283dab --- /dev/null +++ b/src/renderer/src/Navbar.tsx @@ -0,0 +1,57 @@ +import { Button } from './components/ui/button' +import { IconSettings } from './components/ui/icons' +import { A } from '@solidjs/router' +import logo from './assets/deepWork.svg' + +const Navbar = (props) => { + const { isLoggedIn, handleLogout, isNewUser } = props + + return ( + <> +
+ + +
+ + ) +} + +export default Navbar diff --git a/src/renderer/src/Onboarding.tsx b/src/renderer/src/Onboarding.tsx new file mode 100644 index 0000000..37cdb89 --- /dev/null +++ b/src/renderer/src/Onboarding.tsx @@ -0,0 +1,40 @@ +import { createSignal, For } from 'solid-js' +import UnproductiveApps from './UnproductiveApps' +import UnproductiveWebsites from './UnproductiveWebsites' +import { Button } from './components/ui/button' +import { useNavigate } from '@solidjs/router' + +const Onboarding = () => { + const [step, setStep] = createSignal(1) + const [unproductiveSites, setUnproductiveSites] = createSignal([]) + const [unproductiveApps, setUnproductiveApps] = createSignal([]) + const navigate = useNavigate() + + const handleNext = () => { + if (step() === 2) { + navigate('/analytics') + } else { + setStep(step() + 1) + } + } + return ( +
+ {step() === 1 && ( + + )} + {step() === 2 && ( + + )} + {' '} +
+ ) +} +export default Onboarding diff --git a/src/renderer/src/Signup.tsx b/src/renderer/src/Signup.tsx index 7c33ecb..ec64b0f 100644 --- a/src/renderer/src/Signup.tsx +++ b/src/renderer/src/Signup.tsx @@ -62,8 +62,8 @@ function Signup() { localStorage.setItem('user', JSON.stringify(user)) sendUserToBackend(user) setIsLoggedIn(true) - navigate('/') - console.info('Navigating to home') + navigate('/onboarding') + console.info('Navigating to Onboarding') } catch (error) { console.error('Signup error:', error) setSignUpError('Sign-up failed. Please try again.') diff --git a/src/renderer/src/UnproductiveApps.tsx b/src/renderer/src/UnproductiveApps.tsx new file mode 100644 index 0000000..b81316e --- /dev/null +++ b/src/renderer/src/UnproductiveApps.tsx @@ -0,0 +1,94 @@ +import { createSignal, For, onMount, onCleanup } from 'solid-js' +import { Button } from './components/ui/button' + +const UnproductiveApps = () => { + const [apps, setApps] = createSignal([]) + const [unproductiveApps, setUnproductiveApps] = createSignal([]) // List of apps marked as unproductive + + // Fetch stored unproductive apps from Electron store on mount + onMount(() => { + // Fetch the app icons + window.electron.ipcRenderer.send('fetch-app-icons') + window.electron.ipcRenderer.on('app-icons-response', (event, appData) => { + const sortedApps = appData.sort((a, b) => a.name.localeCompare(b.name)) + setApps(sortedApps) + }) + + // Fetch the unproductive apps from Electron store + window.electron.ipcRenderer.send('fetch-unproductive-apps') + window.electron.ipcRenderer.on( + 'unproductive-apps-response', + (event, storedUnproductiveApps) => { + setUnproductiveApps(storedUnproductiveApps || []) + } + ) + + // Clean up IPC listeners when component unmounts + onCleanup(() => { + window.electron.ipcRenderer.removeAllListeners('app-icons-response') + window.electron.ipcRenderer.removeAllListeners('unproductive-apps-response') + }) + }) + + // Toggle unproductive apps and persist them + const toggleUnproductive = (app) => { + setUnproductiveApps((prev) => { + const updatedApps = prev.includes(app) + ? prev.filter((unproductiveApp) => unproductiveApp !== app) // Remove from unproductive apps + : [...prev, app] // Add to unproductive apps + + // Persist updated unproductive apps to Electron store + window.electron.ipcRenderer.send('update-unproductive-apps', updatedApps) + return updatedApps + }) + } + + const fetchApps = () => { + window.electron.ipcRenderer.send('fetch-app-icons') + } + + return ( +
+

Select Unproductive Apps

+ +
+
    + + {(app) => ( +
  • + {`${app.name} + {app.name} + +
  • + )} +
    +
+
+ +
+

Unproductive Apps:

+
    + + {(app) => ( +
  • + {`${app.name} + {app.name} +
  • + )} +
    +
+
+
+ ) +} + +export default UnproductiveApps diff --git a/src/renderer/src/UnproductiveWebsites.tsx b/src/renderer/src/UnproductiveWebsites.tsx new file mode 100644 index 0000000..2d38ef7 --- /dev/null +++ b/src/renderer/src/UnproductiveWebsites.tsx @@ -0,0 +1,97 @@ +import { createSignal, For, onMount, onCleanup } from 'solid-js' +import { TextField, TextFieldLabel, TextFieldInput } from './components/ui/text-field' +import { Button } from './components/ui/button' + +const UnproductiveWebsites = () => { + const [site, setSite] = createSignal('') + const [unproductiveSites, setUnproductiveSites] = createSignal([]) + + const getFavicon = (url: string) => { + try { + const domain = new URL(url).hostname + return `https://www.google.com/s2/favicons?domain=${domain}` + } catch (error) { + console.error('Invalid URL format:', url) + return '' + } + } + + onMount(() => { + window.electron.ipcRenderer.send('fetch-unproductive-urls') // Request URLs from main process + window.electron.ipcRenderer.on('unproductive-urls-response', (event, urls) => { + setUnproductiveSites(urls || []) + console.log('Unproductive URLs received from main process:', urls) + }) + + onCleanup(() => { + window.electron.ipcRenderer.removeAllListeners('unproductive-urls-response') + }) + }) + + const addSite = () => { + if (site().trim()) { + const updatedSites = [...unproductiveSites(), site().trim()] + setUnproductiveSites(updatedSites) + setSite('') + console.log('Unproductive URLs updated:', updatedSites) + window.electron.ipcRenderer.send('add-unproductive-url', updatedSites) + } + } + + const handleRemoveSite = (url: string) => { + const updatedSites = unproductiveSites().filter((item) => item !== url) + setUnproductiveSites(updatedSites) + window.electron.ipcRenderer.send('remove-unproductive-url', updatedSites) + } + + return ( +
+

Unproductive Websites

+
+ + Unproductive Websites + setSite(e.currentTarget.value)} + class="w-full p-2 border border-gray-300 rounded" + /> + + + + +
+ +
    + + {(site) => ( +
  • + {`${site} + {site} + +
  • + )} +
    +
+
+ ) +} + +export default UnproductiveWebsites diff --git a/src/renderer/src/lib/AuthContext.tsx b/src/renderer/src/lib/AuthContext.tsx index 0a1175e..705a054 100644 --- a/src/renderer/src/lib/AuthContext.tsx +++ b/src/renderer/src/lib/AuthContext.tsx @@ -1,5 +1,6 @@ import { createContext, useContext, createSignal, Accessor, Setter, JSX } from 'solid-js' - +import { render } from 'solid-js/web' +import App from '../App' // Create a context with two signals: `isLoggedIn` and `setIsLoggedIn` type AuthContextType = [Accessor, Setter] const AuthContext = createContext()