fontastic 1.0.1 → 1.1.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.
- package/README.md +8 -19
- package/app/core/menu/templates/DarwinTemplate.js +1 -1
- package/app/core/menu/templates/DarwinTemplate.ts +1 -1
- package/app/core/menu/templates/SystemTemplate.js +1 -1
- package/app/core/menu/templates/SystemTemplate.ts +1 -1
- package/app/enums/StorageType.js +2 -0
- package/app/enums/StorageType.ts +2 -0
- package/app/package.json +1 -1
- package/app/types/SystemConfig.ts +2 -1
- package/package.json +2 -2
- package/src/app/core/services/database/database.service.ts +51 -4
- package/src/app/core/services/presentation/presentation.service.ts +15 -9
- package/src/app/layout/header/header.component.html +4 -4
- package/src/app/layout/main/main.component.html +3 -3
- package/src/app/layout/main/main.component.ts +2 -2
- package/src/app/layout/navigation/navigation.component.ts +3 -3
- package/src/app/shared/components/context-menu/context-menu.component.ts +70 -21
- package/src/app/shared/components/datagrid/datagrid.component.html +25 -7
- package/src/app/shared/components/datagrid/datagrid.component.ts +10 -0
- package/src/app/shared/components/{inspector/inspector.component.html → glyphs/glyphs.component.html} +1 -1
- package/src/app/shared/components/glyphs/glyphs.component.ts +60 -0
- package/src/app/shared/components/index.ts +1 -1
- package/src/styles/components/spinner.css +4 -3
- package/src/app/shared/components/inspector/inspector.component.ts +0 -41
package/README.md
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
# Fontastic
|
|
2
2
|
|
|
3
|
-
](https://angular.dev)
|
|
4
|
+
[](https://electronjs.org)
|
|
5
|
+
[](https://www.typescriptlang.org)
|
|
6
|
+
[](LICENSE.md)
|
|
7
|
+
[](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
|
-
[![
|
|
12
|
-
[![
|
|
13
|
-
[![Tweet][twitter-badge]][twitter]
|
|
13
|
+
[](https://github.com/tomshaw/fontastic/stargazers)
|
|
14
|
+
[](https://github.com/tomshaw/fontastic/watchers)
|
|
14
15
|
|
|
15
|
-
Fontastic is
|
|
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
|
|
@@ -122,7 +122,7 @@ class DarwinTemplate {
|
|
|
122
122
|
{ label: 'Toggle Navigation', panel: 'navigation', accelerator: 'Alt+Command+1' },
|
|
123
123
|
{ label: 'Toggle Aside', panel: 'aside', accelerator: 'Alt+Command+2' },
|
|
124
124
|
{ label: 'Toggle Preview', panel: 'preview', accelerator: 'Alt+Command+3' },
|
|
125
|
-
{ label: 'Toggle
|
|
125
|
+
{ label: 'Toggle Glyphs', panel: 'glyphs', accelerator: 'Alt+Command+4' },
|
|
126
126
|
{ label: 'Toggle Toolbar', panel: 'toolbar', accelerator: 'Alt+Command+5' },
|
|
127
127
|
{ label: 'Toggle Grid', panel: 'grid', accelerator: 'Alt+Command+6' },
|
|
128
128
|
{ label: 'Toggle Waterfall', panel: 'waterfall', accelerator: 'Alt+Command+7' },
|
|
@@ -142,7 +142,7 @@ export default class DarwinTemplate {
|
|
|
142
142
|
{ label: 'Toggle Navigation', panel: 'navigation', accelerator: 'Alt+Command+1' },
|
|
143
143
|
{ label: 'Toggle Aside', panel: 'aside', accelerator: 'Alt+Command+2' },
|
|
144
144
|
{ label: 'Toggle Preview', panel: 'preview', accelerator: 'Alt+Command+3' },
|
|
145
|
-
{ label: 'Toggle
|
|
145
|
+
{ label: 'Toggle Glyphs', panel: 'glyphs', accelerator: 'Alt+Command+4' },
|
|
146
146
|
{ label: 'Toggle Toolbar', panel: 'toolbar', accelerator: 'Alt+Command+5' },
|
|
147
147
|
{ label: 'Toggle Grid', panel: 'grid', accelerator: 'Alt+Command+6' },
|
|
148
148
|
{ label: 'Toggle Waterfall', panel: 'waterfall', accelerator: 'Alt+Command+7' },
|
|
@@ -119,7 +119,7 @@ class SystemTemplate {
|
|
|
119
119
|
{ label: 'Toggle Navigation', panel: 'navigation', accelerator: 'Alt+Ctrl+1' },
|
|
120
120
|
{ label: 'Toggle Aside', panel: 'aside', accelerator: 'Alt+Ctrl+2' },
|
|
121
121
|
{ label: 'Toggle Preview', panel: 'preview', accelerator: 'Alt+Ctrl+3' },
|
|
122
|
-
{ label: 'Toggle
|
|
122
|
+
{ label: 'Toggle Glyphs', panel: 'glyphs', accelerator: 'Alt+Ctrl+4' },
|
|
123
123
|
{ label: 'Toggle Toolbar', panel: 'toolbar', accelerator: 'Alt+Ctrl+5' },
|
|
124
124
|
{ label: 'Toggle Grid', panel: 'grid', accelerator: 'Alt+Ctrl+6' },
|
|
125
125
|
{ label: 'Toggle Waterfall', panel: 'waterfall', accelerator: 'Alt+Ctrl+7' },
|
|
@@ -134,7 +134,7 @@ export default class SystemTemplate {
|
|
|
134
134
|
{ label: 'Toggle Navigation', panel: 'navigation', accelerator: 'Alt+Ctrl+1' },
|
|
135
135
|
{ label: 'Toggle Aside', panel: 'aside', accelerator: 'Alt+Ctrl+2' },
|
|
136
136
|
{ label: 'Toggle Preview', panel: 'preview', accelerator: 'Alt+Ctrl+3' },
|
|
137
|
-
{ label: 'Toggle
|
|
137
|
+
{ label: 'Toggle Glyphs', panel: 'glyphs', accelerator: 'Alt+Ctrl+4' },
|
|
138
138
|
{ label: 'Toggle Toolbar', panel: 'toolbar', accelerator: 'Alt+Ctrl+5' },
|
|
139
139
|
{ label: 'Toggle Grid', panel: 'grid', accelerator: 'Alt+Ctrl+6' },
|
|
140
140
|
{ label: 'Toggle Waterfall', panel: 'waterfall', accelerator: 'Alt+Ctrl+7' },
|
package/app/enums/StorageType.js
CHANGED
|
@@ -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
|
package/app/enums/StorageType.ts
CHANGED
package/app/package.json
CHANGED
|
@@ -30,7 +30,7 @@ export interface LayoutPanelType {
|
|
|
30
30
|
navigationEnabled: boolean;
|
|
31
31
|
toolbarEnabled: boolean;
|
|
32
32
|
previewEnabled: boolean;
|
|
33
|
-
|
|
33
|
+
glyphsEnabled: boolean;
|
|
34
34
|
searchEnabled: boolean;
|
|
35
35
|
waterfallEnabled: boolean;
|
|
36
36
|
}
|
|
@@ -46,6 +46,7 @@ export interface LayoutPreviewType {
|
|
|
46
46
|
displayText: string | null;
|
|
47
47
|
wordSpacing: number;
|
|
48
48
|
letterSpacing: number;
|
|
49
|
+
selectedGlyph: number | null;
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
export interface LayoutType {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fontastic",
|
|
3
|
-
"version": "1.0
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.1.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": {
|
|
@@ -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
|
-
|
|
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);
|
|
@@ -61,7 +61,7 @@ export class PresentationService {
|
|
|
61
61
|
navigationEnabled: this.navigationEnabled(),
|
|
62
62
|
toolbarEnabled: this.toolbarEnabled(),
|
|
63
63
|
previewEnabled: this.previewEnabled(),
|
|
64
|
-
|
|
64
|
+
glyphsEnabled: this.glyphsEnabled(),
|
|
65
65
|
searchEnabled: this.searchEnabled(),
|
|
66
66
|
waterfallEnabled: this.waterfallEnabled(),
|
|
67
67
|
};
|
|
@@ -89,6 +89,7 @@ export class PresentationService {
|
|
|
89
89
|
displayText: this.customText(),
|
|
90
90
|
wordSpacing: this.wordSpacing(),
|
|
91
91
|
letterSpacing: this.letterSpacing(),
|
|
92
|
+
selectedGlyph: this.selectedGlyph(),
|
|
92
93
|
};
|
|
93
94
|
if (previewInitialized) {
|
|
94
95
|
untracked(() => this.messageService.set(StorageType.LayoutPreview, settings));
|
|
@@ -130,12 +131,14 @@ export class PresentationService {
|
|
|
130
131
|
readonly letterSpacing = signal(0);
|
|
131
132
|
readonly wordSpacing = signal(0);
|
|
132
133
|
|
|
134
|
+
readonly selectedGlyph = signal<number | null>(null);
|
|
135
|
+
|
|
133
136
|
readonly navigationExpandedIds = signal<number[]>([]);
|
|
134
137
|
|
|
135
138
|
readonly gridEnabled = signal(true);
|
|
136
139
|
readonly toolbarEnabled = signal(true);
|
|
137
140
|
readonly previewEnabled = signal(true);
|
|
138
|
-
readonly
|
|
141
|
+
readonly glyphsEnabled = signal(false);
|
|
139
142
|
readonly asideEnabled = signal(true);
|
|
140
143
|
readonly navigationEnabled = signal(true);
|
|
141
144
|
readonly searchEnabled = signal(false);
|
|
@@ -147,7 +150,7 @@ export class PresentationService {
|
|
|
147
150
|
this.gridEnabled(),
|
|
148
151
|
this.toolbarEnabled(),
|
|
149
152
|
this.previewEnabled(),
|
|
150
|
-
this.
|
|
153
|
+
this.glyphsEnabled(),
|
|
151
154
|
this.asideEnabled(),
|
|
152
155
|
this.navigationEnabled(),
|
|
153
156
|
].filter(Boolean).length,
|
|
@@ -162,8 +165,8 @@ export class PresentationService {
|
|
|
162
165
|
togglePreview() {
|
|
163
166
|
this.previewEnabled.update((v) => !v);
|
|
164
167
|
}
|
|
165
|
-
|
|
166
|
-
this.
|
|
168
|
+
toggleGlyphs() {
|
|
169
|
+
this.glyphsEnabled.update((v) => !v);
|
|
167
170
|
}
|
|
168
171
|
toggleAside() {
|
|
169
172
|
this.asideEnabled.update((v) => !v);
|
|
@@ -176,7 +179,7 @@ export class PresentationService {
|
|
|
176
179
|
this.searchEnabled.set(enabling);
|
|
177
180
|
if (enabling) {
|
|
178
181
|
this.waterfallEnabled.set(false);
|
|
179
|
-
this.
|
|
182
|
+
this.glyphsEnabled.set(false);
|
|
180
183
|
this.previewEnabled.set(false);
|
|
181
184
|
}
|
|
182
185
|
}
|
|
@@ -188,7 +191,7 @@ export class PresentationService {
|
|
|
188
191
|
navigation: () => this.toggleNavigation(),
|
|
189
192
|
aside: () => this.toggleAside(),
|
|
190
193
|
preview: () => this.togglePreview(),
|
|
191
|
-
|
|
194
|
+
glyphs: () => this.toggleGlyphs(),
|
|
192
195
|
toolbar: () => this.toggleToolbar(),
|
|
193
196
|
grid: () => this.toggleGrid(),
|
|
194
197
|
waterfall: () => this.toggleWaterfall(),
|
|
@@ -253,7 +256,7 @@ export class PresentationService {
|
|
|
253
256
|
this.navigationEnabled.set(settings.navigationEnabled);
|
|
254
257
|
this.toolbarEnabled.set(settings.toolbarEnabled);
|
|
255
258
|
this.previewEnabled.set(settings.previewEnabled);
|
|
256
|
-
this.
|
|
259
|
+
this.glyphsEnabled.set(settings.glyphsEnabled);
|
|
257
260
|
if (settings.searchEnabled !== undefined) {
|
|
258
261
|
this.searchEnabled.set(settings.searchEnabled);
|
|
259
262
|
}
|
|
@@ -274,6 +277,9 @@ export class PresentationService {
|
|
|
274
277
|
if (settings.displayText) {
|
|
275
278
|
this.customText.set(settings.displayText);
|
|
276
279
|
}
|
|
280
|
+
if (settings.selectedGlyph !== undefined) {
|
|
281
|
+
this.selectedGlyph.set(settings.selectedGlyph);
|
|
282
|
+
}
|
|
277
283
|
}
|
|
278
284
|
}
|
|
279
285
|
|
|
@@ -281,7 +287,7 @@ export class PresentationService {
|
|
|
281
287
|
this.gridEnabled.set(true);
|
|
282
288
|
this.toolbarEnabled.set(true);
|
|
283
289
|
this.previewEnabled.set(true);
|
|
284
|
-
this.
|
|
290
|
+
this.glyphsEnabled.set(false);
|
|
285
291
|
this.asideEnabled.set(true);
|
|
286
292
|
this.navigationEnabled.set(true);
|
|
287
293
|
this.searchEnabled.set(false);
|
|
@@ -39,11 +39,11 @@
|
|
|
39
39
|
>
|
|
40
40
|
<a
|
|
41
41
|
href="javascript:;"
|
|
42
|
-
(click)="presentation.
|
|
42
|
+
(click)="presentation.toggleGlyphs()"
|
|
43
43
|
class="cursor-pointer text-[10px] font-medium tracking-wide text-center no-underline py-1 px-2 inline-flex flex-col items-center outline-none whitespace-nowrap transition-colors"
|
|
44
|
-
[style.color]="presentation.
|
|
45
|
-
title="Toggle
|
|
46
|
-
><i class="material-symbols-outlined block text-xl leading-none p-0 mb-0.5">info</i>
|
|
44
|
+
[style.color]="presentation.glyphsEnabled() ? 'var(--text-primary)' : 'var(--text-muted)'"
|
|
45
|
+
title="Toggle Glyphs"
|
|
46
|
+
><i class="material-symbols-outlined block text-xl leading-none p-0 mb-0.5">info</i> Glyphs</a
|
|
47
47
|
>
|
|
48
48
|
<a
|
|
49
49
|
href="javascript:;"
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
[style.grid-template-rows]="
|
|
4
4
|
(presentation.searchEnabled() ? '1fr ' : '') +
|
|
5
5
|
(presentation.previewEnabled() ? '1fr ' : '') +
|
|
6
|
-
(presentation.
|
|
6
|
+
(presentation.glyphsEnabled() ? '1fr ' : '') +
|
|
7
7
|
(presentation.waterfallEnabled() ? '1fr ' : '') +
|
|
8
8
|
(presentation.toolbarEnabled() ? '42px ' : '') +
|
|
9
9
|
(presentation.gridEnabled() ? '1fr' : '')
|
|
@@ -19,9 +19,9 @@
|
|
|
19
19
|
<app-preview />
|
|
20
20
|
</app-panel>
|
|
21
21
|
}
|
|
22
|
-
@if (presentation.
|
|
22
|
+
@if (presentation.glyphsEnabled()) {
|
|
23
23
|
<app-panel class="overflow-auto" [style.border-bottom]="'1px solid var(--border-subtle)'">
|
|
24
|
-
<app-
|
|
24
|
+
<app-glyphs />
|
|
25
25
|
</app-panel>
|
|
26
26
|
}
|
|
27
27
|
@if (presentation.waterfallEnabled()) {
|
|
@@ -4,7 +4,7 @@ import { PanelComponent } from '../../shared/components/panel/panel.component';
|
|
|
4
4
|
import { PreviewComponent } from '../../shared/components/preview/preview.component';
|
|
5
5
|
import { DatagridComponent } from '../../shared/components/datagrid/datagrid.component';
|
|
6
6
|
import { ToolbarComponent } from '../../shared/components/toolbar/toolbar.component';
|
|
7
|
-
import {
|
|
7
|
+
import { GlyphsComponent } from '../../shared/components/glyphs/glyphs.component';
|
|
8
8
|
import { SearchComponent } from '../../shared/components/search/search.component';
|
|
9
9
|
import { WaterfallComponent } from '../../shared/components/waterfall/waterfall.component';
|
|
10
10
|
import { PresentationService } from '../../core/services';
|
|
@@ -12,7 +12,7 @@ import { PresentationService } from '../../core/services';
|
|
|
12
12
|
@Component({
|
|
13
13
|
selector: 'app-main',
|
|
14
14
|
standalone: true,
|
|
15
|
-
imports: [PanelComponent, PreviewComponent, DatagridComponent, ToolbarComponent,
|
|
15
|
+
imports: [PanelComponent, PreviewComponent, DatagridComponent, ToolbarComponent, GlyphsComponent, SearchComponent, WaterfallComponent],
|
|
16
16
|
templateUrl: './main.component.html',
|
|
17
17
|
})
|
|
18
18
|
export class MainComponent {
|
|
@@ -241,8 +241,8 @@ export class NavigationComponent implements OnInit, OnDestroy {
|
|
|
241
241
|
this.contextMenuItems = [
|
|
242
242
|
{ label: 'Add Collection', action: 'add-collection', icon: 'create_new_folder' },
|
|
243
243
|
{ label: 'Add Fonts', action: 'add-fonts', icon: 'list_alt' },
|
|
244
|
-
{ label: 'Rename', action: 'rename' },
|
|
245
|
-
{ label: 'Delete', action: 'delete' },
|
|
244
|
+
{ label: 'Rename', action: 'rename', icon: 'edit', separator: true },
|
|
245
|
+
{ label: 'Delete', action: 'delete', icon: 'delete', separator: true },
|
|
246
246
|
];
|
|
247
247
|
|
|
248
248
|
this.contextMenu = { x: event.clientX, y: event.clientY, collection };
|
|
@@ -413,7 +413,7 @@ export class NavigationComponent implements OnInit, OnDestroy {
|
|
|
413
413
|
|
|
414
414
|
this.smartContextMenuItems = [
|
|
415
415
|
{ label: 'Edit Rules', action: 'edit-rules', icon: 'tune' },
|
|
416
|
-
{ label: 'Delete', action: 'delete' },
|
|
416
|
+
{ label: 'Delete', action: 'delete', icon: 'delete', separator: true },
|
|
417
417
|
];
|
|
418
418
|
|
|
419
419
|
this.smartContextMenu = { x: event.clientX, y: event.clientY, smartCollection: sc };
|
|
@@ -4,38 +4,87 @@ export interface ContextMenuItem {
|
|
|
4
4
|
label: string;
|
|
5
5
|
action: string;
|
|
6
6
|
icon?: string;
|
|
7
|
+
separator?: boolean;
|
|
7
8
|
}
|
|
8
9
|
|
|
9
10
|
@Component({
|
|
10
11
|
selector: 'app-context-menu',
|
|
11
12
|
standalone: true,
|
|
12
13
|
template: `
|
|
13
|
-
<div
|
|
14
|
-
class="fixed z-50 min-w-[160px] rounded-lg py-1"
|
|
15
|
-
[style.left.px]="x()"
|
|
16
|
-
[style.top.px]="y()"
|
|
17
|
-
[style.background-color]="'var(--context-bg)'"
|
|
18
|
-
[style.border]="'1px solid var(--context-border)'"
|
|
19
|
-
[style.box-shadow]="'var(--context-shadow)'"
|
|
20
|
-
[style.color]="'var(--text-primary)'"
|
|
21
|
-
>
|
|
14
|
+
<div class="context-menu fixed z-50" [style.left.px]="x()" [style.top.px]="y()">
|
|
22
15
|
@for (item of items(); track item.action) {
|
|
23
|
-
|
|
24
|
-
class="
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
{{ item.label }}
|
|
16
|
+
@if (item.separator) {
|
|
17
|
+
<div class="context-menu-separator"></div>
|
|
18
|
+
}
|
|
19
|
+
<button class="context-menu-item" (click)="menuSelect.emit(item.action)">
|
|
20
|
+
<span class="context-menu-icon">
|
|
21
|
+
@if (item.icon) {
|
|
22
|
+
<span class="material-symbols-outlined" style="font-size: 16px; font-variation-settings: 'opsz' 20, 'wght' 300;">{{
|
|
23
|
+
item.icon
|
|
24
|
+
}}</span>
|
|
25
|
+
}
|
|
26
|
+
</span>
|
|
27
|
+
<span class="context-menu-label">{{ item.label }}</span>
|
|
35
28
|
</button>
|
|
36
29
|
}
|
|
37
30
|
</div>
|
|
38
31
|
`,
|
|
32
|
+
styles: `
|
|
33
|
+
.context-menu {
|
|
34
|
+
min-width: 180px;
|
|
35
|
+
padding: 4px 0;
|
|
36
|
+
border-radius: 6px;
|
|
37
|
+
background-color: var(--context-bg);
|
|
38
|
+
border: 1px solid var(--context-border);
|
|
39
|
+
box-shadow: var(--context-shadow);
|
|
40
|
+
color: var(--text-primary);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.context-menu-item {
|
|
44
|
+
display: flex;
|
|
45
|
+
align-items: center;
|
|
46
|
+
width: 100%;
|
|
47
|
+
padding: 4px 12px 4px 4px;
|
|
48
|
+
margin: 0;
|
|
49
|
+
border: none;
|
|
50
|
+
background: transparent;
|
|
51
|
+
color: var(--text-secondary);
|
|
52
|
+
font-size: 12px;
|
|
53
|
+
line-height: 1.4;
|
|
54
|
+
cursor: pointer;
|
|
55
|
+
border-radius: 0;
|
|
56
|
+
text-align: left;
|
|
57
|
+
gap: 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.context-menu-item:hover {
|
|
61
|
+
background-color: var(--hover-bg);
|
|
62
|
+
color: var(--text-primary);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.context-menu-icon {
|
|
66
|
+
display: flex;
|
|
67
|
+
align-items: center;
|
|
68
|
+
justify-content: center;
|
|
69
|
+
width: 28px;
|
|
70
|
+
flex-shrink: 0;
|
|
71
|
+
color: var(--text-muted);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.context-menu-item:hover .context-menu-icon {
|
|
75
|
+
color: var(--text-primary);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.context-menu-label {
|
|
79
|
+
flex: 1;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.context-menu-separator {
|
|
83
|
+
height: 1px;
|
|
84
|
+
margin: 4px 0;
|
|
85
|
+
background-color: var(--context-border);
|
|
86
|
+
}
|
|
87
|
+
`,
|
|
39
88
|
})
|
|
40
89
|
export class ContextMenuComponent {
|
|
41
90
|
readonly x = input.required<number>();
|
|
@@ -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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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();
|
|
@@ -133,7 +133,7 @@
|
|
|
133
133
|
}
|
|
134
134
|
</div>
|
|
135
135
|
} @else {
|
|
136
|
-
<p class="text-center py-8 text-xs" [style.color]="'var(--text-muted)'">Select a font to
|
|
136
|
+
<p class="text-center py-8 text-xs" [style.color]="'var(--text-muted)'">Select a font to view glyphs</p>
|
|
137
137
|
}
|
|
138
138
|
</div>
|
|
139
139
|
</div>
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Component, inject, signal, computed, effect } from '@angular/core';
|
|
2
|
+
import { DatabaseService, PresentationService } from '../../../core/services';
|
|
3
|
+
|
|
4
|
+
const PAGE_SIZE = 200;
|
|
5
|
+
|
|
6
|
+
@Component({
|
|
7
|
+
selector: 'app-glyphs',
|
|
8
|
+
standalone: true,
|
|
9
|
+
templateUrl: './glyphs.component.html',
|
|
10
|
+
})
|
|
11
|
+
export class GlyphsComponent {
|
|
12
|
+
readonly db = inject(DatabaseService);
|
|
13
|
+
private readonly presentation = inject(PresentationService);
|
|
14
|
+
|
|
15
|
+
readonly currentPage = signal(1);
|
|
16
|
+
readonly selectedGlyph = this.presentation.selectedGlyph;
|
|
17
|
+
|
|
18
|
+
readonly totalPages = computed(() => Math.max(1, Math.ceil(this.db.glyphs().length / PAGE_SIZE)));
|
|
19
|
+
|
|
20
|
+
readonly pageGlyphs = computed(() => {
|
|
21
|
+
const glyphs = this.db.glyphs();
|
|
22
|
+
const start = (this.currentPage() - 1) * PAGE_SIZE;
|
|
23
|
+
return glyphs.slice(start, start + PAGE_SIZE);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
readonly fontFamily = computed(() => {
|
|
27
|
+
const store = this.db.store();
|
|
28
|
+
return store ? store.full_name || store.font_family || '' : '';
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
constructor() {
|
|
32
|
+
// Reset selection when user switches to a different font.
|
|
33
|
+
// Skip the null → savedId transition on startup (restore, not a switch).
|
|
34
|
+
let lastStoreId: number | null = null;
|
|
35
|
+
effect(() => {
|
|
36
|
+
const storeId = this.db.storeId();
|
|
37
|
+
if (lastStoreId !== null && storeId !== lastStoreId) {
|
|
38
|
+
this.currentPage.set(1);
|
|
39
|
+
this.presentation.selectedGlyph.set(null);
|
|
40
|
+
}
|
|
41
|
+
lastStoreId = storeId;
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Navigate to the correct page for the restored/selected glyph.
|
|
45
|
+
effect(() => {
|
|
46
|
+
const glyphs = this.db.glyphs();
|
|
47
|
+
const selected = this.presentation.selectedGlyph();
|
|
48
|
+
if (selected !== null && glyphs.length) {
|
|
49
|
+
const index = glyphs.indexOf(selected);
|
|
50
|
+
if (index >= 0) {
|
|
51
|
+
this.currentPage.set(Math.floor(index / PAGE_SIZE) + 1);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
toChar(codePoint: number): string {
|
|
58
|
+
return String.fromCodePoint(codePoint);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -6,7 +6,7 @@ export * from './datagrid/datagrid.component';
|
|
|
6
6
|
export * from './prompt-dialog/prompt-dialog.component';
|
|
7
7
|
export * from './rule-builder/rule-builder.component';
|
|
8
8
|
export * from './toolbar/toolbar.component';
|
|
9
|
-
export * from './
|
|
9
|
+
export * from './glyphs/glyphs.component';
|
|
10
10
|
export * from './search/search.component';
|
|
11
11
|
export * from './spinner/spinner.component';
|
|
12
12
|
export * from './waterfall/waterfall.component';
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
.app-spinner {
|
|
2
2
|
@apply relative block rounded-full w-[19px] h-[19px] m-0 p-0 opacity-0 overflow-hidden border-4 border-[#00a0be];
|
|
3
3
|
border-left: 4px solid #fff;
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
.app-spinner.is-animating {
|
|
7
|
+
@apply opacity-100 animate-spin;
|
|
7
8
|
}
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import { Component, inject, signal, computed, effect } from '@angular/core';
|
|
2
|
-
import { DatabaseService } from '../../../core/services';
|
|
3
|
-
|
|
4
|
-
const PAGE_SIZE = 200;
|
|
5
|
-
|
|
6
|
-
@Component({
|
|
7
|
-
selector: 'app-inspector',
|
|
8
|
-
standalone: true,
|
|
9
|
-
templateUrl: './inspector.component.html',
|
|
10
|
-
})
|
|
11
|
-
export class InspectorComponent {
|
|
12
|
-
readonly db = inject(DatabaseService);
|
|
13
|
-
|
|
14
|
-
readonly currentPage = signal(1);
|
|
15
|
-
readonly selectedGlyph = signal<number | null>(null);
|
|
16
|
-
|
|
17
|
-
readonly totalPages = computed(() => Math.max(1, Math.ceil(this.db.glyphs().length / PAGE_SIZE)));
|
|
18
|
-
|
|
19
|
-
readonly pageGlyphs = computed(() => {
|
|
20
|
-
const glyphs = this.db.glyphs();
|
|
21
|
-
const start = (this.currentPage() - 1) * PAGE_SIZE;
|
|
22
|
-
return glyphs.slice(start, start + PAGE_SIZE);
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
readonly fontFamily = computed(() => {
|
|
26
|
-
const store = this.db.store();
|
|
27
|
-
return store ? store.full_name || store.font_family || '' : '';
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
constructor() {
|
|
31
|
-
effect(() => {
|
|
32
|
-
this.db.glyphs();
|
|
33
|
-
this.currentPage.set(1);
|
|
34
|
-
this.selectedGlyph.set(null);
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
toChar(codePoint: number): string {
|
|
39
|
-
return String.fromCodePoint(codePoint);
|
|
40
|
-
}
|
|
41
|
-
}
|