fontastic 1.3.2 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/.github/workflows/claude-code-review.yml +1 -1
  2. package/.github/workflows/claude.yml +1 -1
  3. package/.github/workflows/macos.yml +5 -5
  4. package/.github/workflows/release-please.yml +1 -1
  5. package/.github/workflows/release.yml +2 -2
  6. package/.github/workflows/ubuntu.yml +7 -7
  7. package/.github/workflows/windows.yml +5 -5
  8. package/README.md +4 -4
  9. package/angular.json +8 -3
  10. package/app/core/FontFinder.js +17 -12
  11. package/app/core/FontFinder.ts +18 -12
  12. package/app/core/FontManager.js +13 -14
  13. package/app/core/FontManager.ts +12 -13
  14. package/app/core/MessageHandler.js +0 -1
  15. package/app/core/MessageHandler.ts +0 -4
  16. package/app/database/entity/Store.schema.js +1 -0
  17. package/app/database/entity/Store.schema.ts +1 -0
  18. package/app/database/repository/Collection.repository.js +0 -11
  19. package/app/database/repository/Collection.repository.ts +0 -22
  20. package/app/database/repository/Store.repository.js +50 -69
  21. package/app/database/repository/Store.repository.ts +47 -86
  22. package/app/enums/ChannelType.js +0 -1
  23. package/app/enums/ChannelType.ts +0 -1
  24. package/app/main.js +3 -2
  25. package/app/main.ts +4 -3
  26. package/app/package-lock.json +144 -1104
  27. package/app/package.json +4 -6
  28. package/app/preload.js +51 -0
  29. package/app/preload.ts +59 -0
  30. package/app/types/Bridge.js +3 -0
  31. package/app/types/Bridge.ts +19 -0
  32. package/app/types/index.js +1 -0
  33. package/app/types/index.ts +1 -0
  34. package/knip.json +18 -0
  35. package/package.json +44 -53
  36. package/src/app/app.component.spec.ts +3 -3
  37. package/src/app/app.component.ts +2 -15
  38. package/src/app/core/services/database/database.service.ts +8 -15
  39. package/src/app/core/services/electron/electron.service.ts +5 -46
  40. package/src/app/core/services/font-loader/font-loader.service.ts +60 -0
  41. package/src/app/core/services/index.ts +1 -0
  42. package/src/app/core/services/message/message.service.ts +19 -27
  43. package/src/app/core/services/presentation/presentation.service.ts +9 -2
  44. package/src/app/home/home.component.spec.ts +3 -3
  45. package/src/app/home/home.component.ts +4 -8
  46. package/src/app/layout/footer/footer.component.ts +6 -6
  47. package/src/app/shared/components/index.ts +0 -1
  48. package/src/app/shared/components/page-not-found/page-not-found.component.ts +2 -8
  49. package/src/app/shared/components/preview/preview.component.html +1 -0
  50. package/src/app/shared/components/preview/preview.component.ts +3 -31
  51. package/src/app/shared/components/rule-builder/rule-builder.component.html +4 -4
  52. package/src/app/shared/components/rule-builder/rule-builder.component.ts +13 -13
  53. package/src/app/shared/components/waterfall/waterfall.component.ts +1 -1
  54. package/src/app/shared/directives/index.ts +1 -1
  55. package/src/app/shared/directives/lazy-font/lazy-font.directive.ts +23 -0
  56. package/src/app/shared/shared.module.ts +3 -3
  57. package/src/main.ts +2 -2
  58. package/tsconfig.serve.json +4 -16
  59. package/app/helpers/command.js +0 -28
  60. package/app/helpers/command.ts +0 -20
  61. package/app/helpers/random.js +0 -16
  62. package/app/helpers/random.ts +0 -12
  63. package/src/app/shared/components/prompt-dialog/prompt-dialog.component.html +0 -36
  64. package/src/app/shared/components/prompt-dialog/prompt-dialog.component.ts +0 -40
  65. package/src/app/shared/directives/webview/webview.directive.spec.ts +0 -8
  66. package/src/app/shared/directives/webview/webview.directive.ts +0 -9
  67. package/src/styles/themes/dashboard.scss +0 -293
  68. package/src/styles/themes/euphoria.scss +0 -284
  69. package/src/styles/themes/mellow.scss +0 -281
  70. package/src/styles/themes/midnight.scss +0 -284
  71. package/src/styles/themes/passion.scss +0 -281
  72. package/src/styles/themes/swiss.scss +0 -284
