fontastic 1.2.0 → 1.3.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 (74) hide show
  1. package/.release-please-manifest.json +1 -1
  2. package/CHANGELOG.md +22 -0
  3. package/README.md +6 -0
  4. package/app/Application.js +4 -4
  5. package/app/Application.ts +13 -13
  6. package/app/config/database.js +1 -1
  7. package/app/config/database.ts +1 -1
  8. package/app/config/mimes.js +23 -23
  9. package/app/config/mimes.ts +35 -29
  10. package/app/core/ConfigManager.js +27 -0
  11. package/app/core/ConfigManager.ts +28 -0
  12. package/app/core/FontFinder.js +66 -15
  13. package/app/core/FontFinder.ts +81 -22
  14. package/app/core/FontManager.js +20 -18
  15. package/app/core/FontManager.ts +21 -19
  16. package/app/core/FontObject.js +44 -24
  17. package/app/core/FontObject.ts +47 -27
  18. package/app/core/MessageHandler.js +70 -19
  19. package/app/core/MessageHandler.ts +82 -21
  20. package/app/core/SystemManager.js +5 -1
  21. package/app/core/SystemManager.ts +5 -1
  22. package/app/database/entity/Collection.schema.js +20 -18
  23. package/app/database/entity/Collection.schema.ts +22 -21
  24. package/app/database/repository/Collection.repository.js +17 -18
  25. package/app/database/repository/Collection.repository.ts +27 -18
  26. package/app/database/repository/Store.repository.js +13 -18
  27. package/app/database/repository/Store.repository.ts +13 -21
  28. package/app/enums/ChannelType.js +18 -0
  29. package/app/enums/ChannelType.ts +24 -0
  30. package/app/main.js +79 -2
  31. package/app/main.ts +100 -3
  32. package/app/package.json +1 -1
  33. package/app/types/NativeThemeState.js +3 -0
  34. package/app/types/NativeThemeState.ts +4 -0
  35. package/app/types/ScanProgress.js +3 -0
  36. package/app/types/ScanProgress.ts +6 -0
  37. package/app/types/SystemPreferencesState.js +3 -0
  38. package/app/types/SystemPreferencesState.ts +4 -0
  39. package/app/types/index.js +3 -0
  40. package/app/types/index.ts +3 -0
  41. package/package.json +1 -1
  42. package/src/app/core/services/database/database.service.ts +6 -0
  43. package/src/app/core/services/message/message.service.ts +33 -1
  44. package/src/app/core/services/presentation/presentation.service.ts +93 -1
  45. package/src/app/layout/footer/footer.component.html +13 -2
  46. package/src/app/layout/footer/footer.component.ts +18 -2
  47. package/src/app/layout/navigation/navigation.component.html +11 -9
  48. package/src/app/layout/navigation/navigation.component.ts +35 -0
  49. package/src/app/settings/ai-keys/ai-keys.component.ts +13 -18
  50. package/src/app/settings/danger-zone/danger-zone.component.html +8 -0
  51. package/src/app/settings/danger-zone/danger-zone.component.ts +12 -0
  52. package/src/app/settings/news-api/news-api.component.ts +6 -8
  53. package/src/app/settings/theme/theme.component.html +15 -2
  54. package/src/app/settings/theme/theme.component.ts +4 -0
  55. package/src/app/shared/components/datagrid/datagrid.component.html +8 -17
  56. package/src/app/shared/components/datagrid/datagrid.component.ts +6 -10
  57. package/src/app/shared/components/glyphs/glyphs.component.html +5 -15
  58. package/src/app/shared/components/glyphs/glyphs.component.ts +3 -0
  59. package/src/app/shared/components/preview/preview.component.html +1 -1
  60. package/src/app/shared/components/preview/preview.component.ts +3 -8
  61. package/src/app/shared/components/prompt-dialog/prompt-dialog.component.html +2 -2
  62. package/src/app/shared/components/prompt-dialog/prompt-dialog.component.ts +2 -1
  63. package/src/app/shared/components/rule-builder/rule-builder.component.html +18 -6
  64. package/src/app/shared/components/rule-builder/rule-builder.component.ts +34 -2
  65. package/src/app/shared/components/search/search.component.html +9 -36
  66. package/src/app/shared/components/search/search.component.ts +2 -1
  67. package/src/app/shared/components/waterfall/waterfall.component.html +1 -3
  68. package/src/app/shared/components/waterfall/waterfall.component.ts +2 -1
  69. package/src/app/shared/directives/disabled-opacity/disabled-opacity.directive.ts +18 -0
  70. package/src/app/shared/directives/hover-highlight/hover-highlight.directive.ts +38 -0
  71. package/src/app/shared/directives/index.ts +5 -0
  72. package/src/app/shared/directives/modal-backdrop/modal-backdrop.directive.ts +18 -0
  73. package/src/app/shared/directives/scroll-reset/scroll-reset.directive.ts +15 -0
  74. package/src/app/shared/directives/stop-propagation/stop-propagation.directive.ts +12 -0
