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
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "1.2.0"
2
+ ".": "1.3.0"
3
3
  }
package/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.3.0](https://github.com/tomshaw/fontastic/compare/fontastic-v1.2.0...fontastic-v1.3.0) (2026-03-15)
4
+
5
+
6
+ ### 🎉 Features
7
+
8
+ * add smart collection preview functionality and enhance rule builder component ([436b162](https://github.com/tomshaw/fontastic/commit/436b16242ddae4eaaff3279d749b70dc79c9098c))
9
+ * enhance application with secure storage and scan progress features ([7a2576b](https://github.com/tomshaw/fontastic/commit/7a2576b5484ce3d8ce1c06171f60608b43c3e42b))
10
+
11
+
12
+ ### 🛠️ Fixes
13
+
14
+ * add height class to footer layout for improved display ([10da788](https://github.com/tomshaw/fontastic/commit/10da78803ba3ccec3cf9f76646a896a013a8935f))
15
+ * adjust collection order handling in createParent and createChild methods ([3c9a0c4](https://github.com/tomshaw/fontastic/commit/3c9a0c4e2062a3c8a36b32f71193b01ddc57073e))
16
+ * refactor footer component layout and enhance version handling ([dc0475e](https://github.com/tomshaw/fontastic/commit/dc0475ed4c67a326a7710894d9145731a7909b5d))
17
+ * update collection order handling in createParent and createChild methods ([ce855d0](https://github.com/tomshaw/fontastic/commit/ce855d0664ab391fd15fed0bbffd16568a019699))
18
+ * update footer layout to use grid for improved structure and responsiveness ([b2a4dac](https://github.com/tomshaw/fontastic/commit/b2a4dac434f9ea732180f32477854905c0fa80fb))
19
+
20
+
21
+ ### 🏗️ Refactor
22
+
23
+ * standardize string quotes and improve code readability ([f352ddb](https://github.com/tomshaw/fontastic/commit/f352ddb248e6c7dc1e566917ebf57ebabdde3531))
24
+
3
25
  ## [1.2.0](https://github.com/tomshaw/fontastic/compare/fontastic-v1.1.0...fontastic-v1.2.0) (2026-03-14)
4
26
 
5
27
 
package/README.md CHANGED
@@ -23,6 +23,12 @@ Fontastic is an Electron-based font management and cataloging application built
23
23
  - Glyph inspector — browse and examine individual characters and Unicode points
24
24
  - Waterfall preview — compare text rendering across multiple sizes at a glance
25
25
  - Font table viewer — read raw OpenType and TrueType metadata tables
26
+ - System theme sync — automatically matches OS light/dark mode via `nativeTheme`
27
+ - Encrypted storage — API keys secured at rest using OS keychain via `safeStorage`
28
+ - Scan progress — real-time font import progress streamed over `MessageChannelMain`
29
+ - Power aware — pauses activity on system sleep and resumes on wake via `powerMonitor`
30
+ - Session hardening — CSP headers, permission deny-list, and cache management via `session`
31
+ - Accessibility — respects OS reduced motion and accent color via `systemPreferences`
26
32
  - Cross-platform — builds for Windows, macOS, and Linux
27
33
 
28
34
  ## Getting Started
@@ -26,12 +26,12 @@ class Application {
26
26
  const systemManager = new SystemManager_1.default(this.machineId, this.isProduction);
27
27
  const configManager = new ConfigManager_1.default(systemManager);
28
28
  configManager.initialize();
29
+ const menuBuilder = new MenuBuilder_1.default(this.mainWindow, this.isProduction);
29
30
  const connectionManager = new ConnectionManager_1.default(configManager);
30
- yield connectionManager.initialize();
31
+ // Initialize menu and database connection in parallel — menu doesn't depend on DB
32
+ yield Promise.all([connectionManager.initialize(), Promise.resolve(menuBuilder.initialize())]);
31
33
  const fontManager = new FontManager_1.default(systemManager, configManager, connectionManager);
32
- const menuBuilder = new MenuBuilder_1.default(this.mainWindow, this.isProduction);
33
- menuBuilder.initialize();
34
- const messageHandler = new MessageHandler_1.default(systemManager, configManager, connectionManager, fontManager);
34
+ const messageHandler = new MessageHandler_1.default(systemManager, configManager, connectionManager, fontManager, this.mainWindow);
35
35
  messageHandler.initialize();
36
36
  });
37
37
  }
@@ -1,13 +1,12 @@
1
- import SystemManager from "./core/SystemManager";
2
- import ConfigManager from "./core/ConfigManager";
3
- import ConnectionManager from "./core/ConnectionManager";
4
- import FontManager from "./core/FontManager";
5
- import MessageHandler from "./core/MessageHandler";
6
- import MenuBuilder from "./core/menu/MenuBuilder";
7
- import { BrowserWindow } from "electron";
1
+ import SystemManager from './core/SystemManager';
2
+ import ConfigManager from './core/ConfigManager';
3
+ import ConnectionManager from './core/ConnectionManager';
4
+ import FontManager from './core/FontManager';
5
+ import MessageHandler from './core/MessageHandler';
6
+ import MenuBuilder from './core/menu/MenuBuilder';
7
+ import { BrowserWindow } from 'electron';
8
8
 
9
9
  export default class Application {
10
-
11
10
  machineId: string;
12
11
  isProduction: boolean;
13
12
  mainWindow: BrowserWindow;
@@ -24,15 +23,16 @@ export default class Application {
24
23
  const configManager = new ConfigManager(systemManager);
25
24
  configManager.initialize();
26
25
 
26
+ const menuBuilder = new MenuBuilder(this.mainWindow, this.isProduction);
27
+
27
28
  const connectionManager = new ConnectionManager(configManager);
28
- await connectionManager.initialize()
29
29
 
30
- const fontManager = new FontManager(systemManager, configManager, connectionManager);
30
+ // Initialize menu and database connection in parallel — menu doesn't depend on DB
31
+ await Promise.all([connectionManager.initialize(), Promise.resolve(menuBuilder.initialize())]);
31
32
 
32
- const menuBuilder = new MenuBuilder(this.mainWindow, this.isProduction);
33
- menuBuilder.initialize();
33
+ const fontManager = new FontManager(systemManager, configManager, connectionManager);
34
34
 
35
- const messageHandler = new MessageHandler(systemManager, configManager, connectionManager, fontManager);
35
+ const messageHandler = new MessageHandler(systemManager, configManager, connectionManager, fontManager, this.mainWindow);
36
36
  messageHandler.initialize();
37
37
  }
38
38
  }
@@ -146,7 +146,7 @@ exports.dbColumns = [
146
146
  searchable: true,
147
147
  },
148
148
  {
149
- name: 'manufacturer',
149
+ name: 'manufacturer_url',
150
150
  searchable: true,
151
151
  },
152
152
  {
@@ -145,7 +145,7 @@ export const dbColumns = [
145
145
  searchable: true,
146
146
  },
147
147
  {
148
- name: 'manufacturer',
148
+ name: 'manufacturer_url',
149
149
  searchable: true,
150
150
  },
151
151
  {
@@ -3,41 +3,41 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.installable = exports.mimeTypes = exports.fontMimeTypes = void 0;
4
4
  exports.fontMimeTypes = [
5
5
  {
6
- type: "font/ttf",
7
- name: "TrueType",
6
+ type: 'font/ttf',
7
+ name: 'TrueType',
8
8
  description: "TrueType is an outline font standard developed by Apple in the late 1980s as a competitor to Adobe's Type 1 fonts used in PostScript. It has become the most common format for fonts on the classic Mac OS, macOS, and Microsoft Windows operating systems.",
9
- installable: true
9
+ installable: true,
10
10
  },
11
11
  {
12
- type: "font/otf",
13
- name: "OpenType",
12
+ type: 'font/otf',
13
+ name: 'OpenType',
14
14
  description: "OpenType is a format for scalable computer fonts. It was built on its predecessor TrueType, retaining TrueType's basic structure and adding many intricate data structures for prescribing typographic behavior. OpenType is a registered trademark of Microsoft Corporation.",
15
- installable: true
15
+ installable: true,
16
16
  },
17
17
  {
18
- type: "font/woff",
19
- name: "Web Open Font Format",
20
- description: "The Web Open Font Format is a font format for use in web pages. WOFF files are OpenType or TrueType fonts, with format-specific compression applied and additional XML metadata added.",
21
- installable: false
18
+ type: 'font/woff',
19
+ name: 'Web Open Font Format',
20
+ description: 'The Web Open Font Format is a font format for use in web pages. WOFF files are OpenType or TrueType fonts, with format-specific compression applied and additional XML metadata added.',
21
+ installable: false,
22
22
  },
23
23
  {
24
- type: "font/woff2",
25
- name: "Web Open Font Format 2",
26
- description: "A WOFF2 file is a web font file created in the WOFF (Web Open Font Format) 2.0 format, an open format used to deliver webpage fonts on the fly. It is saved as a compressed container that supports TrueType (. TTF) and OpenType (. OTF) fonts. WOFF2 files also support font licensing metadata.",
27
- installable: false
24
+ type: 'font/woff2',
25
+ name: 'Web Open Font Format 2',
26
+ description: 'A WOFF2 file is a web font file created in the WOFF (Web Open Font Format) 2.0 format, an open format used to deliver webpage fonts on the fly. It is saved as a compressed container that supports TrueType (. TTF) and OpenType (. OTF) fonts. WOFF2 files also support font licensing metadata.',
27
+ installable: false,
28
28
  },
29
29
  {
30
- type: "font/ttc",
31
- name: "TrueType Collection",
32
- description: "TrueType Collection (TTC) is an extension of TrueType format that allows combining multiple fonts into a single file, creating substantial space savings for a collection of fonts with many glyphs in common.",
33
- installable: false
30
+ type: 'font/ttc',
31
+ name: 'TrueType Collection',
32
+ description: 'TrueType Collection (TTC) is an extension of TrueType format that allows combining multiple fonts into a single file, creating substantial space savings for a collection of fonts with many glyphs in common.',
33
+ installable: false,
34
34
  },
35
35
  {
36
- type: "font/dfont",
37
- name: "Datafork TrueType",
38
- description: "Datafork TrueType is a font wrapper used on Apple Macintosh computers running Mac OS X. It is a TrueType suitcase with the resource map in the data fork, rather than the resource fork as had been the case in Mac OS 9.",
39
- installable: false
40
- }
36
+ type: 'font/dfont',
37
+ name: 'Datafork TrueType',
38
+ description: 'Datafork TrueType is a font wrapper used on Apple Macintosh computers running Mac OS X. It is a TrueType suitcase with the resource map in the data fork, rather than the resource fork as had been the case in Mac OS 9.',
39
+ installable: false,
40
+ },
41
41
  ];
42
42
  exports.mimeTypes = exports.fontMimeTypes.map((item) => item.type);
43
43
  exports.installable = exports.fontMimeTypes.reduce((prev, curr) => {
@@ -1,54 +1,60 @@
1
1
  interface Mime {
2
2
  type: string;
3
3
  name: string;
4
- description: string,
5
- installable: boolean
4
+ description: string;
5
+ installable: boolean;
6
6
  }
7
7
 
8
8
  export const fontMimeTypes: Mime[] = [
9
9
  {
10
- type: "font/ttf",
11
- name: "TrueType",
12
- description: "TrueType is an outline font standard developed by Apple in the late 1980s as a competitor to Adobe's Type 1 fonts used in PostScript. It has become the most common format for fonts on the classic Mac OS, macOS, and Microsoft Windows operating systems.",
13
- installable: true
10
+ type: 'font/ttf',
11
+ name: 'TrueType',
12
+ description:
13
+ "TrueType is an outline font standard developed by Apple in the late 1980s as a competitor to Adobe's Type 1 fonts used in PostScript. It has become the most common format for fonts on the classic Mac OS, macOS, and Microsoft Windows operating systems.",
14
+ installable: true,
14
15
  },
15
16
  {
16
- type: "font/otf",
17
- name: "OpenType",
18
- description: "OpenType is a format for scalable computer fonts. It was built on its predecessor TrueType, retaining TrueType's basic structure and adding many intricate data structures for prescribing typographic behavior. OpenType is a registered trademark of Microsoft Corporation.",
19
- installable: true
17
+ type: 'font/otf',
18
+ name: 'OpenType',
19
+ description:
20
+ "OpenType is a format for scalable computer fonts. It was built on its predecessor TrueType, retaining TrueType's basic structure and adding many intricate data structures for prescribing typographic behavior. OpenType is a registered trademark of Microsoft Corporation.",
21
+ installable: true,
20
22
  },
21
23
  {
22
- type: "font/woff",
23
- name: "Web Open Font Format",
24
- description: "The Web Open Font Format is a font format for use in web pages. WOFF files are OpenType or TrueType fonts, with format-specific compression applied and additional XML metadata added.",
25
- installable: false
24
+ type: 'font/woff',
25
+ name: 'Web Open Font Format',
26
+ description:
27
+ 'The Web Open Font Format is a font format for use in web pages. WOFF files are OpenType or TrueType fonts, with format-specific compression applied and additional XML metadata added.',
28
+ installable: false,
26
29
  },
27
30
  {
28
- type: "font/woff2",
29
- name: "Web Open Font Format 2",
30
- description: "A WOFF2 file is a web font file created in the WOFF (Web Open Font Format) 2.0 format, an open format used to deliver webpage fonts on the fly. It is saved as a compressed container that supports TrueType (. TTF) and OpenType (. OTF) fonts. WOFF2 files also support font licensing metadata.",
31
- installable: false
31
+ type: 'font/woff2',
32
+ name: 'Web Open Font Format 2',
33
+ description:
34
+ 'A WOFF2 file is a web font file created in the WOFF (Web Open Font Format) 2.0 format, an open format used to deliver webpage fonts on the fly. It is saved as a compressed container that supports TrueType (. TTF) and OpenType (. OTF) fonts. WOFF2 files also support font licensing metadata.',
35
+ installable: false,
32
36
  },
33
37
  {
34
- type: "font/ttc",
35
- name: "TrueType Collection",
36
- description: "TrueType Collection (TTC) is an extension of TrueType format that allows combining multiple fonts into a single file, creating substantial space savings for a collection of fonts with many glyphs in common.",
37
- installable: false
38
+ type: 'font/ttc',
39
+ name: 'TrueType Collection',
40
+ description:
41
+ 'TrueType Collection (TTC) is an extension of TrueType format that allows combining multiple fonts into a single file, creating substantial space savings for a collection of fonts with many glyphs in common.',
42
+ installable: false,
38
43
  },
39
44
  {
40
- type: "font/dfont",
41
- name: "Datafork TrueType",
42
- description: "Datafork TrueType is a font wrapper used on Apple Macintosh computers running Mac OS X. It is a TrueType suitcase with the resource map in the data fork, rather than the resource fork as had been the case in Mac OS 9.",
43
- installable: false
44
- }
45
+ type: 'font/dfont',
46
+ name: 'Datafork TrueType',
47
+ description:
48
+ 'Datafork TrueType is a font wrapper used on Apple Macintosh computers running Mac OS X. It is a TrueType suitcase with the resource map in the data fork, rather than the resource fork as had been the case in Mac OS 9.',
49
+ installable: false,
50
+ },
45
51
  ];
46
52
 
47
53
  export const mimeTypes: string[] = fontMimeTypes.map((item) => item.type);
48
54
 
49
- export const installable: Mime[] = fontMimeTypes.reduce((prev, curr) => {
55
+ export const installable: string[] = fontMimeTypes.reduce((prev: string[], curr) => {
50
56
  if (curr.installable) {
51
57
  prev.push(curr.type);
52
58
  }
53
59
  return prev;
54
- }, []);
60
+ }, []);
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ const electron_1 = require("electron");
3
4
  const database_1 = require("../config/database");
4
5
  const StorageType_1 = require("../enums/StorageType");
5
6
  const Store = require('electron-store');
@@ -26,6 +27,32 @@ class ConfigManager {
26
27
  clear() {
27
28
  return store.clear();
28
29
  }
30
+ // --- Safe Storage (OS keychain encryption) ---
31
+ setSecure(key, value) {
32
+ if (electron_1.safeStorage.isEncryptionAvailable()) {
33
+ const encrypted = electron_1.safeStorage.encryptString(value);
34
+ store.set(key, encrypted.toString('base64'));
35
+ }
36
+ else {
37
+ store.set(key, value);
38
+ }
39
+ }
40
+ getSecure(key) {
41
+ const raw = store.get(key);
42
+ if (!raw)
43
+ return null;
44
+ if (electron_1.safeStorage.isEncryptionAvailable() && typeof raw === 'string') {
45
+ try {
46
+ const buffer = Buffer.from(raw, 'base64');
47
+ return electron_1.safeStorage.decryptString(buffer);
48
+ }
49
+ catch (_a) {
50
+ // Fallback: value may have been stored before encryption was enabled
51
+ return raw;
52
+ }
53
+ }
54
+ return typeof raw === 'string' ? raw : null;
55
+ }
29
56
  toArray() {
30
57
  return store.store;
31
58
  }
@@ -1,3 +1,4 @@
1
+ import { safeStorage } from 'electron';
1
2
  import SystemManager from './SystemManager';
2
3
  import { database } from '../config/database';
3
4
  import { StorageType } from '../enums/StorageType';
@@ -35,6 +36,33 @@ export default class ConfigManager {
35
36
  return store.clear();
36
37
  }
37
38
 
39
+ // --- Safe Storage (OS keychain encryption) ---
40
+
41
+ setSecure(key: string, value: string): void {
42
+ if (safeStorage.isEncryptionAvailable()) {
43
+ const encrypted = safeStorage.encryptString(value);
44
+ store.set(key, encrypted.toString('base64'));
45
+ } else {
46
+ store.set(key, value);
47
+ }
48
+ }
49
+
50
+ getSecure(key: string): string | null {
51
+ const raw = store.get(key);
52
+ if (!raw) return null;
53
+
54
+ if (safeStorage.isEncryptionAvailable() && typeof raw === 'string') {
55
+ try {
56
+ const buffer = Buffer.from(raw, 'base64');
57
+ return safeStorage.decryptString(buffer);
58
+ } catch {
59
+ // Fallback: value may have been stored before encryption was enabled
60
+ return raw;
61
+ }
62
+ }
63
+ return typeof raw === 'string' ? raw : null;
64
+ }
65
+
38
66
  toArray(): any {
39
67
  return store.store;
40
68
  }
@@ -13,58 +13,109 @@ const FontObject_1 = require("./FontObject");
13
13
  const fs = require("fs/promises");
14
14
  const path = require("path");
15
15
  const mimes_1 = require("../config/mimes");
16
- const prettyBytes = require("pretty-bytes");
17
- const mime = require("mime");
16
+ const prettyBytes = require('pretty-bytes');
17
+ const mime = require('mime');
18
+ const SCAN_CONCURRENCY = 10;
18
19
  class FontFinder {
19
- constructor(connectionManager) {
20
+ constructor(connectionManager, onProgress) {
20
21
  this.errors = [];
21
22
  this.counter = 0;
23
+ this.onProgress = null;
22
24
  this.connectionManager = connectionManager;
25
+ this.onProgress = onProgress !== null && onProgress !== void 0 ? onProgress : null;
23
26
  }
24
- isFontFile(filePath) {
27
+ getFontMimeType(filePath) {
25
28
  const fileType = mime.getType(filePath);
26
- return fileType && mimes_1.mimeTypes.includes(fileType);
29
+ return fileType && mimes_1.mimeTypes.includes(fileType) ? fileType : null;
27
30
  }
28
31
  scanFiles(files, options) {
29
32
  return __awaiter(this, void 0, void 0, function* () {
33
+ this.errors = [];
34
+ this.counter = 0;
35
+ const fontFiles = [];
30
36
  for (const fp of files) {
31
- if (this.isFontFile(fp)) {
32
- const stat = yield fs.stat(fp);
33
- yield this.processFont(fp, stat, options);
37
+ const fileType = this.getFontMimeType(fp);
38
+ if (fileType) {
39
+ fontFiles.push({ fp, fileType });
34
40
  }
35
41
  }
42
+ yield this.processInBatches(fontFiles, options);
36
43
  });
37
44
  }
38
45
  scanFolder(dir, options) {
39
46
  return __awaiter(this, void 0, void 0, function* () {
47
+ this.errors = [];
48
+ this.counter = 0;
49
+ const fontFiles = yield this.collectFontFiles(dir);
50
+ yield this.processInBatches(fontFiles, options);
51
+ });
52
+ }
53
+ collectFontFiles(dir) {
54
+ return __awaiter(this, void 0, void 0, function* () {
55
+ const results = [];
40
56
  const entries = yield fs.readdir(dir, { withFileTypes: true });
57
+ const subdirPromises = [];
41
58
  for (const entry of entries) {
42
59
  const fp = path.join(dir, entry.name);
43
60
  if (entry.isDirectory()) {
44
- yield this.scanFolder(fp, options);
61
+ subdirPromises.push(this.collectFontFiles(fp));
62
+ }
63
+ else {
64
+ const fileType = this.getFontMimeType(fp);
65
+ if (fileType) {
66
+ results.push({ fp, fileType });
67
+ }
45
68
  }
46
- else if (this.isFontFile(fp)) {
47
- const stat = yield fs.stat(fp);
48
- yield this.processFont(fp, stat, options);
69
+ }
70
+ if (subdirPromises.length > 0) {
71
+ const subdirResults = yield Promise.all(subdirPromises);
72
+ for (const subResults of subdirResults) {
73
+ results.push(...subResults);
49
74
  }
50
75
  }
76
+ return results;
51
77
  });
52
78
  }
53
- processFont(fp, stat, options) {
79
+ processInBatches(fontFiles, options) {
80
+ return __awaiter(this, void 0, void 0, function* () {
81
+ const total = fontFiles.length;
82
+ for (let i = 0; i < fontFiles.length; i += SCAN_CONCURRENCY) {
83
+ const batch = fontFiles.slice(i, i + SCAN_CONCURRENCY);
84
+ yield Promise.all(batch.map(({ fp, fileType }) => this.processFont(fp, fileType, options)));
85
+ if (this.onProgress) {
86
+ const lastFile = batch[batch.length - 1];
87
+ this.onProgress({
88
+ processed: Math.min(i + SCAN_CONCURRENCY, total),
89
+ total,
90
+ currentFile: path.basename(lastFile.fp),
91
+ errors: this.errors.length,
92
+ });
93
+ }
94
+ }
95
+ });
96
+ }
97
+ processFont(fp, fileType, options) {
54
98
  return __awaiter(this, void 0, void 0, function* () {
55
99
  const font = new FontObject_1.default(fp);
56
100
  if (font.hasError()) {
57
101
  this.errors.push(font.getError());
58
102
  return;
59
103
  }
60
- const fileType = mime.getType(fp);
104
+ let stat;
105
+ try {
106
+ stat = yield fs.stat(fp);
107
+ }
108
+ catch (err) {
109
+ this.errors.push({ file: fp, message: err.message });
110
+ return;
111
+ }
61
112
  const data = Object.assign(Object.assign({ file_path: fp, file_name: path.basename(fp), file_size: stat.size, file_size_pretty: prettyBytes(stat.size), file_type: fileType, installable: mimes_1.installable.includes(fileType) }, options), font.getNamesTable());
62
113
  try {
63
114
  yield this.connectionManager.getStoreRepository().create(data);
64
115
  this.counter++;
65
116
  }
66
117
  catch (err) {
67
- this.errors.push(err.message);
118
+ this.errors.push({ file: fp, message: err.message });
68
119
  }
69
120
  });
70
121
  }
@@ -1,50 +1,103 @@
1
- import ConnectionManager from "./ConnectionManager";
2
- import FontObject from "./FontObject";
3
- import * as fs from "fs/promises";
4
- import * as path from "path";
5
- import { mimeTypes, installable } from "../config/mimes";
1
+ import ConnectionManager from './ConnectionManager';
2
+ import FontObject from './FontObject';
3
+ import * as fs from 'fs/promises';
4
+ import * as path from 'path';
5
+ import { mimeTypes, installable } from '../config/mimes';
6
+ import type { ScanProgress } from '../types';
6
7
 
7
- const prettyBytes = require("pretty-bytes");
8
- const mime = require("mime");
8
+ const prettyBytes = require('pretty-bytes');
9
+ const mime = require('mime');
9
10
 
10
- export default class FontFinder {
11
+ const SCAN_CONCURRENCY = 10;
12
+
13
+ export type ProgressCallback = (progress: ScanProgress) => void;
11
14
 
15
+ export default class FontFinder {
12
16
  connectionManager: ConnectionManager;
13
17
  errors: any[] = [];
14
18
  counter: number = 0;
19
+ private onProgress: ProgressCallback | null = null;
15
20
 
16
- constructor(connectionManager: ConnectionManager) {
21
+ constructor(connectionManager: ConnectionManager, onProgress?: ProgressCallback) {
17
22
  this.connectionManager = connectionManager;
23
+ this.onProgress = onProgress ?? null;
18
24
  }
19
25
 
20
- private isFontFile(filePath: string): boolean {
26
+ private getFontMimeType(filePath: string): string | null {
21
27
  const fileType = mime.getType(filePath);
22
- return fileType && mimeTypes.includes(fileType);
28
+ return fileType && mimeTypes.includes(fileType) ? fileType : null;
23
29
  }
24
30
 
25
31
  async scanFiles(files: string[], options: any) {
32
+ this.errors = [];
33
+ this.counter = 0;
34
+
35
+ const fontFiles: { fp: string; fileType: string }[] = [];
26
36
  for (const fp of files) {
27
- if (this.isFontFile(fp)) {
28
- const stat = await fs.stat(fp);
29
- await this.processFont(fp, stat, options);
37
+ const fileType = this.getFontMimeType(fp);
38
+ if (fileType) {
39
+ fontFiles.push({ fp, fileType });
30
40
  }
31
41
  }
42
+
43
+ await this.processInBatches(fontFiles, options);
32
44
  }
33
45
 
34
46
  async scanFolder(dir: string, options: any) {
47
+ this.errors = [];
48
+ this.counter = 0;
49
+
50
+ const fontFiles = await this.collectFontFiles(dir);
51
+ await this.processInBatches(fontFiles, options);
52
+ }
53
+
54
+ private async collectFontFiles(dir: string): Promise<{ fp: string; fileType: string }[]> {
55
+ const results: { fp: string; fileType: string }[] = [];
35
56
  const entries = await fs.readdir(dir, { withFileTypes: true });
57
+
58
+ const subdirPromises: Promise<{ fp: string; fileType: string }[]>[] = [];
59
+
36
60
  for (const entry of entries) {
37
61
  const fp = path.join(dir, entry.name);
38
62
  if (entry.isDirectory()) {
39
- await this.scanFolder(fp, options);
40
- } else if (this.isFontFile(fp)) {
41
- const stat = await fs.stat(fp);
42
- await this.processFont(fp, stat, options);
63
+ subdirPromises.push(this.collectFontFiles(fp));
64
+ } else {
65
+ const fileType = this.getFontMimeType(fp);
66
+ if (fileType) {
67
+ results.push({ fp, fileType });
68
+ }
69
+ }
70
+ }
71
+
72
+ if (subdirPromises.length > 0) {
73
+ const subdirResults = await Promise.all(subdirPromises);
74
+ for (const subResults of subdirResults) {
75
+ results.push(...subResults);
43
76
  }
44
77
  }
78
+
79
+ return results;
45
80
  }
46
81
 
47
- private async processFont(fp: string, stat: any, options: any) {
82
+ private async processInBatches(fontFiles: { fp: string; fileType: string }[], options: any) {
83
+ const total = fontFiles.length;
84
+ for (let i = 0; i < fontFiles.length; i += SCAN_CONCURRENCY) {
85
+ const batch = fontFiles.slice(i, i + SCAN_CONCURRENCY);
86
+ await Promise.all(batch.map(({ fp, fileType }) => this.processFont(fp, fileType, options)));
87
+
88
+ if (this.onProgress) {
89
+ const lastFile = batch[batch.length - 1];
90
+ this.onProgress({
91
+ processed: Math.min(i + SCAN_CONCURRENCY, total),
92
+ total,
93
+ currentFile: path.basename(lastFile.fp),
94
+ errors: this.errors.length,
95
+ });
96
+ }
97
+ }
98
+ }
99
+
100
+ private async processFont(fp: string, fileType: string, options: any) {
48
101
  const font = new FontObject(fp);
49
102
 
50
103
  if (font.hasError()) {
@@ -52,7 +105,13 @@ export default class FontFinder {
52
105
  return;
53
106
  }
54
107
 
55
- const fileType = mime.getType(fp);
108
+ let stat;
109
+ try {
110
+ stat = await fs.stat(fp);
111
+ } catch (err: any) {
112
+ this.errors.push({ file: fp, message: err.message });
113
+ return;
114
+ }
56
115
 
57
116
  const data = {
58
117
  file_path: fp,
@@ -68,8 +127,8 @@ export default class FontFinder {
68
127
  try {
69
128
  await this.connectionManager.getStoreRepository().create(data);
70
129
  this.counter++;
71
- } catch (err) {
72
- this.errors.push(err.message);
130
+ } catch (err: any) {
131
+ this.errors.push({ file: fp, message: err.message });
73
132
  }
74
133
  }
75
134
  }