package/app/package.json CHANGED
@@ -5,19 +5,17 @@
5
5
  "name": "Tom Shaw",
6
6
  "email": ""
7
7
  },
8
- "version": "1.3.2",
8
+ "version": "1.4.0",
9
9
  "main": "main.js",
10
10
  "private": true,
11
11
  "dependencies": {
12
- "electron-log": "^4.4.8",
12
+ "electron-log": "^5.4.4",
13
13
  "electron-store": "^8.2.0",
14
14
  "mime": "^3.0.0",
15
- "node-fetch": "^2.7.0",
16
15
  "node-machine-id": "^1.1.12",
17
16
  "fontkit": "^2.0.4",
18
17
  "pretty-bytes": "^5.6.0",
19
- "better-sqlite3": "^12.6.2",
20
- "sudo-prompt": "^9.2.1",
21
- "typeorm": "^0.3.20"
18
+ "better-sqlite3": "^12.11.1",
19
+ "typeorm": "^1.0.0"
22
20
  }
23
21
  }
package/app/preload.js ADDED
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ var _a, _b, _c;
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const electron_1 = require("electron");
5
+ // NOTE: This file runs in the sandboxed preload context. It must stay
6
+ // self-contained — sandboxed preloads cannot require local modules, so the
7
+ // scan-progress channel name is duplicated here rather than imported.
8
+ const SCAN_PROGRESS_PORT_CHANNEL = 'IPC_SCAN_PROGRESS_PORT';
9
+ let nextSubscriptionId = 0;
10
+ const subscriptions = new Map();
11
+ // MessagePorts cannot cross the context bridge directly; forward them into
12
+ // the isolated world through window.postMessage, which supports transferables.
13
+ electron_1.ipcRenderer.on(SCAN_PROGRESS_PORT_CHANNEL, (event) => {
14
+ window.postMessage({ type: SCAN_PROGRESS_PORT_CHANNEL }, '*', event.ports);
15
+ });
16
+ electron_1.contextBridge.exposeInMainWorld('fontastic', {
17
+ versions: {
18
+ app: '',
19
+ electron: (_a = process.versions.electron) !== null && _a !== void 0 ? _a : '',
20
+ chrome: (_b = process.versions.chrome) !== null && _b !== void 0 ? _b : '',
21
+ node: (_c = process.versions.node) !== null && _c !== void 0 ? _c : '',
22
+ },
23
+ invoke: (channel, args) => electron_1.ipcRenderer.invoke(channel, args),
24
+ send: (channel, args) => electron_1.ipcRenderer.send(channel, args),
25
+ once: (channel, listener) => {
26
+ electron_1.ipcRenderer.once(channel, (_event, ...args) => listener(...args));
27
+ },
28
+ on: (channel, listener) => {
29
+ const wrapped = (_event, ...args) => listener(...args);
30
+ const id = ++nextSubscriptionId;
31
+ subscriptions.set(id, { channel, wrapped });
32
+ electron_1.ipcRenderer.on(channel, wrapped);
33
+ return id;
34
+ },
35
+ off: (subscriptionId) => {
36
+ const sub = subscriptions.get(subscriptionId);
37
+ if (sub) {
38
+ electron_1.ipcRenderer.removeListener(sub.channel, sub.wrapped);
39
+ subscriptions.delete(subscriptionId);
40
+ }
41
+ },
42
+ removeAllListeners: (channel) => {
43
+ for (const [id, sub] of subscriptions) {
44
+ if (sub.channel === channel) {
45
+ subscriptions.delete(id);
46
+ }
47
+ }
48
+ electron_1.ipcRenderer.removeAllListeners(channel);
49
+ },
50
+ });
51
+ //# sourceMappingURL=preload.js.map
package/app/preload.ts ADDED
@@ -0,0 +1,59 @@
1
+ import { contextBridge, ipcRenderer, IpcRendererEvent } from 'electron';
2
+
3
+ // NOTE: This file runs in the sandboxed preload context. It must stay
4
+ // self-contained — sandboxed preloads cannot require local modules, so the
5
+ // scan-progress channel name is duplicated here rather than imported.
6
+ const SCAN_PROGRESS_PORT_CHANNEL = 'IPC_SCAN_PROGRESS_PORT';
7
+
8
+ type Listener = (...args: unknown[]) => void;
9
+
10
+ let nextSubscriptionId = 0;
11
+ const subscriptions = new Map<number, { channel: string; wrapped: (event: IpcRendererEvent, ...args: unknown[]) => void }>();
12
+
13
+ // MessagePorts cannot cross the context bridge directly; forward them into
14
+ // the isolated world through window.postMessage, which supports transferables.
15
+ ipcRenderer.on(SCAN_PROGRESS_PORT_CHANNEL, (event) => {
16
+ window.postMessage({ type: SCAN_PROGRESS_PORT_CHANNEL }, '*', event.ports);
17
+ });
18
+
19
+ contextBridge.exposeInMainWorld('fontastic', {
20
+ versions: {
21
+ app: '',
22
+ electron: process.versions.electron ?? '',
23
+ chrome: process.versions.chrome ?? '',
24
+ node: process.versions.node ?? '',
25
+ },
26
+
27
+ invoke: (channel: string, args?: unknown): Promise<unknown> => ipcRenderer.invoke(channel, args),
28
+
29
+ send: (channel: string, args?: unknown): void => ipcRenderer.send(channel, args),
30
+
31
+ once: (channel: string, listener: Listener): void => {
32
+ ipcRenderer.once(channel, (_event, ...args) => listener(...args));
33
+ },
34
+
35
+ on: (channel: string, listener: Listener): number => {
36
+ const wrapped = (_event: IpcRendererEvent, ...args: unknown[]) => listener(...args);
37
+ const id = ++nextSubscriptionId;
38
+ subscriptions.set(id, { channel, wrapped });
39
+ ipcRenderer.on(channel, wrapped);
40
+ return id;
41
+ },
42
+
43
+ off: (subscriptionId: number): void => {
44
+ const sub = subscriptions.get(subscriptionId);
45
+ if (sub) {
46
+ ipcRenderer.removeListener(sub.channel, sub.wrapped);
47
+ subscriptions.delete(subscriptionId);
48
+ }
49
+ },
50
+
51
+ removeAllListeners: (channel: string): void => {
52
+ for (const [id, sub] of subscriptions) {
53
+ if (sub.channel === channel) {
54
+ subscriptions.delete(id);
55
+ }
56
+ }
57
+ ipcRenderer.removeAllListeners(channel);
58
+ },
59
+ });
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=Bridge.js.map
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The typed API exposed to the renderer by app/preload.ts via contextBridge.
3
+ * Available in the renderer as `window.fontastic`.
4
+ */
5
+ export interface FontasticBridge {
6
+ versions: {
7
+ app: string;
8
+ electron: string;
9
+ chrome: string;
10
+ node: string;
11
+ };
12
+ invoke<T = unknown>(channel: string, args?: unknown): Promise<T>;
13
+ send(channel: string, args?: unknown): void;
14
+ once(channel: string, listener: (...args: any[]) => void): void;
15
+ /** Subscribes and returns a subscription id for `off()`. */
16
+ on(channel: string, listener: (...args: any[]) => void): number;
17
+ off(subscriptionId: number): void;
18
+ removeAllListeners(channel: string): void;
19
+ }
@@ -27,4 +27,5 @@ __exportStar(require("./SmartCollection"), exports);
27
27
  __exportStar(require("./NativeThemeState"), exports);