@@ -135,9 +135,8 @@ export const StoreRepository = {
135
135
  });
136
136
  }
137
137
 
138
- if (options.take) {
139
- db.limit(options.take);
140
- }
138
+ const MAX_SEARCH_LIMIT = 5000;
139
+ db.limit(Math.min(options.take || 500, MAX_SEARCH_LIMIT));
141
140
 
142
141
  if (options.skip) {
143
142
  db.offset(options.skip);
@@ -322,13 +321,7 @@ export const StoreRepository = {
322
321
  data.version = item.version;
323
322
  }
324
323
 
325
- //@todo Log errors in log table.
326
- return await this.createQueryBuilder()
327
- .insert()
328
- .into(Store)
329
- .values(data)
330
- .execute()
331
- .catch((err: any) => console.log('insert-error', err));
324
+ return await this.createQueryBuilder().insert().into(Store).values(data).execute();
332
325
  },
333
326
 
334
327
  async evaluateSmartRules(rules: SmartCollectionRule[], matchType: string, options: any = {}) {
@@ -437,19 +430,18 @@ export const StoreRepository = {
437
430
  },
438
431
 
439
432
  async fetchSystemStats() {
440
- const rowCount = await this.createQueryBuilder().select('COUNT(*)', 'total').getRawOne();
441
-
442
- const favoriteCount = await this.createQueryBuilder().select('COUNT(*)', 'total').where('store.favorite = 1').getRawOne();
443
-
444
- const systemCount = await this.createQueryBuilder().select('COUNT(*)', 'total').where('store.system = 1').getRawOne();
445
-
446
- const temporaryCount = await this.createQueryBuilder().select('COUNT(*)', 'total').where('store.temporary = 1').getRawOne();
433
+ const stats = await this.createQueryBuilder()
434
+ .select('COUNT(*)', 'rowCount')
435
+ .addSelect('SUM(CASE WHEN store.favorite = 1 THEN 1 ELSE 0 END)', 'favoriteCount')
436
+ .addSelect('SUM(CASE WHEN store.system = 1 THEN 1 ELSE 0 END)', 'systemCount')
437
+ .addSelect('SUM(CASE WHEN store.temporary = 1 THEN 1 ELSE 0 END)', 'temporaryCount')
438
+ .getRawOne();
447
439
 
448
440
  return {
449
- rowCount: rowCount.total,
450
- favoriteCount: favoriteCount.total,
451
- systemCount: systemCount.total,
452
- temporaryCount: temporaryCount.total,
441
+ rowCount: Number(stats.rowCount) || 0,
442
+ favoriteCount: Number(stats.favoriteCount) || 0,
443
+ systemCount: Number(stats.systemCount) || 0,
444
+ temporaryCount: Number(stats.temporaryCount) || 0,
453
445
  };
454
446
  },
455
447
  };
@@ -63,6 +63,24 @@ var ChannelType;
63
63
  ChannelType["IPC_SMART_COLLECTION_UPDATE"] = "IPC_SMART_COLLECTION_UPDATE";
64
64
  ChannelType["IPC_SMART_COLLECTION_DELETE"] = "IPC_SMART_COLLECTION_DELETE";
65
65
  ChannelType["IPC_SMART_COLLECTION_EVALUATE"] = "IPC_SMART_COLLECTION_EVALUATE";
66
+ ChannelType["IPC_SMART_COLLECTION_PREVIEW"] = "IPC_SMART_COLLECTION_PREVIEW";
66
67
  ChannelType["IPC_TOGGLE_PANEL"] = "IPC_TOGGLE_PANEL";
