Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
828 changes: 756 additions & 72 deletions dist-electron/main.cjs

Large diffs are not rendered by default.

49 changes: 20 additions & 29 deletions dist-electron/preload.cjs
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
Expand All @@ -12,25 +10,16 @@ var __copyProps = (to, from, except, desc) => {
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);

// electron/preload.ts
var preload_exports = {};
module.exports = __toCommonJS(preload_exports);
var electron = __toESM(require("electron"), 1);
var { contextBridge, ipcRenderer } = electron;
var import_electron = require("electron");
var api = {
getUsageSnapshot: async () => {
try {
const snapshot = await ipcRenderer.invoke("usage:snapshot");
const snapshot = await import_electron.ipcRenderer.invoke("usage:snapshot");
return { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), ...snapshot };
} catch {
return {
Expand All @@ -44,14 +33,14 @@ var api = {
},
getSuggestionFeed: async () => {
try {
return await ipcRenderer.invoke("suggestions:list");
return await import_electron.ipcRenderer.invoke("suggestions:list");
} catch {
return [];
}
},
clearUsageData: async () => {
try {
return await ipcRenderer.invoke("usage:clear");
return await import_electron.ipcRenderer.invoke("usage:clear");
} catch {
return {
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
Expand All @@ -64,7 +53,7 @@ var api = {
},
getNotificationSummary: async () => {
try {
return await ipcRenderer.invoke("notifications:summary");
return await import_electron.ipcRenderer.invoke("notifications:summary");
} catch {
return {
total: 0,
Expand All @@ -75,68 +64,70 @@ var api = {
},
setTheme: async (theme) => {
try {
return await ipcRenderer.invoke("theme:set", theme);
return await import_electron.ipcRenderer.invoke("theme:set", theme);
} catch {
return false;
}
},
// Settings
getSettings: async () => {
try {
return await ipcRenderer.invoke("settings:get");
return await import_electron.ipcRenderer.invoke("settings:get");
} catch {
return {
minimizeToTray: true,
startWithWindows: false,
timeLimits: [],
timeLimitNotificationsEnabled: true
timeLimitNotificationsEnabled: true,
language: "zh-CN"
};
}
},
setSettings: async (settings) => {
try {
return await ipcRenderer.invoke("settings:set", settings);
return await import_electron.ipcRenderer.invoke("settings:set", settings);
} catch {
return {
minimizeToTray: true,
startWithWindows: false,
timeLimits: [],
timeLimitNotificationsEnabled: true
timeLimitNotificationsEnabled: true,
language: "zh-CN"
};
}
},
// Time limits
getTimeLimits: async () => {
try {
return await ipcRenderer.invoke("timelimits:get");
return await import_electron.ipcRenderer.invoke("timelimits:get");
} catch {
return [];
}
},
setTimeLimits: async (limits) => {
try {
return await ipcRenderer.invoke("timelimits:set", limits);
return await import_electron.ipcRenderer.invoke("timelimits:set", limits);
} catch {
return [];
}
},
addTimeLimit: async (limit) => {
try {
return await ipcRenderer.invoke("timelimits:add", limit);
return await import_electron.ipcRenderer.invoke("timelimits:add", limit);
} catch {
return [];
}
},
removeTimeLimit: async (appId) => {
try {
return await ipcRenderer.invoke("timelimits:remove", appId);
return await import_electron.ipcRenderer.invoke("timelimits:remove", appId);
} catch {
return [];
}
},
getTimeLimitAlerts: async () => {
try {
return await ipcRenderer.invoke("timelimits:alerts");
return await import_electron.ipcRenderer.invoke("timelimits:alerts");
} catch {
return [];
}
Expand All @@ -146,10 +137,10 @@ var api = {
const handler = (_event, data) => {
callback(data);
};
ipcRenderer.on("time-limit-exceeded", handler);
import_electron.ipcRenderer.on("time-limit-exceeded", handler);
return () => {
ipcRenderer.removeListener("time-limit-exceeded", handler);
import_electron.ipcRenderer.removeListener("time-limit-exceeded", handler);
};
}
};
contextBridge.exposeInMainWorld("screenforge", api);
import_electron.contextBridge.exposeInMainWorld("screenforge", api);
102 changes: 59 additions & 43 deletions electron/main.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
import * as electron from 'electron'
import { app, BrowserWindow, Tray, Menu, nativeImage, Notification, ipcMain } from 'electron'
import path from 'node:path'
import * as fs from 'node:fs'
import { createNotificationTracker } from './notifications'
import { createUsageTracker } from './telemetry'

const { app, BrowserWindow, Tray, Menu, nativeImage, Notification } = electron
const { ipcMain } = electron
import { defaultLocale, normalizeLocale, translate, type LocaleCode } from '../src/i18n/core'

const isDev = Boolean(process.env.VITE_DEV_SERVER_URL)

const usageTracker = createUsageTracker()
const notificationTracker = createNotificationTracker()

let mainWindow: electron.BrowserWindow | null = null
let tray: electron.Tray | null = null
let mainWindow: BrowserWindow | null = null
let tray: Tray | null = null
let isQuitting = false

const ZOOM_STEP = 0.1
Expand Down Expand Up @@ -57,6 +55,7 @@ interface AppSettings {
startWithWindows: boolean
timeLimits: AppTimeLimit[]
timeLimitNotificationsEnabled: boolean
language: LocaleCode
}

// Track which alerts have been shown today to avoid spam
Expand All @@ -71,11 +70,15 @@ let settings: AppSettings = {
startWithWindows: false,
timeLimits: [],
timeLimitNotificationsEnabled: true,
language: defaultLocale,
}

let shownAlerts: TimeLimitAlert[] = []

// Get today's date in Windows local timezone
const mt = (key: string, params?: Record<string, string | number>) =>
translate(settings.language, key, params)

const getTodayDateString = (): string => {
const now = new Date()
const year = now.getFullYear()
Expand Down Expand Up @@ -105,6 +108,7 @@ const loadSettings = () => {
startWithWindows: loaded.startWithWindows ?? false,
timeLimits: loaded.timeLimits ?? [],
timeLimitNotificationsEnabled: loaded.timeLimitNotificationsEnabled ?? true,
language: normalizeLocale(loaded.language),
}
}
} catch {
Expand Down Expand Up @@ -188,8 +192,12 @@ const checkTimeLimits = () => {

// Show notification
const notification = new Notification({
title: 'Time Limit Reached',
body: `You've used ${appName} for ${usedMinutes} minutes today. Your limit is ${limit.limitMinutes} minutes.`,
title: mt('native.timeLimitReachedTitle'),
body: mt('native.timeLimitReachedBody', {
appName,
usedMinutes,
limitMinutes: limit.limitMinutes,
}),
icon: undefined,
silent: false,
})
Expand Down Expand Up @@ -226,8 +234,8 @@ const generateSuggestions = () => {
if (snapshot.usageEntries.length === 0) {
suggestions.push({
id: 'welcome',
title: 'Welcome to ScreenForge!',
detail: 'Keep the app running to track your screen time automatically.',
title: mt('suggestions.welcome.title'),
detail: mt('suggestions.welcome.detail'),
})
return suggestions
}
Expand All @@ -250,8 +258,8 @@ const generateSuggestions = () => {
if (entertainmentMinutes > 0 && entertainmentMinutes / totalMinutes > 0.3) {
suggestions.push({
id: 'entertainment',
title: 'High entertainment usage',
detail: 'Consider setting time limits for entertainment apps to boost productivity.',
title: mt('suggestions.entertainment.title'),
detail: mt('suggestions.entertainment.detail'),
})
}

Expand All @@ -260,8 +268,8 @@ const generateSuggestions = () => {
if (socialMinutes > 0 && socialMinutes / totalMinutes > 0.2) {
suggestions.push({
id: 'social',
title: 'Social apps taking over',
detail: 'Try scheduling specific times for checking social media.',
title: mt('suggestions.social.title'),
detail: mt('suggestions.social.detail'),
})
}

Expand All @@ -270,29 +278,55 @@ const generateSuggestions = () => {
if (productiveMinutes > 0 && productiveMinutes / totalMinutes > 0.5) {
suggestions.push({
id: 'productive',
title: 'Great focus!',
detail: 'You\'re spending most of your time on productive tasks. Keep it up!',
title: mt('suggestions.productive.title'),
detail: mt('suggestions.productive.detail'),
})
}

// General tips
if (suggestions.length === 0) {
suggestions.push({
id: 'balance',
title: 'Balanced usage',
detail: 'Your screen time is well distributed across different activities.',
title: mt('suggestions.balance.title'),
detail: mt('suggestions.balance.detail'),
})
}

suggestions.push({
id: 'breaks',
title: 'Remember to take breaks',
detail: 'Use the 20-20-20 rule: every 20 minutes, look at something 20 feet away for 20 seconds.',
title: mt('suggestions.breaks.title'),
detail: mt('suggestions.breaks.detail'),
})

return suggestions
}

const buildTrayMenu = () => Menu.buildFromTemplate([
{
label: mt('native.trayShow'),
click: () => {
if (mainWindow) {
mainWindow.show()
mainWindow.focus()
}
},
},
{ type: 'separator' },
{
label: mt('native.trayQuit'),
click: () => {
isQuitting = true
app.quit()
},
},
])

const updateTrayMenu = () => {
if (!tray) return
tray.setToolTip(mt('native.trayTooltip'))
tray.setContextMenu(buildTrayMenu())
}

const createTray = () => {
// Create the ScreenForge tray icon programmatically
// 16x16 icon with monitor shape in blue accent color
Expand Down Expand Up @@ -346,29 +380,7 @@ const createTray = () => {
const trayIcon = nativeImage.createFromBuffer(canvas, { width: size, height: size })

tray = new Tray(trayIcon)
tray.setToolTip('ScreenForge - Screen Time Tracker')

const contextMenu = Menu.buildFromTemplate([
{
label: 'Show ScreenForge',
click: () => {
if (mainWindow) {
mainWindow.show()
mainWindow.focus()
}
},
},
{ type: 'separator' },
{
label: 'Quit',
click: () => {
isQuitting = true
app.quit()
},
},
])

tray.setContextMenu(contextMenu)
updateTrayMenu()

// Double-click to show window
tray.on('double-click', () => {
Expand Down Expand Up @@ -564,6 +576,10 @@ app.whenReady().then(() => {
if (typeof newSettings.timeLimitNotificationsEnabled === 'boolean') {
settings.timeLimitNotificationsEnabled = newSettings.timeLimitNotificationsEnabled
}
if (newSettings.language) {
settings.language = normalizeLocale(newSettings.language)
updateTrayMenu()
}
saveSettings()
return settings
})
Expand Down
4 changes: 2 additions & 2 deletions electron/notifications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ if ($unique.Count -eq 0) {
`

try {
const { stdout, stderr } = await execFileAsync('powershell', [
const { stdout } = await execFileAsync('powershell', [
'-NoProfile',
'-ExecutionPolicy',
'Bypass',
Expand Down Expand Up @@ -291,7 +291,7 @@ if ($unique.Count -eq 0) {
}

export const createNotificationTracker = () => {
let persistedData = loadPersistedData()
const persistedData = loadPersistedData()
let lastError: string | undefined
let saveTimeout: NodeJS.Timeout | null = null

Expand Down
Loading