28
28
  __exportStar(require("./SystemPreferencesState"), exports);
29
29
  __exportStar(require("./ScanProgress"), exports);
30
+ __exportStar(require("./Bridge"), exports);
30
31
  //# sourceMappingURL=index.js.map
@@ -11,3 +11,4 @@ export * from './SmartCollection';
11
11
  export * from './NativeThemeState';
12
12
  export * from './SystemPreferencesState';
13
13
  export * from './ScanProgress';
14
+ export * from './Bridge';
package/knip.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "$schema": "https://unpkg.com/knip@6/schema.json",
3
+ "entry": ["src/main.ts", "app/main.ts", "e2e/**/*.spec.ts", "scripts/*.js"],
4
+ "project": ["src/**/*.ts", "app/**/*.ts"],
5
+ "ignore": ["app/**/*.js"],
6
+ "ignoreDependencies": [
7
+ "@fontsource-variable/inter",
8
+ "material-icons",
9
+ "electron-debug",
10
+ "electron-reloader",
11
+ "@angular-eslint/schematics",
12
+ "@angular-eslint/builder",
13
+ "@angular/language-service",
14
+ "@vitest/browser-playwright",
15
+ "@vitest/coverage-v8",
16
+ "@vitest/ui"
17
+ ]
18
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fontastic",
3
- "version": "1.3.2",
3
+ "version": "1.4.0",
4
4
  "description": "Fontastic is an Electron-based font management and cataloging application built for organizing, browsing, and inspecting font libraries.",