68
+ // Native Theme
69
+ ChannelType["IPC_GET_NATIVE_THEME"] = "IPC_GET_NATIVE_THEME";
70
+ ChannelType["IPC_NATIVE_THEME_CHANGED"] = "IPC_NATIVE_THEME_CHANGED";
71
+ // Safe Storage
72
+ ChannelType["IPC_SAFE_STORE"] = "IPC_SAFE_STORE";
73
+ ChannelType["IPC_SAFE_RETRIEVE"] = "IPC_SAFE_RETRIEVE";
74
+ // Session
75
+ ChannelType["IPC_CLEAR_CACHE"] = "IPC_CLEAR_CACHE";
76
+ // Power Monitor
77
+ ChannelType["IPC_POWER_SUSPEND"] = "IPC_POWER_SUSPEND";
78
+ ChannelType["IPC_POWER_RESUME"] = "IPC_POWER_RESUME";
79
+ ChannelType["IPC_POWER_SHUTDOWN"] = "IPC_POWER_SHUTDOWN";
80
+ ChannelType["IPC_POWER_LOCK_SCREEN"] = "IPC_POWER_LOCK_SCREEN";
81
+ // System Preferences
82
+ ChannelType["IPC_GET_SYSTEM_PREFERENCES"] = "IPC_GET_SYSTEM_PREFERENCES";
83
+ // Scan Progress (MessageChannelMain)
84
+ ChannelType["IPC_SCAN_PROGRESS_PORT"] = "IPC_SCAN_PROGRESS_PORT";
67
85
  })(ChannelType || (exports.ChannelType = ChannelType = {}));
68
86
  //# sourceMappingURL=ChannelType.js.map
@@ -68,6 +68,30 @@ export enum ChannelType {
68
68
  IPC_SMART_COLLECTION_UPDATE = 'IPC_SMART_COLLECTION_UPDATE',
69
69
  IPC_SMART_COLLECTION_DELETE = 'IPC_SMART_COLLECTION_DELETE',
70
70
  IPC_SMART_COLLECTION_EVALUATE = 'IPC_SMART_COLLECTION_EVALUATE',
71
+ IPC_SMART_COLLECTION_PREVIEW = 'IPC_SMART_COLLECTION_PREVIEW',
71
72
 
72
73
  IPC_TOGGLE_PANEL = 'IPC_TOGGLE_PANEL',
74
+
75
+ // Native Theme
76
+ IPC_GET_NATIVE_THEME = 'IPC_GET_NATIVE_THEME',
77
+ IPC_NATIVE_THEME_CHANGED = 'IPC_NATIVE_THEME_CHANGED',
78
+
79
+ // Safe Storage
80
+ IPC_SAFE_STORE = 'IPC_SAFE_STORE',
81
+ IPC_SAFE_RETRIEVE = 'IPC_SAFE_RETRIEVE',
82
+
83
+ // Session
84
+ IPC_CLEAR_CACHE = 'IPC_CLEAR_CACHE',
85
+
86
+ // Power Monitor
87
+ IPC_POWER_SUSPEND = 'IPC_POWER_SUSPEND',
88
+ IPC_POWER_RESUME = 'IPC_POWER_RESUME',
89
+ IPC_POWER_SHUTDOWN = 'IPC_POWER_SHUTDOWN',
90
+ IPC_POWER_LOCK_SCREEN = 'IPC_POWER_LOCK_SCREEN',
91
+
92
+ // System Preferences
93
+ IPC_GET_SYSTEM_PREFERENCES = 'IPC_GET_SYSTEM_PREFERENCES',
94
+
95
+ // Scan Progress (MessageChannelMain)
96
+ IPC_SCAN_PROGRESS_PORT = 'IPC_SCAN_PROGRESS_PORT',
73
97
  }
package/app/main.js CHANGED
@@ -1,10 +1,20 @@
1
1
  "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
2
11
  Object.defineProperty(exports, "__esModule", { value: true });
3
12
  const electron_1 = require("electron");
4
13
  const path = require("path");
5
14
  const fs = require("fs");
6
15
  const node_machine_id_1 = require("node-machine-id");
7
16
  const Application_1 = require("./Application");
17
+ const ChannelType_1 = require("./enums/ChannelType");
8
18
  electron_1.app.name = 'Fontastic';
9
19
  const iconPath = path.join(__dirname, '..', 'src', 'assets', 'icons', 'favicon.512x512.png');
