fontastic 1.0.0 → 1.0.2
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/.github/workflows/macos.yml +0 -7
- package/.github/workflows/ubuntu.yml +0 -7
- package/.github/workflows/windows.yml +0 -7
- package/README.md +1 -0
- package/app/core/ConnectionManager.js +7 -1
- package/app/core/ConnectionManager.ts +11 -3
- package/app/core/MessageHandler.js +24 -0
- package/app/core/MessageHandler.ts +29 -0
- package/app/core/menu/templates/DarwinTemplate.js +16 -1
- package/app/core/menu/templates/DarwinTemplate.ts +16 -1
- package/app/core/menu/templates/SystemTemplate.js +16 -1
- package/app/core/menu/templates/SystemTemplate.ts +16 -1
- package/app/database/entity/SmartCollection.schema.js +66 -0
- package/app/database/entity/SmartCollection.schema.ts +39 -0
- package/app/database/entity/index.js +1 -0
- package/app/database/entity/index.ts +2 -1
- package/app/database/repository/SmartCollection.repository.js +47 -0
- package/app/database/repository/SmartCollection.repository.ts +30 -0
- package/app/database/repository/Store.repository.js +107 -0
- package/app/database/repository/Store.repository.ts +106 -0
- package/app/database/repository/index.js +1 -0
- package/app/database/repository/index.ts +2 -1
- package/app/enums/ChannelType.js +5 -0
- package/app/enums/ChannelType.ts +6 -0
- package/app/enums/StorageType.js +1 -0
- package/app/enums/StorageType.ts +1 -0
- package/app/package.json +1 -1
- package/app/types/FontMetrics.js +3 -0
- package/app/types/SmartCollection.js +3 -0
- package/app/types/SmartCollection.ts +5 -0
- package/app/types/SystemConfig.ts +2 -1
- package/app/types/index.js +2 -0
- package/app/types/index.ts +1 -0
- package/package.json +1 -1
- package/src/app/core/services/database/database.service.ts +70 -1
- package/src/app/core/services/message/message.service.ts +23 -0
- package/src/app/core/services/presentation/presentation.service.ts +62 -9
- package/src/app/layout/header/header.component.html +5 -5
- package/src/app/layout/layout.component.html +1 -1
- 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.html +70 -4
- package/src/app/layout/navigation/navigation.component.ts +131 -27
- package/src/app/shared/components/context-menu/context-menu.component.ts +70 -21
- package/src/app/shared/components/datagrid/datagrid.component.html +82 -2
- 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 +2 -1
- package/src/app/shared/components/rule-builder/rule-builder.component.html +94 -0
- package/src/app/shared/components/rule-builder/rule-builder.component.ts +136 -0
- package/src/app/shared/components/toolbar/toolbar.component.html +0 -81
- package/src/app/shared/components/toolbar/toolbar.component.ts +1 -2
- package/src/styles/base/variables.css +16 -0
- package/src/styles/components/spinner.css +4 -3
- package/src/app/shared/components/inspector/inspector.component.ts +0 -41
|
@@ -4,10 +4,58 @@
|
|
|
4
4
|
<app-library />
|
|
5
5
|
</app-collapsible-panel>
|
|
6
6
|
|
|
7
|
+
<app-collapsible-panel title="Smart Collections">
|
|
8
|
+
<span
|
|
9
|
+
panelActions
|
|
10
|
+
class="material-symbols-outlined cursor-pointer"
|
|
11
|
+
[style.color]="'var(--text-muted)'"
|
|
12
|
+
style="
|
|
13
|
+
font-size: 18px;
|
|
14
|
+
font-variation-settings:
|
|
15
|
+
'opsz' 20,
|
|
16
|
+
'wght' 300;
|
|
17
|
+
"
|
|
18
|
+
title="New Smart Collection"
|
|
19
|
+
(click)="openCreateSmartCollection()"
|
|
20
|
+
>add</span
|
|
21
|
+
>
|
|
22
|
+
<ul class="flex flex-col py-0.5">
|
|
23
|
+
@for (sc of db.smartCollections(); track sc.id) {
|
|
24
|
+
<li>
|
|
25
|
+
<a
|
|
26
|
+
class="flex items-center px-3 py-1 text-xs font-normal cursor-pointer transition-colors"
|
|
27
|
+
[style.background-color]="isSmartSelected(sc) ? 'var(--selected-bg)' : 'transparent'"
|
|
28
|
+
[style.color]="isSmartSelected(sc) ? 'var(--text-primary)' : 'inherit'"
|
|
29
|
+
(click)="selectSmartCollection(sc)"
|
|
30
|
+
(dblclick)="editSmartCollection(sc)"
|
|
31
|
+
(contextmenu)="onSmartContextMenu($event, sc)"
|
|
32
|
+
(mouseenter)="$any($event.target).style.backgroundColor = isSmartSelected(sc) ? 'var(--selected-bg)' : 'var(--hover-bg)'"
|
|
33
|
+
(mouseleave)="$any($event.target).style.backgroundColor = isSmartSelected(sc) ? 'var(--selected-bg)' : 'transparent'"
|
|
34
|
+
>
|
|
35
|
+
<span
|
|
36
|
+
class="material-symbols-outlined icon-sm mr-1"
|
|
37
|
+
[style.color]="'var(--text-muted)'"
|
|
38
|
+
style="
|
|
39
|
+
font-size: 16px;
|
|
40
|
+
font-variation-settings:
|
|
41
|
+
'opsz' 20,
|
|
42
|
+
'wght' 300;
|
|
43
|
+
"
|
|
44
|
+
>auto_awesome</span
|
|
45
|
+
>
|
|
46
|
+
{{ sc.title }}
|
|
47
|
+
</a>
|
|
48
|
+
</li>
|
|
49
|
+
} @empty {
|
|
50
|
+
<li class="px-3 py-1 text-xs" [style.color]="'var(--text-muted)'">No smart collections</li>
|
|
51
|
+
}
|
|
52
|
+
</ul>
|
|
53
|
+
</app-collapsible-panel>
|
|
54
|
+
|
|
7
55
|
<app-collapsible-panel title="Explorer" class="flex-1 min-h-0">
|
|
8
56
|
<span
|
|
9
57
|
panelActions
|
|
10
|
-
class="material-symbols-outlined cursor-pointer
|
|
58
|
+
class="material-symbols-outlined cursor-pointer"
|
|
11
59
|
[style.color]="'var(--text-muted)'"
|
|
12
60
|
style="
|
|
13
61
|
font-size: 18px;
|
|
@@ -15,9 +63,9 @@
|
|
|
15
63
|
'opsz' 20,
|
|
16
64
|
'wght' 300;
|
|
17
65
|
"
|
|
18
|
-
|
|
19
|
-
(click)="
|
|
20
|
-
>
|
|
66
|
+
title="New Collection"
|
|
67
|
+
(click)="openCreateRootCollection()"
|
|
68
|
+
>add</span
|
|
21
69
|
>
|
|
22
70
|
<ul class="flex-1 overflow-auto flex flex-col py-0.5">
|
|
23
71
|
<ng-container *ngTemplateOutlet="subtree; context: { $implicit: tree(), level: 0 }" />
|
|
@@ -122,5 +170,23 @@
|
|
|
122
170
|
(cancelled)="onCollectionCancelled()"
|
|
123
171
|
/>
|
|
124
172
|
}
|
|
173
|
+
|
|
174
|
+
@if (smartContextMenu) {
|
|
175
|
+
<app-context-menu
|
|
176
|
+
[x]="smartContextMenu.x"
|
|
177
|
+
[y]="smartContextMenu.y"
|
|
178
|
+
[items]="smartContextMenuItems"
|
|
179
|
+
(menuSelect)="onSmartContextMenuSelect($event)"
|
|
180
|
+
(menuClose)="closeSmartContextMenu()"
|
|
181
|
+
/>
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
@if (showRuleBuilder) {
|
|
185
|
+
<app-rule-builder
|
|
186
|
+
[smartCollection]="editingSmartCollection"
|
|
187
|
+
(saved)="onRuleBuilderSaved($event)"
|
|
188
|
+
(cancelled)="onRuleBuilderCancelled()"
|
|
189
|
+
/>
|
|
190
|
+
}
|
|
125
191
|
</div>
|
|
126
192
|
</nav>
|
|
@@ -1,14 +1,17 @@
|
|
|
1
|
-
import { Component, inject, computed } from '@angular/core';
|
|
1
|
+
import { Component, inject, computed, 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 } from '../../shared/components';
|
|
7
|
+
import { PromptDialogComponent, 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';
|
|
11
11
|
import type { Collection } from '@main/database/entity/Collection.schema';
|
|
12
|
+
import type { SmartCollection } from '@main/database/entity/SmartCollection.schema';
|
|
13
|
+
import type { SmartCollectionRule } from '@main/types';
|
|
14
|
+
import { ChannelType } from '@main/enums';
|
|
12
15
|
|
|
13
16
|
export interface TreeNode {
|
|
14
17
|
collection: Collection;
|
|
@@ -24,17 +27,34 @@ export interface TreeNode {
|
|
|
24
27
|
CollapsiblePanelComponent,
|
|
25
28
|
ContextMenuComponent,
|
|
26
29
|
PromptDialogComponent,
|
|
30
|
+
RuleBuilderComponent,
|
|
27
31
|
AutofocusDirective,
|
|
28
32
|
LibraryComponent,
|
|
29
33
|
NewsStatsComponent,
|
|
30
34
|
],
|
|
31
35
|
templateUrl: './navigation.component.html',
|
|
32
36
|
})
|
|
33
|
-
export class NavigationComponent {
|
|
37
|
+
export class NavigationComponent implements OnInit, OnDestroy {
|
|
34
38
|
readonly db = inject(DatabaseService);
|
|
35
39
|
private message = inject(MessageService);
|
|
36
40
|
private presentation = inject(PresentationService);
|
|
37
41
|
|
|
42
|
+
private menuToggleListener = (_event: any, panel: string) => {
|
|
43
|
+
if (panel === 'expand-collections') {
|
|
44
|
+
this.expandAll();
|
|
45
|
+
} else if (panel === 'collapse-collections') {
|
|
46
|
+
this.collapseAll();
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
ngOnInit() {
|
|
51
|
+
this.message.on(ChannelType.IPC_TOGGLE_PANEL, this.menuToggleListener);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
ngOnDestroy() {
|
|
55
|
+
this.message.removeListener(ChannelType.IPC_TOGGLE_PANEL, this.menuToggleListener);
|
|
56
|
+
}
|
|
57
|
+
|
|
38
58
|
readonly tree = computed<TreeNode[]>(() => {
|
|
39
59
|
const all = this.db.collections();
|
|
40
60
|
const buildChildren = (parentId: number): TreeNode[] =>
|
|
@@ -50,12 +70,17 @@ export class NavigationComponent {
|
|
|
50
70
|
contextMenu: { x: number; y: number; collection: Collection } | null = null;
|
|
51
71
|
editingId: number | null = null;
|
|
52
72
|
editingTitle = '';
|
|
53
|
-
expandedIds = new Set<number>();
|
|
54
73
|
allExpanded = false;
|
|
55
74
|
|
|
56
75
|
showCollectionDialog = false;
|
|
57
76
|
pendingParentId: number | null = null;
|
|
58
77
|
|
|
78
|
+
// Smart Collection state
|
|
79
|
+
showRuleBuilder = false;
|
|
80
|
+
editingSmartCollection: SmartCollection | null = null;
|
|
81
|
+
smartContextMenu: { x: number; y: number; smartCollection: SmartCollection } | null = null;
|
|
82
|
+
smartContextMenuItems: ContextMenuItem[] = [];
|
|
83
|
+
|
|
59
84
|
// Drag state
|
|
60
85
|
draggedNode: TreeNode | null = null;
|
|
61
86
|
dropTargetId: number | null = null;
|
|
@@ -187,7 +212,7 @@ export class NavigationComponent {
|
|
|
187
212
|
this.db.collections.set(updated);
|
|
188
213
|
|
|
189
214
|
if (newParentId !== 0) {
|
|
190
|
-
this.
|
|
215
|
+
this.presentation.expandNavigationId(newParentId);
|
|
191
216
|
}
|
|
192
217
|
|
|
193
218
|
try {
|
|
@@ -216,8 +241,8 @@ export class NavigationComponent {
|
|
|
216
241
|
this.contextMenuItems = [
|
|
217
242
|
{ label: 'Add Collection', action: 'add-collection', icon: 'create_new_folder' },
|
|
218
243
|
{ label: 'Add Fonts', action: 'add-fonts', icon: 'list_alt' },
|
|
219
|
-
{ label: 'Rename', action: 'rename' },
|
|
220
|
-
{ label: 'Delete', action: 'delete' },
|
|
244
|
+
{ label: 'Rename', action: 'rename', icon: 'edit', separator: true },
|
|
245
|
+
{ label: 'Delete', action: 'delete', icon: 'delete', separator: true },
|
|
221
246
|
];
|
|
222
247
|
|
|
223
248
|
this.contextMenu = { x: event.clientX, y: event.clientY, collection };
|
|
@@ -246,6 +271,11 @@ export class NavigationComponent {
|
|
|
246
271
|
}
|
|
247
272
|
}
|
|
248
273
|
|
|
274
|
+
openCreateRootCollection() {
|
|
275
|
+
this.pendingParentId = 0;
|
|
276
|
+
this.showCollectionDialog = true;
|
|
277
|
+
}
|
|
278
|
+
|
|
249
279
|
onCollectionConfirmed(name: string) {
|
|
250
280
|
this.db.collectionCreate({ title: name, parentId: this.pendingParentId });
|
|
251
281
|
this.showCollectionDialog = false;
|
|
@@ -302,32 +332,30 @@ export class NavigationComponent {
|
|
|
302
332
|
}
|
|
303
333
|
|
|
304
334
|
isExpanded(id: number): boolean {
|
|
305
|
-
return this.
|
|
335
|
+
return this.presentation.isNavigationExpanded(id);
|
|
306
336
|
}
|
|
307
337
|
|
|
308
338
|
toggleExpand(event: Event, id: number) {
|
|
309
339
|
event.stopPropagation();
|
|
310
|
-
|
|
311
|
-
this.expandedIds.delete(id);
|
|
312
|
-
} else {
|
|
313
|
-
this.expandedIds.add(id);
|
|
314
|
-
}
|
|
340
|
+
this.presentation.toggleNavigationExpanded(id);
|
|
315
341
|
}
|
|
316
342
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
343
|
+
expandAll() {
|
|
344
|
+
const allIds: number[] = [];
|
|
345
|
+
const collectIds = (nodes: TreeNode[]) => {
|
|
346
|
+
for (const node of nodes) {
|
|
347
|
+
allIds.push(node.collection.id);
|
|
348
|
+
collectIds(node.children);
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
collectIds(this.tree());
|
|
352
|
+
this.presentation.setAllNavigationExpanded(allIds);
|
|
353
|
+
this.allExpanded = true;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
collapseAll() {
|
|
357
|
+
this.presentation.clearAllNavigationExpanded();
|
|
358
|
+
this.allExpanded = false;
|
|
331
359
|
}
|
|
332
360
|
|
|
333
361
|
isSelected(collection: Collection): boolean {
|
|
@@ -359,6 +387,82 @@ export class NavigationComponent {
|
|
|
359
387
|
this.editingTitle = '';
|
|
360
388
|
}
|
|
361
389
|
|
|
390
|
+
// Smart Collection methods
|
|
391
|
+
|
|
392
|
+
selectSmartCollection(sc: SmartCollection) {
|
|
393
|
+
this.db.selectSmartCollection(sc.id);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
isSmartSelected(sc: SmartCollection): boolean {
|
|
397
|
+
return this.db.activeSmartCollectionId() === sc.id;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
openCreateSmartCollection() {
|
|
401
|
+
this.editingSmartCollection = null;
|
|
402
|
+
this.showRuleBuilder = true;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
editSmartCollection(sc: SmartCollection) {
|
|
406
|
+
this.editingSmartCollection = sc;
|
|
407
|
+
this.showRuleBuilder = true;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
onSmartContextMenu(event: MouseEvent, sc: SmartCollection) {
|
|
411
|
+
event.preventDefault();
|
|
412
|
+
event.stopPropagation();
|
|
413
|
+
|
|
414
|
+
this.smartContextMenuItems = [
|
|
415
|
+
{ label: 'Edit Rules', action: 'edit-rules', icon: 'tune' },
|
|
416
|
+
{ label: 'Delete', action: 'delete', icon: 'delete', separator: true },
|
|
417
|
+
];
|
|
418
|
+
|
|
419
|
+
this.smartContextMenu = { x: event.clientX, y: event.clientY, smartCollection: sc };
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
onSmartContextMenuSelect(action: string) {
|
|
423
|
+
if (!this.smartContextMenu) return;
|
|
424
|
+
const sc = this.smartContextMenu.smartCollection;
|
|
425
|
+
this.smartContextMenu = null;
|
|
426
|
+
|
|
427
|
+
switch (action) {
|
|
428
|
+
case 'edit-rules':
|
|
429
|
+
this.editingSmartCollection = sc;
|
|
430
|
+
this.showRuleBuilder = true;
|
|
431
|
+
break;
|
|
432
|
+
case 'delete':
|
|
433
|
+
this.db.smartCollectionDelete(sc.id);
|
|
434
|
+
break;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
closeSmartContextMenu() {
|
|
439
|
+
this.smartContextMenu = null;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
onRuleBuilderSaved(data: { title: string; rules: SmartCollectionRule[]; match_type: string }) {
|
|
443
|
+
const rulesJson = JSON.stringify(data.rules);
|
|
444
|
+
if (this.editingSmartCollection) {
|
|
445
|
+
this.db.smartCollectionUpdate(this.editingSmartCollection.id, {
|
|
446
|
+
title: data.title,
|
|
447
|
+
rules: rulesJson,
|
|
448
|
+
match_type: data.match_type,
|
|
449
|
+
});
|
|
450
|
+
} else {
|
|
451
|
+
this.db.smartCollectionCreate({
|
|
452
|
+
title: data.title,
|
|
453
|
+
rules: rulesJson,
|
|
454
|
+
match_type: data.match_type,
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
this.showRuleBuilder = false;
|
|
458
|
+
this.editingSmartCollection = null;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
onRuleBuilderCancelled() {
|
|
462
|
+
this.showRuleBuilder = false;
|
|
463
|
+
this.editingSmartCollection = null;
|
|
464
|
+
}
|
|
465
|
+
|
|
362
466
|
findRootParentId(collection: Collection): number {
|
|
363
467
|
const all = this.db.collections();
|
|
364
468
|
let current = collection;
|
|
@@ -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>();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
<div class="panel">
|
|
2
|
-
<div class="
|
|
1
|
+
<div class="panel grid" [style.grid-template-rows]="db.totalPages() > 1 ? '1fr 32px' : '1fr'">
|
|
2
|
+
<div class="overflow-hidden scrollbox-y">
|
|
3
3
|
<table class="w-full border-separate border-spacing-0">
|
|
4
4
|
<thead
|
|
5
5
|
class="sticky top-0 z-10"
|
|
@@ -93,4 +93,84 @@
|
|
|
93
93
|
</tbody>
|
|
94
94
|
</table>
|
|
95
95
|
</div>
|
|
96
|
+
@if (db.totalPages() > 1) {
|
|
97
|
+
<div
|
|
98
|
+
class="flex items-center gap-2 px-3"
|
|
99
|
+
[style.background-color]="'var(--surface-2)'"
|
|
100
|
+
[style.border-top]="'1px solid var(--border-subtle)'"
|
|
101
|
+
>
|
|
102
|
+
<div class="flex items-center gap-0.5 text-[11px]">
|
|
103
|
+
<button
|
|
104
|
+
class="px-1 py-0.5 rounded cursor-pointer disabled:cursor-default transition-colors"
|
|
105
|
+
[style.color]="'var(--text-secondary)'"
|
|
106
|
+
[disabled]="db.currentPage() === 1"
|
|
107
|
+
[class.opacity-30]="db.currentPage() === 1"
|
|
108
|
+
(click)="db.firstPage()"
|
|
109
|
+
>
|
|
110
|
+
<span
|
|
111
|
+
class="material-symbols-outlined text-sm"
|
|
112
|
+
style="
|
|
113
|
+
font-variation-settings:
|
|
114
|
+
'opsz' 20,
|
|
115
|
+
'wght' 300;
|
|
116
|
+
"
|
|
117
|
+
>first_page</span
|
|
118
|
+
>
|
|
119
|
+
</button>
|
|
120
|
+
<button
|
|
121
|
+
class="px-1 py-0.5 rounded cursor-pointer disabled:cursor-default transition-colors"
|
|
122
|
+
[style.color]="'var(--text-secondary)'"
|
|
123
|
+
[disabled]="db.currentPage() === 1"
|
|
124
|
+
[class.opacity-30]="db.currentPage() === 1"
|
|
125
|
+
(click)="db.prevPage()"
|
|
126
|
+
>
|
|
127
|
+
<span
|
|
128
|
+
class="material-symbols-outlined text-sm"
|
|
129
|
+
style="
|
|
130
|
+
font-variation-settings:
|
|
131
|
+
'opsz' 20,
|
|
132
|
+
'wght' 300;
|
|
133
|
+
"
|
|
134
|
+
>chevron_left</span
|
|
135
|
+
>
|
|
136
|
+
</button>
|
|
137
|
+
<span class="px-1.5 select-none" [style.color]="'var(--text-muted)'">{{ db.currentPage() }} / {{ db.totalPages() }}</span>
|
|
138
|
+
<button
|
|
139
|
+
class="px-1 py-0.5 rounded cursor-pointer disabled:cursor-default transition-colors"
|
|
140
|
+
[style.color]="'var(--text-secondary)'"
|
|
141
|
+
[disabled]="db.currentPage() === db.totalPages()"
|
|
142
|
+
[class.opacity-30]="db.currentPage() === db.totalPages()"
|
|
143
|
+
(click)="db.nextPage()"
|
|
144
|
+
>
|
|
145
|
+
<span
|
|
146
|
+
class="material-symbols-outlined text-sm"
|
|
147
|
+
style="
|
|
148
|
+
font-variation-settings:
|
|
149
|
+
'opsz' 20,
|
|
150
|
+
'wght' 300;
|
|
151
|
+
"
|
|
152
|
+
>chevron_right</span
|
|
153
|
+
>
|
|
154
|
+
</button>
|
|
155
|
+
<button
|
|
156
|
+
class="px-1 py-0.5 rounded cursor-pointer disabled:cursor-default transition-colors"
|
|
157
|
+
[style.color]="'var(--text-secondary)'"
|
|
158
|
+
[disabled]="db.currentPage() === db.totalPages()"
|
|
159
|
+
[class.opacity-30]="db.currentPage() === db.totalPages()"
|
|
160
|
+
(click)="db.lastPage()"
|
|
161
|
+
>
|
|
162
|
+
<span
|
|
163
|
+
class="material-symbols-outlined text-sm"
|
|
164
|
+
style="
|
|
165
|
+
font-variation-settings:
|
|
166
|
+
'opsz' 20,
|
|
167
|
+
'wght' 300;
|
|
168
|
+
"
|
|
169
|
+
>last_page</span
|
|
170
|
+
>
|
|
171
|
+
</button>
|
|
172
|
+
</div>
|
|
173
|
+
<span class="text-[11px]" [style.color]="'var(--text-muted)'"> {{ db.storeCount() }} fonts </span>
|
|
174
|
+
</div>
|
|
175
|
+
}
|
|
96
176
|
</div>
|
|
@@ -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
|
+
}
|
|
@@ -4,8 +4,9 @@ export * from './page-not-found/page-not-found.component';
|
|
|
4
4
|
export * from './panel/panel.component';
|
|
5
5
|
export * from './datagrid/datagrid.component';
|
|
6
6
|
export * from './prompt-dialog/prompt-dialog.component';
|
|
7
|
+
export * from './rule-builder/rule-builder.component';
|
|
7
8
|
export * from './toolbar/toolbar.component';
|
|
8
|
-
export * from './
|
|
9
|
+
export * from './glyphs/glyphs.component';
|
|
9
10
|
export * from './search/search.component';
|
|
10
11
|
export * from './spinner/spinner.component';
|
|
11
12
|
export * from './waterfall/waterfall.component';
|