5
5
  "homepage": "https://github.com/tomshaw/fontastic",
6
6
  "private": false,
@@ -12,7 +12,7 @@
12
12
  "angular",
13
13
  "angular 21",
14
14
  "electron",
15
- "electron 40",
15
+ "electron 43",
16
16
  "nodejs",
17
17
  "typescript",
18
18
  "vitest",
@@ -51,72 +51,63 @@
51
51
  "prepare": "husky"
52
52
  },
53
53
  "dependencies": {
54
- "@angular/cdk": "21.2.1",
55
- "@angular/common": "21.2.0",
56
- "@angular/compiler": "21.2.0",
57
- "@angular/core": "21.2.0",
58
- "@angular/forms": "21.2.0",
59
- "@angular/language-service": "21.2.0",
60
- "@angular/platform-browser": "21.2.0",
61
- "@angular/platform-browser-dynamic": "21.2.0",
62
- "@angular/router": "21.2.0",
54
+ "@angular/common": "21.2.17",
55
+ "@angular/compiler": "21.2.17",
56
+ "@angular/core": "21.2.17",
57
+ "@angular/forms": "21.2.17",
58
+ "@angular/platform-browser": "21.2.17",
59
+ "@angular/router": "21.2.17",
63
60
  "@fontsource-variable/inter": "5.2.8",
64
- "electron-store": "11.0.2",
65
- "fontkit": "2.0.4",
66
61
  "material-icons": "1.13.14",
67
62
  "rxjs": "7.8.2",
68
- "tslib": "2.8.1",
69
- "zone.js": "0.16.1"
63
+ "tslib": "2.8.1"
70
64
  },
