fontastic 1.0.2 → 1.2.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.
@@ -12,12 +12,6 @@ on:
12
12
 
13
13
  jobs:
14
14
  claude-review:
15
- # Optional: Filter by PR author
16
- # if: |
17
- # github.event.pull_request.user.login == 'external-contributor' ||
18
- # github.event.pull_request.user.login == 'new-developer' ||
19
- # github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
20
-
21
15
  runs-on: ubuntu-latest
22
16
  permissions:
23
17
  contents: read
@@ -0,0 +1,25 @@
1
+ on:
2
+ push:
3
+ branches:
4
+ - master
5
+ workflow_dispatch:
6
+
7
+ concurrency:
8
+ group: ${{ github.workflow }}-${{ github.ref }}
9
+ cancel-in-progress: true
10
+
11
+ permissions:
12
+ contents: write
13
+ pull-requests: write
14
+
15
+ name: release-please
16
+
17
+ jobs:
18
+ release-please:
19
+ runs-on: ubuntu-latest
20
+ steps:
21
+ - uses: googleapis/release-please-action@v4.4.0
22
+ with:
23
+ token: ${{ secrets.RELEASE_PLEASE_TOKEN }}
24
+ config-file: release-please-config.json
25
+ manifest-file: .release-please-manifest.json
@@ -1,117 +1,14 @@
1
- name: Release
1
+ name: Release Build
2
2
 
3
3
  on:
4
- workflow_dispatch:
5
- inputs:
6
- bump:
7
- description: 'Version bump type'
8
- required: true
9
- type: choice
10
- options: [patch, minor, major]
4
+ release:
5
+ types: [published]
11
6
 
12
7
  permissions:
13
8
  contents: write
14
9
 
15
10
  jobs:
16
- version:
17
- runs-on: ubuntu-22.04
18
- outputs:
19
- version: ${{ steps.version.outputs.version }}
20
- steps:
21
- - name: Guard — owner only
22
- if: github.actor != github.repository_owner
23
- run: |
24
- echo "Only the repository owner can trigger releases."
25
- exit 1
26
-
27
- - name: Checkout
28
- uses: actions/checkout@v6
29
- with:
30
- fetch-depth: 0
31
-
32
- - name: Setup Node 24
33
- uses: actions/setup-node@v6
34
- with:
35
- node-version: '24'
36
-
37
- - name: Configure git
38
- run: |
39
- git config user.name "github-actions[bot]"
40
- git config user.email "github-actions[bot]@users.noreply.github.com"
41
-
42
- - name: Bump version
43
- env:
44
- BUMP: ${{ github.event.inputs.bump }}
45
- run: |
46
- npm version "$BUMP" --no-git-tag-version
47
- node -e "const fs=require('fs');const app=JSON.parse(fs.readFileSync('./app/package.json'));app.version=require('./package.json').version;fs.writeFileSync('./app/package.json',JSON.stringify(app,null,2)+'\n');"
48
-
49
- - name: Capture new version
50
- id: version
51
- run: echo "version=$(jq -r .version package.json)" >> "$GITHUB_OUTPUT"
52
-
53
- - name: Generate changelog
54
- run: |
55
- node -e "
56
- const { execSync } = require('child_process');
57
- const fs = require('fs');
58
-
59
- const lastTag = execSync('git describe --tags --abbrev=0').toString().trim();
60
- const log = execSync('git log ' + lastTag + '..HEAD --format=%s').toString().trim();
61
-
62
- const types = [
63
- ['feat', '### Features'],
64
- ['fix', '### Bug Fixes'],
65
- ['refactor', '### Refactoring'],
66
- ['revert', '### Reverts'],
67
- ['perf', '### Performance'],
68
- ];
69
- const typeMap = Object.fromEntries(types.map(([k, v]) => [k, v]));
70
-
71
- const pattern = /^(\w+)(\(.+?\))?(!)?: (.+)$/;
72
- const groups = {};
73
-
74
- for (const line of log.split('\n')) {
75
- const m = line.match(pattern);
76
- if (!m) continue;
77
- const type = m[1];
78
- const scope = m[2] ? m[2].slice(1, -1) : null;
79
- const msg = m[4];
80
- const label = typeMap[type];
81
- if (!label) continue;
82
- if (!groups[type]) groups[type] = [];
83
- groups[type].push(scope ? '- **' + scope + '**: ' + msg : '- ' + msg);
84
- }
85
-
86
- const sections = types
87
- .filter(([k]) => groups[k])
88
- .map(([k, header]) => header + '\n' + groups[k].join('\n'))
89
- .join('\n\n');
90
-
91
- const body = sections || '_No conventional commits found since last release._';
92
- fs.writeFileSync('CHANGELOG_BODY.md', body);
93
- console.log('Changelog written.');
94
- "
95
-
96
- - name: Commit, tag, and push
97
- env:
98
- VERSION: ${{ steps.version.outputs.version }}
99
- run: |
100
- git add package.json app/package.json
101
- git commit -m "[Bumped Version] ${VERSION}"
102
- git push origin master
103
- git tag "${VERSION}"
104
- git push origin "${VERSION}"
105
-
106
- - name: Create GitHub Release
107
- env:
108
- GH_TOKEN: ${{ github.token }}
109
- VERSION: ${{ steps.version.outputs.version }}
110
- run: gh release create "${VERSION}" --notes-file CHANGELOG_BODY.md --title "${VERSION}"
111
-
112
-
113
11
  build:
114
- needs: version
115
12
  strategy:
116
13
  matrix:
117
14
  include:
@@ -126,7 +23,7 @@ jobs:
126
23
  - name: Checkout tag
127
24
  uses: actions/checkout@v6
128
25
  with:
129
- ref: ${{ needs.version.outputs.version }}
26
+ ref: ${{ github.event.release.tag_name }}
130
27
 
131
28
  - name: Setup Node 24
132
29
  uses: actions/setup-node@v6
@@ -152,13 +49,12 @@ jobs:
152
49
  - name: Upload artifacts to release
153
50
  env:
154
51
  GH_TOKEN: ${{ github.token }}
155
- VERSION: ${{ needs.version.outputs.version }}
156
52
  shell: bash
157
53
  run: |
158
54
  shopt -s nullglob
159
55
  files=(release/*.dmg release/*.AppImage release/*.exe release/*.zip release/*.deb release/*.rpm)
160
56
  if [ ${#files[@]} -gt 0 ]; then
161
- gh release upload "${VERSION}" "${files[@]}" --clobber
57
+ gh release upload "${{ github.event.release.tag_name }}" "${files[@]}" --clobber
162
58
  else
163
59
  echo "No release artifacts found"
164
60
  exit 1
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "1.2.0"
3
+ }
package/CHANGELOG.md ADDED
@@ -0,0 +1,17 @@
1
+ # Changelog
2
+
3
+ ## [1.2.0](https://github.com/tomshaw/fontastic/compare/fontastic-v1.1.0...fontastic-v1.2.0) (2026-03-14)
4
+
5
+
6
+ ### 🎉 Features
7
+
8
+ * **app:** enhance application plist patching and update icon handling ([297c052](https://github.com/tomshaw/fontastic/commit/297c052a2561962d8d6d1dd2f28f31bf5dcb66ba))
9
+ * **app:** set application name to 'Fontastic' and add plist patching script ([af31cf6](https://github.com/tomshaw/fontastic/commit/af31cf61153b82c8a625755f5fc460b6d8205291))
10
+ * **collection:** implement root collection creation request handling ([cba01d0](https://github.com/tomshaw/fontastic/commit/cba01d098998797d914c75f6cd32f814b3703fb0))
11
+ * **navigation:** implement smart collection creation with input handling ([5cb72ae](https://github.com/tomshaw/fontastic/commit/5cb72ae9f576e0fb8dc1372ab411ea4e2d764191))
12
+ * **release:** add release-please configuration and manifest files ([5cb72ae](https://github.com/tomshaw/fontastic/commit/5cb72ae9f576e0fb8dc1372ab411ea4e2d764191))
13
+
14
+
15
+ ### 🛠️ Fixes
16
+
17
+ * **release:** update release configuration and manifest version ([759486b](https://github.com/tomshaw/fontastic/commit/759486b556982d90c5e3eaef38cfa0c214e7ac38))
package/README.md CHANGED
@@ -1,18 +1,19 @@
1
1
  # Fontastic
2
2
 
3
- ![Maintained][maintained-badge]
4
- [![Make a pull request][prs-badge]][prs]
5
- [![License][license-badge]](LICENSE.md)
3
+ [![Angular](https://img.shields.io/badge/Angular-21-dd0031?style=plastic&logo=angular&logoColor=white)](https://angular.dev)
4
+ [![Electron](https://img.shields.io/badge/Electron-40-47848f?style=plastic&logo=electron&logoColor=white)](https://electronjs.org)
5
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.9-3178c6?style=plastic&logo=typescript&logoColor=white)](https://www.typescriptlang.org)
6
+ [![License](https://img.shields.io/badge/License-MIT-f59e0b?style=plastic)](LICENSE.md)
7
+ [![PRs Welcome](https://img.shields.io/badge/PRs-Welcome-22c55e?style=plastic)](http://makeapullrequest.com)
6
8
 
7
9
  [![Linux Build][linux-build-badge]][linux-build]
8
10
  [![MacOS Build][macos-build-badge]][macos-build]
9
11
  [![Windows Build][windows-build-badge]][windows-build]
10
12
 
11
- [![Watch on GitHub][github-watch-badge]][github-watch]
12
- [![Star on GitHub][github-star-badge]][github-star]
13
- [![Tweet][twitter-badge]][twitter]
13
+ [![GitHub Stars](https://img.shields.io/github/stars/tomshaw/fontastic?style=plastic&logo=github&label=Stars)](https://github.com/tomshaw/fontastic/stargazers)
14
+ [![GitHub Watchers](https://img.shields.io/github/watchers/tomshaw/fontastic?style=plastic&logo=github&label=Watchers)](https://github.com/tomshaw/fontastic/watchers)
14
15
 
15
- Fontastic is a cross-platform font management and cataloging application built with Angular and Electron.
16
+ Fontastic is an Electron-based font management and cataloging application built for organizing, browsing, and inspecting font libraries.
16
17
 
17
18
  ## Features
18
19
 
@@ -83,12 +84,6 @@ Fontastic is open-sourced software licensed under the [MIT license](https://open
83
84
 
84
85
  [repo]: https://github.com/tomshaw/fontastic
85
86
 
86
- [maintained-badge]: https://img.shields.io/badge/maintained-yes-brightgreen
87
- [license-badge]: https://img.shields.io/badge/license-MIT-blue.svg
88
- [license]: https://github.com/tomshaw/fontastic/blob/main/LICENSE.md
89
- [prs-badge]: https://img.shields.io/badge/PRs-welcome-red.svg
90
- [prs]: http://makeapullrequest.com
91
-
92
87
  [linux-build-badge]: https://github.com/tomshaw/fontastic/workflows/Linux%20Build/badge.svg
93
88
  [linux-build]: https://github.com/tomshaw/fontastic/actions?query=workflow%3A%22Linux+Build%22
94
89
  [macos-build-badge]: https://github.com/tomshaw/fontastic/workflows/MacOS%20Build/badge.svg
@@ -96,9 +91,3 @@ Fontastic is open-sourced software licensed under the [MIT license](https://open
96
91
  [windows-build-badge]: https://github.com/tomshaw/fontastic/workflows/Windows%20Build/badge.svg
97
92
  [windows-build]: https://github.com/tomshaw/fontastic/actions?query=workflow%3A%22Windows+Build%22
98
93
 
99
- [github-watch-badge]: https://img.shields.io/github/watchers/tomshaw/fontastic.svg?style=social
100
- [github-watch]: https://github.com/tomshaw/fontastic/watchers
101
- [github-star-badge]: https://img.shields.io/github/stars/tomshaw/fontastic.svg?style=social
102
- [github-star]: https://github.com/tomshaw/fontastic/stargazers
103
- [twitter]: https://twitter.com/intent/tweet?text=Check%20out%20fontastic!%20https://github.com/tomshaw/fontastic%20%F0%9F%91%8D
104
- [twitter-badge]: https://img.shields.io/twitter/url/https/github.com/tomshaw/fontastic.svg?style=social
@@ -17,5 +17,7 @@ var StorageType;
17
17
  StorageType["LayoutTheme"] = "layout.theme";
18
18
  StorageType["AiKeys"] = "ai.keys";
19
19
  StorageType["NavigationExpanded"] = "navigation.expanded";
20
+ StorageType["SortColumn"] = "datagrid.sort.column";
21
+ StorageType["SortDirection"] = "datagrid.sort.direction";
20
22
  })(StorageType || (exports.StorageType = StorageType = {}));
21
23
  //# sourceMappingURL=StorageType.js.map
@@ -13,4 +13,6 @@ export enum StorageType {
13
13
  LayoutTheme = 'layout.theme',
14
14
  AiKeys = 'ai.keys',
15
15
  NavigationExpanded = 'navigation.expanded',
16
+ SortColumn = 'datagrid.sort.column',
17
+ SortDirection = 'datagrid.sort.direction',
16
18
  }
package/app/main.js CHANGED
@@ -5,10 +5,19 @@ const path = require("path");
5
5
  const fs = require("fs");
6
6
  const node_machine_id_1 = require("node-machine-id");
7
7
  const Application_1 = require("./Application");
8
+ electron_1.app.name = 'Fontastic';
9
+ const iconPath = path.join(__dirname, '..', 'src', 'assets', 'icons', 'favicon.512x512.png');
10
+ electron_1.app.setAboutPanelOptions({
11
+ applicationName: 'Fontastic',
12
+ applicationVersion: electron_1.app.getVersion(),
13
+ iconPath,
14
+ });
8
15
  let win = null;
9
16
  let resolveAppReady;
10
- const appReadyPromise = new Promise(resolve => { resolveAppReady = resolve; });
11
- const args = process.argv.slice(1), serve = args.some(val => val === '--serve');
17
+ const appReadyPromise = new Promise((resolve) => {
18
+ resolveAppReady = resolve;
19
+ });
20
+ const args = process.argv.slice(1), serve = args.some((val) => val === '--serve');
12
21
  function createWindow() {
13
22
  const size = electron_1.screen.getPrimaryDisplay().workAreaSize;
14
23
  // Create the browser window.
@@ -21,15 +30,15 @@ function createWindow() {
21
30
  nodeIntegration: true,
22
31
  allowRunningInsecureContent: serve,
23
32
  contextIsolation: false,
24
- webSecurity: !serve
33
+ webSecurity: !serve,
25
34
  },
26
35
  });
27
36
  (0, node_machine_id_1.machineId)(true).then((machineId) => new Application_1.default(machineId, !serve, win).initialize().then(() => resolveAppReady()));
28
37
  if (serve) {
29
- Promise.resolve().then(() => require('electron-debug')).then(debug => {
38
+ Promise.resolve().then(() => require('electron-debug')).then((debug) => {
30
39
  debug.default({ isEnabled: true, showDevTools: true });
31
40
  });
32
- Promise.resolve().then(() => require('electron-reloader')).then(reloader => {
41
+ Promise.resolve().then(() => require('electron-reloader')).then((reloader) => {
33
42
  const reloaderFn = reloader.default || reloader;
34
43
  // watchRenderer: false — Angular dev server handles HMR for the renderer.
35
44
  // Without this, reloader triggers spurious reloads on macOS (issue #840).
@@ -58,10 +67,12 @@ function createWindow() {
58
67
  return win;
59
68
  }
60
69
  try {
61
- electron_1.protocol.registerSchemesAsPrivileged([{
70
+ electron_1.protocol.registerSchemesAsPrivileged([
71
+ {
62
72
  scheme: 'font',
63
- privileges: { bypassCSP: true, supportFetchAPI: true }
64
- }]);
73
+ privileges: { bypassCSP: true, supportFetchAPI: true },
74
+ },
75
+ ]);
65
76
  electron_1.ipcMain.handle('app:get-version', () => electron_1.app.getVersion());
66
77
  electron_1.ipcMain.handle('app:ready', () => appReadyPromise);
67
78
  // This method will be called when Electron has finished
package/app/main.ts CHANGED
@@ -1,18 +1,29 @@
1
- import {app, BrowserWindow, ipcMain, net, protocol, screen} from 'electron';
1
+ import { app, BrowserWindow, ipcMain, nativeImage, net, protocol, screen } from 'electron';
2
2
  import * as path from 'path';
3
3
  import * as fs from 'fs';
4
4
  import { machineId } from 'node-machine-id';
5
5
  import Application from './Application';
6
6
 
7
+ app.name = 'Fontastic';
8
+
9
+ const iconPath = path.join(__dirname, '..', 'src', 'assets', 'icons', 'favicon.512x512.png');
10
+
11
+ app.setAboutPanelOptions({
12
+ applicationName: 'Fontastic',
13
+ applicationVersion: app.getVersion(),
14
+ iconPath,
15
+ });
16
+
7
17
  let win: BrowserWindow | null = null;
8
18
  let resolveAppReady: () => void;
9
- const appReadyPromise = new Promise<void>(resolve => { resolveAppReady = resolve; });
19
+ const appReadyPromise = new Promise<void>((resolve) => {
20
+ resolveAppReady = resolve;
21
+ });
10
22
 
11
23
  const args = process.argv.slice(1),
12
- serve = args.some(val => val === '--serve');
24
+ serve = args.some((val) => val === '--serve');
13
25
 
14
26
  function createWindow(): BrowserWindow {
15
-
16
27
  const size = screen.getPrimaryDisplay().workAreaSize;
17
28
 
18
29
  // Create the browser window.
@@ -25,20 +36,18 @@ function createWindow(): BrowserWindow {
25
36
  nodeIntegration: true,
26
37
  allowRunningInsecureContent: serve,
27
38
  contextIsolation: false,
28
- webSecurity: !serve
39
+ webSecurity: !serve,
29
40
  },
30
41
  });
31
42
 
32
- machineId(true).then((machineId: string) =>
33
- new Application(machineId, !serve, win!).initialize().then(() => resolveAppReady())
34
- );
43
+ machineId(true).then((machineId: string) => new Application(machineId, !serve, win!).initialize().then(() => resolveAppReady()));
35
44
 
36
45
  if (serve) {
37
- import('electron-debug').then(debug => {
38
- debug.default({isEnabled: true, showDevTools: true});
46
+ import('electron-debug').then((debug) => {
47
+ debug.default({ isEnabled: true, showDevTools: true });
39
48
  });
40
49
 
41
- import('electron-reloader').then(reloader => {
50
+ import('electron-reloader').then((reloader) => {
42
51
  const reloaderFn = (reloader as any).default || reloader;
43
52
  // watchRenderer: false — Angular dev server handles HMR for the renderer.
44
53
  // Without this, reloader triggers spurious reloads on macOS (issue #840).
@@ -50,7 +59,7 @@ function createWindow(): BrowserWindow {
50
59
  let pathIndex = './browser/index.html';
51
60
 
52
61
  if (fs.existsSync(path.join(__dirname, '../dist/browser/index.html'))) {
53
- // Path when running electron in local folder
62
+ // Path when running electron in local folder
54
63
  pathIndex = '../dist/browser/index.html';
55
64
  }
56
65
 
@@ -71,10 +80,12 @@ function createWindow(): BrowserWindow {
71
80
  }
72
81
 
73
82
  try {
74
- protocol.registerSchemesAsPrivileged([{
75
- scheme: 'font',
76
- privileges: { bypassCSP: true, supportFetchAPI: true }
77
- }]);
83
+ protocol.registerSchemesAsPrivileged([
84
+ {
85
+ scheme: 'font',
86
+ privileges: { bypassCSP: true, supportFetchAPI: true },
87
+ },
88
+ ]);
78
89
 
79
90
  ipcMain.handle('app:get-version', () => app.getVersion());
80
91
  ipcMain.handle('app:ready', () => appReadyPromise);
@@ -107,7 +118,6 @@ try {
107
118
  createWindow();
108
119
  }
109
120
  });
110
-
111
121
  } catch (e) {
112
122
  // Catch Error
113
123
  // throw e;
package/app/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "name": "Tom Shaw",
6
6
  "email": ""
7
7
  },
8
- "version": "1.0.2",
8
+ "version": "1.2.0",
9
9
  "main": "main.js",
10
10
  "private": true,
11
11
  "dependencies": {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "fontastic",
3
- "version": "1.0.2",
4
- "description": "A gorgeous multi-platform font management application.",
3
+ "version": "1.2.0",
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,
7
7
  "author": {
@@ -24,7 +24,7 @@
24
24
  ],
25
25
  "main": "app/main.js",
26
26
  "scripts": {
27
- "postinstall": "electron-builder install-app-deps",
27
+ "postinstall": "electron-builder install-app-deps && node scripts/patch-electron-plist.js",
28
28
  "ng": "ng",
29
29
  "start": "npm-run-all -p electron:serve ng:serve",
30
30
  "ng:serve": "ng serve -c dev -o",
@@ -0,0 +1,20 @@
1
+ {
2
+ "last-release-sha": "c7f3f9afba466a150f90e374f8a401daf1a56d11",
3
+ "packages": {
4
+ ".": {
5
+ "release-type": "node",
6
+ "extra-files": ["app/package.json"],
7
+ "changelog-sections": [
8
+ { "type": "feat", "section": "🎉 Features", "hidden": false },
9
+ { "type": "fix", "section": "🛠️ Fixes", "hidden": false },
10
+ { "type": "docs", "section": "📄 Documentation", "hidden": false },
11
+ { "type": "perf", "section": "⚡ Performance", "hidden": false },
12
+ { "type": "refactor", "section": "🏗️ Refactor", "hidden": false },
13
+ { "type": "chore", "section": "♻️ Chores", "hidden": true },
14
+ { "type": "test", "section": "♻️ Chores", "hidden": true },
15
+ { "type": "build", "section": "⚙️ Automation", "hidden": true },
16
+ { "type": "ci", "section": "⚙️ Automation", "hidden": true }
17
+ ]
18
+ }
19
+ }
20
+ }
@@ -0,0 +1,41 @@
1
+ const { execSync } = require('child_process');
2
+ const path = require('path');
3
+ const fs = require('fs');
4
+
5
+ if (process.platform !== 'darwin') {
6
+ process.exit(0);
7
+ }
8
+
9
+ const electronAppDir = path.join(
10
+ __dirname,
11
+ '..',
12
+ 'node_modules',
13
+ 'electron',
14
+ 'dist',
15
+ 'Electron.app'
16
+ );
17
+
18
+ const plist = path.join(electronAppDir, 'Contents', 'Info.plist');
19
+
20
+ if (!fs.existsSync(plist)) {
21
+ process.exit(0);
22
+ }
23
+
24
+ const pkg = require(path.join(__dirname, '..', 'package.json'));
25
+ const appName = 'Fontastic';
26
+ const appVersion = pkg.version;
27
+
28
+ execSync(`/usr/libexec/PlistBuddy -c "Set :CFBundleName ${appName}" "${plist}"`);
29
+ execSync(`/usr/libexec/PlistBuddy -c "Set :CFBundleDisplayName ${appName}" "${plist}"`);
30
+ execSync(`/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString ${appVersion}" "${plist}"`);
31
+
32
+ // Copy app icon into the Electron.app bundle
33
+ const srcIcon = path.join(__dirname, '..', 'src', 'assets', 'icons', 'favicon.icns');
34
+ const destIcon = path.join(electronAppDir, 'Contents', 'Resources', 'electron.icns');
35
+
36
+ if (fs.existsSync(srcIcon)) {
37
+ fs.copyFileSync(srcIcon, destIcon);
38
+ console.log('Copied app icon into Electron.app bundle');
39
+ }
40
+
41
+ console.log(`Patched Electron.app plist: name="${appName}", version="${appVersion}"`);
@@ -137,24 +137,26 @@ export class DatabaseService {
137
137
  private fetchCurrentPage(extraOptions: any = {}) {
138
138
  const skip = (this.currentPage() - 1) * this.pageSize();
139
139
  const take = this.pageSize();
140
+ const sortOrder = this.getSortOrder();
140
141
 
141
142
  const smartCollectionId = this.activeSmartCollectionId();
142
143
  if (smartCollectionId) {
143
- this.smartCollectionEvaluate(smartCollectionId, { skip, take });
144
+ this.smartCollectionEvaluate(smartCollectionId, { skip, take, ...(sortOrder ? { order: sortOrder } : {}) });
144
145
  return;
145
146
  }
146
147
 
147
148
  const searchWhere = this.activeSearchWhere();
148
149
  if (searchWhere) {
149
150
  const searchOrder = this.activeSearchOrder();
150
- this.storeSearch({ where: searchWhere, skip, take, ...(searchOrder ? { order: searchOrder } : {}) });
151
+ const order = sortOrder ?? searchOrder;
152
+ this.storeSearch({ where: searchWhere, skip, take, ...(order ? { order } : {}) });
151
153
  return;
152
154
  }
153
155
 
154
156
  const collectionId = this.collectionId();
155
157
  const filter = this.activeFilter();
156
158
 
157
- const options: any = { skip, take, ...extraOptions };
159
+ const options: any = { skip, take, ...extraOptions, ...(sortOrder ? { order: sortOrder } : {}) };
158
160
 
159
161
  if (collectionId) {
160
162
  options.collectionId = collectionId;
@@ -172,17 +174,24 @@ export class DatabaseService {
172
174
 
173
175
  constructor() {
174
176
  this.electron.ready.then(async () => {
175
- const [collections, smartCollections, savedCollectionId, savedStoreId] = await Promise.all([
177
+ const [collections, smartCollections, savedCollectionId, savedStoreId, savedSortColumn, savedSortDirection] = await Promise.all([
176
178
  this.message.collectionFetch({}),
177
179
  this.message.smartCollectionFind(),
178
180
  this.message.get(StorageType.CollectionId, null),
179
181
  this.message.get(StorageType.StoreId, null),
182
+ this.message.get(StorageType.SortColumn, null),
183
+ this.message.get(StorageType.SortDirection, null),
180
184
  ]);
181
185
 
182
186
  this.collections.set(collections);
183
187
  this.smartCollections.set(smartCollections);
184
188
  console.log('System Boot:', collections);
185
189
 
190
+ if (savedSortColumn) {
191
+ this.sortColumn.set(savedSortColumn);
192
+ this.sortDirection.set(savedSortDirection === 'DESC' ? 'DESC' : 'ASC');
193
+ }
194
+
186
195
  if (savedCollectionId) {
187
196
  this.collectionId.set(savedCollectionId);
188
197
  }
@@ -398,6 +407,44 @@ export class DatabaseService {
398
407
 
399
408
  readonly activeSearchOrder = signal<{ column: string; direction: string } | null>(null);
400
409
 
410
+ // Datagrid sort (persisted)
411
+ readonly sortColumn = signal<string | null>(null);
412
+ readonly sortDirection = signal<'ASC' | 'DESC'>('ASC');
413
+
414
+ toggleSort(column: string) {
415
+ const current = this.sortColumn();
416
+ if (current === column) {
417
+ if (this.sortDirection() === 'ASC') {
418
+ this.sortDirection.set('DESC');
419
+ } else {
420
+ // Clear sort
421
+ this.sortColumn.set(null);
422
+ this.sortDirection.set('ASC');
423
+ }
424
+ } else {
425
+ this.sortColumn.set(column);
426
+ this.sortDirection.set('ASC');
427
+ }
428
+
429
+ // Persist
430
+ const col = this.sortColumn();
431
+ if (col) {
432
+ this.message.set(StorageType.SortColumn, col);
433
+ this.message.set(StorageType.SortDirection, this.sortDirection());
434
+ } else {
435
+ this.message.set(StorageType.SortColumn, null);
436
+ this.message.set(StorageType.SortDirection, null);
437
+ }
438
+
439
+ this.currentPage.set(1);
440
+ this.fetchCurrentPage();
441
+ }
442
+
443
+ private getSortOrder(): { column: string; direction: string } | null {
444
+ const col = this.sortColumn();
445
+ return col ? { column: col, direction: this.sortDirection() } : null;
446
+ }
447
+
401
448
  selectSearch(where: { key: string; value: any }[], order?: { column: string; direction: string }) {
402
449
  this.parentId.set(null);
403
450
  this.collectionId.set(null);
@@ -133,6 +133,13 @@ export class PresentationService {
133
133
 
134
134
  readonly selectedGlyph = signal<number | null>(null);
135
135
 
136
+ readonly createRootCollectionRequest = signal(0);
137
+
138
+ requestCreateRootCollection() {
139
+ this.navigationEnabled.set(true);
140
+ this.createRootCollectionRequest.update((v) => v + 1);
141
+ }
142
+
136
143
  readonly navigationExpandedIds = signal<number[]>([]);
137
144
 
138
145
  readonly gridEnabled = signal(true);
@@ -106,13 +106,3 @@
106
106
  </div>
107
107
  </div>
108
108
  </header>
109
-
110
- @if (showCollectionDialog) {
111
- <app-prompt-dialog
112
- title="New Collection"
113
- placeholder="Collection name"
114
- confirmText="Create"
115
- (confirmed)="onCollectionConfirmed($event)"
116
- (cancelled)="onCollectionCancelled()"
117
- />
118
- }
@@ -1,42 +1,23 @@
1
- import { Component, inject, effect } from '@angular/core';
1
+ import { Component, inject } from '@angular/core';
2
2
  import { Router } from '@angular/router';
3
- import { PromptDialogComponent } from '../../shared/components';
4
- import { DatabaseService, PresentationService } from '../../core/services';
3
+ import { PresentationService } from '../../core/services';
5
4
 
6
5
  @Component({
7
6
  selector: 'app-header',
8
7
  standalone: true,
9
- imports: [PromptDialogComponent],
8
+ imports: [],
10
9
  templateUrl: './header.component.html',
11
10
  })
12
11
  export class HeaderComponent {
13
- readonly db = inject(DatabaseService);
14
12
  private router = inject(Router);
15
13
  readonly presentation = inject(PresentationService);
16
14
 
17
- constructor() {
18
- effect(() => {
19
- console.log('Selected collection ID:', this.db.collectionId());
20
- });
21
- }
22
-
23
15
  currentUser: { name: string } | null = null;
24
16
  gravatarUrl = '';
25
17
 
26
- showCollectionDialog = false;
27
-
28
18
  handleCreateCollection(event: Event) {
29
19
  event.stopPropagation();
30
- this.showCollectionDialog = true;
31
- }
32
-
33
- onCollectionConfirmed(name: string) {
34
- this.db.collectionCreate({ title: name });
35
- this.showCollectionDialog = false;
36
- }
37
-
38
- onCollectionCancelled() {
39
- this.showCollectionDialog = false;
20
+ this.presentation.requestCreateRootCollection();
40
21
  }
41
22
 
42
23
  handleToggleSearch(_event: Event) {
@@ -16,7 +16,7 @@
16
16
  'wght' 300;
17
17
  "
18
18
  title="New Smart Collection"
19
- (click)="openCreateSmartCollection()"
19
+ (click)="$event.stopPropagation(); openCreateSmartCollection()"
20
20
  >add</span
21
21
  >
22
22
  <ul class="flex flex-col py-0.5">
@@ -64,10 +64,39 @@
64
64
  'wght' 300;
65
65
  "
66
66
  title="New Collection"
67
- (click)="openCreateRootCollection()"
67
+ (click)="$event.stopPropagation(); openCreateRootCollection()"
68
68
  >add</span
69
69
  >
70
70
  <ul class="flex-1 overflow-auto flex flex-col py-0.5">
71
+ @if (isCreating && pendingParentId === 0) {
72
+ <li>
73
+ <a class="flex items-center pr-3 py-1 text-xs font-normal" style="padding-left: 14px">
74
+ <span
75
+ class="material-symbols-outlined icon-sm mr-1"
76
+ [style.color]="'var(--text-muted)'"
77
+ style="
78
+ font-variation-settings:
79
+ 'opsz' 20,
80
+ 'wght' 300;
81
+ "
82
+ >chevron_right</span
83
+ >
84
+ <input
85
+ class="border rounded px-1 w-full outline-none text-xs"
86
+ [style.background-color]="'var(--input-bg)'"
87
+ [style.border-color]="'var(--input-border)'"
88
+ [style.color]="'var(--text-primary)'"
89
+ [(ngModel)]="creatingTitle"
90
+ (blur)="saveCreating()"
91
+ (keydown.enter)="saveCreating()"
92
+ (keydown.escape)="cancelCreating()"
93
+ (click)="$event.stopPropagation()"
94
+ placeholder="Collection name"
95
+ appAutofocus
96
+ />
97
+ </a>
98
+ </li>
99
+ }
71
100
  <ng-container *ngTemplateOutlet="subtree; context: { $implicit: tree(), level: 0 }" />
72
101
  <li
73
102
  class="flex-1 min-h-6"
@@ -142,8 +171,37 @@
142
171
  {{ node.collection.title }}
143
172
  }
144
173
  </a>
145
- @if (node.children.length && isExpanded(node.collection.id)) {
174
+ @if ((node.children.length && isExpanded(node.collection.id)) || (isCreating && pendingParentId === node.collection.id)) {
146
175
  <ul>
176
+ @if (isCreating && pendingParentId === node.collection.id) {
177
+ <li>
178
+ <a class="flex items-center pr-3 py-1 text-xs font-normal" [style.padding-left.px]="14 + (level + 1) * 14">
179
+ <span
180
+ class="material-symbols-outlined icon-sm mr-1"
181
+ [style.color]="'var(--text-muted)'"
182
+ style="
183
+ font-variation-settings:
184
+ 'opsz' 20,
185
+ 'wght' 300;
186
+ "
187
+ >chevron_right</span
188
+ >
189
+ <input
190
+ class="border rounded px-1 w-full outline-none text-xs"
191
+ [style.background-color]="'var(--input-bg)'"
192
+ [style.border-color]="'var(--input-border)'"
193
+ [style.color]="'var(--text-primary)'"
194
+ [(ngModel)]="creatingTitle"
195
+ (blur)="saveCreating()"
196
+ (keydown.enter)="saveCreating()"
197
+ (keydown.escape)="cancelCreating()"
198
+ (click)="$event.stopPropagation()"
199
+ placeholder="Collection name"
200
+ appAutofocus
201
+ />
202
+ </a>
203
+ </li>
204
+ }
147
205
  <ng-container *ngTemplateOutlet="subtree; context: { $implicit: node.children, level: level + 1 }" />
148
206
  </ul>
149
207
  }
@@ -161,16 +219,6 @@
161
219
  />
162
220
  }
163
221
 
164
- @if (showCollectionDialog) {
165
- <app-prompt-dialog
166
- title="New Collection"
167
- placeholder="Collection name"
168
- confirmText="Create"
169
- (confirmed)="onCollectionConfirmed($event)"
170
- (cancelled)="onCollectionCancelled()"
171
- />
172
- }
173
-
174
222
  @if (smartContextMenu) {
175
223
  <app-context-menu
176
224
  [x]="smartContextMenu.x"
@@ -1,10 +1,10 @@
1
- import { Component, inject, computed, OnInit, OnDestroy } from '@angular/core';
1
+ import { Component, inject, computed, effect, OnInit, OnDestroy } from '@angular/core';
2
2
  import { CommonModule } from '@angular/common';
3
3
  import { FormsModule } from '@angular/forms';
4
4
  import { DatabaseService, MessageService, PresentationService } from '../../core/services';
5
5
  import { CollapsiblePanelComponent } from '../../shared/components/collapsible-panel/collapsible-panel.component';
6
6
  import { ContextMenuComponent, ContextMenuItem } from '../../shared/components/context-menu/context-menu.component';
7
- import { PromptDialogComponent, RuleBuilderComponent } from '../../shared/components';
7
+ import { RuleBuilderComponent } from '../../shared/components';
8
8
  import { AutofocusDirective } from '../../shared/directives/autofocus/autofocus.directive';
9
9
  import { LibraryComponent } from './library/library.component';
10
10
  import { NewsStatsComponent } from './stats/stats.component';
@@ -26,7 +26,6 @@ export interface TreeNode {
26
26
  FormsModule,
27
27
  CollapsiblePanelComponent,
28
28
  ContextMenuComponent,
29
- PromptDialogComponent,
30
29
  RuleBuilderComponent,
31
30
  AutofocusDirective,
32
31
  LibraryComponent,
@@ -39,6 +38,15 @@ export class NavigationComponent implements OnInit, OnDestroy {
39
38
  private message = inject(MessageService);
40
39
  private presentation = inject(PresentationService);
41
40
 
41
+ constructor() {
42
+ effect(() => {
43
+ const req = this.presentation.createRootCollectionRequest();
44
+ if (req > 0) {
45
+ this.openCreateRootCollection();
46
+ }
47
+ });
48
+ }
49
+
42
50
  private menuToggleListener = (_event: any, panel: string) => {
43
51
  if (panel === 'expand-collections') {
44
52
  this.expandAll();
@@ -72,7 +80,8 @@ export class NavigationComponent implements OnInit, OnDestroy {
72
80
  editingTitle = '';
73
81
  allExpanded = false;
74
82
 
75
- showCollectionDialog = false;
83
+ isCreating = false;
84
+ creatingTitle = '';
76
85
  pendingParentId: number | null = null;
77
86
 
78
87
  // Smart Collection state
@@ -256,7 +265,9 @@ export class NavigationComponent implements OnInit, OnDestroy {
256
265
  switch (action) {
257
266
  case 'add-collection':
258
267
  this.pendingParentId = collection.id;
259
- this.showCollectionDialog = true;
268
+ this.isCreating = true;
269
+ this.creatingTitle = '';
270
+ this.presentation.expandNavigationId(collection.id);
260
271
  break;
261
272
  case 'add-fonts':
262
273
  this.handleAddFonts(collection);
@@ -273,17 +284,24 @@ export class NavigationComponent implements OnInit, OnDestroy {
273
284
 
274
285
  openCreateRootCollection() {
275
286
  this.pendingParentId = 0;
276
- this.showCollectionDialog = true;
287
+ this.isCreating = true;
288
+ this.creatingTitle = '';
277
289
  }
278
290
 
279
- onCollectionConfirmed(name: string) {
280
- this.db.collectionCreate({ title: name, parentId: this.pendingParentId });
281
- this.showCollectionDialog = false;
282
- this.pendingParentId = null;
291
+ saveCreating() {
292
+ const title = this.creatingTitle.trim();
293
+ if (title) {
294
+ this.db.collectionCreate({ title, parentId: this.pendingParentId });
295
+ if (this.pendingParentId) {
296
+ this.presentation.expandNavigationId(this.pendingParentId);
297
+ }
298
+ }
299
+ this.cancelCreating();
283
300
  }
284
301
 
285
- onCollectionCancelled() {
286
- this.showCollectionDialog = false;
302
+ cancelCreating() {
303
+ this.isCreating = false;
304
+ this.creatingTitle = '';
287
305
  this.pendingParentId = null;
288
306
  }
289
307
 
@@ -8,13 +8,31 @@
8
8
  >
9
9
  <tr class="text-left text-[11px] uppercase tracking-wide" [style.color]="'var(--text-muted)'">
10
10
  <th class="w-10 px-2 py-2 text-center font-medium"></th>
11
- <th class="px-4 py-2 font-medium">Name</th>
12
- <th class="px-4 py-2 font-medium">Family</th>
13
- <th class="px-4 py-2 font-medium">Style</th>
14
- <th class="px-4 py-2 font-medium">Type</th>
15
- <th class="px-4 py-2 font-medium">Size</th>
16
- <th class="px-4 py-2 font-medium">Version</th>
17
- <th class="px-4 py-2 font-medium">Designer</th>
11
+ @for (col of sortableColumns; track col.field) {
12
+ <th
13
+ class="px-4 py-2 font-medium cursor-pointer select-none transition-colors hover:brightness-125"
14
+ [class.max-w-48]="col.field === 'designer'"
15
+ (click)="db.toggleSort(col.field)"
16
+ >
17
+ <div class="flex items-center justify-between gap-1">
18
+ <span>{{ col.label }}</span>
19
+ @if (db.sortColumn() === col.field) {
20
+ <span
21
+ class="material-symbols-outlined"
22
+ style="
23
+ font-size: 14px;
24
+ font-variation-settings:
25
+ 'opsz' 20,
26
+ 'wght' 300;
27
+ "
28
+ [style.color]="'var(--accent)'"
29
+ >
30
+ {{ db.sortDirection() === 'ASC' ? 'arrow_upward' : 'arrow_downward' }}
31
+ </span>
32
+ }
33
+ </div>
34
+ </th>
35
+ }
18
36
  <th class="w-10 px-2 py-2 text-center font-medium"></th>
19
37
  <th class="w-10 px-2 py-2 text-center font-medium"></th>
20
38
  <th class="w-10 px-2 py-2 text-center font-medium"></th>
@@ -11,6 +11,16 @@ export class DatagridComponent {
11
11
  private messageService = inject(MessageService);
12
12
  private el = inject(ElementRef);
13
13
 
14
+ readonly sortableColumns = [
15
+ { field: 'full_name', label: 'Name' },
16
+ { field: 'font_family', label: 'Family' },
17
+ { field: 'font_subfamily', label: 'Style' },
18
+ { field: 'file_type', label: 'Type' },
19
+ { field: 'file_size', label: 'Size' },
20
+ { field: 'version', label: 'Version' },
21
+ { field: 'designer', label: 'Designer' },
22
+ ];
23
+
14
24
  constructor() {
15
25
  effect(() => {
16
26
  this.db.currentPage();
Binary file
Binary file
Binary file
package/src/favicon.ico CHANGED
Binary file