diff --git a/README.md b/README.md
index 3a405e2..1e992f7 100644
--- a/README.md
+++ b/README.md
@@ -34,7 +34,7 @@
-
+
@@ -55,9 +55,15 @@
## 📥 Installation
-### Windows
+### Download
-Download the latest installer from the [Releases](https://github.com/altaskur/OpenTimeTracker/releases) page.
+Download the latest installer for your platform from the [Releases](https://github.com/altaskur/OpenTimeTracker/releases) page:
+
+| Platform | Format | Architecture |
+| -------- | ----------------------- | -------------------------- |
+| Windows | `.exe` (NSIS installer) | x64 |
+| macOS | `.dmg` | x64, arm64 (Apple Silicon) |
+| Linux | `.AppImage`, `.deb` | x64 |
### Build from Source
@@ -90,7 +96,10 @@ npm run dev
| `npm start` | Start Angular dev server (port 4200) |
| `npm run dev` | Build and run Electron app |
| `npm run build` | Production build |
+| `npm run dist` | Generate installer for current OS |
| `npm run dist:win` | Generate Windows installer |
+| `npm run dist:mac` | Generate macOS installer (DMG) |
+| `npm run dist:linux` | Generate Linux installers |
| `npm test` | Run Angular tests |
| `npm run test:electron` | Run Electron tests |
| `npm run lint` | Run ESLint |
diff --git a/electron/src/utils/paths.spec.ts b/electron/src/utils/paths.spec.ts
index 07efc0a..6c954d1 100644
--- a/electron/src/utils/paths.spec.ts
+++ b/electron/src/utils/paths.spec.ts
@@ -1,4 +1,13 @@
-import { describe, it, expect, beforeEach, vi, type Mock, type Mocked } from 'vitest';
+import {
+ describe,
+ it,
+ expect,
+ beforeEach,
+ afterEach,
+ vi,
+ type Mock,
+ type Mocked,
+} from 'vitest';
import path from 'node:path';
import { app } from 'electron';
import {
@@ -17,9 +26,25 @@ import {
*/
describe('Paths Utility', () => {
const mockApp = app as Mocked;
+ const originalResourcesPath = process.resourcesPath;
beforeEach(() => {
vi.clearAllMocks();
+ // Mock process.resourcesPath for packaged mode tests
+ Object.defineProperty(process, 'resourcesPath', {
+ value: '/mock/app/resources',
+ configurable: true,
+ writable: true,
+ });
+ });
+
+ afterEach(() => {
+ // Restore original value
+ Object.defineProperty(process, 'resourcesPath', {
+ value: originalResourcesPath,
+ configurable: true,
+ writable: true,
+ });
});
describe('isPackaged', () => {
@@ -150,9 +175,6 @@ describe('Paths Utility', () => {
value: true,
configurable: true,
});
- (mockApp.getPath as Mock).mockReturnValue(
- 'C:\\Program Files\\OpenTimeTracker\\OpenTimeTracker.exe',
- );
const result = getIndexPath();
@@ -180,9 +202,6 @@ describe('Paths Utility', () => {
value: true,
configurable: true,
});
- (mockApp.getPath as Mock).mockReturnValue(
- 'C:\\Program Files\\OpenTimeTracker\\OpenTimeTracker.exe',
- );
const result = getPreloadPath();
@@ -210,14 +229,11 @@ describe('Paths Utility', () => {
value: true,
configurable: true,
});
- (mockApp.getPath as Mock).mockReturnValue(
- 'C:\\Program Files\\OpenTimeTracker\\OpenTimeTracker.exe',
- );
const result = getTemplateDatabasePath();
expect(result).toContain('resources');
- expect(result).toContain('app.asar');
+ expect(result).toContain('app.asar.unpacked');
expect(result).toContain('template.db');
});
});
diff --git a/electron/src/utils/paths.ts b/electron/src/utils/paths.ts
index fcef0d0..d1c1062 100644
--- a/electron/src/utils/paths.ts
+++ b/electron/src/utils/paths.ts
@@ -55,12 +55,12 @@ export const getBackupPath = (): string => {
* Gets the path to the Angular index.html file.
* In development: dist/OpenTimeTracker/browser/index.html
* In production: resources/app.asar/dist/OpenTimeTracker/browser/index.html
+ * Note: Uses process.resourcesPath for cross-platform compatibility (Windows/macOS/Linux)
*/
export const getIndexPath = (): string => {
if (isPackaged()) {
return path.join(
- path.dirname(app.getPath('exe')),
- 'resources',
+ process.resourcesPath,
'app.asar',
'dist',
'OpenTimeTracker',
@@ -80,12 +80,12 @@ export const getIndexPath = (): string => {
/**
* Gets the path to the preload script.
+ * Note: Uses process.resourcesPath for cross-platform compatibility (Windows/macOS/Linux)
*/
export const getPreloadPath = (): string => {
if (isPackaged()) {
return path.join(
- path.dirname(app.getPath('exe')),
- 'resources',
+ process.resourcesPath,
'app.asar',
'dist',
'electron',
@@ -100,14 +100,14 @@ export const getPreloadPath = (): string => {
* Gets the path to the template database file.
* Used to initialize a new database with pre-created schema.
* In development: prisma/template.db
- * In production: resources/app.asar/prisma/template.db
+ * In production: resources/app.asar.unpacked/prisma/template.db
+ * Note: Template is in asarUnpack so we use app.asar.unpacked for cross-platform compatibility
*/
export const getTemplateDatabasePath = (): string => {
if (isPackaged()) {
return path.join(
- path.dirname(app.getPath('exe')),
- 'resources',
- 'app.asar',
+ process.resourcesPath,
+ 'app.asar.unpacked',
'prisma',
'template.db',
);
diff --git a/package.json b/package.json
index d95d7f4..13a93e9 100644
--- a/package.json
+++ b/package.json
@@ -16,8 +16,9 @@
"dist": "npm run build && electron-builder",
"dist:win": "npm run build && electron-builder --win",
"dist:linux": "npm run build && electron-builder --linux",
+ "dist:mac": "npm run build && electron-builder --mac",
"pack": "electron-builder --dir",
- "prisma:generate": "prisma generate",
+ "prisma:generate": "cross-env DATABASE_URL=file:./dist/data/timetracker.db prisma generate",
"prisma:push": "cross-env DATABASE_URL=file:./dist/data/timetracker.db prisma db push",
"prisma:studio": "cross-env DATABASE_URL=file:./dist/data/timetracker.db prisma studio",
"prisma:migrate": "cross-env DATABASE_URL=file:./dist/data/timetracker.db prisma migrate dev",
@@ -57,7 +58,8 @@
"node_modules/@prisma/client/**/*",
"node_modules/@prisma/adapter-better-sqlite3/**/*",
"node_modules/better-sqlite3/**/*",
- "dist/electron/generated/**/*"
+ "dist/electron/generated/**/*",
+ "prisma/template.db"
],
"win": {
"target": [
@@ -99,6 +101,34 @@
"maintainer": "altaskur",
"synopsis": "Time tracking application",
"description": "Free and open source time tracking application for developers and teams"
+ },
+ "mac": {
+ "target": [
+ {
+ "target": "dmg",
+ "arch": [
+ "x64",
+ "arm64"
+ ]
+ }
+ ],
+ "icon": "public/icon-512.png",
+ "category": "public.app-category.productivity"
+ },
+ "dmg": {
+ "title": "${productName} ${version}",
+ "contents": [
+ {
+ "x": 130,
+ "y": 220
+ },
+ {
+ "x": 410,
+ "y": 220,
+ "type": "link",
+ "path": "/Applications"
+ }
+ ]
}
},
"prettier": {
diff --git a/prisma.config.ts b/prisma.config.ts
index 941bebb..22cf7be 100644
--- a/prisma.config.ts
+++ b/prisma.config.ts
@@ -1,7 +1,9 @@
import path from 'node:path';
-import { defineConfig } from 'prisma/config';
+import { defineConfig, env } from 'prisma/config';
export default defineConfig({
- earlyAccess: true,
schema: path.join(import.meta.dirname, 'prisma', 'schema.prisma'),
+ datasource: {
+ url: env('DATABASE_URL') ?? 'file:./dist/data/timetracker.db',
+ },
});
diff --git a/prisma/template.db b/prisma/template.db
index 8c07e95..3282ac5 100644
Binary files a/prisma/template.db and b/prisma/template.db differ
diff --git a/public/icon-512.png b/public/icon-512.png
new file mode 100644
index 0000000..188187d
Binary files /dev/null and b/public/icon-512.png differ
diff --git a/scripts/update-db-template.mjs b/scripts/update-db-template.mjs
index 81ae375..7e03053 100644
--- a/scripts/update-db-template.mjs
+++ b/scripts/update-db-template.mjs
@@ -17,9 +17,9 @@ const templatePath = join(__dirname, "..", "prisma", "template.db");
console.log("Regenerating database template...\n");
try {
- console.log("1. Resetting database with migrations...");
+ console.log("1. Resetting database with schema...");
execSync(
- "cross-env DATABASE_URL=file:./dist/data/timetracker.db npx prisma migrate reset --force --skip-seed --skip-generate",
+ "cross-env DATABASE_URL=file:./dist/data/timetracker.db npx prisma db push --force-reset --accept-data-loss",
{
stdio: "inherit",
cwd: join(__dirname, ".."),