71
65
  "devDependencies": {
72
- "@angular-eslint/builder": "21.2.0",
73
- "@angular-eslint/eslint-plugin": "21.2.0",
74
- "@angular-eslint/eslint-plugin-template": "21.2.0",
75
- "@angular-eslint/schematics": "21.2.0",
76
- "@angular-eslint/template-parser": "21.2.0",
77
- "@angular/build": "21.2.0",
78
- "@angular/cli": "21.2.0",
79
- "@angular/compiler-cli": "21.2.0",
80
- "@ngx-translate/core": "17.0.0",
81
- "@ngx-translate/http-loader": "17.0.0",
82
- "@playwright/test": "1.58.2",
83
- "@tailwindcss/postcss": "4.2.1",
84
- "@types/node": "24.10.15",
85
- "@typescript-eslint/eslint-plugin": "8.56.1",
86
- "@typescript-eslint/parser": "8.56.1",
87
- "@vitest/browser-playwright": "4.0.18",
88
- "@vitest/coverage-v8": "4.0.18",
89
- "@vitest/ui": "4.0.18",
90
- "electron": "40.6.1",
91
- "electron-builder": "26.8.1",
66
+ "@angular-eslint/builder": "21.4.0",
67
+ "@angular-eslint/eslint-plugin": "21.4.0",
68
+ "@angular-eslint/eslint-plugin-template": "21.4.0",
69
+ "@angular-eslint/schematics": "21.4.0",
70
+ "@angular-eslint/template-parser": "21.4.0",
71
+ "@angular/build": "21.2.18",
72
+ "@angular/cli": "21.2.18",
73
+ "@angular/compiler-cli": "21.2.17",
74
+ "@angular/language-service": "21.2.17",
75
+ "@eslint/js": "10.0.1",
76
+ "@ngx-translate/core": "18.0.0",
77
+ "@ngx-translate/http-loader": "18.0.0",
78
+ "@playwright/test": "1.61.1",
79
+ "@tailwindcss/postcss": "4.3.2",
80
+ "@types/node": "24.13.2",
81
+ "@typescript-eslint/eslint-plugin": "8.62.1",
82
+ "@typescript-eslint/parser": "8.62.1",
83
+ "@vitest/browser-playwright": "4.1.9",
84
+ "@vitest/coverage-v8": "4.1.9",
85
+ "@vitest/ui": "4.1.9",
86
+ "electron": "43.0.0",
87
+ "electron-builder": "26.15.3",
92
88
  "electron-debug": "4.1.0",
93
89
  "electron-reloader": "1.2.3",
94
- "eslint": "9.39.1",
90
+ "eslint": "10.6.0",
95
91
  "eslint-config-prettier": "10.1.8",
96
- "eslint-plugin-import": "2.32.0",
97
- "eslint-plugin-jsdoc": "62.7.1",
98
- "eslint-plugin-prefer-arrow": "1.2.3",
99
- "globals": "17.4.0",
92
+ "globals": "17.7.0",
100
93
  "husky": "9.1.7",
101
- "jsdom": "28.1.0",
102
- "knip": "5.85.0",
103
- "lint-staged": "16.3.2",
94
+ "jsdom": "29.1.1",
95
+ "knip": "6.24.0",
96
+ "lint-staged": "17.0.8",
104
97
  "npm-run-all": "4.1.5",
105
- "playwright": "1.58.2",
106
- "postcss": "8.5.7",
107
- "prettier": "3.8.1",
108
- "tailwindcss": "4.2.1",
109
- "ts-node": "10.9.2",
98
+ "playwright": "1.61.1",
99
+ "postcss": "8.5.16",
100
+ "prettier": "3.9.4",
101
+ "tailwindcss": "4.3.2",
110
102
  "typescript": "5.9.3",
111
- "vitest": "4.0.18",
112
- "wait-on": "9.0.4",
113
- "webdriver-manager": "13.0.2"
103
+ "vitest": "4.1.9",
104
+ "wait-on": "9.0.10"
114
105
  },
115
106
  "engines": {
116
107
  "node": ">= 22.12.0 || >= 24.0.0",
117
- "typescript": ">= 5.8.0 < 5.9.0"
108
+ "typescript": ">= 5.9.0 < 6.0.0"
118
109
  },
119
110
  "browserslist": [
120
- "chrome 140"
111
+ "chrome 150"
121
112
  ]
122
113
  }
@@ -1,6 +1,6 @@
1
1
  import { TestBed } from '@angular/core/testing';
2
2
  import { AppComponent } from './app.component';
3
- import { TranslateModule } from '@ngx-translate/core';
3
+ import { provideTranslateService } from '@ngx-translate/core';
4
4
  import { ElectronService } from './core/services';
5
5
  import { provideRouter } from '@angular/router';
6
6
 
@@ -8,8 +8,8 @@ describe('AppComponent', () => {
8
8
  beforeEach(async () => {
9
9
  await TestBed.configureTestingModule({
10
10
  declarations: [],
11
- imports: [AppComponent, TranslateModule.forRoot()],
12
- providers: [provideRouter([]), ElectronService],
11
+ imports: [AppComponent],
12
+ providers: [provideRouter([]), provideTranslateService(), ElectronService],
13
13
  }).compileComponents();
14
14
  });
15
15
 
@@ -1,6 +1,5 @@
1
1
  import { Component, afterNextRender, inject } from '@angular/core';
2
2
  import { ElectronService } from './core/services';
3
- import { TranslateService } from '@ngx-translate/core';
4
3
  import { APP_CONFIG } from '../environments/environment';
5
4
  import { RouterOutlet } from '@angular/router';
6
5
 
@@ -12,22 +11,10 @@ import { RouterOutlet } from '@angular/router';
12
11
  })