10
20
  electron_1.app.setAboutPanelOptions({
@@ -18,6 +28,32 @@ const appReadyPromise = new Promise((resolve) => {
18
28
  resolveAppReady = resolve;
19
29
  });
20
30
  const args = process.argv.slice(1), serve = args.some((val) => val === '--serve');
31
+ // --- Native Theme ---
32
+ function getNativeThemeState() {
33
+ return {
34
+ shouldUseDarkColors: electron_1.nativeTheme.shouldUseDarkColors,
35
+ themeSource: electron_1.nativeTheme.themeSource,
36
+ };
37
+ }
38
+ electron_1.ipcMain.handle(ChannelType_1.ChannelType.IPC_GET_NATIVE_THEME, () => getNativeThemeState());
39
+ electron_1.nativeTheme.on('updated', () => {
40
+ if (win && !win.isDestroyed()) {
41
+ win.webContents.send(ChannelType_1.ChannelType.IPC_NATIVE_THEME_CHANGED, getNativeThemeState());
42
+ }
43
+ });
44
+ // --- System Preferences ---
45
+ function getSystemPreferencesState() {
46
+ var _a, _b;
47
+ let reduceMotion = false;
48
+ if (process.platform === 'darwin') {
49
+ reduceMotion = electron_1.systemPreferences.getAnimationSettings().prefersReducedMotion;
50
+ }
51
+ return {
52
+ accentColor: (_b = (_a = electron_1.systemPreferences.getAccentColor) === null || _a === void 0 ? void 0 : _a.call(electron_1.systemPreferences)) !== null && _b !== void 0 ? _b : '',
53
+ reduceMotion,
54
+ };
55
+ }
56
+ electron_1.ipcMain.handle(ChannelType_1.ChannelType.IPC_GET_SYSTEM_PREFERENCES, () => getSystemPreferencesState());
21
57
  function createWindow() {
22
58
  const size = electron_1.screen.getPrimaryDisplay().workAreaSize;
23
59
  // Create the browser window.
@@ -80,9 +116,50 @@ try {
80
116
  // Some APIs can only be used after this event occurs.
81
117
  // Added 400 ms to fix the black background issue while using transparent window. More detais at https://github.com/electron/electron/issues/15947
82
118
  electron_1.app.on('ready', () => {
83
- electron_1.protocol.handle('font', (request) => {
119
+ electron_1.protocol.handle('font', (request) => __awaiter(void 0, void 0, void 0, function* () {
84
120
  const filePath = decodeURIComponent(request.url.replace('font://', ''));
85
- return electron_1.net.fetch(`file://${filePath}`);
121
+ try {
122
+ return yield electron_1.net.fetch(`file://${filePath}`);
123
+ }
124
+ catch (_a) {
125
+ return new Response('Not found', { status: 404 });
126
+ }
127
+ }));
128
+ // --- Session: deny unnecessary permissions ---
129
+ electron_1.session.defaultSession.setPermissionRequestHandler((_webContents, permission, callback) => {
130
+ const allowed = ['clipboard-read', 'clipboard-sanitized-write'];
131
+ callback(allowed.includes(permission));
132
+ });
133
+ // --- Session: set CSP in production ---
134
+ if (!serve) {
135
+ electron_1.session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
136
+ callback({
137
+ responseHeaders: Object.assign(Object.assign({}, details.responseHeaders), { 'Content-Security-Policy': [
138
+ "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; font-src 'self' font:; img-src 'self' data: https:;",
139
+ ] }),
140
+ });
141
+ });
142
+ }
143
+ // --- Power Monitor ---
144
+ electron_1.powerMonitor.on('suspend', () => {
145
+ if (win && !win.isDestroyed()) {
146
+ win.webContents.send(ChannelType_1.ChannelType.IPC_POWER_SUSPEND);
147
+ }
148
+ });
149
+ electron_1.powerMonitor.on('resume', () => {
150
+ if (win && !win.isDestroyed()) {
151
+ win.webContents.send(ChannelType_1.ChannelType.IPC_POWER_RESUME);
152
+ }
153
+ });
154
+ electron_1.powerMonitor.on('shutdown', () => {
155
+ if (win && !win.isDestroyed()) {
156
+ win.webContents.send(ChannelType_1.ChannelType.IPC_POWER_SHUTDOWN);
157
+ }
158
+ });
159
+ electron_1.powerMonitor.on('lock-screen', () => {
160
+ if (win && !win.isDestroyed()) {
161
+ win.webContents.send(ChannelType_1.ChannelType.IPC_POWER_LOCK_SCREEN);
162
+ }
86
163
  });
87
164
  setTimeout(createWindow, 400);
88
165
  });
package/app/main.ts CHANGED
@@ -1,8 +1,22 @@
1
- import { app, BrowserWindow, ipcMain, nativeImage, net, protocol, screen } from 'electron';
1
+ import {
2
+ app,
3
+ BrowserWindow,
4
+ ipcMain,
5
+ nativeImage,
6
+ nativeTheme,
7
+ net,
8
+ powerMonitor,
9
+ protocol,
10
+ screen,
11
+ session,
12
+ systemPreferences,
13
+ } from 'electron';
2
14
  import * as path from 'path';
3
15
  import * as fs from 'fs';
4
16
  import { machineId } from 'node-machine-id';
5
17
  import Application from './Application';
18
+ import { ChannelType } from './enums/ChannelType';
19
+ import type { NativeThemeState, SystemPreferencesState } from './types';
6
20
 
7
21
  app.name = 'Fontastic';
8
22
 
@@ -23,6 +37,39 @@ const appReadyPromise = new Promise<void>((resolve) => {
23
37
  const args = process.argv.slice(1),
24
38
  serve = args.some((val) => val === '--serve');
25
39
 
40
+ // --- Native Theme ---
41
+
42
+ function getNativeThemeState(): NativeThemeState {
43
+ return {
44
+ shouldUseDarkColors: nativeTheme.shouldUseDarkColors,
45
+ themeSource: nativeTheme.themeSource,
46
+ };
47
+ }
48
+
49
+ ipcMain.handle(ChannelType.IPC_GET_NATIVE_THEME, () => getNativeThemeState());
50
+
51
+ nativeTheme.on('updated', () => {
52
+ if (win && !win.isDestroyed()) {
53
+ win.webContents.send(ChannelType.IPC_NATIVE_THEME_CHANGED, getNativeThemeState());
54
+ }
55
+ });
56
+
57
+ // --- System Preferences ---
58
+
59
+ function getSystemPreferencesState(): SystemPreferencesState {
60
+ let reduceMotion = false;
61
+ if (process.platform === 'darwin') {
62
+ reduceMotion = systemPreferences.getAnimationSettings().prefersReducedMotion;
63
+ }
64
+
65
+ return {
66
+ accentColor: systemPreferences.getAccentColor?.() ?? '',
67
+ reduceMotion,
68
+ };
69
+ }
70
+
71
+ ipcMain.handle(ChannelType.IPC_GET_SYSTEM_PREFERENCES, () => getSystemPreferencesState());
72
+
26
73
  function createWindow(): BrowserWindow {
27
74
  const size = screen.getPrimaryDisplay().workAreaSize;
28
75
 
@@ -95,10 +142,60 @@ try {
95
142
  // Some APIs can only be used after this event occurs.
96
143
  // Added 400 ms to fix the black background issue while using transparent window. More detais at https://github.com/electron/electron/issues/15947
97
144
  app.on('ready', () => {
98
- protocol.handle('font', (request) => {
145
+ protocol.handle('font', async (request) => {
99
146
  const filePath = decodeURIComponent(request.url.replace('font://', ''));
100
- return net.fetch(`file://${filePath}`);
147
+ try {
148
+ return await net.fetch(`file://${filePath}`);
149
+ } catch {
150
+ return new Response('Not found', { status: 404 });
151
+ }
101
152
  });
153
+
154
+ // --- Session: deny unnecessary permissions ---
155
+ session.defaultSession.setPermissionRequestHandler((_webContents, permission, callback) => {
156
+ const allowed = ['clipboard-read', 'clipboard-sanitized-write'];
157
+ callback(allowed.includes(permission));
158
+ });
159
+
160
+ // --- Session: set CSP in production ---
161
+ if (!serve) {
162
+ session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
163
+ callback({
164
+ responseHeaders: {
165
+ ...details.responseHeaders,
166
+ 'Content-Security-Policy': [
167
+ "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; font-src 'self' font:; img-src 'self' data: https:;",
168
+ ],
169
+ },
170
+ });
171
+ });
172
+ }
173
+
174
+ // --- Power Monitor ---
175
+ powerMonitor.on('suspend', () => {
176
+ if (win && !win.isDestroyed()) {
177
+ win.webContents.send(ChannelType.IPC_POWER_SUSPEND);
178
+ }
179
+ });
180
+
181
+ powerMonitor.on('resume', () => {
182
+ if (win && !win.isDestroyed()) {
183
+ win.webContents.send(ChannelType.IPC_POWER_RESUME);
184
+ }
185
+ });
186
+
187
+ powerMonitor.on('shutdown', () => {
188
+ if (win && !win.isDestroyed()) {
189
+ win.webContents.send(ChannelType.IPC_POWER_SHUTDOWN);
190
+ }
191
+ });
192
+
193
+ powerMonitor.on('lock-screen', () => {
194
+ if (win && !win.isDestroyed()) {
195
+ win.webContents.send(ChannelType.IPC_POWER_LOCK_SCREEN);
196
+ }
197
+ });
198
+
102
199
  setTimeout(createWindow, 400);
103
200
  });
104
201
 
package/app/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "name": "Tom Shaw",
6
6
  "email": ""
7
7
  },
8
- "version": "1.2.0",
8
+ "version": "1.3.0",
9
9
  "main": "main.js",
10
10
  "private": true,
11
11
  "dependencies": {
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=NativeThemeState.js.map
@@ -0,0 +1,4 @@
1
+ export interface NativeThemeState {
2
+ shouldUseDarkColors: boolean;
3
+ themeSource: 'system' | 'light' | 'dark';
4
+ }
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=ScanProgress.js.map
@@ -0,0 +1,6 @@
1
+ export interface ScanProgress {
2
+ processed: number;
3
+ total: number;
4
+ currentFile: string;
5
+ errors: number;
6
+ }
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=SystemPreferencesState.js.map
@@ -0,0 +1,4 @@
1
+ export interface SystemPreferencesState {
2
+ accentColor: string;
3
+ reduceMotion: boolean;
4
+ }
@@ -24,4 +24,7 @@ __exportStar(require("./SystemStats"), exports);
24
24
  __exportStar(require("./SystemConfig"), exports);
25
25
  __exportStar(require("./ImportOptions"), exports);
26
26
  __exportStar(require("./SmartCollection"), exports);
27
+ __exportStar(require("./NativeThemeState"), exports);
28
+ __exportStar(require("./SystemPreferencesState"), exports);
29
+ __exportStar(require("./ScanProgress"), exports);
27
30
  //# sourceMappingURL=index.js.map
@@ -8,3 +8,6 @@ export * from './SystemStats';
8
8
  export * from './SystemConfig';
9
9
  export * from './ImportOptions';
10
10
  export * from './SmartCollection';
11
+ export * from './NativeThemeState';
12
+ export * from './SystemPreferencesState';
13
+ export * from './ScanProgress';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fontastic",
3
- "version": "1.2.0",
3
+ "version": "1.3.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,
@@ -371,6 +371,12 @@ export class DatabaseService {
371
371
  );
372
372
  }
373
373
 
374
+ smartCollectionPreview(rules: any[], matchType: string): Promise<number> {
375
+ return this.message.smartCollectionPreview(rules, matchType).then((result) => {
376
+ return result[1] as number;
377
+ });
378
+ }
379
+
374
380
  // Store
375
381
 
376
382
  storeFind(args: any): Promise<Store[]> {
@@ -4,7 +4,7 @@ import type { Collection } from '@main/database/entity/Collection.schema';
4
4
  import type { Logger } from '@main/database/entity/Logger.schema';
5
5
  import type { Store, StoreManyAndCountType } from '@main/database/entity/Store.schema';
6
6
  import { ChannelType } from '@main/enums';
7
- import type { FontMetrics, SystemConfig, SystemStats } from '@main/types';
7
+ import type { FontMetrics, NativeThemeState, SystemConfig, SystemPreferencesState, SystemStats } from '@main/types';
8
8
  import type { SmartCollection } from '@main/database/entity/SmartCollection.schema';
9
9
 
10
10
  @Injectable({
@@ -236,6 +236,10 @@ export class MessageService {
236
236
  return this.invoke<StoreManyAndCountType>(ChannelType.IPC_SMART_COLLECTION_EVALUATE, { id, ...options });
237
237
  }
238
238
 
239
+ smartCollectionPreview(rules: any[], matchType: string): Promise<StoreManyAndCountType> {
240
+ return this.invoke<StoreManyAndCountType>(ChannelType.IPC_SMART_COLLECTION_PREVIEW, { rules, match_type: matchType });
241
+ }
242
+
239
243
  // Store
240
244
 
241
245
  storeFind(args: any): Promise<Store[]> {
@@ -311,4 +315,32 @@ export class MessageService {
311
315
  loggerTruncate(): Promise<Logger[]> {
312
316
  return this.invoke<Logger[]>(ChannelType.IPC_LOGGER_TRUNCATE);
313
317
  }
318
+
319
+ // Native Theme
320
+
321
+ getNativeTheme(): Promise<NativeThemeState> {
322
+ return this.invoke<NativeThemeState>(ChannelType.IPC_GET_NATIVE_THEME);
323
+ }
324
+
325
+ // Safe Storage
326
+
327
+ safeStore(key: string, value: string): Promise<void> {
328
+ return this.invoke(ChannelType.IPC_SAFE_STORE, { key, value });
329
+ }
330
+
331
+ safeRetrieve(key: string): Promise<string | null> {
332
+ return this.invoke<string | null>(ChannelType.IPC_SAFE_RETRIEVE, key);
333
+ }
334
+
335
+ // Session
336
+
337
+ clearCache(): Promise<void> {
338
+ return this.invoke(ChannelType.IPC_CLEAR_CACHE);
339
+ }
340
+
341
+ // System Preferences
342
+
343
+ getSystemPreferences(): Promise<SystemPreferencesState> {
344
+ return this.invoke<SystemPreferencesState>(ChannelType.IPC_GET_SYSTEM_PREFERENCES);
345
+ }
314
346
  }
@@ -2,7 +2,7 @@ import { Injectable, inject, signal, computed, effect, untracked } from '@angula
2
2
  import { ElectronService } from '../electron/electron.service';
3
3
  import { MessageService } from '../message/message.service';
4
4
  import { ChannelType, StorageType } from '@main/enums';
5
- import type { LayoutPanelType, LayoutPreviewType } from '@main/types';
5
+ import type { LayoutPanelType, LayoutPreviewType, NativeThemeState, ScanProgress } from '@main/types';
6
6
 
7
7
  @Injectable({ providedIn: 'root' })
8
8
  export class PresentationService {
@@ -21,6 +21,7 @@ export class PresentationService {
21
21
  this.loadingCount--;
22
22
  if (this.loadingCount === 0) {
23
23
  this.loading.set(false);
24
+ this.scanProgress.set(null);
24
25
  }
25
26
  }
26
27
 
@@ -28,6 +29,20 @@ export class PresentationService {
28
29
 
29
30
  readonly theme = signal<string>('light');
30
31
 
32
+ // Native Theme: auto-sync with OS dark mode
33
+ readonly autoTheme = signal(false);
34
+ readonly nativeThemeState = signal<NativeThemeState | null>(null);
35
+
36
+ // System Preferences
37
+ readonly systemAccentColor = signal('');
38
+ readonly reduceMotion = signal(false);
39
+
40
+ // Power Monitor
41
+ readonly suspended = signal(false);
42
+
43
+ // Scan Progress (MessageChannelMain)
44
+ readonly scanProgress = signal<ScanProgress | null>(null);
45
+
31
46
  constructor() {
32
47
  effect(() => {
33
48
  const current = this.theme();
@@ -37,12 +52,21 @@ export class PresentationService {
37
52
  }
38
53
  });
39
54
 
55
+ // Apply reduce-motion class based on OS preference
56
+ effect(() => {
57
+ document.documentElement.classList.toggle('reduce-motion', this.reduceMotion());
58
+ });
59
+
40
60
  if (this.electronService.isElectron) {
41
61
  this.loadThemeSettings();
42
62
  this.loadLayoutSettings();
43
63
  this.loadPreviewSettings();
44
64
  this.loadNavigationExpandedSettings();
45
65
  this.listenForMenuToggle();
66
+ this.initNativeTheme();
67
+ this.initSystemPreferences();
68
+ this.initPowerMonitor();
69
+ this.initScanProgress();
46
70
 
47
71
  let themeInitialized = false;
48
72
  effect(() => {
@@ -241,6 +265,74 @@ export class PresentationService {
241
265
  this.navigationExpandedIds.set([]);
242
266
  }
243
267
 
268
+ // --- Native Theme ---
269
+
270
+ private async initNativeTheme() {
271
+ const state = await this.messageService.getNativeTheme();
272
+ this.nativeThemeState.set(state);
273
+ this.applyAutoTheme(state);
274
+
275
+ this.messageService.on(ChannelType.IPC_NATIVE_THEME_CHANGED, (_event: any, state: NativeThemeState) => {
276
+ this.nativeThemeState.set(state);
277
+ this.applyAutoTheme(state);
278
+ });
279
+ }
280
+
281
+ private applyAutoTheme(state: NativeThemeState) {
282
+ if (this.autoTheme()) {
283
+ this.theme.set(state.shouldUseDarkColors ? 'midnight' : 'light');
284
+ }
285
+ }
286
+
287
+ setAutoTheme(enabled: boolean) {
288
+ this.autoTheme.set(enabled);
289
+ if (enabled) {
290
+ const state = this.nativeThemeState();
291
+ if (state) {
292
+ this.theme.set(state.shouldUseDarkColors ? 'midnight' : 'light');
293
+ }
294
+ }
295
+ }
296
+
297
+ // --- System Preferences ---
298
+
299
+ private async initSystemPreferences() {
300
+ const prefs = await this.messageService.getSystemPreferences();
301
+ this.systemAccentColor.set(prefs.accentColor);
302
+ this.reduceMotion.set(prefs.reduceMotion);
303
+ }
304
+
305
+ // --- Power Monitor ---
306
+
307
+ private initPowerMonitor() {
308
+ this.messageService.on(ChannelType.IPC_POWER_SUSPEND, () => {
309
+ this.suspended.set(true);
310
+ });
311
+
312
+ this.messageService.on(ChannelType.IPC_POWER_RESUME, () => {
313
+ this.suspended.set(false);
314
+ });
315
+ }
316
+
317
+ // --- Scan Progress (MessageChannelMain) ---
318
+
319
+ private initScanProgress() {
320
+ this.electronService.ipcRenderer.on(ChannelType.IPC_SCAN_PROGRESS_PORT, (event: any) => {
321
+ const port = event.ports[0];
322
+ if (!port) return;
323
+
324
+ port.onmessage = (msgEvent: MessageEvent<ScanProgress>) => {
325
+ this.scanProgress.set(msgEvent.data);
326
+ };
327
+
328
+ port.onclose = () => {
329
+ this.scanProgress.set(null);
330
+ };
331
+
332
+ port.start();
333
+ });
334
+ }
335
+
244
336
  private async loadNavigationExpandedSettings() {
245
337
  const ids = (await this.messageService.get(StorageType.NavigationExpanded, null)) as number[] | null;
246
338
  if (Array.isArray(ids)) {
@@ -1,5 +1,16 @@
1
- <div class="panel">
2
- <div class="flex items-center px-2">
1
+ <div class="panel h-full w-full flex items-center justify-between px-2">
2
+ <div class="flex items-center gap-2">
3
3
  <app-spinner [animating]="presentation.loading()" />
4
+ @if (presentation.scanProgress(); as progress) {
5
+ <span class="text-[10px] truncate" [style.color]="'var(--text-muted)'">
6
+ {{ progress.currentFile }} ({{ progress.processed }}/{{ progress.total }})
7
+ </span>
8
+ }
9
+ </div>
10
+ <div class="flex items-center gap-3">
11
+ <span class="text-[10px]" [style.color]="'var(--text-muted)'">Fontastic v{{ appVersion() }}</span>
12
+ <span class="text-[10px]" [style.color]="'var(--text-muted)'">Electron {{ electronVersion() }}</span>
13
+ <span class="text-[10px]" [style.color]="'var(--text-muted)'">Chrome {{ chromeVersion() }}</span>
14
+ <span class="text-[10px]" [style.color]="'var(--text-muted)'">Node {{ nodeVersion() }}</span>
4
15
  </div>
5
16
  </div>
@@ -1,6 +1,6 @@
1
- import { Component, inject } from '@angular/core';
1
+ import { Component, inject, signal } from '@angular/core';
2
2
  import { SpinnerComponent } from '../../shared/components';
3
- import { PresentationService } from '../../core/services';
3
+ import { ElectronService, PresentationService } from '../../core/services';
4
4
 
5
5
  @Component({
6
6
  selector: 'app-footer',
@@ -10,4 +10,20 @@ import { PresentationService } from '../../core/services';
10
10
  })
11
11
  export class FooterComponent {
12
12
  readonly presentation = inject(PresentationService);
13
+ private readonly electron = inject(ElectronService);
14
+
15
+ readonly appVersion = signal('');
16
+ readonly electronVersion = signal('');
17
+ readonly chromeVersion = signal('');
18
+ readonly nodeVersion = signal('');
19
+
20
+ constructor() {
21
+ if (this.electron.isElectron) {
22
+ const versions = (window as any).process.versions;
23
+ this.electronVersion.set(versions.electron ?? '');
24
+ this.chromeVersion.set(versions.chrome ?? '');
25
+ this.nodeVersion.set(versions.node ?? '');
26
+ this.electron.ipcRenderer.invoke('app:get-version').then((v: string) => this.appVersion.set(v));
27
+ }
28
+ }
13
29
  }