13
12
  export class AppComponent {
14
13
  private electronService = inject(ElectronService);
15
- private translate = inject(TranslateService);
16
14
 
17
15
  constructor() {
18
- const electronService = this.electronService;
19
-
20
- this.translate.setDefaultLang('en');
21
- console.log('APP_CONFIG', APP_CONFIG);
22
-
23
- if (electronService.isElectron) {
24
- console.log(process.env);
25
- console.log('Run in electron');
26
- console.log('Electron ipcRenderer', this.electronService.ipcRenderer);
27
- console.log('NodeJS childProcess', this.electronService.childProcess);
28
- void this.electronService.ipcRenderer.invoke('app:get-version').then((v) => console.log('App version:', v));
29
- } else {
30
- console.log('Run in browser');
16
+ if (!APP_CONFIG.production) {
17
+ console.log('APP_CONFIG', APP_CONFIG, this.electronService.isElectron ? 'Run in electron' : 'Run in browser');
31
18
  }
32
19
 
33
20
  afterNextRender(() => {
@@ -93,6 +93,12 @@ export class DatabaseService {
93
93
  this.fetchCurrentPage();
94
94
  }
95
95
 
96
+ private static readonly filterWhereMap: Record<string, { key: string; value: number }[]> = {
97
+ all: [],
98
+ favorites: [{ key: 'store.favorite', value: 1 }],
99
+ system: [{ key: 'store.system', value: 1 }],
100
+ };
101
+
96
102
  selectFilter(filter: string) {
97
103
  this.parentId.set(null);
98
104
  this.collectionId.set(null);
@@ -101,13 +107,7 @@ export class DatabaseService {
101
107
  this.activeSmartCollectionId.set(null);
102
108
  this.currentPage.set(1);
103
109
 
104
- const whereMap: Record<string, { key: string; value: number }[]> = {
105
- all: [],
106
- favorites: [{ key: 'store.favorite', value: 1 }],
107
- system: [{ key: 'store.system', value: 1 }],
108
- };
109
-
110
- this.fetchCurrentPage({ where: whereMap[filter] ?? [] });
110
+ this.fetchCurrentPage({ where: DatabaseService.filterWhereMap[filter] ?? [] });
111
111
  }
112
112
 
113
113
  goToPage(page: number) {
@@ -161,12 +161,7 @@ export class DatabaseService {
161
161
  if (collectionId) {
162
162
  options.collectionId = collectionId;
163
163
  } else if (filter) {
164
- const whereMap: Record<string, { key: string; value: number }[]> = {
165
- all: [],
166
- favorites: [{ key: 'store.favorite', value: 1 }],
167
- system: [{ key: 'store.system', value: 1 }],
168
- };
169
- options.where = whereMap[filter] ?? [];
164
+ options.where = DatabaseService.filterWhereMap[filter] ?? [];
170
165
  }
171
166
 
172
167
  this.storeFetch(options);
@@ -185,7 +180,6 @@ export class DatabaseService {
185
180
 
186
181
  this.collections.set(collections);
187
182
  this.smartCollections.set(smartCollections);
188
- console.log('System Boot:', collections);
189
183
 
190
184
  if (savedSortColumn) {
191
185
  this.sortColumn.set(savedSortColumn);
@@ -258,7 +252,6 @@ export class DatabaseService {
258
252
  return this.track(
259
253
  this.message.collectionCreate(args).then((result) => {
260
254
  this.collections.set(result);
261
- console.log('Collection Created:', result);
262
255
  return result;
263
256
  }),
264
257
  );
@@ -1,62 +1,21 @@
1
1
  import { Injectable } from '@angular/core';
2
-
3
- // If you import a module but never use any of the imported values other than as TypeScript types,
4
- // the resulting javascript file will look as if you never imported the module at all.
5
- import { ipcRenderer, webFrame } from 'electron';
6
- import * as childProcess from 'child_process';
7
- import * as fs from 'fs';
2
+ import type { FontasticBridge } from '@main/types';
8
3
 
9
4
  @Injectable({
10
5
  providedIn: 'root',
11
6
  })
12
7
  export class ElectronService {
13
- ipcRenderer!: typeof ipcRenderer;
14
- webFrame!: typeof webFrame;
15
- childProcess!: typeof childProcess;
16
- fs!: typeof fs;
8
+ /** The typed IPC bridge exposed by the preload script, or undefined in a plain browser. */
9
+ readonly bridge?: FontasticBridge = (window as any).fontastic;
17
10
 
18
11
  /** Resolves when the main process signals that IPC handlers and DB are ready. */
19
12
  readonly ready: Promise<void>;
20
13
 
21
14
  constructor() {
22
- if (this.isElectron) {
23
- this.ipcRenderer = (window as any).require('electron').ipcRenderer;
24
- this.webFrame = (window as any).require('electron').webFrame;
25
-
26
- this.fs = (window as any).require('fs');
27
-
28
- this.childProcess = (window as any).require('child_process');
29
- this.childProcess.exec('node -v', (error, stdout, stderr) => {
30
- if (error) {
31
- console.error(`error: ${error.message}`);
32
- return;
33
- }
34
- if (stderr) {
35
- console.error(`stderr: ${stderr}`);
36
- return;
37
- }
38
- console.log(`stdout:\n${stdout}`);
39
- });
40
-
41
- // Notes :
42
- // * A NodeJS's dependency imported with 'window.require' MUST BE present in `dependencies` of both `app/package.json`
43
- // and `package.json (root folder)` in order to make it work here in Electron's Renderer process (src folder)
44
- // because it will loaded at runtime by Electron.
45
- // * A NodeJS's dependency imported with TS module import (ex: import { Dropbox } from 'dropbox') CAN only be present
46
- // in `dependencies` of `package.json (root folder)` because it is loaded during build phase and does not need to be
47
- // in the final bundle. Reminder : only if not used in Electron's Main process (app folder)
48
-
49
- // If you want to use a NodeJS 3rd party deps in Renderer process,
50
- // ipcRenderer.invoke can serve many common use cases.
51
- // https://www.electronjs.org/docs/latest/api/ipc-renderer#ipcrendererinvokechannel-args
52
-
53
- this.ready = this.ipcRenderer.invoke('app:ready');
54
- } else {
55
- this.ready = Promise.resolve();
56
- }
15
+ this.ready = this.bridge ? this.bridge.invoke<void>('app:ready') : Promise.resolve();
57
16
  }
58
17
 
59
18
  get isElectron(): boolean {
60
- return !!(window && window.process && window.process.type);
19
+ return !!this.bridge;
61
20
  }
62
21
  }
@@ -0,0 +1,60 @@
1
+ import { Injectable } from '@angular/core';
2
+ import type { Store } from '@main/database/entity/Store.schema';
3
+
4
+ /**
5
+ * Registers font files with the document on demand. Elements are observed
6
+ * with a shared IntersectionObserver so a font is only fetched once its
7
+ * preview row is about to scroll into view.
8
+ */
9
+ @Injectable({ providedIn: 'root' })
10
+ export class FontLoaderService {
11
+ private registered = new Set<string>();
12
+ private pending = new WeakMap<Element, Store>();
13
+
14
+ private observer = new IntersectionObserver(
15
+ (entries) => {
16
+ for (const entry of entries) {
17
+ if (!entry.isIntersecting) continue;
18
+ const store = this.pending.get(entry.target);
19
+ this.unobserve(entry.target);
20
+ if (store) {
21
+ this.register(store);
22
+ }
23
+ }
24
+ },
25
+ { rootMargin: '200px' },
26
+ );
27
+
28
+ observe(element: Element, store: Store) {
29
+ if (this.registered.has(this.key(store))) return;
30
+ this.pending.set(element, store);
31
+ this.observer.observe(element);
32
+ }
33
+
34
+ unobserve(element: Element) {
35
+ this.observer.unobserve(element);
36
+ this.pending.delete(element);
37
+ }
38
+
39
+ register(store: Store) {
40
+ const key = this.key(store);
41
+ if (this.registered.has(key)) return;
42
+
43
+ const url = `font://${store.file_path}`;
44
+ const fontFace = new FontFace(store.full_name || store.font_family, `url('${url}')`);
45
+
46
+ fontFace
47
+ .load()
48
+ .then((loaded) => {
49
+ document.fonts.add(loaded);
50
+ this.registered.add(key);
51
+ })
52
+ .catch((err) => {
53
+ console.warn(`Failed to load font: ${store.file_name}`, err);
54
+ });
55
+ }
56
+
57
+ private key(store: Store): string {
58
+ return `${store.id}-${store.file_path}`;
59
+ }
60
+ }
@@ -1,4 +1,5 @@
1
1
  export * from './electron/electron.service';
2
+ export * from './font-loader/font-loader.service';
2
3
  export * from './database/database.service';
3
4
  export * from './message/message.service';
4
5
  export * from './news/news.service';
@@ -13,47 +13,43 @@ import type { SmartCollection } from '@main/database/entity/SmartCollection.sche
13
13
  export class MessageService {
14
14
  private electron = inject(ElectronService);
15
15
 
16
+ /** Maps registered listeners to their bridge subscription ids. */
17
+ private subscriptions = new Map<(...args: any[]) => void, number>();
18
+
16
19
  // Low-level IPC
17
20
 
18
- on(channel: string, listener: (...args: any[]) => void): Electron.IpcRenderer | undefined {
19
- if (this.electron.isElectron) {
20
- return this.electron.ipcRenderer.on(channel, listener);
21
+ on(channel: string, listener: (...args: any[]) => void): void {
22
+ const id = this.electron.bridge?.on(channel, listener);
23
+ if (id !== undefined) {
24
+ this.subscriptions.set(listener, id);
21
25
  }
22
- return undefined;
23
26
  }
24
27
 
25
- once(channel: string, listener: (...args: any[]) => void): Electron.IpcRenderer | undefined {
26
- if (this.electron.isElectron) {
27
- return this.electron.ipcRenderer.once(channel, listener);
28
- }
29
- return undefined;
28
+ once(channel: string, listener: (...args: any[]) => void): void {
29
+ this.electron.bridge?.once(channel, listener);
30
30
  }
31
31
 
32
32
  send(channel: string, args?: any): void {
33
- if (this.electron.isElectron) {
34
- this.electron.ipcRenderer.send(channel, args);
35
- }
33
+ this.electron.bridge?.send(channel, args);
36
34
  }
37
35
 
38
36
  invoke<T = any>(channel: string, args?: any): Promise<T> {
39
- if (this.electron.isElectron) {
40
- return this.electron.ipcRenderer.invoke(channel, args);
37
+ if (this.electron.bridge) {
38
+ return this.electron.bridge.invoke<T>(channel, args);
41
39
  }
42
40
  return Promise.reject('Not running in Electron');
43
41
  }
44
42
 
45
- removeListener(channel: string, listener: (...args: any[]) => void): Electron.IpcRenderer | undefined {
46
- if (this.electron.isElectron) {
47
- return this.electron.ipcRenderer.removeListener(channel, listener);
43
+ removeListener(channel: string, listener: (...args: any[]) => void): void {
44
+ const id = this.subscriptions.get(listener);
45
+ if (id !== undefined) {
46
+ this.electron.bridge?.off(id);
47
+ this.subscriptions.delete(listener);
48
48
  }
49
- return undefined;
50
49
  }
51
50
 
52
- removeAllListeners(channel: string): Electron.IpcRenderer | undefined {
53
- if (this.electron.isElectron) {
54
- return this.electron.ipcRenderer.removeAllListeners(channel);
55
- }
56
- return undefined;
51
+ removeAllListeners(channel: string): void {
52
+ this.electron.bridge?.removeAllListeners(channel);
57
53
  }
58
54
 
59
55
  // System
@@ -104,10 +100,6 @@ export class MessageService {
104
100
 
105
101
  // Font Manager
106
102
 
107
- exec(args: any): Promise<any> {
108
- return this.invoke(ChannelType.IPC_EXEC_CMD, args);
109
- }
110
-
111
103
  systemAuthenticate(args: any): Promise<any> {
112
104
  return this.invoke(ChannelType.IPC_AUTH_USER, args);
113
105
  }